Compare commits

...

398 Commits

Author SHA1 Message Date
Alex X
fce41f4fc1 Update wyoming readme about events 2025-04-24 22:06:36 +03:00
Alex X
c50e894a42 Add PlayFile function to wyoming server 2025-04-24 21:23:16 +03:00
Alex X
890fd78a6a Remove errors from wyoming server handlers 2025-04-24 18:32:42 +03:00
Alex X
518cae1476 Add support events to wyoming server 2025-04-24 17:13:51 +03:00
Alex X
545a105ba0 Add support body to expr fetch func 2025-04-22 16:37:10 +03:00
Alex X
70b4bf779e Change wyoming Event.Data type to string 2025-04-22 16:35:44 +03:00
Alex X
7cf672da84 Add readme for exec and wyoming modules 2025-04-22 14:19:40 +03:00
Alex X
80f57a0292 Add support snd mode for wyoming module 2025-04-22 13:16:57 +03:00
Alex X
3b7309d9f7 Add support mic mode for wyoming module 2025-04-22 11:49:08 +03:00
Alex X
6df1e68a5f Update wyoming producer and backchannel 2025-04-22 10:26:00 +03:00
Alex X
df2e982090 Add logs to wyoming module 2025-04-22 10:25:22 +03:00
Alex X
902af5e5d7 Add wyoming module 2025-04-22 06:37:42 +03:00
Alex X
7fe23c7bc5 Add wav backchannel (not used yet) 2025-04-21 20:33:13 +03:00
Alex X
d0c3cb066c Rewrite exec backchannel 2025-04-21 20:33:13 +03:00
Alex X
5666943559 Change alsa source name for discovery API 2025-04-21 20:18:28 +03:00
Alex X
1b41f61247 Add supported codec check for alsa source 2025-04-21 20:18:28 +03:00
Alex X
e1342f06b7 Change codec channels from uint16 to uint8 2025-04-21 20:18:28 +03:00
Alex X
f535595d1f Add universal PCM transcoder 2025-04-21 20:18:28 +03:00
Alex X
7415776e4d Add support alsa source 2025-04-21 20:18:28 +03:00
Alex X
bad7caa187 Add ioctl package 2025-04-21 20:17:52 +03:00
Alex X
2473eee66b Add warn log for match media func 2025-04-21 20:17:52 +03:00
Alex X
f45fef29d8 Add support eseecloud source #1690 2025-04-08 19:55:51 +03:00
Alex X
699a995e8c Fix deadlock on write to track channel 2025-04-08 11:33:04 +03:00
Alex X
ce02b03a73 Merge pull request #1682 from infastin/fix/sender-deadlock
fix(core): potential sender goroutine deadlock
2025-04-08 11:26:18 +03:00
Alex X
3e1b01073b Increased compression when compiling linux binaries 2025-04-07 18:04:38 +03:00
Alex X
3e4dce2413 Update dependencies 2025-04-07 18:03:41 +03:00
Alex X
fef3091ecc Fix support webrtc creality format #1600 2025-04-07 17:43:51 +03:00
Alex X
af7509ebaf Update pion/webrtc library to v4 2025-04-07 16:56:38 +03:00
infastin
487527f5a5 chore: remove mutexes 2025-04-05 13:50:02 +05:00
Alex X
bfd26560b1 Add flussonic source #1678 2025-04-04 19:59:52 +03:00
Alex X
be3a1c5b5f Rewrite ivideon source 2025-04-04 19:58:05 +03:00
infastin
0669cfbebf fix(core): potential sender loop deadlock 2025-03-31 19:12:07 +05:00
Alex X
d99bf122ea Fix SPS parsing in some cases 2025-03-27 20:52:49 +03:00
Alex X
ed5581d1d9 Add readme for docker 2025-03-22 19:16:00 +03:00
Alex X
71c59cfe50 Add rockchip docker image 2025-03-22 18:37:30 +03:00
Alex X
0e49a066ba Docker files refactoring 2025-03-22 16:41:32 +03:00
Alex X
fcb786cf60 Add readme for FFmpeg hardware 2025-03-22 10:56:25 +03:00
Alex X
c56b2cdd62 Merge pull request #1203 from MarcA711/update-rockchip-presets
Update FFmpeg presets for Rockchip boards
2025-03-22 10:38:49 +03:00
Alex X
6309d323dc Update hardware support for Rockchip 2025-03-22 10:32:33 +03:00
Alex X
c52f3ebdd6 Fix wrong URL in hls.html example 2025-03-17 14:52:33 +03:00
Alex X
47f32a5f55 Fix support linux + riscv64 #1639 2025-03-13 15:33:23 +03:00
Alex X
60250a32c2 Fix support HKSV for HomeKit cameras #684 2025-03-12 22:28:30 +03:00
Alex X
6a4c73db03 Fix possible panic for tlv8.UnmarshalBase64 2025-03-12 06:05:21 +03:00
Alex X
fa580c516e Update version to 1.9.9 2025-03-10 05:51:40 +03:00
Alex X
7f4c450553 Merge pull request #1629 from hsakoh/feature/addSwitchBotSupport
Add client for SwitchBot Camera WebRTC
2025-03-09 18:48:40 +03:00
Alex X
761ff7ed5a Update readme for SwitchBot 2025-03-09 18:48:18 +03:00
Alex X
117d767f05 Code refactoring for SwitchBot format support #1629 2025-03-09 18:44:32 +03:00
Alex X
8405bfe6f9 Merge pull request #1632 from Klutrem/master
feat: x-www-form-urlencoded support
2025-03-09 17:46:46 +03:00
Alex X
ccdb1479f7 Code refactoring for RtspToWeb format support #1632 2025-03-09 17:46:13 +03:00
Alex X
c8f68f44af Optimize imports 2025-03-09 17:26:06 +03:00
Alex X
3954a555f8 Fix extra slash for RTSP SETUP #1236 2025-03-09 17:08:25 +03:00
Alex X
b6934922fa Improve ONVIF server #1304 2025-03-09 16:26:10 +03:00
Alex X
944e6f5569 Update Reolink links in the docs 2025-03-09 11:40:23 +03:00
Alex X
d51b36e80d Add readme to RTMP module 2025-03-09 07:21:10 +03:00
Alex X
c9724e2024 Fix RTMP server handshake for FFmpeg #1318 2025-03-09 07:20:40 +03:00
Alex X
830e476120 Fix data race for memory logger #1487 2025-03-08 14:11:29 +03:00
Alex X
fe2e372997 Add examples to streams module readme 2025-03-08 07:31:49 +03:00
Alex X
a15deedf0d Fix YAML patch in some cases #1626 2025-03-07 21:44:23 +03:00
klutrem
22bf8163cd returned url variable name 2025-03-06 16:08:43 +03:00
klutrem
b8390331af feat: x-www-form-urlencoded support 2025-03-06 16:03:44 +03:00
hsakoh
47b740ff35 Add client for SwitchBot Camera WebRTC (supports special SessionDescription). 2025-03-05 09:32:33 +09:00
Alex X
39c14e6556 Fix support streaming to YouTube #1574 2025-03-01 21:36:32 +03:00
Alex X
57cd791348 Fix ONVIF client GetCapabilities request 2025-03-01 20:00:59 +03:00
Alex X
3c612e284e Update dependencies 2025-02-27 21:43:30 +03:00
Alex X
8cd1ab5c8f Update go build version to 1.24 2025-02-27 21:05:10 +03:00
Alex X
a6c22cadb8 Merge pull request #1620 from DRuggeri/master
Correct slight syntax error in example
2025-02-27 20:37:27 +03:00
Alex X
a628ecf72b Update readme about new WebRTC default settings and filters logic 2025-02-27 15:56:40 +03:00
Daniel Ruggeri
8d70233d83 Correct slight syntax error in example 2025-02-27 06:35:30 -06:00
Alex X
ae89600201 Fix WebUI editor after Save 2025-02-27 15:01:05 +03:00
Alex X
934d43b525 Update WebRTC server operation in closed docker containers 2025-02-27 14:30:56 +03:00
Alex X
858c04bacf Fix situation when WebRTC candidate pair changes multiple times #1282 2025-02-26 21:39:56 +03:00
Alex X
2a5355b1f8 Fix WebRTC server with static UDP port 2025-02-26 21:34:46 +03:00
Alex X
5cf2ac4c3e Fix escape quotes for DOT format #1603 2025-02-26 17:00:05 +03:00
Alex X
71173da5ad Add useful links to webrtc readme 2025-02-26 15:52:04 +03:00
Alex X
e304f4f34f Add support creality format for webrtc client #1600 2025-02-25 19:40:19 +03:00
Alex X
7d37f645ba Improved limited HomeKit server support for open source projects 2025-02-25 19:26:30 +03:00
Alex X
c50738005d Update mDNS server handler 2025-02-25 16:16:38 +03:00
Alex X
effff6f88d Fix concurrent SRTP sessions map read and map write #1489 2025-02-24 22:04:14 +03:00
Alex X
45b223a2ef Fix panic on reading nil TLV8 #1507 2025-02-24 21:55:10 +03:00
Alex X
90544ba713 Fix panic for concurrent streams map read and map write #1612 2025-02-24 21:02:33 +03:00
Alex X
e55c2e9598 Merge pull request #1438 from huynhquangtoan/master
Fix "panic: send on closed channel"
2025-02-24 20:27:53 +03:00
Alex X
6ee52474e1 Code refactoring for panic: send on closed channel 2025-02-24 18:13:16 +03:00
Alex X
4bf9f0b96c Merge pull request #1137 from felipecrs/patch-2
Add note about requesting multiple backchannel on Dahua Doorbell
2025-02-24 17:13:33 +03:00
Alex X
79e2fa89df Merge pull request #1167 from skrashevich/feat-logging-to-file
feat(logging): add file output option for logging configuration
2025-02-24 16:25:20 +03:00
Alex X
2ad0ded73f Merge branch 'master' into feat-logging-to-file 2025-02-24 16:25:08 +03:00
Alex X
1ab05e5c3b Merge pull request #1205 from skrashevich/fix-keep-netmap-selections
fix(network): preserve selected nodes and edges on data reload
2025-02-24 16:23:19 +03:00
Alex X
4b4a1644ff Code refactoring for network view 2025-02-24 16:21:12 +03:00
Alex X
7fd0ec8ce6 Code refactoring for logs to file 2025-02-24 15:21:37 +03:00
Alex X
2d1e08b50e Merge pull request #1223 from robvanoostenrijk/freebsd-builds
FreeBSD Binaries (Attempt #2)
2025-02-24 12:44:59 +03:00
Alex X
b881c52118 Code refactoring for FreeBSD binaries 2025-02-24 12:44:09 +03:00
Alex X
6fb59949a2 Rewrite exec handler 2025-02-23 21:16:53 +03:00
Alex X
7d41dc21c1 Merge pull request #1264 from OnFreund/patch-1
Install ffplay in container
2025-02-22 12:28:54 +03:00
Alex X
6365968dc3 Merge pull request #1253 from jamal/nest-rtsp
nest: add support for RTSP cameras
2025-02-22 11:41:24 +03:00
Alex X
1abb3c8c22 Code refactoring for Nest RTSP source 2025-02-22 11:39:32 +03:00
Alex X
33f4bb45d1 Merge pull request #1284 from skrashevich/fix-apiinit-datarace
refactor(api): move port extraction logic to Init function for prevent data race
2025-02-21 15:50:22 +03:00
Alex X
4897994b35 Merge pull request #1432 from seydx/rtsp-backchannel 2025-02-18 17:08:37 +03:00
Alex X
0a773c82af Code refactoring for RTSP backchannel 2025-02-18 16:59:00 +03:00
seydx
b34d970076 remove duplicated code 2025-02-18 11:52:57 +01:00
Alex X
19cf781431 Merge pull request #1511 from fmcloudconsulting/fix/rtsp-server-interleaved
Accept rtsp client without interleaved parameter
2025-02-18 12:50:51 +03:00
Alex X
637e65e5a0 Code refactoring for RTSP transport header processing 2025-02-18 12:49:33 +03:00
Alex X
b3f83fd363 Merge pull request #1522 from subbyte/authlog
Improve RTSP server authentication handling and auditing
2025-02-18 12:26:54 +03:00
Alex X
02ac3a6814 Code refactoring for RTSP auth 2025-02-18 12:24:06 +03:00
Alex X
97891d36ab Merge pull request #1607 from felipecrs/patch-3
Fix typo in RTMP docs
2025-02-18 11:05:57 +03:00
Felipe Santos
65c87d5e0f Fix typo in RTMP docs 2025-02-17 18:07:31 -03:00
Alex X
ae3b53540e Merge pull request #1543 from cavefire/h200-child-devices
Fix H200 + D230 Doorbell stream
2025-02-17 17:09:20 +03:00
Alex X
0e9009b0de Merge pull request #1568 from seydx/ring
Ring: Fix snapshot producer MarshalJSON and prevent nil reference during stop
2025-02-17 17:08:26 +03:00
Alex X
be2864c34b Code refactoring after #1588 2025-02-17 17:07:36 +03:00
Alex X
c9bdac2e03 Merge pull request #1588 from thomaspurchas/master
Handle malformed fmtp lines
2025-02-17 17:03:01 +03:00
seydx
040de3d973 Merge branch 'AlexxIT:master' into rtsp-backchannel 2025-02-10 17:11:40 +01:00
seydx
1703380ebc Merge branch 'AlexxIT:master' into ring 2025-02-10 17:11:20 +01:00
Alex X
e935885cd3 Update general H265 support for WebRTC #1439 2025-02-10 17:11:08 +01:00
Julian
da809bb9d7 Update build.yml
Fix 
Build binaries
This request has been automatically failed because it uses a deprecated version of `actions/upload-artifact: v3`. Learn more: https://github.blog/changelog/2024-04-16-deprecation-notice-v3-of-the-artifact-actions/
2025-02-10 17:11:08 +01:00
Thomas Purchas
c39c9aa1da Handle malformed fmtp lines 2025-02-08 23:12:45 +00:00
Alex X
ad61662cc4 Update general H265 support for WebRTC #1439 2025-02-07 10:17:15 +03:00
Alex X
e42bcd0115 Merge pull request #1580 from notjulian/master
Update build.yml
2025-02-03 09:20:19 +03:00
seydx
ad8c025393 Add backchannel support for rtsp server 2025-02-03 00:03:17 +01:00
Alex X
1b0db3c8b0 Add readme for V4L2 module 2025-02-03 00:02:05 +01:00
Alex X
f9a8c1969c Improve delay for MSE player 2025-02-03 00:02:05 +01:00
Alex X
645c11f0bd Ignore unknown NAL unit types for RTP/H264 #1570 2025-02-03 00:02:05 +01:00
Alex X
ece49a158e Add support H264, H265, NV12 for V4L2 source #1546 2025-02-03 00:02:05 +01:00
Alex X
b139b8fdd6 Add readme for V4L2 module 2025-02-02 23:56:07 +01:00
Alex X
b14aa4f0dc Improve delay for MSE player 2025-02-02 23:56:07 +01:00
Alex X
9b392a22e1 Ignore unknown NAL unit types for RTP/H264 #1570 2025-02-02 23:56:07 +01:00
Alex X
36547a7343 Add support H264, H265, NV12 for V4L2 source #1546 2025-02-02 23:56:07 +01:00
Julian
876390aa68 Update build.yml
Fix 
Build binaries
This request has been automatically failed because it uses a deprecated version of `actions/upload-artifact: v3`. Learn more: https://github.blog/changelog/2024-04-16-deprecation-notice-v3-of-the-artifact-actions/
2025-02-02 22:15:19 +00:00
Alex X
297ecfbae3 Add readme for V4L2 module 2025-02-02 15:41:30 +03:00
Alex X
eeb0012e7f Improve delay for MSE player 2025-02-02 14:46:37 +03:00
Alex X
35cf82f11c Ignore unknown NAL unit types for RTP/H264 #1570 2025-02-02 11:01:44 +03:00
Alex X
82f6c2c550 Add support H264, H265, NV12 for V4L2 source #1546 2025-01-26 16:09:50 +03:00
seydx
3e3988a67f minor improvements 2025-01-25 16:11:39 +01:00
seydx
2f4694dc95 Merge branch 'master' into rtsp-backchannel 2025-01-25 14:20:12 +01:00
Alex X
f072dab07b Correcting code formatting after #1567 2025-01-25 11:18:51 +03:00
Alex X
fc02e6f4a5 Merge pull request #1567 from seydx/ring
Add Ring camera integration
2025-01-25 11:12:04 +03:00
seydx
0651a09a3c add snapshot producer 2025-01-24 22:35:04 +01:00
seydx
2c5f1e0417 add 2fa 2025-01-24 19:37:17 +01:00
seydx
c9682ca64d remove unnecessary prints and use mutex for ws 2025-01-24 18:02:47 +01:00
seydx
bceb024588 enable speaker for two way audio 2025-01-24 17:37:50 +01:00
seydx
17bba4d4a2 skip empty ICE candidates 2025-01-24 12:47:25 +01:00
seydx
485448cbc7 initial ring implementation 2025-01-24 12:38:45 +01:00
seydx
244dad447b Merge branch 'master' into rtsp-backchannel 2025-01-12 08:19:45 +01:00
Alex X
22e63a7367 Fix comment about OpenIPC 2025-01-10 19:57:10 +03:00
Alex X
83907132b5 Update about packed and planar YUV formats 2025-01-10 15:01:01 +03:00
Alex X
7dc9beb171 Add ws and ffmpeg modules to go2rtc_mjpeg 2025-01-10 14:56:42 +03:00
Alex X
0664e46a4b Fix v4l2 source for MIPS 2025-01-10 12:57:37 +03:00
Timo Christeleit
2ca97a42c5 Update pkg/tapo/client.go
Co-authored-by: Sergey Vilgelm <523825+SVilgelm@users.noreply.github.com>
2025-01-09 09:44:23 +01:00
Alex X
773e415dff Code refactoring for v4l2 device 2025-01-09 07:18:36 +03:00
Xiaokui Shu
9e673559c4 Improve log formatting with Msgf 2025-01-08 21:31:37 -05:00
Alex X
879ef603fe Update v4l2 discovery 2025-01-09 00:34:10 +03:00
Alex X
7e0a163f12 Add support mips arch for v4l2 source 2025-01-09 00:28:33 +03:00
Timo Christeleit
8e4088e08f fix tapo h200 + d230 doorbell stream 2025-01-08 11:03:17 +01:00
Alex X
59161c663b Add support framerate param for v4l2 source 2025-01-08 08:35:42 +03:00
Alex X
93252fc5d2 Change ListSizes function for V4L2 device 2025-01-08 08:35:19 +03:00
Alex X
e4b8d1807d Add support snapshot for raw image format 2025-01-08 08:35:08 +03:00
Alex X
33e0ccdd10 Fix build for mipsle 2025-01-07 00:19:53 +03:00
Alex X
d59139a2ab Add support v4l2 source 2025-01-06 23:47:35 +03:00
Alex X
df831833b1 Collect list of dependency license 2025-01-06 19:31:03 +03:00
Alex X
c065db6da1 Code refactoring after #1539 2025-01-06 06:32:13 +03:00
Alex X
a55be809f3 Merge pull request #1539 from BrunoTCouto/onvif-RateControl
fix(onvif): Add RateControl to fix Unifi Protect integration
2025-01-06 06:30:29 +03:00
Bruno Tomassetti Couto
a9e1ebc0a8 Improve ONVIF server by adding rate control for video encoder configuration 2025-01-05 22:54:20 -03:00
Alex X
55af09a350 Add support fix JPEG from some MJPEG sources 2025-01-05 11:03:44 +03:00
Alex X
199fdd6728 Update version to 1.9.8 2025-01-03 16:24:31 +03:00
Alex X
4035e91672 Fix ONVIF XML tag parsing in some cases 2025-01-03 15:08:38 +03:00
Alex X
bc9194d740 Update go dependencies 2025-01-03 13:57:15 +03:00
Alex X
f601c47218 Improve ONVIF server 2025-01-03 13:19:40 +03:00
huynhquangtoan
066d559377 Merge branch 'AlexxIT:master' into master 2024-12-31 10:16:29 +07:00
Alex X
2c3219ffcb Merge pull request #1520 from acortelyou/feat/unifi
Extend onvif server to support Unifi Protect
2024-12-30 20:33:33 +03:00
Alex Cortelyou
cf88bf9c23 Remove inaccurate comments 2024-12-29 16:22:49 -08:00
Alex Cortelyou
b8303b9a22 Remove optional fields, normalize indentation 2024-12-29 16:16:49 -08:00
Alex X
a3f084dcde RTMP server enhancement to support OpenIPC cameras 2024-12-29 22:37:04 +03:00
Alex X
0d6b8fc6fc Fix OPUS/48000/1 for RTSP from some cameras #1506 2024-12-29 11:44:56 +03:00
Xiaokui Shu
261a936bb8 Add rtsp server failed auth logging 2024-12-25 17:09:23 -05:00
Alex Cortelyou
159d9425a7 Remove non-essential fields 2024-12-24 11:08:18 -08:00
Alex Cortelyou
3a50b3678d Extend onvif server to support Unifi Protect 2024-12-23 23:43:39 -08:00
fmcloudconsulting
6fa352f407 fix: don't require unicast param and fix typo (tr instead of transport) 2024-12-17 19:06:15 +01:00
fmcloudconsulting
4b80b2c233 fix: typo 2024-12-17 17:36:18 +01:00
fmcloudconsulting
d881755503 chore: lint 2024-12-17 17:30:10 +01:00
fmcloudconsulting
fd125ecc68 fix: return 461 if client requested an invalid transport method 2024-12-17 17:28:13 +01:00
fmcloudconsulting
29f7f1a57d feat: accept rtsp client without interleaved parameter 2024-12-16 22:50:35 +01:00
Alex X
8ecaabfce9 Add support VIGI cameras #1470 2024-12-16 20:25:01 +03:00
seydx
1797ff67c0 Merge branch 'master' into rtsp-backchannel 2024-12-14 11:30:37 +01:00
Alex X
f1ba5e95ec Fix parsing RTSP Transport header #1235 2024-12-06 12:34:31 +03:00
Alex X
d8c0f9d1d9 Update support doorbird source #1060 2024-12-05 10:55:14 +03:00
Rob van Oostenrijk
df4b5fc87d Merge branch 'master' into freebsd-builds 2024-12-01 07:19:40 +04:00
Alex X
d7cdc8b3b0 Merge pull request #1477 from oeiber/patch-1
Removing additional '&' in rawURL
2024-11-24 19:00:38 +03:00
oeiber
5b53ca7cf1 Removing double additional '&' in rawURL 2024-11-24 16:19:58 +01:00
Alex X
194d1dae51 Add support doorbird source #1060 2024-11-24 13:09:13 +03:00
seydx
61322ede6c Merge branch 'AlexxIT:master' into rtsp-backchannel 2024-11-15 01:06:50 +01:00
Alex X
a8edaedc8b Fix broken incoming sources after v1.9.7 #1458 2024-11-15 01:05:50 +01:00
Alex X
25145f72e5 Fix broken incoming sources after v1.9.7 #1458 2024-11-14 19:39:26 +03:00
seydx
3ddf8b5922 Merge branch 'master' into rtsp-backchannel 2024-11-13 11:23:16 +01:00
Alex X
dbe9e4aade Update version to 1.9.7 2024-11-11 20:20:53 +03:00
Alex X
715be4dad0 Merge pull request #1450 from edenhaus/ffmpeg-codec-not-matched-error
Lower codec not matched error for ffmpeg to debug
2024-11-11 18:05:52 +03:00
Alex X
570b7d0d97 Code refactoring for #1450 2024-11-11 17:49:22 +03:00
Alex X
80ac0ab17f Merge pull request #1448 from edenhaus/imporve-codec-not-matched-error
Improve codec not matched error by including kind
2024-11-11 16:37:25 +03:00
Alex X
9ee8174d5f Code refactoring for #1448 2024-11-11 16:36:51 +03:00
Robert Resch
831aa03c9f Implement suggestion 2024-11-11 11:16:12 +01:00
Robert Resch
d372597bdb Lower codec not matched error for ffmpeg to debug 2024-11-11 09:27:21 +01:00
Alex X
172437b6fc Merge pull request #1449 from amarshall/credentials-dir
Read from credential files
2024-11-11 07:11:29 +03:00
Andrew Marshall
7640a42bfc Read from credential files
See https://systemd.io/CREDENTIALS/. This will also work for Docker
Secrets by setting `CREDENTIALS_DIRECTORY=/run/secrets`.
2024-11-10 17:33:22 -05:00
Robert Resch
fde04bd625 Improve codec not matched error by including kind 2024-11-10 19:27:59 +01:00
Alex X
ad14a5ccba Merge pull request #1447 from Jerome1998/patch-1
Updated Roborock part in the README.md file
2024-11-10 16:44:16 +03:00
Jerome
2348d12e9d Update README.md 2024-11-10 13:13:31 +01:00
Alex X
5cafc05e13 Merge pull request #1446 from eltociear/patch-1
docs: update README.md
2024-11-10 07:08:37 +03:00
Ikko Eltociear Ashimine
e982257271 docs: update README.md
shapshot -> snapshot
2024-11-10 09:00:37 +09:00
Alex X
340fd81778 Fix loop request, ex. camera1: ffmpeg:camera1 2024-11-09 18:17:41 +03:00
MrToan
223f94077f Fix "panic: send on closed channel" 2024-11-07 08:49:06 +07:00
seydx
f13aa21d0f Add backchannel support for rtsp server 2024-11-03 16:33:08 +01:00
Alex X
2c34a17d88 Fix stop for webrtc stream #1428 2024-11-02 20:50:33 +03:00
Alex X
6b005a666e Fix yet another broken SDP from CN cameras #1426 2024-11-01 12:09:44 +03:00
Alex X
1d1bcb0a63 Code refactoring for UnmarshalSDP 2024-11-01 12:08:06 +03:00
Alex X
3f5f1328e7 Fix webrtc:ws source after 1.9.5 #1425 2024-10-31 20:09:11 +03:00
Alex X
8cca8decde Update version to 1.9.6 2024-10-29 17:50:00 +03:00
Alex X
be5bbd3b9b Fix FFmpeg tests 2024-10-29 14:39:54 +03:00
Alex X
3f94a754e4 Fix WebRTC card stuck in loading #1417 2024-10-29 14:39:37 +03:00
Alex X
780f378fb1 Update version to 1.9.5 2024-10-28 22:47:55 +03:00
Alex X
b874c17bcb Update dependencies 2024-10-28 22:47:36 +03:00
Alex X
16e4831499 Add the option to pass ICE servers with an async WebRTC offer #1408 2024-10-24 23:31:21 +03:00
Alex X
9d709f0db8 Merge pull request #1407 from edenhaus/streams-api-multiple-sources
Extend streams API to allow multiple sources
2024-10-24 20:47:19 +03:00
Alex X
a8d394efd7 Update PUT /api/streams for support multiple src params 2024-10-24 20:46:31 +03:00
Robert Resch
95a5283c86 Extend streams API to allow multiple sources 2024-10-22 16:31:31 +02:00
Alex X
ef7d898747 Merge pull request #1355 from michelepra/concurrent_map_fix
data race for streams map
2024-09-27 21:11:18 +03:00
Michele Prà
388c408080 defer used wisely 2024-09-27 18:14:41 +02:00
Alex X
7b77e41253 Add support arm/v6 to Dockerfile 2024-09-22 07:24:25 +03:00
Alex X
c0bfebf3a4 Merge pull request #1362 from edenhaus/armhf
Build the docker image for linux/arm/v6
2024-09-20 13:50:02 +03:00
Robert Resch
6f9f1c3a35 Build the docker image for linux/arm/v6 2024-09-19 16:48:37 +02:00
Michele Prà
8128edad43 Update streams.go 2024-09-16 16:42:22 +02:00
Michele Prà
eb8a13d8c2 data race for streams map
https://go.dev/doc/articles/race_detector
2024-09-16 12:42:34 +02:00
Alex X
8399edce6a Fix RTSP AAC audio from very buggy noname camera #1328 2024-09-05 11:58:05 +03:00
Alex X
2311d5eabe Change go version to 1.20 for Windows 7 support 2024-09-01 17:54:01 +03:00
Alex X
afc8f4fdf6 Merge pull request #1297 from cthach/fix-nest-extend-stream
fix(nest): Resource leak due to lack of closing HTTP response bodies
2024-08-07 16:38:38 +03:00
Chris Thach
66de2f91b6 Fix resource leak in Nest source due to lack of closing HTTP response bodies 2024-08-06 22:25:55 +00:00
Alex X
bd88695e59 Fix AnnexB parsing in some cases 2024-08-04 10:18:24 +03:00
Sergey Krashevich
23e8f7e0aa refactor(api): move port extraction logic to Init function for prevent data race 2024-07-28 05:34:49 +03:00
Alex X
d559ec0208 Fix wrong media values in SDP for some cameras #1278 2024-07-26 17:00:16 +03:00
Alex X
ed99025bd6 Add support S16LE (PCM-LE) for RTSP server 2024-07-26 14:47:42 +03:00
Alex X
57d48f53e0 Fix PCM audio quality for WebRTC 2024-07-26 14:15:53 +03:00
Alex X
68fa42249e Fix PCM audio from Hikvision cameras 2024-07-26 14:01:43 +03:00
Alex X
c5bc761a52 Fix RTSP MJPEG source quality in some cases #559 2024-07-26 07:55:15 +03:00
Alex X
3762bdbccd Fix mjpeg source for Foscam G2 camera #1258 2024-07-18 13:52:50 +03:00
On Freund
c81caa4d2c Install ffplay in container 2024-07-17 13:37:56 +03:00
Jamal Fanaian
13dd3084c2 Carry protocol info in stream URL 2024-07-11 18:47:05 -07:00
Jamal Fanaian
e1021a96af go fmt 2024-07-11 17:58:31 -07:00
Jamal Fanaian
5b0781253f Add support for Nest cameras with RTSP 2024-07-11 17:54:04 -07:00
Rob van Oostenrijk
a04b7eed28 Update README.md 2024-06-22 19:23:14 +04:00
Rob van Oostenrijk
c47427633c Update build.sh 2024-06-22 19:20:39 +04:00
Rob van Oostenrijk
56e2c6650d Update build.cmd 2024-06-22 19:15:07 +04:00
Rob van Oostenrijk
82f0fb8a79 Merge branch 'AlexxIT:master' into master 2024-06-22 18:23:28 +04:00
Sergey Krashevich
0e5b293b1f fix(network): preserve selected nodes and edges on data reload 2024-06-19 12:20:04 +03:00
Alex X
eaae7aee39 Fix stream info for publishing RTMP 2024-06-19 06:53:27 +03:00
Alex X
a4885c2c3a Update version to 1.9.4 2024-06-18 21:33:36 +03:00
MarcA711
2b69eb2fd0 update FFmpeg presets for Rockchip boards 2024-06-18 17:56:04 +00:00
Alex X
f5aaee006e Merge pull request #1168 from skrashevich/fix-flags-daemon
fix(app): Refactor daemon initialization and add syscall import
2024-06-18 20:53:38 +03:00
Alex X
db6745e8ff Code refactoring after #1168 2024-06-18 20:35:17 +03:00
Alex X
ba34855602 Merge pull request #1196 from skrashevich/feat-network-dot-enhancements
refactor(webui): enhance network visualization in network.html
2024-06-16 22:24:11 +03:00
Alex X
e6fa97c738 Code refactoring after #1196 2024-06-16 22:12:52 +03:00
Sergey Krashevich
5b481a27c6 fix(network): enable autoResize in network settings 2024-06-16 21:57:48 +03:00
Alex X
bdc7ff1035 Fix forwarded remote_addr in the network 2024-06-16 19:04:34 +03:00
Alex X
da5f060741 Add killsignal and killtimeout to exec/rtsp 2024-06-16 19:03:57 +03:00
Alex X
a56d335380 Fix homekit producer remote_addr 2024-06-16 15:26:18 +03:00
Sergey Krashevich
d8aed552bc fix(network): ensure consistent node positions by storing and reusing seed 2024-06-16 15:22:33 +03:00
Alex X
d7286fa06e Merge pull request #1195 from skrashevich/fix-append-dot
fix(streams): handle missing codec_name in appendDOT function
2024-06-16 15:20:51 +03:00
Alex X
906f554d74 Code refactoring after #1195 2024-06-16 15:19:50 +03:00
Sergey Krashevich
cb44d5431a feat(network): preserve pan and scale on data reload 2024-06-16 15:01:40 +03:00
Sergey Krashevich
a69eb8a66e style(network): add flex-grow to network div and move script tag 2024-06-16 14:54:02 +03:00
Sergey Krashevich
1b411b1fed refactor(streams): optimize label generation with strings.Builder
feat(network): add periodic data fetching and network update
2024-06-16 10:19:17 +03:00
Sergey Krashevich
5d57959608 fix(streams): handle missing codec_name in appendDOT function 2024-06-16 08:59:06 +03:00
Alex X
31e57c2ff8 Fix errors output for webrtc client and server 2024-06-16 06:37:42 +03:00
Alex X
734393d638 Add streaming network visualisation 2024-06-16 06:36:24 +03:00
Alex X
96504e2fb0 BIG rewrite stream info 2024-06-16 06:20:45 +03:00
Alex X
ecfe802065 Code refactoring for streams HandleFunc 2024-06-14 12:52:55 +03:00
Alex X
1ac9d54dab Code refactoring for stream MarshalJSON 2024-06-10 16:42:34 +03:00
Sergey Krashevich
72d7e8aaaa refactor(app): remove syscall import and improve error messages 2024-06-08 15:05:26 +03:00
Alex X
0395696866 Fix exec pipe output 2024-06-07 17:59:21 +03:00
Alex X
0667683e4d Restore support old cipher suites after go1.22 #1172 2024-06-07 17:57:36 +03:00
Alex X
aca0781c4b Code refactoring for api/streams 2024-06-07 12:25:58 +03:00
Sergey Krashevich
ac798d9d6d fix(log): handle log file open error by writing to stdout 2024-06-06 19:07:09 +03:00
Sergey Krashevich
b389d0eb9c fix(app): handle daemon process correctly on Unix systems 2024-06-06 18:54:40 +03:00
Sergey Krashevich
e46fc13fea fix(log): ensure fallback to stdout if log file open fails 2024-06-06 18:25:30 +03:00
Sergey Krashevich
bce0b4a8a0 feat(logging): add file output option for logging configuration 2024-06-06 18:20:44 +03:00
Alex X
bf303ed471 Fix -d flag 2024-06-06 17:58:31 +03:00
Alex X
cd777ba2b4 Update version to 1.9.3 2024-06-06 16:01:01 +03:00
Alex X
e3188a0a6d Update docs about config 2024-06-06 15:21:32 +03:00
Alex X
2bab0a014d Update dependencies 2024-06-06 14:34:16 +03:00
Alex X
a01da18018 Merge pull request #1150 from skrashevich/go122
update Go version to 1.22
2024-06-06 14:25:27 +03:00
Alex X
9d5a5c1e45 Merge remote-tracking branch 'origin/master' 2024-06-06 14:15:20 +03:00
Alex X
8377ad1d05 Update codec section in stream info 2024-06-06 13:16:12 +03:00
Alex X
ec33796bd3 Add goweight to useful commands 2024-06-05 20:02:10 +03:00
Alex X
31e4ba2722 Rewrite Receiver/Sender classes 2024-06-05 20:01:47 +03:00
Alex X
e0b1a50356 Add rtsp_client for testing ghost exec process 2024-06-05 20:00:41 +03:00
Alex X
9bb36ebb6c Fix ghost exec/ffmpeg process 2024-06-05 19:59:22 +03:00
Alex X
756be9801e Code refactoring for app module 2024-06-02 07:00:29 +03:00
Alex X
bd73b07ed8 Merge pull request #1147 from skrashevich/docker-ghcr-repo
ci(workflow): add GitHub Container Registry
2024-05-31 07:22:47 +03:00
Sergey Krashevich
df1d44d24e chore(deps): update Go version to 1.22 across project files 2024-05-30 17:12:56 +03:00
Sergey Krashevich
79245eeff4 fix(ci): skip GitHub Container Registry login on pull requests 2024-05-30 11:48:15 +03:00
Sergey Krashevich
aa86c1ec25 ci(workflow): add GitHub Container Registry login and update image paths 2024-05-30 11:28:06 +03:00
Alex X
2ab1d9d774 Add handling if mp4 client drops connection 2024-05-29 17:32:18 +03:00
Alex X
a9e7a73cc8 Add video bitrate setting for HomeKit source 2024-05-28 22:57:43 +03:00
Alex X
ea17b420d6 Fix two-way audio for webrtc client 2024-05-28 21:36:12 +03:00
Alex X
660979dfda Merge pull request #1141 from skrashevich/feat-log-terminal-check
feat(logging): add interactive shell detection for console output
2024-05-28 13:26:14 +03:00
Alex X
a6b9b4993f Code refactoring after #1141 2024-05-28 13:21:33 +03:00
Sergey Krashevich
cc74504ed8 feat(shell): add Windows support for TTY detection 2024-05-28 10:19:51 +03:00
Sergey Krashevich
791239be12 Merge branch 'master' into feat-log-terminal-check 2024-05-28 09:15:01 +03:00
Sergey Krashevich
a79061c7c2 feat(logging): add interactive shell detection for console output 2024-05-28 09:10:51 +03:00
Alex X
50ad3b20c4 Add config schema.json 2024-05-28 09:08:57 +03:00
Alex X
649de0131c Change logs timestamp format in WebUI 2024-05-27 20:25:09 +03:00
Alex X
8cb513cb89 Add log level for ffmpeg module 2024-05-27 20:24:24 +03:00
Alex X
3932dbaa84 Add print exec stderr to logs for debug level 2024-05-27 20:23:55 +03:00
Alex X
4534b4d8ca Add more log customization options 2024-05-26 21:28:34 +03:00
Alex X
8e571a66e3 Code refactoring for debug packet logger 2024-05-26 00:19:26 +03:00
Alex X
0ccfcb0ec0 Fix timestamps for RTMP client 2024-05-26 00:18:56 +03:00
Alex X
8bae4631d2 Fix support some RTSP servers 2024-05-26 00:18:36 +03:00
Alex X
268629f551 Fix pix_fmt for publishing to RTMP servers 2024-05-25 19:45:29 +03:00
Alex X
0bd2fcde54 Update color index func for ascii stream 2024-05-25 13:52:55 +03:00
Alex X
6f34cf0c95 Add streaming to rawvideo format 2024-05-25 11:55:28 +03:00
Alex X
f8bc25d0ae Add support rawvideo format 2024-05-25 08:22:38 +03:00
Alex X
8749562c96 Fix detection webrtc without audio #1106 2024-05-24 20:41:46 +03:00
Alex X
d9d2bdff44 Add timeout query param to RTSP incoming source #1118 2024-05-24 16:26:06 +03:00
Felipe Santos
562046c278 Fix link to audio codec change tip 2024-05-24 10:12:34 -03:00
Felipe Santos
4cc28977cb Add note about unicast=true&proto=Onvif 2024-05-24 10:10:21 -03:00
Felipe Santos
3ce4624aee Add note about requesting multiple backchannel on Dahua Doorbell 2024-05-24 10:03:51 -03:00
Alex X
b3e9ed23ac Add /api/ffmpeg for playing files and tts on cameras with two-way audio 2024-05-24 15:57:18 +03:00
Alex X
bf3f81ccac Update ffmpeg pkg for reading files and parsing ffmpeg version 2024-05-24 11:06:51 +03:00
Alex X
ff39e2e496 Change import log for hass module from debug to trace 2024-05-24 11:04:26 +03:00
Alex X
d2346a2aed Fix FFmpeg producer codecs 2024-05-24 07:48:44 +03:00
Alex X
8f57b1acb6 Fix TTS template 2024-05-24 07:48:17 +03:00
Alex X
6fafd10482 Add stream source validation for dynamic streams 2024-05-23 17:40:27 +03:00
Alex X
c726651b8b Add ffmpeg version checker 2024-05-23 17:31:02 +03:00
Alex X
02af2e2849 Code refactoring for FFmpeg producer 2024-05-23 12:40:29 +03:00
Alex X
6d9c7012b0 Add output/aac for ffmpeg source 2024-05-23 12:24:41 +03:00
Alex X
8a7712a4c8 Add ffmpeg auto codec selection logic 2024-05-22 18:49:43 +03:00
Alex X
82fa803a37 Add ffmpeg virtual tests 2024-05-22 18:48:40 +03:00
Alex X
78a74da8d6 Fix aac.DecodeConfig sampleRate parsing 2024-05-22 18:46:30 +03:00
Alex X
53242ea02f Add ffmpeg tts source 2024-05-22 13:00:39 +03:00
Alex X
af05083a1f Code refactoring for ffmpeg device and virtual 2024-05-22 12:58:21 +03:00
Alex X
c41bddbbea Add using wav format for ffmpeg transcoding to PCMA/PCMU 2024-05-21 17:50:15 +03:00
Alex X
54c8ca0112 Add wav format to magic producer 2024-05-21 17:48:31 +03:00
Alex X
a518488289 Add debug logs for run RTSP pipe 2024-05-21 17:46:43 +03:00
Alex X
99cc21aacb Code refactoring for magic producer 2024-05-20 14:24:04 +03:00
Alex X
bc8295baee Improve play audio on RTSP backchannel 2024-05-19 11:56:33 +03:00
Alex X
50f9913c41 Add hls.html 2024-05-19 10:33:11 +03:00
Alex X
4c135b5a46 Add binaries to gitignore 2024-05-19 07:25:50 +03:00
Alex X
686fb374e9 Remove PCMU for two way for DVRIP source #1111 2024-05-18 17:14:55 +03:00
Alex X
2b3e6a2730 Merge pull request #1122 from isegals/master
Update client.go
2024-05-18 17:09:02 +03:00
Alex X
9143729042 Merge pull request #1123 from skrashevich/fix-vcs-tags-in-docker-builds
add git to build stage
2024-05-18 16:20:42 +03:00
Sergey Krashevich
3952f0ba0f add git to build stage 2024-05-18 13:47:02 +03:00
isegals
7a131822db Update client.go
Add  "AudioFormat":{"EncodeType":"G711_ALAW"} to suppoet new firmware
2024-05-18 11:14:16 +03:00
Alex X
b2399f3bb3 Update version to 1.9.2 2024-05-17 15:57:11 +03:00
Alex X
2a8a3f1cbf Merge pull request #1113 from skrashevich/ui-move-probe-link
Refactor probe link placement in UI
2024-05-17 15:52:52 +03:00
Alex X
b1ba5bab62 Update readme for ASCII 2024-05-17 14:51:55 +03:00
Alex X
6878f05e57 Fix ESC codes duplicates for ASCII stream 2024-05-17 14:34:24 +03:00
Alex X
d428a8964a Fix writers for MJPEG and ASCII 2024-05-17 14:32:59 +03:00
Alex X
f432e72dd0 Add support custom color for ascii streaming 2024-05-16 22:02:18 +03:00
Alex X
2929db9cec Fix w/h variables for ascii streaming 2024-05-16 22:01:57 +03:00
Alex X
6d967bc1f9 Improve ascii stream for any one symbol 2024-05-16 17:33:09 +03:00
Alex X
83c0053b2c Fix blinking for ASCII stream 2024-05-16 15:28:47 +03:00
Alex X
ecfd7404f5 Add UTF8 support for ASCII streaming 2024-05-16 14:01:43 +03:00
Alex X
41badbfb8e Add support streaming as ascii to terminal 2024-05-16 12:00:41 +03:00
Sergey Krashevich
0cb013a7fd Refactor probe link placement in UI
Moved the 'probe' link from the global templates array to individual
stream status columns for improved clarity and accessibility. This
change enhances the interface by contextualizing the 'probe' option,
making it directly accessible alongside each stream's online status and
info link, thereby streamlining the user experience and emphasizing the
function's importance on a per-stream basis. This adjustment follows
usability feedback indicating that users prefer immediate access to
stream diagnostics.
2024-05-15 12:41:30 +03:00
Alex X
75020d4df7 Add probe link to WebUI 2024-05-15 10:36:29 +03:00
Alex X
69c288b154 Fix codec name for probe producer 2024-05-15 10:31:43 +03:00
Alex X
0ea651db62 Fix links in the manifest.json 2024-05-15 10:23:25 +03:00
Alex X
4823e60a92 Add probe stream API #998 2024-05-15 07:44:18 +03:00
Alex X
c4949eb81f Add example about rpi5 cam to readme #1041 2024-05-15 05:36:28 +03:00
Alex X
aa4c81c266 Add pix_fmt to H265 transcoding string 2024-05-14 21:21:27 +03:00
Alex X
063fef5813 Add auto reconnect for broken MSE stream 2024-05-14 21:20:47 +03:00
Alex X
d9fb734c85 Fix stop pending producer on multiple mode requests 2024-05-14 19:31:42 +03:00
Alex X
a51156cf18 Add instant start for WebRTC consumer 2024-05-14 17:34:47 +03:00
Alex X
32e0ee4a10 Merge pull request #1071 from skrashevich/refactr-syscall-more-generic
refactor(sysctl): consolidate platform-specific syscall files
2024-05-13 19:00:10 +03:00
Alex X
e6bea97936 Merge pull request #1099 from skrashevich/add-favicon
feat(web): Add favicon
2024-05-13 18:23:58 +03:00
Alex X
9776e09ca7 Code refactoring after #1099 2024-05-13 18:22:35 +03:00
Alex X
ad273d3a98 Merge pull request #1098 from skrashevich/ci-docs-docker-tags-and-readme
ci+docs: docker images
2024-05-13 15:03:10 +03:00
Alex X
69c301e79f Remove docker examples from readme (move to dockerhub) 2024-05-13 15:02:39 +03:00
Alex X
8f2bb3f34b Add support key=value pair for cli config 2024-05-13 14:14:28 +03:00
Alex X
e4ff6d224f Update logo.gif 2024-05-13 13:29:44 +03:00
Alex X
00751459a2 Merge pull request #1107 from skrashevich/version-display-enhance
feat(version): Enhancements to Version Display Functionality
2024-05-13 12:45:22 +03:00
Alex X
874c07b887 Code refactoring for #1107 2024-05-13 12:42:55 +03:00
Alex X
152df3ef5d Fix pkt_size key name in json format 2024-05-13 07:18:48 +03:00
Alex X
c950bb0252 Update api.ws log messages 2024-05-13 07:00:51 +03:00
Sergey Krashevich
dd7ea2657a feat(app): enhance CLI with shorthand flags and dynamic versioning
- Added shorthand flag support for `config`, `daemon`, and `version`
- Implemented dynamic version string generation using build info
- Updated flag usage output to include shorthand options and help command
2024-05-12 22:10:58 +03:00
Alex X
5889791847 Merge pull request #1100 from skrashevich/upd-ace-1-33-1
upd(editor): upgrade Ace editor to version 1.33.1
2024-05-12 18:40:37 +03:00
Alex X
9160403b99 Fix device_id and device_private for HomeKit config 2024-05-12 15:59:50 +03:00
Alex X
5ccbd7c1c2 Rename param source to video for ffmpeg virtual source 2024-05-12 15:58:17 +03:00
Alex X
778245dd1c Set default max-bundle for video-rtc.js viewer 2024-05-12 15:56:56 +03:00
Alex X
205018c96a Improve WebRTC candidates handling 2024-05-12 15:55:32 +03:00
Sergey Krashevich
eaba451a47 refactor(app): streamline version info retrieval and formatting 2024-05-12 06:46:45 +03:00
Sergey Krashevich
b7c11db604 feat(version): enhance version command output with VCS revision and timestamp 2024-05-12 06:36:25 +03:00
Alex X
f7b98044e6 Fix Kasa KC200 cameras 2024-05-10 22:50:33 +03:00
Sergey Krashevich
1b1bdb37db feat(branding): prepend 'go2rtc -' to page titles in add, editor, and log pages 2024-05-09 12:06:46 +03:00
Sergey Krashevich
ab453d275e feat(editor): upgrade Ace editor to version 1.33.1 2024-05-09 11:30:26 +03:00
Alex X
ee387b79e1 Update version output 2024-05-09 08:21:19 +03:00
Sergey Krashevich
e71ed5e7eb feat(icons): add favicon and apple-touch-icon links across all pages
Added favicon, apple-touch-icon, and related meta tags to all HTML pages to ensure consistent branding and improve user experience on various platforms.
2024-05-09 06:30:14 +03:00
Sergey Krashevich
122a550599 feat(icons): add website icons and update GH Pages workflow to include icon changes 2024-05-09 06:25:33 +03:00
Sergey Krashevich
f3f08afac8 ci(build.yml): enable latest tag and onlatest for hardware suffix
This commit updates the GitHub Actions workflow to ensure that images built with a hardware suffix are tagged as 'latest'. Additionally, it modifies the README.md to enhance the documentation around the Docker container deployment, including basic and GPU-accelerated deployment instructions.
2024-05-09 06:07:50 +03:00
Alex X
a0030194cb Add gif logo 2024-05-08 13:04:59 +03:00
Sergey Krashevich
f158ffb33e Merge remote-tracking branch 'upstream/master' into refactr-syscall-more-generic 2024-05-07 14:45:48 +03:00
Rob van Oostenrijk
2b8ced9c59 Update build.yml 2024-05-06 08:06:08 +04:00
Sergey Krashevich
abe617a346 refactor(ffmpeg): generalize device and hardware support for multiple OS
- Rename `device_freebsd.go` to `device_bsd.go` and `hardware_freebsd.go` to `hardware_bsd.go` to reflect broader BSD support (FreeBSD, NetBSD, OpenBSD, Dragonfly).
- Update build tags in `device_bsd.go` and `hardware_bsd.go` to include FreeBSD, NetBSD, OpenBSD, and Dragonfly.
- Rename `device_linux.go` to `device_unix.go` and `hardware_linux.go` to `hardware_unix.go` to generalize Unix support excluding Darwin-based systems and BSDs.
- Add specific build tags to `device_darwin.go`, `device_unix.go`, `hardware_darwin.go`, and `hardware_unix.go` to correctly target their respective operating systems.
- Ensure Windows-specific files (`device_windows.go` and `hardware_windows.go`) are correctly tagged for building on Windows.
2024-05-01 09:04:19 +03:00
Sergey Krashevich
e080eac204 refactor(mdns): consolidate platform-specific syscall files
- Rename `syscall_linux.go` to `syscall.go` with build constraints for non-BSD and non-Windows platforms.
- Merge `syscall_darwin.go` into `syscall_bsd.go` and adjust build constraints for BSD platforms (Darwin, FreeBSD, OpenBSD).
- Remove redundant `syscall_freebsd.go`.
- Add build constraints to `syscall_windows.go` for Windows platform.
2024-04-30 01:55:51 +03:00
340 changed files with 16157 additions and 3995 deletions

View File

@@ -19,7 +19,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v5
with: { go-version: '1.21' }
with: { go-version: '1.24' }
- name: Build go2rtc_win64
env: { GOOS: windows, GOARCH: amd64 }
@@ -29,7 +29,7 @@ jobs:
with: { name: go2rtc_win64, path: go2rtc.exe }
- name: Build go2rtc_win32
env: { GOOS: windows, GOARCH: 386 }
env: { GOOS: windows, GOARCH: 386, GOTOOLCHAIN: go1.20.14 }
run: go build -ldflags "-s -w" -trimpath
- name: Upload go2rtc_win32
uses: actions/upload-artifact@v4
@@ -85,7 +85,7 @@ jobs:
with: { name: go2rtc_linux_mipsel, path: go2rtc }
- name: Build go2rtc_mac_amd64
env: { GOOS: darwin, GOARCH: amd64 }
env: { GOOS: darwin, GOARCH: amd64, GOTOOLCHAIN: go1.20.14 }
run: go build -ldflags "-s -w" -trimpath
- name: Upload go2rtc_mac_amd64
uses: actions/upload-artifact@v4
@@ -102,14 +102,14 @@ jobs:
env: { GOOS: freebsd, GOARCH: amd64 }
run: go build -ldflags "-s -w" -trimpath
- name: Upload go2rtc_freebsd_amd64
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with: { name: go2rtc_freebsd_amd64, path: go2rtc }
- name: Build go2rtc_freebsd_arm64
env: { GOOS: freebsd, GOARCH: arm64 }
run: go build -ldflags "-s -w" -trimpath
- name: Upload go2rtc_freebsd_arm64
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with: { name: go2rtc_freebsd_arm64, path: go2rtc }
docker-master:
@@ -123,7 +123,9 @@ jobs:
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ github.repository }}
images: |
${{ github.repository }}
ghcr.io/${{ github.repository }}
tags: |
type=ref,event=branch
type=semver,pattern={{version}},enable=false
@@ -142,13 +144,23 @@ jobs:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
file: docker/Dockerfile
platforms: |
linux/amd64
linux/386
linux/arm/v6
linux/arm/v7
linux/arm64/v8
push: ${{ github.event_name != 'pull_request' }}
@@ -168,10 +180,67 @@ jobs:
id: meta-hw
uses: docker/metadata-action@v5
with:
images: ${{ github.repository }}
images: |
${{ github.repository }}
ghcr.io/${{ github.repository }}
flavor: |
suffix=-hardware
latest=false
suffix=-hardware,onlatest=true
latest=auto
tags: |
type=ref,event=branch
type=semver,pattern={{version}},enable=false
type=match,pattern=v(.*),group=1
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to DockerHub
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
file: docker/hardware.Dockerfile
platforms: linux/amd64
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta-hw.outputs.tags }}
labels: ${{ steps.meta-hw.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
docker-rockchip:
name: Build docker rockchip
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Docker meta
id: meta-rk
uses: docker/metadata-action@v5
with:
images: |
${{ github.repository }}
ghcr.io/${{ github.repository }}
flavor: |
suffix=-rockchip,onlatest=true
latest=auto
tags: |
type=ref,event=branch
type=semver,pattern={{version}},enable=false
@@ -190,14 +259,22 @@ jobs:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
file: hardware.Dockerfile
platforms: linux/amd64
file: docker/rockchip.Dockerfile
platforms: linux/arm64
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta-hw.outputs.tags }}
labels: ${{ steps.meta-hw.outputs.labels }}
tags: ${{ steps.meta-rk.outputs.tags }}
labels: ${{ steps.meta-rk.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max

View File

@@ -26,7 +26,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: '1.21'
go-version: '1.24'
- name: Build Go binary
run: go build -ldflags "-s -w" -trimpath -o ./go2rtc
@@ -79,6 +79,7 @@ jobs:
uses: docker/build-push-action@v5
with:
context: .
file: docker/Dockerfile
platforms: linux/${{ matrix.platform }}
push: false
load: true
@@ -92,7 +93,7 @@ jobs:
uses: docker/build-push-action@v5
with:
context: .
file: hardware.Dockerfile
file: docker/hardware.Dockerfile
platforms: linux/amd64
push: false
load: true

5
.gitignore vendored
View File

@@ -4,6 +4,11 @@
go2rtc.yaml
go2rtc.json
go2rtc_freebsd*
go2rtc_linux*
go2rtc_mac*
go2rtc_win*
0_test.go
.DS_Store

View File

@@ -1,6 +1,6 @@
<h1 align="center">
![go2rtc](assets/logo.png)
![go2rtc](assets/logo.gif)
<br>
[![stars](https://img.shields.io/github/stars/AlexxIT/go2rtc?style=flat-square&logo=github)](https://github.com/AlexxIT/go2rtc/stargazers)
[![docker pulls](https://img.shields.io/docker/pulls/alexxit/go2rtc?style=flat-square&logo=docker&logoColor=white&label=pulls)](https://hub.docker.com/r/alexxit/go2rtc)
@@ -115,8 +115,8 @@ Ultimate camera streaming application with support RTSP, WebRTC, HomeKit, FFmpeg
Download binary for your OS from [latest release](https://github.com/AlexxIT/go2rtc/releases/):
- `go2rtc_win64.zip` - Windows 64-bit
- `go2rtc_win32.zip` - Windows 32-bit
- `go2rtc_win64.zip` - Windows 10+ 64-bit
- `go2rtc_win32.zip` - Windows 7+ 32-bit
- `go2rtc_win_arm64.zip` - Windows ARM 64-bit
- `go2rtc_linux_amd64` - Linux 64-bit
- `go2rtc_linux_i386` - Linux 32-bit
@@ -124,14 +124,16 @@ Download binary for your OS from [latest release](https://github.com/AlexxIT/go2
- `go2rtc_linux_arm` - Linux ARM 32-bit (ex. Raspberry 32-bit OS)
- `go2rtc_linux_armv6` - Linux ARMv6 (for old Raspberry 1 and Zero)
- `go2rtc_linux_mipsel` - Linux MIPS (ex. [Xiaomi Gateway 3](https://github.com/AlexxIT/XiaomiGateway3), [Wyze cameras](https://github.com/gtxaspec/wz_mini_hacks))
- `go2rtc_mac_amd64.zip` - Mac Intel 64-bit
- `go2rtc_mac_arm64.zip` - Mac ARM 64-bit
- `go2rtc_mac_amd64.zip` - macOS 10.13+ Intel 64-bit
- `go2rtc_mac_arm64.zip` - macOS ARM 64-bit
- `go2rtc_freebsd_amd64.zip` - FreeBSD 64-bit
- `go2rtc_freebsd_arm64.zip` - FreeBSD ARM 64-bit
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).
The Docker container [`alexxit/go2rtc`](https://hub.docker.com/r/alexxit/go2rtc) supports multiple architectures including `amd64`, `386`, `arm64`, and `arm`. This container offers the same functionality as the [Home Assistant Add-on](#go2rtc-home-assistant-add-on) but is designed to operate independently of Home Assistant. It comes preinstalled with [FFmpeg](#source-ffmpeg), [ngrok](#module-ngrok), and [Python](#source-echo).
### go2rtc: Home Assistant Add-on
@@ -170,7 +172,7 @@ Available modules:
- [api](#module-api) - HTTP API (important for WebRTC support)
- [rtsp](#module-rtsp) - RTSP Server (important for FFmpeg support)
- [webrtc](#module-webrtc) - WebRTC Server
- [mp4](#module-mp4) - MSE, MP4 stream and MP4 shapshot Server
- [mp4](#module-mp4) - MSE, MP4 stream and MP4 snapshot Server
- [hls](#module-hls) - HLS TS or fMP4 stream Server
- [mjpeg](#module-mjpeg) - MJPEG Server
- [ffmpeg](#source-ffmpeg) - FFmpeg integration
@@ -231,7 +233,7 @@ streams:
sonoff_camera: rtsp://rtsp:12345678@192.168.1.123/av_stream/ch0
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
- rtsp://admin:password@192.168.1.123/cam/realmonitor?channel=1&subtype=1#backchannel=0
amcrest_doorbell:
- rtsp://username:password@192.168.1.123:554/cam/realmonitor?channel=1&subtype=0#backchannel=0
unifi_camera: rtspx://192.168.1.123:7441/fD6ouM72bWoFijxK
@@ -241,7 +243,7 @@ streams:
**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)
- **Dahua Doorbell** users may want to change [audio codec](https://github.com/AlexxIT/go2rtc/issues/49#issuecomment-2127107379) for proper 2-way audio. Make sure not to request backchannel multiple times by adding `#backchannel=0` to other stream sources of the same doorbell. The `unicast=true&proto=Onvif` is preferred for 2-way audio as this makes the doorbell accept multiple codecs for the incoming audio
- **Reolink** users may want NOT to use RTSP protocol at all, some camera models have a very awful unusable stream implementation
- **Ubiquiti UniFi** users may want to disable HTTPS verification. Use `rtspx://` prefix instead of `rtsps://`. And don't use `?enableSrtp` [suffix](https://github.com/AlexxIT/go2rtc/issues/81)
- **TP-Link Tapo** users may skip login and password, because go2rtc support login [without them](https://drmnsamoliu.github.io/video.html)
@@ -350,7 +352,7 @@ streams:
mjpeg: ffmpeg:http://185.97.122.128/cgi-bin/faststream.jpg#video=h264
# [RTSP] video with rotation, should be transcoded, so select H264
rotate: ffmpeg:rtsp://rtsp:12345678@192.168.1.123/av_stream/ch0#video=h264#rotate=90
rotate: ffmpeg: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/internal/ffmpeg/ffmpeg.go): `h264`, `h265`, `opus`, `pcmu`, `pcmu/16000`, `pcmu/48000`, `pcma`, `pcma/16000`, `pcma/48000`, `aac`, `aac/16000`.
@@ -429,6 +431,7 @@ streams:
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 -
pi5cam_h264: exec:libcamera-vid -t 0 --libav-format h264 -o -
canon: exec:gphoto2 --capture-movie --stdout#killsignal=2#killtimeout=5
play_pcma: exec:ffplay -fflags nobuffer -f alaw -ar 8000 -i -#backchannel=1
play_pcm48k: exec:ffplay -fflags nobuffer -f s16be -ar 48000 -i -#backchannel=1
@@ -552,11 +555,16 @@ echo -n "cloud password" | shasum -a 256 | awk '{print toupper($0)}'
[TP-Link Kasa](https://www.kasasmart.com/) non-standard protocol [more info](https://medium.com/@hu3vjeen/reverse-engineering-tp-link-kc100-bac4641bf1cd).
- `username` - urlsafe email, `alex@gmail.com` -> `alex%40gmail.com`
- `password` - base64password, `secret1` -> `c2VjcmV0MQ==`
```yaml
streams:
kasa: kasa://user:pass@192.168.1.123:19443/https/stream/mixed
kc401: kasa://username:password@192.168.1.123:19443/https/stream/mixed
```
Tested: KD110, KC200, KC401, KC420WS, EC71.
#### Source: GoPro
*[New in v1.8.3](https://github.com/AlexxIT/go2rtc/releases/tag/v1.8.3)*
@@ -642,10 +650,11 @@ 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
- Roborock Qrevo 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.
Source support load Roborock credentials from Home Assistant [custom integration](https://github.com/humbertogontijo/homeassistant-roborock) or the [core integration](https://www.home-assistant.io/integrations/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.
If you have graphic pin for your vacuum - add it as numeric pin (lines: 123, 456, 789) to the end of the roborock-link.
#### Source: WebRTC
@@ -673,13 +682,18 @@ Supports connection to [Wyze](https://www.wyze.com/) cameras, using WebRTC proto
Supports [Amazon Kinesis Video Streams](https://aws.amazon.com/kinesis/video-streams/), using WebRTC protocol. You need to specify signalling WebSocket URL with all credentials in query params, `client_id` and `ice_servers` list in [JSON format](https://developer.mozilla.org/en-US/docs/Web/API/RTCIceServer).
**switchbot**
Support connection to [SwitchBot](https://us.switch-bot.com/) cameras that are based on Kinesis Video Streams. Specifically, this includes [Pan/Tilt Cam Plus 2K](https://us.switch-bot.com/pages/switchbot-pan-tilt-cam-plus-2k) and [Pan/Tilt Cam Plus 3K](https://us.switch-bot.com/pages/switchbot-pan-tilt-cam-plus-3k). `Outdoor Spotlight Cam 1080P`, `Outdoor Spotlight Cam 2K`, `Pan/Tilt Cam`, `Pan/Tilt Cam 2K`, `Indoor Cam` are based on Tuya, so this feature is not available.
```yaml
streams:
webrtc-whep: webrtc:http://192.168.1.123:1984/api/webrtc?src=camera1
webrtc-go2rtc: webrtc:ws://192.168.1.123:1984/api/ws?src=camera1
webrtc-openipc: webrtc:ws://192.168.1.123/webrtc_ws#format=openipc#ice_servers=[{"urls":"stun:stun.kinesisvideo.eu-north-1.amazonaws.com:443"}]
webrtc-wyze: webrtc:http://192.168.1.123:5000/signaling/camera1?kvs#format=wyze
webrtc-kinesis: webrtc:wss://...amazonaws.com/?...#format=kinesis#client_id=...#ice_servers=[{...},{...}]
webrtc-whep: webrtc:http://192.168.1.123:1984/api/webrtc?src=camera1
webrtc-go2rtc: webrtc:ws://192.168.1.123:1984/api/ws?src=camera1
webrtc-openipc: webrtc:ws://192.168.1.123/webrtc_ws#format=openipc#ice_servers=[{"urls":"stun:stun.kinesisvideo.eu-north-1.amazonaws.com:443"}]
webrtc-wyze: webrtc:http://192.168.1.123:5000/signaling/camera1?kvs#format=wyze
webrtc-kinesis: webrtc:wss://...amazonaws.com/?...#format=kinesis#client_id=...#ice_servers=[{...},{...}]
webrtc-switchbot: webrtc:wss://...amazonaws.com/?...#format=switchbot#resolution=hd#client_id=...#ice_servers=[{...},{...}]
```
**PS.** For `kinesis` sources you can use [echo](#source-echo) to get connection params using `bash`/`python` or any other script language.
@@ -773,7 +787,7 @@ POST http://localhost:1984/api/streams?dst=camera1&src=ffmpeg:http://example.com
You can publish any stream to streaming services (YouTube, Telegram, etc.) via RTMP/RTMPS. Important:
- Supported codecs: H264 for video and AAC for audio
- Pixel format should be `yuv420p`, for cameras with `yuvj420p` format you SHOULD use [transcoding](#source-ffmpeg)
- AAC audio is required for YouTube, videos without audio will not work
- You don't need to enable [RTMP module](#module-rtmp) listening for this task
You can use API:
@@ -786,16 +800,19 @@ Or config file:
```yaml
publish:
# publish stream "tplink_tapo" to Telegram
tplink_tapo: rtmps://xxx-x.rtmp.t.me/s/xxxxxxxxxx:xxxxxxxxxxxxxxxxxxxxxx
# publish stream "other_camera" to Telegram and YouTube
other_camera:
# publish stream "video_audio_transcode" to Telegram
video_audio_transcode:
- rtmps://xxx-x.rtmp.t.me/s/xxxxxxxxxx:xxxxxxxxxxxxxxxxxxxxxx
- rtmps://xxx.rtmp.youtube.com/live2/xxxx-xxxx-xxxx-xxxx-xxxx
# publish stream "audio_transcode" to Telegram and YouTube
audio_transcode:
- rtmps://xxx-x.rtmp.t.me/s/xxxxxxxxxx:xxxxxxxxxxxxxxxxxxxxxx
- rtmp://xxx.rtmp.youtube.com/live2/xxxx-xxxx-xxxx-xxxx-xxxx
streams:
# for TP-Link cameras it's important to use transcoding because of wrong pixel format
tplink_tapo: ffmpeg:rtsp://user:pass@192.168.1.123/stream1#video=h264#hardware#audio=aac
video_audio_transcode:
- ffmpeg:rtsp://user:pass@192.168.1.123/stream1#video=h264#hardware#audio=aac
audio_transcode:
- ffmpeg:rtsp://user:pass@192.168.1.123/stream1#video=copy#audio=aac
```
- **Telegram Desktop App** > Any public or private channel or group (where you admin) > Live stream > Start with... > Start streaming.
@@ -871,7 +888,7 @@ Read more about [codecs filters](#codecs-filters).
You can get any stream as RTMP-stream: `rtmp://192.168.1.123/{stream_name}`. Only H264/AAC codecs supported right now.
[Incoming stream](#incoming-sources) in RTMP-format tested only with [OBS Studio](https://obsproject.com/) and Dahua camera. Different FFmpeg versions has differnt problems with this format.
[Incoming stream](#incoming-sources) in RTMP-format tested only with [OBS Studio](https://obsproject.com/) and Dahua camera. Different FFmpeg versions has different problems with this format.
```yaml
rtmp:
@@ -1187,6 +1204,10 @@ API examples:
- You can use `rotate` param with `90`, `180`, `270` or `-90` values
- You can use `hardware`/`hw` param [read more](https://github.com/AlexxIT/go2rtc/wiki/Hardware-acceleration)
**PS.** This module also supports streaming to the server console (terminal) in the **animated ASCII art** format ([read more](https://github.com/AlexxIT/go2rtc/blob/master/internal/mjpeg/README.md)):
[![](https://img.youtube.com/vi/sHj_3h_sX7M/mqdefault.jpg)](https://www.youtube.com/watch?v=sHj_3h_sX7M)
### Module: Log
You can set different log levels for different modules.

BIN
assets/logo.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 KiB

View File

@@ -2,11 +2,7 @@
# 0. Prepare images
ARG PYTHON_VERSION="3.11"
ARG GO_VERSION="1.21"
ARG NGROK_VERSION="3"
FROM python:${PYTHON_VERSION}-alpine AS base
FROM ngrok/ngrok:${NGROK_VERSION}-alpine AS ngrok
ARG GO_VERSION="1.24"
# 1. Build go2rtc binary
@@ -20,6 +16,8 @@ ENV GOARCH=${TARGETARCH}
WORKDIR /build
RUN apk add git
# Cache dependencies
COPY go.mod go.sum ./
RUN --mount=type=cache,target=/root/.cache/go-build go mod download
@@ -28,21 +26,14 @@ COPY . .
RUN --mount=type=cache,target=/root/.cache/go-build CGO_ENABLED=0 go build -ldflags "-s -w" -trimpath
# 2. Collect all files
FROM scratch AS rootfs
COPY --from=build /build/go2rtc /usr/local/bin/
COPY --from=ngrok /bin/ngrok /usr/local/bin/
# 3. Final image
FROM base
# 2. Final image
FROM python:${PYTHON_VERSION}-alpine AS base
# Install ffmpeg, tini (for signal handling),
# and other common tools for the echo source.
# alsa-plugins-pulse for ALSA support (+0MB)
# font-droid for FFmpeg drawtext filter (+2MB)
RUN apk add --no-cache tini ffmpeg bash curl jq alsa-plugins-pulse font-droid
RUN apk add --no-cache tini ffmpeg ffplay bash curl jq alsa-plugins-pulse font-droid
# Hardware Acceleration for Intel CPU (+50MB)
ARG TARGETARCH
@@ -54,7 +45,7 @@ RUN if [ "${TARGETARCH}" = "amd64" ]; then apk add --no-cache libva-intel-driver
# Hardware: AMD and NVidia VDPAU (not sure about this)
# RUN libva-vdpau-driver mesa-vdpau-gallium (+150MB total)
COPY --from=rootfs / /
COPY --from=build /build/go2rtc /usr/local/bin/
ENTRYPOINT ["/sbin/tini", "--"]
VOLUME /config

50
docker/README.md Normal file
View File

@@ -0,0 +1,50 @@
## Versions
- `alexxit/go2rtc:latest` - latest release based on `alpine` (`amd64`, `386`, `arm/v6`, `arm/v7`, `arm64`) with support hardware transcoding for Intel iGPU and Raspberry
- `alexxit/go2rtc:latest-hardware` - latest release based on `debian 13` (`amd64`) with support hardware transcoding for Intel iGPU, AMD GPU and NVidia GPU
- `alexxit/go2rtc:latest-rockchip` - latest release based on `debian 12` (`arm64`) with support hardware transcoding for Rockchip RK35xx
- `alexxit/go2rtc:master` - latest unstable version based on `alpine`
- `alexxit/go2rtc:master-hardware` - latest unstable version based on `debian 13` (`amd64`)
- `alexxit/go2rtc:master-rockchip` - latest unstable version based on `debian 12` (`arm64`)
## Docker compose
```yaml
services:
go2rtc:
image: alexxit/go2rtc
network_mode: host # important for WebRTC, HomeKit, UDP cameras
privileged: true # only for FFmpeg hardware transcoding
restart: unless-stopped # autorestart on fail or config change from WebUI
environment:
- TZ=Atlantic/Bermuda # timezone in logs
volumes:
- "~/go2rtc:/config" # folder for go2rtc.yaml file (edit from WebUI)
```
## Basic Deployment
```bash
docker run -d \
--name go2rtc \
--network host \
--privileged \
--restart unless-stopped \
-e TZ=Atlantic/Bermuda \
-v ~/go2rtc:/config \
alexxit/go2rtc
```
## Deployment with GPU Acceleration
```bash
docker run -d \
--name go2rtc \
--network host \
--privileged \
--restart unless-stopped \
-e TZ=Atlantic/Bermuda \
--gpus all \
-v ~/go2rtc:/config \
alexxit/go2rtc:latest-hardware
```

View File

@@ -4,16 +4,11 @@
# only debian 13 (trixie) has latest ffmpeg
# https://packages.debian.org/trixie/ffmpeg
ARG DEBIAN_VERSION="trixie-slim"
ARG GO_VERSION="1.21-bookworm"
ARG NGROK_VERSION="3"
FROM debian:${DEBIAN_VERSION} AS base
FROM golang:${GO_VERSION} AS go
FROM ngrok/ngrok:${NGROK_VERSION} AS ngrok
ARG GO_VERSION="1.24-bookworm"
# 1. Build go2rtc binary
FROM --platform=$BUILDPLATFORM go AS build
FROM --platform=$BUILDPLATFORM golang:${GO_VERSION} AS build
ARG TARGETPLATFORM
ARG TARGETOS
ARG TARGETARCH
@@ -31,34 +26,28 @@ COPY . .
RUN --mount=type=cache,target=/root/.cache/go-build CGO_ENABLED=0 go build -ldflags "-s -w" -trimpath
# 2. Collect all files
FROM scratch AS rootfs
# 2. Final image
FROM debian:${DEBIAN_VERSION}
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),
# Install ffmpeg, 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)
# mesa-va-drivers for AMD APU
# 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 trixie non-free' > /etc/apt/sources.list.d/debian-non-free.list && \
apt-get -y update && apt-get -y install tini ffmpeg \
apt-get -y update && apt-get -y install ffmpeg tini \
python3 curl jq \
intel-media-va-driver-non-free \
mesa-va-drivers \
libasound2-plugins && \
apt-get clean && rm -rf /var/lib/apt/lists/*
COPY --link --from=rootfs / /
COPY --from=build /build/go2rtc /usr/local/bin/
ENTRYPOINT ["/usr/bin/tini", "--"]
VOLUME /config

View File

@@ -0,0 +1,50 @@
# syntax=docker/dockerfile:labs
# 0. Prepare images
ARG PYTHON_VERSION="3.13-slim-bookworm"
ARG GO_VERSION="1.24-bookworm"
# 1. Build go2rtc binary
FROM --platform=$BUILDPLATFORM golang:${GO_VERSION} AS build
ARG TARGETPLATFORM
ARG TARGETOS
ARG TARGETARCH
ENV GOOS=${TARGETOS}
ENV GOARCH=${TARGETARCH}
WORKDIR /build
# Cache dependencies
COPY go.mod go.sum ./
RUN --mount=type=cache,target=/root/.cache/go-build go mod download
COPY . .
RUN --mount=type=cache,target=/root/.cache/go-build CGO_ENABLED=0 go build -ldflags "-s -w" -trimpath
# 2. Final image
FROM python:${PYTHON_VERSION}
# 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, tini (for signal handling),
# and other common tools for the echo source.
# libasound2-plugins for ALSA support
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked --mount=type=cache,target=/var/lib/apt,sharing=locked \
apt-get -y update && apt-get -y install tini \
curl jq \
libasound2-plugins && \
apt-get clean && rm -rf /var/lib/apt/lists/*
COPY --from=build /build/go2rtc /usr/local/bin/
ADD --chmod=755 https://github.com/MarcA711/Rockchip-FFmpeg-Builds/releases/download/6.1-8-no_extra_dump/ffmpeg /usr/local/bin
ENTRYPOINT ["/usr/bin/tini", "--"]
VOLUME /config
WORKDIR /config
CMD ["go2rtc", "-config", "/config/go2rtc.yaml"]

View File

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

View File

@@ -54,7 +54,7 @@ var chars = map[string]string{
"21C": "Third Party Camera Active",
"21D": "Camera Operating Mode Indicator",
"11B": "Night Vision",
"129": "Supported Data Stream Transport Configuration",
//"129": "Supported Data Stream Transport Configuration",
"37": "Version",
"131": "Setup Data Stream Transport",
"130": "Supported Data Stream Transport Configuration",

View File

@@ -0,0 +1,5 @@
## Example
```shell
go run examples/onvif_client/main.go http://admin:password@192.168.10.90 GetAudioEncoderConfigurations
```

View File

@@ -0,0 +1,75 @@
package main
import (
"log"
"net/url"
"os"
"github.com/AlexxIT/go2rtc/pkg/onvif"
)
func main() {
var rawURL = os.Args[1]
var operation = os.Args[2]
var token string
if len(os.Args) > 3 {
token = os.Args[3]
}
client, err := onvif.NewClient(rawURL)
if err != nil {
log.Panic(err)
}
var b []byte
switch operation {
case onvif.ServiceGetServiceCapabilities:
b, err = client.MediaRequest(operation)
case onvif.DeviceGetCapabilities,
onvif.DeviceGetDeviceInformation,
onvif.DeviceGetDiscoveryMode,
onvif.DeviceGetDNS,
onvif.DeviceGetHostname,
onvif.DeviceGetNetworkDefaultGateway,
onvif.DeviceGetNetworkInterfaces,
onvif.DeviceGetNetworkProtocols,
onvif.DeviceGetNTP,
onvif.DeviceGetScopes,
onvif.DeviceGetServices,
onvif.DeviceGetSystemDateAndTime,
onvif.DeviceSystemReboot:
b, err = client.DeviceRequest(operation)
case onvif.MediaGetProfiles,
onvif.MediaGetVideoEncoderConfigurations,
onvif.MediaGetVideoSources,
onvif.MediaGetVideoSourceConfigurations,
onvif.MediaGetAudioEncoderConfigurations,
onvif.MediaGetAudioSources,
onvif.MediaGetAudioSourceConfigurations:
b, err = client.MediaRequest(operation)
case onvif.MediaGetProfile:
b, err = client.GetProfile(token)
case onvif.MediaGetVideoSourceConfiguration:
b, err = client.GetVideoSourceConfiguration(token)
case onvif.MediaGetStreamUri:
b, err = client.GetStreamUri(token)
case onvif.MediaGetSnapshotUri:
b, err = client.GetSnapshotUri(token)
default:
log.Printf("unknown action\n")
}
if err != nil {
log.Printf("%s\n", err)
}
u, err := url.Parse(rawURL)
if err != nil {
log.Fatal(err)
}
if err = os.WriteFile(u.Hostname()+"_"+operation+".xml", b, 0644); err != nil {
log.Printf("%s\n", err)
}
}

View File

@@ -0,0 +1,39 @@
package main
import (
"log"
"os"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/rtsp"
"github.com/AlexxIT/go2rtc/pkg/shell"
)
func main() {
client := rtsp.NewClient(os.Args[1])
if err := client.Dial(); err != nil {
log.Panic(err)
}
client.Medias = []*core.Media{
{
Kind: core.KindAudio,
Direction: core.DirectionRecvonly,
Codecs: []*core.Codec{
{Name: core.CodecPCMU, ClockRate: 8000},
},
ID: "streamid=0",
},
}
if err := client.Announce(); err != nil {
log.Panic(err)
}
if _, err := client.SetupMedia(client.Medias[0]); err != nil {
log.Panic(err)
}
if err := client.Record(); err != nil {
log.Panic(err)
}
shell.RunUntilSignal()
}

65
go.mod
View File

@@ -1,48 +1,49 @@
module github.com/AlexxIT/go2rtc
go 1.21
go 1.20
require (
github.com/asticode/go-astits v1.13.0
github.com/expr-lang/expr v1.16.5
github.com/gorilla/websocket v1.5.1
github.com/miekg/dns v1.1.59
github.com/pion/ice/v2 v2.3.24
github.com/pion/interceptor v0.1.29
github.com/pion/rtcp v1.2.14
github.com/pion/rtp v1.8.6
github.com/pion/sdp/v3 v3.0.9
github.com/pion/srtp/v2 v2.0.18
github.com/pion/stun v0.6.1
github.com/pion/webrtc/v3 v3.2.40
github.com/rs/zerolog v1.32.0
github.com/expr-lang/expr v1.17.2
github.com/google/uuid v1.6.0
github.com/gorilla/websocket v1.5.3
github.com/mattn/go-isatty v0.0.20
github.com/miekg/dns v1.1.63
github.com/pion/ice/v4 v4.0.9
github.com/pion/interceptor v0.1.37
github.com/pion/rtcp v1.2.15
github.com/pion/rtp v1.8.13
github.com/pion/sdp/v3 v3.0.11
github.com/pion/srtp/v3 v3.0.4
github.com/pion/stun/v3 v3.0.0
github.com/pion/webrtc/v4 v4.0.14
github.com/rs/zerolog v1.34.0
github.com/sigurn/crc16 v0.0.0-20240131213347-83fcde1e29d1
github.com/sigurn/crc8 v0.0.0-20220107193325-2243fe600f9f
github.com/stretchr/testify v1.9.0
github.com/stretchr/testify v1.10.0
github.com/tadglines/go-pkgs v0.0.0-20210623144937-b983b20f54f9
golang.org/x/crypto v0.23.0
golang.org/x/crypto v0.33.0
gopkg.in/yaml.v3 v3.0.1
)
require (
github.com/asticode/go-astikit v0.30.0 // indirect
github.com/asticode/go-astikit v0.54.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/kr/pretty v0.2.1 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/pion/datachannel v1.5.6 // indirect
github.com/pion/dtls/v2 v2.2.11 // indirect
github.com/pion/logging v0.2.2 // indirect
github.com/pion/mdns v0.0.12 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/pion/datachannel v1.5.10 // indirect
github.com/pion/dtls/v3 v3.0.6 // indirect
github.com/pion/logging v0.2.3 // indirect
github.com/pion/mdns/v2 v2.0.7 // indirect
github.com/pion/randutil v0.1.0 // indirect
github.com/pion/sctp v1.8.16 // indirect
github.com/pion/transport/v2 v2.2.5 // indirect
github.com/pion/turn/v2 v2.1.6 // indirect
github.com/pion/sctp v1.8.37 // indirect
github.com/pion/transport/v3 v3.0.7 // indirect
github.com/pion/turn/v4 v4.0.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
golang.org/x/mod v0.17.0 // indirect
golang.org/x/net v0.25.0 // indirect
golang.org/x/sync v0.7.0 // indirect
golang.org/x/sys v0.20.0 // indirect
golang.org/x/tools v0.20.0 // indirect
github.com/wlynxg/anet v0.0.5 // indirect
golang.org/x/mod v0.20.0 // indirect
golang.org/x/net v0.35.0 // indirect
golang.org/x/sync v0.11.0 // indirect
golang.org/x/sys v0.30.0 // indirect
golang.org/x/tools v0.24.0 // indirect
)

224
go.sum
View File

@@ -1,194 +1,104 @@
github.com/asticode/go-astikit v0.30.0 h1:DkBkRQRIxYcknlaU7W7ksNfn4gMFsB0tqMJflxkRsZA=
github.com/asticode/go-astikit v0.30.0/go.mod h1:h4ly7idim1tNhaVkdVBeXQZEE3L0xblP7fCWbgwipF0=
github.com/asticode/go-astikit v0.54.0 h1:uq9eurgisdkYwJU9vSWIQaPH4MH0cac82sQH00kmSNQ=
github.com/asticode/go-astikit v0.54.0/go.mod h1:fV43j20UZYfXzP9oBn33udkvCvDvCDhzjVqoLFuuYZE=
github.com/asticode/go-astits v1.13.0 h1:XOgkaadfZODnyZRR5Y0/DWkA9vrkLLPLeeOvDwfKZ1c=
github.com/asticode/go-astits v1.13.0/go.mod h1:QSHmknZ51pf6KJdHKZHJTLlMegIrhega3LPWz3ND/iI=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/expr-lang/expr v1.16.5 h1:m2hvtguFeVaVNTHj8L7BoAyt7O0PAIBaSVbjdHgRXMs=
github.com/expr-lang/expr v1.16.5/go.mod h1:uCkhfG+x7fcZ5A5sXHKuQ07jGZRl6J0FCAaf2k4PtVQ=
github.com/expr-lang/expr v1.17.2 h1:o0A99O/Px+/DTjEnQiodAgOIK9PPxL8DtXhBRKC+Iso=
github.com/expr-lang/expr v1.17.2/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/miekg/dns v1.1.59 h1:C9EXc/UToRwKLhK5wKU/I4QVsBUc8kE6MkHBkeypWZs=
github.com/miekg/dns v1.1.59/go.mod h1:nZpewl5p6IvctfgrckopVx2OlSEHPRO/U4SYkRklrEk=
github.com/pion/datachannel v1.5.6 h1:1IxKJntfSlYkpUj8LlYRSWpYiTTC02nUrOE8T3DqGeg=
github.com/pion/datachannel v1.5.6/go.mod h1:1eKT6Q85pRnr2mHiWHxJwO50SfZRtWHTsNIVb/NfGW4=
github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s=
github.com/pion/dtls/v2 v2.2.10 h1:u2Axk+FyIR1VFTPurktB+1zoEPGIW3bmyj3LEFrXjAA=
github.com/pion/dtls/v2 v2.2.10/go.mod h1:d9SYc9fch0CqK90mRk1dC7AkzzpwJj6u2GU3u+9pqFE=
github.com/pion/dtls/v2 v2.2.11 h1:9U/dpCYl1ySttROPWJgqWKEylUdT0fXp/xst6JwY5Ks=
github.com/pion/dtls/v2 v2.2.11/go.mod h1:d9SYc9fch0CqK90mRk1dC7AkzzpwJj6u2GU3u+9pqFE=
github.com/pion/ice/v2 v2.3.19 h1:1GoMRTMnB6bCP4aGy2MjxK3w4laDkk+m7svJb/eqybc=
github.com/pion/ice/v2 v2.3.19/go.mod h1:KXJJcZK7E8WzrBEYnV4UtqEZsGeWfHxsNqhVcVvgjxw=
github.com/pion/ice/v2 v2.3.24 h1:RYgzhH/u5lH0XO+ABatVKCtRd+4U1GEaCXSMjNr13tI=
github.com/pion/ice/v2 v2.3.24/go.mod h1:KXJJcZK7E8WzrBEYnV4UtqEZsGeWfHxsNqhVcVvgjxw=
github.com/pion/interceptor v0.1.29 h1:39fsnlP1U8gw2JzOFWdfCU82vHvhW9o0rZnZF56wF+M=
github.com/pion/interceptor v0.1.29/go.mod h1:ri+LGNjRUc5xUNtDEPzfdkmSqISixVTBF/z/Zms/6T4=
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.12 h1:CiMYlY+O0azojWDmxdNr7ADGrnZ+V6Ilfner+6mSVK8=
github.com/pion/mdns v0.0.12/go.mod h1:VExJjv8to/6Wqm1FXK+Ii/Z9tsVk/F5sD/N70cnYFbk=
github.com/miekg/dns v1.1.63 h1:8M5aAw6OMZfFXTT7K5V0Eu5YiiL8l7nUAkyN6C9YwaY=
github.com/miekg/dns v1.1.63/go.mod h1:6NGHfjhpmr5lt3XPLuyfDJi5AXbNIPM9PY6H6sF1Nfs=
github.com/pion/datachannel v1.5.10 h1:ly0Q26K1i6ZkGf42W7D4hQYR90pZwzFOjTq5AuCKk4o=
github.com/pion/datachannel v1.5.10/go.mod h1:p/jJfC9arb29W7WrxyKbepTU20CFgyx5oLo8Rs4Py/M=
github.com/pion/dtls/v3 v3.0.6 h1:7Hkd8WhAJNbRgq9RgdNh1aaWlZlGpYTzdqjy9x9sK2E=
github.com/pion/dtls/v3 v3.0.6/go.mod h1:iJxNQ3Uhn1NZWOMWlLxEEHAN5yX7GyPvvKw04v9bzYU=
github.com/pion/ice/v4 v4.0.9 h1:VKgU4MwA2LUDVLq+WBkpEHTcAb8c5iCvFMECeuPOZNk=
github.com/pion/ice/v4 v4.0.9/go.mod h1:y3M18aPhIxLlcO/4dn9X8LzLLSma84cx6emMSu14FGw=
github.com/pion/interceptor v0.1.37 h1:aRA8Zpab/wE7/c0O3fh1PqY0AJI3fCSEM5lRWJVorwI=
github.com/pion/interceptor v0.1.37/go.mod h1:JzxbJ4umVTlZAf+/utHzNesY8tmRkM2lVmkS82TTj8Y=
github.com/pion/logging v0.2.3 h1:gHuf0zpoh1GW67Nr6Gj4cv5Z9ZscU7g/EaoC/Ke/igI=
github.com/pion/logging v0.2.3/go.mod h1:z8YfknkquMe1csOrxK5kc+5/ZPAzMxbKLX5aXpbpC90=
github.com/pion/mdns/v2 v2.0.7 h1:c9kM8ewCgjslaAmicYMFQIde2H9/lrZpjBkN8VwoVtM=
github.com/pion/mdns/v2 v2.0.7/go.mod h1:vAdSYNAT0Jy3Ru0zl2YiW3Rm/fJCwIeM0nToenfOJKA=
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.12/go.mod h1:sn6qjxvnwyAkkPzPULIbVqSKI5Dv54Rv7VG0kNxh9L4=
github.com/pion/rtcp v1.2.14 h1:KCkGV3vJ+4DAJmvP0vaQShsb0xkRfWkO540Gy102KyE=
github.com/pion/rtcp v1.2.14/go.mod h1:sn6qjxvnwyAkkPzPULIbVqSKI5Dv54Rv7VG0kNxh9L4=
github.com/pion/rtp v1.8.3/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU=
github.com/pion/rtp v1.8.6 h1:MTmn/b0aWWsAzux2AmP8WGllusBVw4NPYPVFFd7jUPw=
github.com/pion/rtp v1.8.6/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU=
github.com/pion/sctp v1.8.13/go.mod h1:YKSgO/bO/6aOMP9LCie1DuD7m+GamiK2yIiPM6vH+GA=
github.com/pion/sctp v1.8.16 h1:PKrMs+o9EMLRvFfXq59WFsC+V8mN1wnKzqrv+3D/gYY=
github.com/pion/sctp v1.8.16/go.mod h1:P6PbDVA++OJMrVNg2AL3XtYHV4uD6dvfyOovCgMs0PE=
github.com/pion/sdp/v3 v3.0.9 h1:pX++dCHoHUwq43kuwf3PyJfHlwIj4hXA7Vrifiq0IJY=
github.com/pion/sdp/v3 v3.0.9/go.mod h1:B5xmvENq5IXJimIO4zfp6LAe1fD9N+kFv+V/1lOdz8M=
github.com/pion/srtp/v2 v2.0.18 h1:vKpAXfawO9RtTRKZJbG4y0v1b11NZxQnxRl85kGuUlo=
github.com/pion/srtp/v2 v2.0.18/go.mod h1:0KJQjA99A6/a0DOVTu1PhDSw0CXF2jTkqOoMg3ODqdA=
github.com/pion/stun v0.6.1 h1:8lp6YejULeHBF8NmV8e2787BogQhduZugh5PdhDyyN4=
github.com/pion/stun v0.6.1/go.mod h1:/hO7APkX4hZKu/D0f2lHzNyvdkTGtIy3NDmLR7kSz/8=
github.com/pion/transport/v2 v2.2.1/go.mod h1:cXXWavvCnFF6McHTft3DWS9iic2Mftcz1Aq29pGcU5g=
github.com/pion/transport/v2 v2.2.2/go.mod h1:OJg3ojoBJopjEeECq2yJdXH9YVrUJ1uQ++NjXLOUorc=
github.com/pion/transport/v2 v2.2.3/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0=
github.com/pion/transport/v2 v2.2.4/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0=
github.com/pion/transport/v2 v2.2.5 h1:iyi25i/21gQck4hfRhomF6SktmUQjRsRW4WJdhfc3Kc=
github.com/pion/transport/v2 v2.2.5/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0=
github.com/pion/transport/v3 v3.0.1/go.mod h1:UY7kiITrlMv7/IKgd5eTUcaahZx5oUN3l9SzK5f5xE0=
github.com/pion/transport/v3 v3.0.2 h1:r+40RJR25S9w3jbA6/5uEPTzcdn7ncyU44RWCbHkLg4=
github.com/pion/transport/v3 v3.0.2/go.mod h1:nIToODoOlb5If2jF9y2Igfx3PFYWfuXi37m0IlWa/D0=
github.com/pion/turn/v2 v2.1.3/go.mod h1:huEpByKKHix2/b9kmTAM3YoX6MKP+/D//0ClgUYR2fY=
github.com/pion/turn/v2 v2.1.6 h1:Xr2niVsiPTB0FPtt+yAWKFUkU1eotQbGgpTIld4x1Gc=
github.com/pion/turn/v2 v2.1.6/go.mod h1:huEpByKKHix2/b9kmTAM3YoX6MKP+/D//0ClgUYR2fY=
github.com/pion/webrtc/v3 v3.2.39 h1:Lf2SIMGdE3M9VNm48KpoX5pR8SJ6TsMnktzOkc/oB0o=
github.com/pion/webrtc/v3 v3.2.39/go.mod h1:AQ8p56OLbm3MjhYovYdgPuyX6oc+JcKx/HFoCGFcYzA=
github.com/pion/webrtc/v3 v3.2.40 h1:Wtfi6AZMQg+624cvCXUuSmrKWepSB7zfgYDOYqsSOVU=
github.com/pion/webrtc/v3 v3.2.40/go.mod h1:M1RAe3TNTD1tzyvqHrbVODfwdPGSXOUo/OgpoGGJqFY=
github.com/pion/rtcp v1.2.15 h1:LZQi2JbdipLOj4eBjK4wlVoQWfrZbh3Q6eHtWtJBZBo=
github.com/pion/rtcp v1.2.15/go.mod h1:jlGuAjHMEXwMUHK78RgX0UmEJFV4zUKOFHR7OP+D3D0=
github.com/pion/rtp v1.8.13 h1:8uSUPpjSL4OlwZI8Ygqu7+h2p9NPFB+yAZ461Xn5sNg=
github.com/pion/rtp v1.8.13/go.mod h1:8uMBJj32Pa1wwx8Fuv/AsFhn8jsgw+3rUC2PfoBZ8p4=
github.com/pion/sctp v1.8.37 h1:ZDmGPtRPX9mKCiVXtMbTWybFw3z/hVKAZgU81wcOrqs=
github.com/pion/sctp v1.8.37/go.mod h1:cNiLdchXra8fHQwmIoqw0MbLLMs+f7uQ+dGMG2gWebE=
github.com/pion/sdp/v3 v3.0.11 h1:VhgVSopdsBKwhCFoyyPmT1fKMeV9nLMrEKxNOdy3IVI=
github.com/pion/sdp/v3 v3.0.11/go.mod h1:88GMahN5xnScv1hIMTqLdu/cOcUkj6a9ytbncwMCq2E=
github.com/pion/srtp/v3 v3.0.4 h1:2Z6vDVxzrX3UHEgrUyIGM4rRouoC7v+NiF1IHtp9B5M=
github.com/pion/srtp/v3 v3.0.4/go.mod h1:1Jx3FwDoxpRaTh1oRV8A/6G1BnFL+QI82eK4ms8EEJQ=
github.com/pion/stun/v3 v3.0.0 h1:4h1gwhWLWuZWOJIJR9s2ferRO+W3zA/b6ijOI6mKzUw=
github.com/pion/stun/v3 v3.0.0/go.mod h1:HvCN8txt8mwi4FBvS3EmDghW6aQJ24T+y+1TKjB5jyU=
github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0=
github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo=
github.com/pion/turn/v4 v4.0.0 h1:qxplo3Rxa9Yg1xXDxxH8xaqcyGUtbHYw4QSCvmFWvhM=
github.com/pion/turn/v4 v4.0.0/go.mod h1:MuPDkm15nYSklKpN8vWJ9W2M0PlyQZqYt1McGuxG7mA=
github.com/pion/webrtc/v4 v4.0.14 h1:nyds/sFRR+HvmWoBa6wrL46sSfpArE0qR883MBW96lg=
github.com/pion/webrtc/v4 v4.0.14/go.mod h1:R3+qTnQTS03UzwDarYecgioNf7DYgTsldxnCXB821Kk=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/profile v1.4.0/go.mod h1:NWz/XGvpEW1FyYQ7fCx4dqYBLlfTcE+A9FLAkNKqjFE=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.32.0 h1:keLypqrlIjaFsbmJOBdB/qvyF8KEtCWHwobLp5l/mQ0=
github.com/rs/zerolog v1.32.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
github.com/sigurn/crc16 v0.0.0-20240131213347-83fcde1e29d1 h1:NVK+OqnavpyFmUiKfUMHrpvbCi2VFoWTrcpI7aDaJ2I=
github.com/sigurn/crc16 v0.0.0-20240131213347-83fcde1e29d1/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/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
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.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
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/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-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.13.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU=
github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0=
golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o=
golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
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.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
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.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.20.0 h1:hz/CVckiOxybQvFw6h7b/q80NTr9IUQb4s1IIzW7KNY=
golang.org/x/tools v0.20.0/go.mod h1:WvitBU7JJf6A4jOdg4S1tviW9bhUxkgeCui/0JHctQg=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24=
golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

7
internal/alsa/alsa.go Normal file
View File

@@ -0,0 +1,7 @@
//go:build !(linux && (386 || amd64 || arm || arm64 || mipsle))
package alsa
func Init() {
// not supported
}

View File

@@ -0,0 +1,83 @@
//go:build linux && (386 || amd64 || arm || arm64 || mipsle)
package alsa
import (
"fmt"
"net/http"
"os"
"strconv"
"strings"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/alsa"
"github.com/AlexxIT/go2rtc/pkg/alsa/device"
)
func Init() {
streams.HandleFunc("alsa", alsa.Open)
api.HandleFunc("api/alsa", apiAlsa)
}
func apiAlsa(w http.ResponseWriter, r *http.Request) {
files, err := os.ReadDir("/dev/snd/")
if err != nil {
return
}
var sources []*api.Source
for _, file := range files {
if !strings.HasPrefix(file.Name(), "pcm") {
continue
}
path := "/dev/snd/" + file.Name()
dev, err := device.Open(path)
if err != nil {
continue
}
info, err := dev.Info()
if err == nil {
formats := formatsToString(dev.ListFormats())
r1, r2 := dev.RangeRates()
c1, c2 := dev.RangeChannels()
source := &api.Source{
Name: info.ID,
Info: fmt.Sprintf("Formats: %s, Rates: %d-%d, Channels: %d-%d", formats, r1, r2, c1, c2),
URL: "alsa:device?audio=" + path,
}
if !strings.Contains(source.Name, info.Name) {
source.Name += ", " + info.Name
}
sources = append(sources, source)
}
_ = dev.Close()
}
api.ResponseSources(w, sources)
}
func formatsToString(formats []byte) string {
var s string
for i, format := range formats {
if i > 0 {
s += " "
}
switch format {
case 2:
s += "s16le"
case 10:
s += "s32le"
default:
s += strconv.Itoa(int(format))
}
}
return s
}

View File

@@ -69,6 +69,8 @@ func Init() {
}
if cfg.Mod.Listen != "" {
_, port, _ := net.SplitHostPort(cfg.Mod.Listen)
Port, _ = strconv.Atoi(port)
go listen("tcp", cfg.Mod.Listen)
}
@@ -92,10 +94,6 @@ func listen(network, address string) {
log.Info().Str("addr", address).Msg("[api] listen")
if network == "tcp" {
Port = ln.Addr().(*net.TCPAddr).Port
}
server := http.Server{
Handler: Handler,
ReadHeaderTimeout: 5 * time.Second, // Example: Set to 5 seconds

View File

@@ -1,8 +1,9 @@
package api
import (
"github.com/AlexxIT/go2rtc/www"
"net/http"
"github.com/AlexxIT/go2rtc/www"
)
func initStatic(staticDir string) {

View File

@@ -1,6 +1,7 @@
package ws
import (
"encoding/json"
"io"
"net/http"
"net/url"
@@ -38,20 +39,19 @@ type Message struct {
Value any `json:"value,omitempty"`
}
func (m *Message) String() string {
func (m *Message) String() (value string) {
if s, ok := m.Value.(string); ok {
return s
}
return ""
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
}
func (m *Message) Unmarshal(v any) error {
b, err := json.Marshal(m.Value)
if err != nil {
return err
}
return ""
return json.Unmarshal(b, v)
}
type WSHandler func(tr *Transport, msg *Message) error
@@ -83,7 +83,7 @@ func initWS(origin string) {
if o.Host == r.Host {
return true
}
log.Trace().Msgf("[api.ws] origin=%s, host=%s", o.Host, r.Host)
log.Trace().Msgf("[api] ws origin=%s, host=%s", o.Host, r.Host)
// https://github.com/AlexxIT/go2rtc/issues/118
if i := strings.IndexByte(o.Host, ':'); i > 0 {
return o.Host[:i] == r.Host
@@ -127,7 +127,7 @@ func apiWS(w http.ResponseWriter, r *http.Request) {
break
}
log.Trace().Str("type", msg.Type).Msg("[api.ws] msg")
log.Trace().Str("type", msg.Type).Msg("[api] ws msg")
if handler := wsHandlers[msg.Type]; handler != nil {
go func() {

70
internal/app/README.md Normal file
View File

@@ -0,0 +1,70 @@
- By default go2rtc will search config file `go2rtc.yaml` in current work directory
- go2rtc support multiple config files:
- `go2rtc -c config1.yaml -c config2.yaml -c config3.yaml`
- go2rtc support inline config as multiple formats from command line:
- **YAML**: `go2rtc -c '{log: {format: text}}'`
- **JSON**: `go2rtc -c '{"log":{"format":"text"}}'`
- **key=value**: `go2rtc -c log.format=text`
- Every next config will overwrite previous (but only defined params)
```
go2rtc -config "{log: {format: text}}" -config /config/go2rtc.yaml -config "{rtsp: {listen: ''}}" -config /usr/local/go2rtc/go2rtc.yaml
```
or simple version
```
go2rtc -c log.format=text -c /config/go2rtc.yaml -c rtsp.listen='' -c /usr/local/go2rtc/go2rtc.yaml
```
## Environment variables
There is support for loading external variables into the config. First, they will be attempted to be loaded from [credential files](https://systemd.io/CREDENTIALS). If `CREDENTIALS_DIRECTORY` is not set, then the key will be loaded from an environment variable. If no environment variable is set, then the string will be left as-is.
```yaml
streams:
camera1: rtsp://rtsp:${CAMERA_PASSWORD}@192.168.1.123/av_stream/ch0
rtsp:
username: ${RTSP_USER:admin} # "admin" if "RTSP_USER" not set
password: ${RTSP_PASS:secret} # "secret" if "RTSP_PASS" not set
```
## JSON Schema
Editors like [GoLand](https://www.jetbrains.com/go/) and [VS Code](https://code.visualstudio.com/) supports autocomplete and syntax validation.
```yaml
# yaml-language-server: $schema=https://raw.githubusercontent.com/AlexxIT/go2rtc/master/website/schema.json
```
## Defaults
- Default values may change in updates
- FFmpeg module has many presets, they are not listed here because they may also change in updates
```yaml
api:
listen: ":1984"
ffmpeg:
bin: "ffmpeg"
log:
format: "color"
level: "info"
output: "stdout"
time: "UNIXMS"
rtsp:
listen: ":8554"
default_query: "video&audio"
srtp:
listen: ":8443"
webrtc:
listen: ":8555/tcp"
ice_servers:
- urls: [ "stun:stun.l.google.com:19302" ]
```

View File

@@ -1,144 +1,101 @@
package app
import (
"errors"
"flag"
"fmt"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"github.com/AlexxIT/go2rtc/pkg/shell"
"github.com/AlexxIT/go2rtc/pkg/yaml"
"github.com/rs/zerolog/log"
"runtime/debug"
)
var Version = "1.9.1"
var UserAgent = "go2rtc/" + Version
var (
Version string
UserAgent string
ConfigPath string
Info = make(map[string]any)
)
var ConfigPath string
var Info = map[string]any{
"version": Version,
}
const usage = `Usage of go2rtc:
-c, --config Path to config file or config string as YAML or JSON, support multiple
-d, --daemon Run in background
-v, --version Print version and exit
`
func Init() {
var confs Config
var config flagConfig
var daemon bool
var version bool
flag.Var(&confs, "config", "go2rtc config (path to file or raw text), support multiple")
if runtime.GOOS != "windows" {
flag.BoolVar(&daemon, "daemon", false, "Run program in background")
}
flag.BoolVar(&version, "version", false, "Print the version of the application and exit")
flag.Var(&config, "config", "")
flag.Var(&config, "c", "")
flag.BoolVar(&daemon, "daemon", false, "")
flag.BoolVar(&daemon, "d", false, "")
flag.BoolVar(&version, "version", false, "")
flag.BoolVar(&version, "v", false, "")
flag.Usage = func() { fmt.Print(usage) }
flag.Parse()
revision, vcsTime := readRevisionTime()
if version {
fmt.Println("Current version:", Version)
fmt.Printf("go2rtc version %s (%s) %s/%s\n", Version, revision, runtime.GOOS, runtime.GOARCH)
os.Exit(0)
}
if daemon {
args := os.Args[1:]
for i, arg := range args {
if arg == "-daemon" {
args[i] = ""
}
if daemon && os.Getppid() != 1 {
if runtime.GOOS == "windows" {
fmt.Println("Daemon mode is not supported on Windows")
os.Exit(1)
}
// Re-run the program in background and exit
cmd := exec.Command(os.Args[0], args...)
cmd := exec.Command(os.Args[0], os.Args[1:]...)
if err := cmd.Start(); err != nil {
log.Fatal().Err(err).Send()
fmt.Println("Failed to start daemon:", err)
os.Exit(1)
}
fmt.Println("Running in daemon mode with PID:", cmd.Process.Pid)
os.Exit(0)
}
if confs == nil {
confs = []string{"go2rtc.yaml"}
}
UserAgent = "go2rtc/" + Version
for _, conf := range confs {
if conf[0] != '{' {
// config as file
if ConfigPath == "" {
ConfigPath = conf
}
Info["version"] = Version
Info["revision"] = revision
data, _ := os.ReadFile(conf)
if data == nil {
continue
}
initConfig(config)
initLogger()
data = []byte(shell.ReplaceEnvVars(string(data)))
configs = append(configs, data)
} else {
// config as raw YAML
configs = append(configs, []byte(conf))
}
}
platform := fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH)
Logger.Info().Str("version", Version).Str("platform", platform).Str("revision", revision).Msg("go2rtc")
Logger.Debug().Str("version", runtime.Version()).Str("vcs.time", vcsTime).Msg("build")
if ConfigPath != "" {
if !filepath.IsAbs(ConfigPath) {
if cwd, err := os.Getwd(); err == nil {
ConfigPath = filepath.Join(cwd, ConfigPath)
Logger.Info().Str("path", ConfigPath).Msg("config")
}
}
func readRevisionTime() (revision, vcsTime string) {
if info, ok := debug.ReadBuildInfo(); ok {
for _, setting := range info.Settings {
switch setting.Key {
case "vcs.revision":
if len(setting.Value) > 7 {
revision = setting.Value[:7]
} else {
revision = setting.Value
}
case "vcs.time":
vcsTime = setting.Value
case "vcs.modified":
if setting.Value == "true" {
revision = "mod." + revision
}
}
}
Info["config_path"] = ConfigPath
}
var cfg struct {
Mod map[string]string `yaml:"log"`
}
LoadConfig(&cfg)
log.Logger = NewLogger(cfg.Mod["format"], cfg.Mod["level"])
modules = cfg.Mod
log.Info().Msgf("go2rtc version %s %s/%s", Version, runtime.GOOS, runtime.GOARCH)
migrateStore()
return
}
func LoadConfig(v any) {
for _, data := range configs {
if err := yaml.Unmarshal(data, v); err != nil {
log.Warn().Err(err).Msg("[app] read config")
}
}
}
func PatchConfig(key string, value any, path ...string) error {
if ConfigPath == "" {
return errors.New("config file disabled")
}
// empty config is OK
b, _ := os.ReadFile(ConfigPath)
b, err := yaml.Patch(b, key, value, path...)
if err != nil {
return err
}
return os.WriteFile(ConfigPath, b, 0644)
}
// internal
type Config []string
func (c *Config) String() string {
return strings.Join(*c, " ")
}
func (c *Config) Set(value string) error {
*c = append(*c, value)
return nil
}
var configs [][]byte

109
internal/app/config.go Normal file
View File

@@ -0,0 +1,109 @@
package app
import (
"errors"
"os"
"path/filepath"
"strings"
"github.com/AlexxIT/go2rtc/pkg/shell"
"github.com/AlexxIT/go2rtc/pkg/yaml"
)
func LoadConfig(v any) {
for _, data := range configs {
if err := yaml.Unmarshal(data, v); err != nil {
Logger.Warn().Err(err).Send()
}
}
}
func PatchConfig(path []string, value any) error {
if ConfigPath == "" {
return errors.New("config file disabled")
}
// empty config is OK
b, _ := os.ReadFile(ConfigPath)
b, err := yaml.Patch(b, path, value)
if err != nil {
return err
}
return os.WriteFile(ConfigPath, b, 0644)
}
type flagConfig []string
func (c *flagConfig) String() string {
return strings.Join(*c, " ")
}
func (c *flagConfig) Set(value string) error {
*c = append(*c, value)
return nil
}
var configs [][]byte
func initConfig(confs flagConfig) {
if confs == nil {
confs = []string{"go2rtc.yaml"}
}
for _, conf := range confs {
if len(conf) == 0 {
continue
}
if conf[0] == '{' {
// config as raw YAML or JSON
configs = append(configs, []byte(conf))
} else if data := parseConfString(conf); data != nil {
configs = append(configs, data)
} else {
// config as file
if ConfigPath == "" {
ConfigPath = conf
}
if data, _ = os.ReadFile(conf); data == nil {
continue
}
data = []byte(shell.ReplaceEnvVars(string(data)))
configs = append(configs, data)
}
}
if ConfigPath != "" {
if !filepath.IsAbs(ConfigPath) {
if cwd, err := os.Getwd(); err == nil {
ConfigPath = filepath.Join(cwd, ConfigPath)
}
}
Info["config_path"] = ConfigPath
}
}
func parseConfString(s string) []byte {
i := strings.IndexByte(s, '=')
if i < 0 {
return nil
}
items := strings.Split(s[:i], ".")
if len(items) < 2 {
return nil
}
// `log.level=trace` => `{log: {level: trace}}`
var pre string
var suf = s[i+1:]
for _, item := range items {
pre += "{" + item + ": "
suf += "}"
}
return []byte(pre + suf)
}

View File

@@ -3,60 +3,123 @@ package app
import (
"io"
"os"
"strings"
"sync"
"github.com/mattn/go-isatty"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
)
var MemoryLog *circularBuffer
func NewLogger(format string, level string) zerolog.Logger {
var writer io.Writer = os.Stdout
if format != "json" {
writer = zerolog.ConsoleWriter{
Out: writer, TimeFormat: "15:04:05.000", NoColor: format == "text",
}
}
MemoryLog = newBuffer(16)
writer = zerolog.MultiLevelWriter(writer, MemoryLog)
zerolog.TimeFieldFormat = zerolog.TimeFormatUnixMs
lvl, err := zerolog.ParseLevel(level)
if err != nil || lvl == zerolog.NoLevel {
lvl = zerolog.InfoLevel
}
return zerolog.New(writer).With().Timestamp().Logger().Level(lvl)
}
var MemoryLog = newBuffer()
func GetLogger(module string) zerolog.Logger {
if s, ok := modules[module]; ok {
lvl, err := zerolog.ParseLevel(s)
if err == nil {
return log.Level(lvl)
return Logger.Level(lvl)
}
log.Warn().Err(err).Caller().Send()
Logger.Warn().Err(err).Caller().Send()
}
return log.Logger
return Logger
}
// modules log levels
var modules map[string]string
// initLogger support:
// - output: empty (only to memory), stderr, stdout
// - format: empty (autodetect color support), color, json, text
// - time: empty (disable timestamp), UNIXMS, UNIXMICRO, UNIXNANO
// - level: disabled, trace, debug, info, warn, error...
func initLogger() {
var cfg struct {
Mod map[string]string `yaml:"log"`
}
const chunkSize = 1 << 16
cfg.Mod = modules // defaults
LoadConfig(&cfg)
var writer io.Writer
switch output, path, _ := strings.Cut(modules["output"], ":"); output {
case "stderr":
writer = os.Stderr
case "stdout":
writer = os.Stdout
case "file":
if path == "" {
path = "go2rtc.log"
}
// if fail - only MemoryLog will be available
writer, _ = os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
}
timeFormat := modules["time"]
if writer != nil {
if format := modules["format"]; format != "json" {
console := &zerolog.ConsoleWriter{Out: writer}
switch format {
case "text":
console.NoColor = true
case "color":
console.NoColor = false // useless, but anyway
default:
// autodetection if output support color
// go-isatty - dependency for go-colorable - dependency for ConsoleWriter
console.NoColor = !isatty.IsTerminal(writer.(*os.File).Fd())
}
if timeFormat != "" {
console.TimeFormat = "15:04:05.000"
} else {
console.PartsOrder = []string{
zerolog.LevelFieldName,
zerolog.CallerFieldName,
zerolog.MessageFieldName,
}
}
writer = console
}
writer = zerolog.MultiLevelWriter(writer, MemoryLog)
} else {
writer = MemoryLog
}
lvl, _ := zerolog.ParseLevel(modules["level"])
Logger = zerolog.New(writer).Level(lvl)
if timeFormat != "" {
zerolog.TimeFieldFormat = timeFormat
Logger = Logger.With().Timestamp().Logger()
}
}
var Logger zerolog.Logger
// modules log levels
var modules = map[string]string{
"format": "", // useless, but anyway
"level": "info",
"output": "stdout", // TODO: change to stderr someday
"time": zerolog.TimeFormatUnixMs,
}
const (
chunkCount = 16
chunkSize = 1 << 16
)
type circularBuffer struct {
chunks [][]byte
r, w int
mu sync.Mutex
}
func newBuffer(chunks int) *circularBuffer {
b := &circularBuffer{chunks: make([][]byte, 0, chunks)}
func newBuffer() *circularBuffer {
b := &circularBuffer{chunks: make([][]byte, 0, chunkCount)}
// create first chunk
b.chunks = append(b.chunks, make([]byte, 0, chunkSize))
return b
@@ -65,16 +128,17 @@ func newBuffer(chunks int) *circularBuffer {
func (b *circularBuffer) Write(p []byte) (n int, err error) {
n = len(p)
b.mu.Lock()
// check if chunk has size
if len(b.chunks[b.w])+n > chunkSize {
// increase write chunk index
if b.w++; b.w == cap(b.chunks) {
if b.w++; b.w == chunkCount {
b.w = 0
}
// check overflow
if b.r == b.w {
// increase read chunk index
if b.r++; b.r == cap(b.chunks) {
if b.r++; b.r == chunkCount {
b.r = 0
}
}
@@ -89,29 +153,34 @@ func (b *circularBuffer) Write(p []byte) (n int, err error) {
}
b.chunks[b.w] = append(b.chunks[b.w], p...)
b.mu.Unlock()
return
}
func (b *circularBuffer) WriteTo(w io.Writer) (n int64, err error) {
for i := b.r; ; {
var nn int
if nn, err = w.Write(b.chunks[i]); err != nil {
return
}
n += int64(nn)
buf := make([]byte, 0, chunkCount*chunkSize)
// use temp buffer inside mutex because w.Write can take some time
b.mu.Lock()
for i := b.r; ; {
buf = append(buf, b.chunks[i]...)
if i == b.w {
break
}
if i++; i == cap(b.chunks) {
if i++; i == chunkCount {
i = 0
}
}
return
b.mu.Unlock()
nn, err := w.Write(buf)
return int64(nn), err
}
func (b *circularBuffer) Reset() {
b.mu.Lock()
b.chunks[0] = b.chunks[0][:0]
b.r = 0
b.w = 0
b.mu.Unlock()
}

View File

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

View File

@@ -7,13 +7,7 @@ import (
)
func Init() {
streams.HandleFunc("bubble", handle)
}
func handle(url string) (core.Producer, error) {
conn := bubble.NewClient(url)
if err := conn.Dial(); err != nil {
return nil, err
}
return conn, nil
streams.HandleFunc("bubble", func(source string) (core.Producer, error) {
return bubble.Dial(source)
})
}

View File

@@ -2,16 +2,8 @@ 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

@@ -0,0 +1,36 @@
package doorbird
import (
"net/url"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/doorbird"
)
func Init() {
streams.RedirectFunc("doorbird", func(rawURL string) (string, error) {
u, err := url.Parse(rawURL)
if err != nil {
return "", err
}
// https://www.doorbird.com/downloads/api_lan.pdf
switch u.Query().Get("media") {
case "video":
u.Path = "/bha-api/video.cgi"
case "audio":
u.Path = "/bha-api/audio-receive.cgi"
default:
return "", nil
}
u.Scheme = "http"
return u.String(), nil
})
streams.HandleFunc("doorbird", func(source string) (core.Producer, error) {
return doorbird.Dial(source)
})
}

View File

@@ -10,26 +10,16 @@ import (
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/dvrip"
"github.com/rs/zerolog/log"
)
func Init() {
streams.HandleFunc("dvrip", handle)
streams.HandleFunc("dvrip", dvrip.Dial)
// DVRIP client autodiscovery
api.HandleFunc("api/dvrip", apiDvrip)
}
func handle(url string) (core.Producer, error) {
client, err := dvrip.Dial(url)
if err != nil {
return nil, err
}
return client, nil
}
const Port = 34569 // UDP port number for dvrip discovery
func apiDvrip(w http.ResponseWriter, r *http.Request) {
@@ -92,10 +82,7 @@ func sendBroadcasts(conn *net.UDPConn) {
for i := 0; i < 3; i++ {
time.Sleep(100 * time.Millisecond)
if _, err = conn.WriteToUDP(data, addr); err != nil {
log.Err(err).Caller().Send()
}
_, _ = conn.WriteToUDP(data, addr)
}
}

View File

@@ -0,0 +1,10 @@
package eseecloud
import (
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/eseecloud"
)
func Init() {
streams.HandleFunc("eseecloud", eseecloud.Dial)
}

12
internal/exec/README.md Normal file
View File

@@ -0,0 +1,12 @@
## Backchannel
- You can check audio card names in the **Go2rtc > WebUI > Add**
- You can specify multiple backchannel lines with different codecs
```yaml
sources:
two_way_audio_win:
- exec:ffmpeg -hide_banner -f dshow -i "audio=Microphone (High Definition Audio Device)" -c pcm_s16le -ar 16000 -ac 1 -f wav -
- exec:ffplay -nodisp -probesize 32 -f s16le -ar 16000 -#backchannel=1#audio=s16le/16000
- exec:ffplay -nodisp -probesize 32 -f alaw -ar 8000 -#backchannel=1#audio=alaw/8000
```

View File

@@ -1,6 +1,7 @@
package exec
import (
"bufio"
"crypto/md5"
"encoding/hex"
"errors"
@@ -8,9 +9,9 @@ import (
"io"
"net/url"
"os"
"os/exec"
"strings"
"sync"
"syscall"
"time"
"github.com/AlexxIT/go2rtc/internal/app"
@@ -18,9 +19,9 @@ import (
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/magic"
"github.com/AlexxIT/go2rtc/pkg/pcm"
pkg "github.com/AlexxIT/go2rtc/pkg/rtsp"
"github.com/AlexxIT/go2rtc/pkg/shell"
"github.com/AlexxIT/go2rtc/pkg/stdin"
"github.com/rs/zerolog"
)
@@ -48,9 +49,11 @@ func Init() {
log = app.GetLogger("exec")
}
func execHandle(rawURL string) (core.Producer, error) {
func execHandle(rawURL string) (prod core.Producer, err error) {
rawURL, rawQuery, _ := strings.Cut(rawURL, "#")
query := streams.ParseQuery(rawQuery)
var path string
var query url.Values
// RTSP flow should have `{output}` inside URL
// pipe flow may have `#{params}` inside URL
@@ -62,60 +65,88 @@ func execHandle(rawURL string) (core.Producer, error) {
sum := md5.Sum([]byte(rawURL))
path = "/" + hex.EncodeToString(sum[:])
rawURL = rawURL[:i] + "rtsp://127.0.0.1:" + rtsp.Port + path + rawURL[i+8:]
} else if i = strings.IndexByte(rawURL, '#'); i > 0 {
query = streams.ParseQuery(rawURL[i+1:])
rawURL = rawURL[:i]
}
args := shell.QuoteSplit(rawURL[5:]) // remove `exec:`
cmd := exec.Command(args[0], args[1:]...)
if log.Debug().Enabled() {
cmd.Stderr = os.Stderr
cmd := shell.NewCommand(rawURL[5:]) // remove `exec:`
cmd.Stderr = &logWriter{
buf: make([]byte, 512),
debug: log.Debug().Enabled(),
}
if s := query.Get("killsignal"); s != "" {
sig := syscall.Signal(core.Atoi(s))
cmd.Cancel = func() error {
log.Debug().Msgf("[exec] kill with signal=%d", sig)
return cmd.Process.Signal(sig)
}
}
if s := query.Get("killtimeout"); s != "" {
cmd.WaitDelay = time.Duration(core.Atoi(s)) * time.Second
}
if query.Get("backchannel") == "1" {
return pcm.NewBackchannel(cmd, query.Get("audio"))
}
if path == "" {
return handlePipe(rawURL, cmd, query)
prod, err = handlePipe(rawURL, cmd)
} else {
prod, err = handleRTSP(rawURL, cmd, path)
}
return handleRTSP(rawURL, cmd, path)
if err != nil {
_ = cmd.Close()
}
return
}
func handlePipe(_ string, cmd *exec.Cmd, query url.Values) (core.Producer, error) {
if query.Get("backchannel") == "1" {
return stdin.NewClient(cmd)
}
r, err := PipeCloser(cmd, query)
func handlePipe(source string, cmd *shell.Command) (core.Producer, error) {
stdout, err := cmd.StdoutPipe()
if err != nil {
return nil, err
}
rd := struct {
io.Reader
io.Closer
}{
// add buffer for pipe reader to reduce syscall
bufio.NewReaderSize(stdout, core.BufferSize),
// stop cmd on close pipe call
cmd,
}
log.Debug().Strs("args", cmd.Args).Msg("[exec] run pipe")
ts := time.Now()
if err = cmd.Start(); err != nil {
return nil, err
}
prod, err := magic.Open(r)
prod, err := magic.Open(rd)
if err != nil {
_ = r.Close()
return nil, fmt.Errorf("exec/pipe: %w\n%s", err, cmd.Stderr)
}
return prod, err
if info, ok := prod.(core.Info); ok {
info.SetProtocol("pipe")
setRemoteInfo(info, source, cmd.Args)
}
log.Debug().Stringer("launch", time.Since(ts)).Msg("[exec] run pipe")
return prod, nil
}
func handleRTSP(url string, cmd *exec.Cmd, path string) (core.Producer, error) {
stderr := limitBuffer{buf: make([]byte, 512)}
if cmd.Stderr != nil {
cmd.Stderr = io.MultiWriter(cmd.Stderr, &stderr)
} else {
cmd.Stderr = &stderr
}
func handleRTSP(source string, cmd *shell.Command, path string) (core.Producer, error) {
if log.Trace().Enabled() {
cmd.Stdout = os.Stdout
}
waiter := make(chan core.Producer)
waiter := make(chan *pkg.Conn, 1)
waitersMu.Lock()
waiters[path] = waiter
@@ -127,30 +158,31 @@ func handleRTSP(url string, cmd *exec.Cmd, path string) (core.Producer, error) {
waitersMu.Unlock()
}()
log.Debug().Str("url", url).Str("cmd", fmt.Sprintf("%s", strings.Join(cmd.Args, " "))).Msg("[exec] run")
log.Debug().Strs("args", cmd.Args).Msg("[exec] run rtsp")
ts := time.Now()
if err := cmd.Start(); err != nil {
log.Error().Err(err).Str("url", url).Msg("[exec]")
log.Error().Err(err).Str("source", source).Msg("[exec]")
return nil, err
}
done := make(chan error, 1)
go func() {
done <- cmd.Wait()
}()
timeout := time.NewTimer(30 * time.Second)
defer timeout.Stop()
select {
case <-time.After(time.Second * 60):
_ = cmd.Process.Kill()
log.Error().Str("url", url).Msg("[exec] timeout")
return nil, errors.New("timeout")
case <-done:
// limit message size
return nil, errors.New("exec: " + stderr.String())
case <-timeout.C:
// haven't received data from app in timeout
log.Error().Str("source", source).Msg("[exec] timeout")
return nil, errors.New("exec: timeout")
case <-cmd.Done():
// app fail before we receive any data
return nil, fmt.Errorf("exec/rtsp\n%s", cmd.Stderr)
case prod := <-waiter:
log.Debug().Stringer("launch", time.Since(ts)).Msg("[exec] run")
// app started successfully
log.Debug().Stringer("launch", time.Since(ts)).Msg("[exec] run rtsp")
setRemoteInfo(prod, source, cmd.Args)
prod.OnClose = cmd.Close
return prod, nil
}
}
@@ -159,25 +191,63 @@ func handleRTSP(url string, cmd *exec.Cmd, path string) (core.Producer, error) {
var (
log zerolog.Logger
waiters = map[string]chan core.Producer{}
waiters = make(map[string]chan *pkg.Conn)
waitersMu sync.Mutex
)
type limitBuffer struct {
buf []byte
n int
type logWriter struct {
buf []byte
debug bool
n int
}
func (l *limitBuffer) String() string {
func (l *logWriter) String() string {
if l.n == len(l.buf) {
return string(l.buf) + "..."
}
return string(l.buf[:l.n])
}
func (l *limitBuffer) Write(p []byte) (int, error) {
func (l *logWriter) Write(p []byte) (n int, err error) {
if l.n < cap(l.buf) {
l.n += copy(l.buf[l.n:], p)
}
return len(p), nil
n = len(p)
if l.debug {
if p = trimSpace(p); p != nil {
log.Debug().Msgf("[exec] %s", p)
}
}
return
}
func trimSpace(b []byte) []byte {
start := 0
stop := len(b)
for ; start < stop; start++ {
if b[start] >= ' ' {
break // trim all ASCII before 0x20
}
}
for ; ; stop-- {
if stop == start {
return nil // skip empty output
}
if b[stop-1] > ' ' {
break // trim all ASCII before 0x21
}
}
return b[start:stop]
}
func setRemoteInfo(info core.Info, source string, args []string) {
info.SetSource(source)
if i := core.Index(args, "-i"); i > 0 && i < len(args)-1 {
rawURL := args[i+1]
if u, err := url.Parse(rawURL); err == nil && u.Host != "" {
info.SetRemoteAddr(u.Host)
info.SetURL(rawURL)
}
}
}

View File

@@ -1,56 +0,0 @@
package exec
import (
"bufio"
"errors"
"io"
"net/url"
"os/exec"
"syscall"
"time"
"github.com/AlexxIT/go2rtc/pkg/core"
)
// PipeCloser - return StdoutPipe that Kill cmd on Close call
func PipeCloser(cmd *exec.Cmd, query url.Values) (io.ReadCloser, error) {
stdout, err := cmd.StdoutPipe()
if err != nil {
return nil, err
}
// add buffer for pipe reader to reduce syscall
return &pipeCloser{bufio.NewReaderSize(stdout, core.BufferSize), stdout, cmd, query}, nil
}
type pipeCloser struct {
io.Reader
io.Closer
cmd *exec.Cmd
query url.Values
}
func (p *pipeCloser) Close() error {
return errors.Join(p.Closer.Close(), p.Kill(), p.Wait())
}
func (p *pipeCloser) Kill() error {
if s := p.query.Get("killsignal"); s != "" {
log.Trace().Msgf("[exec] kill with custom sig=%s", s)
sig := syscall.Signal(core.Atoi(s))
return p.cmd.Process.Signal(sig)
}
return p.cmd.Process.Kill()
}
func (p *pipeCloser) Wait() error {
if s := p.query.Get("killtimeout"); s != "" {
timeout := time.Duration(core.Atoi(s)) * time.Second
timer := time.AfterFunc(timeout, func() {
log.Trace().Msgf("[exec] kill after timeout=%s", s)
_ = p.cmd.Process.Kill()
})
defer timer.Stop() // stop timer if Wait ends before timeout
}
return p.cmd.Wait()
}

View File

@@ -12,7 +12,7 @@ func Init() {
log := app.GetLogger("expr")
streams.RedirectFunc("expr", func(url string) (string, error) {
v, err := expr.Run(url[5:])
v, err := expr.Eval(url[5:], nil)
if err != nil {
return "", err
}

View File

@@ -45,6 +45,13 @@
[video4linux2,v4l2 @ 0x7f7de7c58bc0] Compressed: mjpeg : Motion-JPEG : 640x480 160x120 176x144 320x176 320x240 352x288 432x240 544x288 640x360 752x416 800x448 800x600 864x480 960x544 960x720 1024x576 1184x656 1280x720 1280x960
```
## TTS
```yaml
streams:
tts: ffmpeg:#input=-readrate 1 -readrate_initial_burst 0.001 -f lavfi -i "flite=text='1 2 3 4 5 6 7 8 9 0'"#audio=pcma
```
## Useful links
- https://superuser.com/questions/564402/explanation-of-x264-tune

51
internal/ffmpeg/api.go Normal file
View File

@@ -0,0 +1,51 @@
package ffmpeg
import (
"net/http"
"strings"
"github.com/AlexxIT/go2rtc/internal/streams"
)
func apiFFmpeg(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
http.Error(w, "", http.StatusMethodNotAllowed)
return
}
query := r.URL.Query()
dst := query.Get("dst")
stream := streams.Get(dst)
if stream == nil {
http.Error(w, "", http.StatusNotFound)
return
}
var src string
if s := query.Get("file"); s != "" {
if streams.Validate(s) == nil {
src = "ffmpeg:" + s + "#audio=auto#input=file"
}
} else if s = query.Get("live"); s != "" {
if streams.Validate(s) == nil {
src = "ffmpeg:" + s + "#audio=auto"
}
} else if s = query.Get("text"); s != "" {
if strings.IndexAny(s, `'"&%$`) < 0 {
src = "ffmpeg:tts?text=" + s
if s = query.Get("voice"); s != "" {
src += "&voice=" + s
}
src += "#audio=auto"
}
}
if src == "" {
http.Error(w, "", http.StatusBadRequest)
return
}
if err := stream.Play(src); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}

View File

@@ -1,3 +1,5 @@
//go:build freebsd || netbsd || openbsd || dragonfly
package device
import (

View File

@@ -1,3 +1,5 @@
//go:build darwin || ios
package device
import (

View File

@@ -1,3 +1,5 @@
//go:build unix && !darwin && !freebsd && !netbsd && !openbsd && !dragonfly
package device
import (

View File

@@ -1,3 +1,5 @@
//go:build windows
package device
import (

View File

@@ -1,11 +1,9 @@
package device
import (
"errors"
"net/http"
"net/url"
"strconv"
"strings"
"sync"
"github.com/AlexxIT/go2rtc/internal/api"
@@ -17,24 +15,15 @@ func Init(bin string) {
api.HandleFunc("api/ffmpeg/devices", apiDevices)
}
func GetInput(src string) (string, error) {
i := strings.IndexByte(src, '?')
if i < 0 {
return "", errors.New("empty query: " + src)
}
query, err := url.ParseQuery(src[i+1:])
func GetInput(src string) string {
query, err := url.ParseQuery(src)
if err != nil {
return "", err
return ""
}
runonce.Do(initDevices)
if input := queryToInput(query); input != "" {
return input, nil
}
return "", errors.New("wrong query: " + src)
return queryToInput(query)
}
var Bin string

View File

@@ -4,33 +4,55 @@ import (
"net/url"
"strings"
"github.com/AlexxIT/go2rtc/internal/api"
"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/ffmpeg/virtual"
"github.com/AlexxIT/go2rtc/internal/rtsp"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/ffmpeg"
"github.com/rs/zerolog"
)
func Init() {
var cfg struct {
Mod map[string]string `yaml:"ffmpeg"`
Log struct {
Level string `yaml:"ffmpeg"`
} `yaml:"log"`
}
cfg.Mod = defaults // will be overriden from yaml
cfg.Log.Level = "error"
app.LoadConfig(&cfg)
if app.GetLogger("exec").GetLevel() >= 0 {
defaults["global"] += " -v error"
log = app.GetLogger("ffmpeg")
// zerolog levels: trace debug info warn error fatal panic disabled
// FFmpeg levels: trace debug verbose info warning error fatal panic quiet
if cfg.Log.Level == "warn" {
cfg.Log.Level = "warning"
}
defaults["global"] += " -v " + cfg.Log.Level
streams.RedirectFunc("ffmpeg", func(url string) (string, error) {
if _, err := Version(); err != nil {
return "", err
}
args := parseArgs(url[7:])
if core.Contains(args.Codecs, "auto") {
return "", nil // force call streams.HandleFunc("ffmpeg")
}
return "exec:" + args.String(), nil
})
streams.HandleFunc("ffmpeg", NewProducer)
api.HandleFunc("api/ffmpeg", apiFFmpeg)
device.Init(defaults["bin"])
hardware.Init(defaults["bin"])
}
@@ -49,16 +71,25 @@ var defaults = map[string]string{
// output
"output": "-user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}",
"output/mjpeg": "-f mjpeg -",
"output/raw": "-f yuv4mpegpipe -",
"output/aac": "-f adts -",
"output/wav": "-f wav -",
// `-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
// `-pix_fmt:v yuv420p` - important for Telegram
"h264": "-c:v libx264 -g 50 -profile:v high -level:v 4.1 -preset:v superfast -tune:v zerolatency -pix_fmt:v yuv420p",
"h265": "-c:v libx265 -g 50 -profile:v main -level:v 5.1 -preset:v superfast -tune:v zerolatency",
"h265": "-c:v libx265 -g 50 -profile:v main -level:v 5.1 -preset:v superfast -tune:v zerolatency -pix_fmt:v yuv420p",
"mjpeg": "-c:v mjpeg",
//"mjpeg": "-c:v mjpeg -force_duplicated_matrix:v 1 -huffman:v 0 -pix_fmt:v yuvj420p",
"raw": "-c:v rawvideo",
"raw/gray8": "-c:v rawvideo -pix_fmt:v gray8",
"raw/yuv420p": "-c:v rawvideo -pix_fmt:v yuv420p",
"raw/yuv422p": "-c:v rawvideo -pix_fmt:v yuv422p",
"raw/yuv444p": "-c:v rawvideo -pix_fmt:v yuv444p",
// https://ffmpeg.org/ffmpeg-codecs.html#libopus-1
// https://github.com/pion/webrtc/issues/1514
// https://ffmpeg.org/ffmpeg-resampler.html
@@ -82,6 +113,7 @@ var defaults = map[string]string{
"pcm/48000": "-c:a pcm_s16be -ar:a 48000 -ac:a 1",
"pcml": "-c:a pcm_s16le -ar:a 8000 -ac:a 1",
"pcml/8000": "-c:a pcm_s16le -ar:a 8000 -ac:a 1",
"pcml/16000": "-c:a pcm_s16le -ar:a 16000 -ac:a 1",
"pcml/44100": "-c:a pcm_s16le -ar:a 44100 -ac:a 1",
// hardware Intel and AMD on Linux
@@ -98,8 +130,9 @@ var defaults = map[string]string{
// hardware Rockchip
// important to use custom ffmpeg https://github.com/AlexxIT/go2rtc/issues/768
// hevc - doesn't have a profile setting
"h264/rkmpp": "-c:v h264_rkmpp_encoder -g 50 -bf 0 -profile:v high -level:v 4.1",
"h265/rkmpp": "-c:v hevc_rkmpp_encoder -g 50 -bf 0 -level:v 5.1",
"h264/rkmpp": "-c:v h264_rkmpp -g 50 -bf 0 -profile:v high -level:v 4.1",
"h265/rkmpp": "-c:v hevc_rkmpp -g 50 -bf 0 -profile:v main -level:v 5.1",
"mjpeg/rkmpp": "-c:v mjpeg_rkmpp",
// hardware NVidia on Linux and Windows
// preset=p2 - faster, tune=ll - low latency
@@ -116,6 +149,8 @@ var defaults = map[string]string{
"h265/videotoolbox": "-c:v hevc_videotoolbox -g 50 -bf 0 -profile:v main -level:v 5.1",
}
var log zerolog.Logger
// configTemplate - return template from config (defaults) if exist or return raw template
func configTemplate(template string) string {
if s := defaults[template]; s != "" {
@@ -140,11 +175,13 @@ func inputTemplate(name, s string, query url.Values) string {
func parseArgs(s string) *ffmpeg.Args {
// init FFmpeg arguments
args := &ffmpeg.Args{
Bin: defaults["bin"],
Global: defaults["global"],
Output: defaults["output"],
Bin: defaults["bin"],
Global: defaults["global"],
Output: defaults["output"],
Version: verAV,
}
var source = s
var query url.Values
if i := strings.IndexByte(s, '#'); i >= 0 {
query = streams.ParseQuery(s[i+1:])
@@ -187,17 +224,19 @@ func parseArgs(s string) *ffmpeg.Args {
default:
s += "?video&audio"
}
args.Input = inputTemplate("rtsp", s, query)
} else if strings.HasPrefix(s, "device?") {
var err error
args.Input, err = device.GetInput(s)
if err != nil {
return nil
s += "&source=ffmpeg:" + url.QueryEscape(source)
for _, v := range query["query"] {
s += "&" + v
}
} else if strings.HasPrefix(s, "virtual?") {
var err error
if args.Input, err = virtual.GetInput(s[8:]); err != nil {
return nil
args.Input = inputTemplate("rtsp", s, query)
} else if i = strings.Index(s, "?"); i > 0 {
switch s[:i] {
case "device":
args.Input = device.GetInput(s[i+1:])
case "virtual":
args.Input = virtual.GetInput(s[i+1:])
case "tts":
args.Input = virtual.GetInputTTS(s[i+1:])
}
} else {
args.Input = inputTemplate("file", s, query)
@@ -280,6 +319,12 @@ func parseArgs(s string) *ffmpeg.Args {
}
}
if query["bitrate"] != nil {
// https://trac.ffmpeg.org/wiki/Limiting%20the%20output%20bitrate
b := query["bitrate"][0]
args.AddCodec("-b:v " + b + " -maxrate " + b + " -bufsize " + b)
}
// 4. Process audio codecs
if args.Audio > 0 {
for _, audio := range query["audio"] {
@@ -309,11 +354,27 @@ func parseArgs(s string) *ffmpeg.Args {
args.AddCodec("-an")
}
// transcoding to only mjpeg
if (args.Video == 1 && args.Audio == 0 && query.Get("video") == "mjpeg") ||
// no transcoding from mjpeg input
(args.Video == 0 && args.Audio == 0 && strings.Contains(args.Input, " mjpeg ")) {
args.Output = defaults["output/mjpeg"]
// change otput from RTSP to some other pipe format
switch {
case args.Video == 0 && args.Audio == 0:
// no transcoding from mjpeg input (ffmpeg device with support output as raw MJPEG)
if strings.Contains(args.Input, " mjpeg ") {
args.Output = defaults["output/mjpeg"]
}
case args.Video == 1 && args.Audio == 0:
switch core.Before(query.Get("video"), "/") {
case "mjpeg":
args.Output = defaults["output/mjpeg"]
case "raw":
args.Output = defaults["output/raw"]
}
case args.Video == 0 && args.Audio == 1:
switch core.Before(query.Get("audio"), "/") {
case "aac":
args.Output = defaults["output/aac"]
case "pcma", "pcmu", "pcml":
args.Output = defaults["output/wav"]
}
}
return args

View File

@@ -3,6 +3,7 @@ package ffmpeg
import (
"testing"
"github.com/AlexxIT/go2rtc/pkg/ffmpeg"
"github.com/stretchr/testify/require"
)
@@ -30,7 +31,7 @@ func TestParseArgsFile(t *testing.T) {
{
name: "[FILE] video will be transcoded to H265 and rotate 270º, audio will be skipped",
source: "/media/bbb.mp4#video=h265#rotate=-90",
expect: `ffmpeg -hide_banner -re -i /media/bbb.mp4 -c:v libx265 -g 50 -profile:v main -level:v 5.1 -preset:v superfast -tune:v zerolatency -an -vf "transpose=2" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
expect: `ffmpeg -hide_banner -re -i /media/bbb.mp4 -c:v libx265 -g 50 -profile:v main -level:v 5.1 -preset:v superfast -tune:v zerolatency -pix_fmt:v yuv420p -an -vf "transpose=2" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
},
{
name: "[FILE] video will be output for MJPEG to pipe, audio will be skipped",
@@ -52,85 +53,143 @@ func TestParseArgsFile(t *testing.T) {
}
func TestParseArgsDevice(t *testing.T) {
// [DEVICE] video will be output for MJPEG to pipe, with size 1920x1080
args := parseArgs("device?video=0&video_size=1920x1080")
require.Equal(t, `ffmpeg -hide_banner -f dshow -video_size 1920x1080 -i "video=0" -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
// [DEVICE] video will be transcoded to H265 with framerate 20, audio will be skipped
//args = parseArgs("device?video=0&video_size=1280x720&framerate=20#video=h265#audio=pcma")
args = parseArgs("device?video=0&framerate=20#video=h265")
require.Equal(t, `ffmpeg -hide_banner -f dshow -framerate 20 -i "video=0" -c:v libx265 -g 50 -profile:v main -level:v 5.1 -preset:v superfast -tune:v zerolatency -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
args = parseArgs("device?video=FaceTime HD Camera&audio=Microphone (High Definition Audio Device)")
require.Equal(t, `ffmpeg -hide_banner -f dshow -i "video=FaceTime HD Camera:audio=Microphone (High Definition Audio Device)" -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
tests := []struct {
name string
source string
expect string
}{
{
name: "[DEVICE] video will be output for MJPEG to pipe, with size 1920x1080",
source: "device?video=0&video_size=1920x1080",
expect: `ffmpeg -hide_banner -f dshow -video_size 1920x1080 -i "video=0" -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
},
{
name: "[DEVICE] video will be transcoded to H265 with framerate 20, audio will be skipped",
source: "device?video=0&framerate=20#video=h265",
expect: `ffmpeg -hide_banner -f dshow -framerate 20 -i "video=0" -c:v libx265 -g 50 -profile:v main -level:v 5.1 -preset:v superfast -tune:v zerolatency -pix_fmt:v yuv420p -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
},
{
name: "[DEVICE] video/audio",
source: "device?video=FaceTime HD Camera&audio=Microphone (High Definition Audio Device)",
expect: `ffmpeg -hide_banner -f dshow -i "video=FaceTime HD Camera:audio=Microphone (High Definition Audio Device)" -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
args := parseArgs(test.source)
require.Equal(t, test.expect, args.String())
})
}
}
func TestParseArgsIpCam(t *testing.T) {
// [HTTP] video will be copied
args := parseArgs("http://example.com")
require.Equal(t, `ffmpeg -hide_banner -fflags nobuffer -flags low_delay -i http://example.com -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
// [HTTP-MJPEG] video will be transcoded to H264
args = parseArgs("http://example.com#video=h264")
require.Equal(t, `ffmpeg -hide_banner -fflags nobuffer -flags low_delay -i http://example.com -c:v libx264 -g 50 -profile:v high -level:v 4.1 -preset:v superfast -tune:v zerolatency -pix_fmt:v yuv420p -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
// [HLS] video will be copied, audio will be skipped
args = parseArgs("https://example.com#video=copy")
require.Equal(t, `ffmpeg -hide_banner -fflags nobuffer -flags low_delay -i https://example.com -c:v copy -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
// [RTSP] video will be copied without transcoding codecs
args = parseArgs("rtsp://example.com")
require.Equal(t, `ffmpeg -hide_banner -allowed_media_types video+audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
// [RTSP] video with resize to 1280x720, should be transcoded, so select H265
args = parseArgs("rtsp://example.com#video=h265#width=1280#height=720")
require.Equal(t, `ffmpeg -hide_banner -allowed_media_types video -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:v libx265 -g 50 -profile:v main -level:v 5.1 -preset:v superfast -tune:v zerolatency -an -vf "scale=1280:720" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
// [RTSP] video will be copied, changing RTSP transport from TCP to UDP+TCP
args = parseArgs("rtsp://example.com#input=rtsp/udp")
require.Equal(t, `ffmpeg -hide_banner -allowed_media_types video+audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -i rtsp://example.com -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
// [RTMP] video will be copied, changing RTSP transport from TCP to UDP+TCP
args = parseArgs("rtmp://example.com#input=rtsp/udp")
require.Equal(t, `ffmpeg -hide_banner -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -i rtmp://example.com -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
tests := []struct {
name string
source string
expect string
}{
{
name: "[HTTP] video will be copied",
source: "http://example.com",
expect: `ffmpeg -hide_banner -fflags nobuffer -flags low_delay -i http://example.com -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
},
{
name: "[HTTP-MJPEG] video will be transcoded to H264",
source: "http://example.com#video=h264",
expect: `ffmpeg -hide_banner -fflags nobuffer -flags low_delay -i http://example.com -c:v libx264 -g 50 -profile:v high -level:v 4.1 -preset:v superfast -tune:v zerolatency -pix_fmt:v yuv420p -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
},
{
name: "[HLS] video will be copied, audio will be skipped",
source: "https://example.com#video=copy",
expect: `ffmpeg -hide_banner -fflags nobuffer -flags low_delay -i https://example.com -c:v copy -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
},
{
name: "[RTSP] video will be copied without transcoding codecs",
source: "rtsp://example.com",
expect: `ffmpeg -hide_banner -allowed_media_types video+audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
},
{
name: "[RTSP] video with resize to 1280x720, should be transcoded, so select H265",
source: "rtsp://example.com#video=h265#width=1280#height=720",
expect: `ffmpeg -hide_banner -allowed_media_types video -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:v libx265 -g 50 -profile:v main -level:v 5.1 -preset:v superfast -tune:v zerolatency -pix_fmt:v yuv420p -an -vf "scale=1280:720" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
},
{
name: "[RTSP] video will be copied, changing RTSP transport from TCP to UDP+TCP",
source: "rtsp://example.com#input=rtsp/udp",
expect: `ffmpeg -hide_banner -allowed_media_types video+audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -i rtsp://example.com -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
},
{
name: "[RTMP] video will be copied, changing RTSP transport from TCP to UDP+TCP",
source: "rtmp://example.com#input=rtsp/udp",
expect: `ffmpeg -hide_banner -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -i rtmp://example.com -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
args := parseArgs(test.source)
require.Equal(t, test.expect, args.String())
})
}
}
func TestParseArgsAudio(t *testing.T) {
// [AUDIO] audio will be transcoded to AAC, video will be skipped
args := parseArgs("rtsp:///example.com#audio=aac")
require.Equal(t, `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp:///example.com -c:a aac -vn -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
// [AUDIO] audio will be transcoded to AAC/16000, video will be skipped
args = parseArgs("rtsp:///example.com#audio=aac/16000")
require.Equal(t, `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp:///example.com -c:a aac -ar:a 16000 -ac:a 1 -vn -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
// [AUDIO] audio will be transcoded to OPUS, video will be skipped
args = parseArgs("rtsp:///example.com#audio=opus")
require.Equal(t, `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp:///example.com -c:a libopus -application:a lowdelay -min_comp 0 -vn -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
// [AUDIO] audio will be transcoded to PCMU, video will be skipped
args = parseArgs("rtsp:///example.com#audio=pcmu")
require.Equal(t, `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp:///example.com -c:a pcm_mulaw -ar:a 8000 -ac:a 1 -vn -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
// [AUDIO] audio will be transcoded to PCMU/16000, video will be skipped
args = parseArgs("rtsp:///example.com#audio=pcmu/16000")
require.Equal(t, `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp:///example.com -c:a pcm_mulaw -ar:a 16000 -ac:a 1 -vn -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
// [AUDIO] audio will be transcoded to PCMU/48000, video will be skipped
args = parseArgs("rtsp:///example.com#audio=pcmu/48000")
require.Equal(t, `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp:///example.com -c:a pcm_mulaw -ar:a 48000 -ac:a 1 -vn -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
// [AUDIO] audio will be transcoded to PCMA, video will be skipped
args = parseArgs("rtsp:///example.com#audio=pcma")
require.Equal(t, `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp:///example.com -c:a pcm_alaw -ar:a 8000 -ac:a 1 -vn -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
// [AUDIO] audio will be transcoded to PCMA/16000, video will be skipped
args = parseArgs("rtsp:///example.com#audio=pcma/16000")
require.Equal(t, `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp:///example.com -c:a pcm_alaw -ar:a 16000 -ac:a 1 -vn -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
// [AUDIO] audio will be transcoded to PCMA/48000, video will be skipped
args = parseArgs("rtsp:///example.com#audio=pcma/48000")
require.Equal(t, `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp:///example.com -c:a pcm_alaw -ar:a 48000 -ac:a 1 -vn -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
tests := []struct {
name string
source string
expect string
}{
{
name: "[AUDIO] audio will be transcoded to AAC, video will be skipped",
source: "rtsp://example.com#audio=aac",
expect: `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:a aac -vn -f adts -`,
},
{
name: "[AUDIO] audio will be transcoded to AAC/16000, video will be skipped",
source: "rtsp://example.com#audio=aac/16000",
expect: `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:a aac -ar:a 16000 -ac:a 1 -vn -f adts -`,
},
{
name: "[AUDIO] audio will be transcoded to OPUS, video will be skipped",
source: "rtsp://example.com#audio=opus",
expect: `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:a libopus -application:a lowdelay -min_comp 0 -vn -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
},
{
name: "[AUDIO] audio will be transcoded to PCMU, video will be skipped",
source: "rtsp://example.com#audio=pcmu",
expect: `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:a pcm_mulaw -ar:a 8000 -ac:a 1 -vn -f wav -`,
},
{
name: "[AUDIO] audio will be transcoded to PCMU/16000, video will be skipped",
source: "rtsp://example.com#audio=pcmu/16000",
expect: `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:a pcm_mulaw -ar:a 16000 -ac:a 1 -vn -f wav -`,
},
{
name: "[AUDIO] audio will be transcoded to PCMU/48000, video will be skipped",
source: "rtsp://example.com#audio=pcmu/48000",
expect: `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:a pcm_mulaw -ar:a 48000 -ac:a 1 -vn -f wav -`,
},
{
name: "[AUDIO] audio will be transcoded to PCMA, video will be skipped",
source: "rtsp://example.com#audio=pcma",
expect: `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:a pcm_alaw -ar:a 8000 -ac:a 1 -vn -f wav -`,
},
{
name: "[AUDIO] audio will be transcoded to PCMA/16000, video will be skipped",
source: "rtsp://example.com#audio=pcma/16000",
expect: `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:a pcm_alaw -ar:a 16000 -ac:a 1 -vn -f wav -`,
},
{
name: "[AUDIO] audio will be transcoded to PCMA/48000, video will be skipped",
source: "rtsp://example.com#audio=pcma/48000",
expect: `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:a pcm_alaw -ar:a 48000 -ac:a 1 -vn -f wav -`,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
args := parseArgs(test.source)
require.Equal(t, test.expect, args.String())
})
}
}
func TestParseArgsHwVaapi(t *testing.T) {
@@ -192,15 +251,33 @@ func _TestParseArgsHwV4l2m2m(t *testing.T) {
}
func TestParseArgsHwRKMPP(t *testing.T) {
// [HTTP-MJPEG] video will be transcoded to H264
args := parseArgs("http://example.com#video=h264#hardware=rkmpp")
require.Equal(t, `ffmpeg -hide_banner -fflags nobuffer -flags low_delay -i http://example.com -c:v h264_rkmpp_encoder -g 50 -bf 0 -profile:v high -level:v 4.1 -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
args = parseArgs("http://example.com#video=h264#rotate=180#hardware=rkmpp")
require.Equal(t, `ffmpeg -hide_banner -fflags nobuffer -flags low_delay -i http://example.com -c:v h264_rkmpp_encoder -g 50 -bf 0 -profile:v high -level:v 4.1 -an -vf "transpose=1,transpose=1" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
args = parseArgs("http://example.com#video=h264#height=320#hardware=rkmpp")
require.Equal(t, `ffmpeg -hide_banner -fflags nobuffer -flags low_delay -i http://example.com -c:v h264_rkmpp_encoder -g 50 -bf 0 -profile:v high -level:v 4.1 -height 320 -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
tests := []struct {
name string
source string
expect string
}{
{
name: "[FILE] transcoding to H264",
source: "bbb.mp4#video=h264#hardware=rkmpp",
expect: `ffmpeg -hide_banner -hwaccel rkmpp -hwaccel_output_format drm_prime -afbc rga -re -i bbb.mp4 -c:v h264_rkmpp -g 50 -bf 0 -profile:v high -level:v 4.1 -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
},
{
name: "[FILE] transcoding with rotation",
source: "bbb.mp4#video=h264#rotate=180#hardware=rkmpp",
expect: `ffmpeg -hide_banner -hwaccel rkmpp -hwaccel_output_format drm_prime -afbc rga -re -i bbb.mp4 -c:v h264_rkmpp -g 50 -bf 0 -profile:v high -level:v 4.1 -an -vf "format=drm_prime|nv12,hwupload,vpp_rkrga=transpose=4" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
},
{
name: "[FILE] transcoding with scaling",
source: "bbb.mp4#video=h264#height=320#hardware=rkmpp",
expect: `ffmpeg -hide_banner -hwaccel rkmpp -hwaccel_output_format drm_prime -afbc rga -re -i bbb.mp4 -c:v h264_rkmpp -g 50 -bf 0 -profile:v high -level:v 4.1 -an -vf "format=drm_prime|nv12,hwupload,scale_rkrga=-1:320:force_original_aspect_ratio=0" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
args := parseArgs(test.source)
require.Equal(t, test.expect, args.String())
})
}
}
func _TestParseArgsHwCuda(t *testing.T) {
@@ -292,3 +369,23 @@ func TestDrawText(t *testing.T) {
})
}
}
func TestVersion(t *testing.T) {
verAV = ffmpeg.Version61
tests := []struct {
name string
source string
expect string
}{
{
source: "/media/bbb.mp4",
expect: `ffmpeg -hide_banner -readrate_initial_burst 0.001 -re -i /media/bbb.mp4 -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
args := parseArgs(test.source)
require.Equal(t, test.expect, args.String())
})
}
}

View File

@@ -0,0 +1,106 @@
# Hardware
You **DON'T** need hardware acceleration if:
- you not using [FFmpeg source](https://github.com/AlexxIT/go2rtc#source-ffmpeg)
- you using only `#video=copy` for FFmpeg source
- you using only `#audio=...` (any audio) transcoding for FFmpeg source
You **NEED** hardware acceleration if you using `#video=h264`, `#video=h265`, `#video=mjpeg` (video) transcoding.
## Important
- Acceleration is disabled by default because it can be unstable (it can be changed in future)
- go2rtc can automatically detect supported hardware acceleration if enabled
- go2rtc will enable hardware decoding only if hardware encoding supported
- go2rtc will use the same GPU for decoder and encoder
- Intel and AMD will switch to software decoder if input codec is not supported with hardware decoder
- NVidia will fail if input codec is not supported with hardware decoder
- Raspberry always uses software decoder
```yaml
streams:
# auto select hardware encoder
camera1_hw: ffmpeg:rtsp://rtsp:12345678@192.168.1.123/av_stream/ch0#video=h264#hardware
# manual select hardware encoder (vaapi, cuda, v4l2m2m, dxva2, videotoolbox)
camera1_vaapi: ffmpeg:rtsp://rtsp:12345678@192.168.1.123/av_stream/ch0#video=h264#hardware=vaapi
```
## Docker and Hass Addon
There are two versions of the Docker container and Hass Add-on:
- Latest (alpine) support hardware acceleration for Intel iGPU (CPU with Graphics) and Raspberry.
- Hardware (debian 12) support Intel iGPU, AMD GPU, NVidia GPU.
## Intel iGPU
**Supported on:** Windows binary, Linux binary, Docker, Hass Addon.
If you have Intel CPU Sandy Bridge (2011) with Graphics, you already have support hardware decoding/encoding for `AVC/H.264`.
If you have Intel CPU Skylake (2015) with Graphics, you already have support hardware decoding/encoding for `AVC/H.264`, `HEVC/H.265` and `MJPEG`.
Read more [here](https://en.wikipedia.org/wiki/Intel_Quick_Sync_Video#Hardware_decoding_and_encoding) and [here](https://en.wikipedia.org/wiki/Intel_Graphics_Technology#Capabilities_(GPU_video_acceleration)).
Linux and Docker:
- It may be important to have the latest version of the OS with the latest version of the Linux kernel. For example, on my **Debian 10 (kernel 4.19)** it did not work, but after update to **Debian 11 (kernel 5.10)** all was fine.
- In case of troube check you have `/dev/dri/` folder on your host.
Docker users should add `--privileged` option to container for access to Hardware.
**PS.** Supported via [VAAPI](https://trac.ffmpeg.org/wiki/Hardware/VAAPI) engine on Linux and [DXVA2+QSV](https://trac.ffmpeg.org/wiki/Hardware/QuickSync) engine on Windows.
## AMD GPU
*I don't have the hardware for test support!!!*
**Supported on:** Linux binary, Docker, Hass Addon.
Docker users should install: `alexxit/go2rtc:master-hardware`. Docker users should add `--privileged` option to container for access to Hardware.
Hass Addon users should install **go2rtc master hardware** version.
**PS.** Supported via [VAAPI](https://trac.ffmpeg.org/wiki/Hardware/VAAPI) engine.
## NVidia GPU
**Supported on:** Windows binary, Linux binary, Docker.
Docker users should install: `alexxit/go2rtc:master-hardware`.
Read more [here](https://docs.frigate.video/configuration/hardware_acceleration) and [here](https://jellyfin.org/docs/general/administration/hardware-acceleration/#nvidia-hardware-acceleration-on-docker-linux).
**PS.** Supported via [CUDA](https://trac.ffmpeg.org/wiki/HWAccelIntro#CUDANVENCNVDEC) engine.
## Raspberry Pi 3
**Supported on:** Linux binary, Docker, Hass Addon.
I don't recommend using transcoding on the Raspberry Pi 3. It's extreamly slow, even with hardware acceleration. Also it may fail when transcoding 2K+ stream.
## Raspberry Pi 4
*I don't have the hardware for test support!!!*
**Supported on:** Linux binary, Docker, Hass Addon.
**PS.** Supported via [v4l2m2m](https://lalitm.com/hw-encoding-raspi/) engine.
## macOS
In my tests, transcoding is faster on the M1 CPU than on the M1 GPU. Transcoding time on M1 CPU better than any Intel iGPU and comparable to NVidia RTX 2070.
**PS.** Supported via [videotoolbox](https://trac.ffmpeg.org/wiki/HWAccelIntro#VideoToolbox) engine.
## Rockchip
- Important to use custom FFmpeg with Rockchip support from [@nyanmisaka](https://github.com/nyanmisaka/ffmpeg-rockchip)
- Static binaries from [@MarcA711](https://github.com/MarcA711/Rockchip-FFmpeg-Builds/releases/)
- Important to have Linux kernel 5.10 or 6.1
**Tested**
- [Orange Pi 3B](https://www.armbian.com/orangepi3b/) with Armbian 6.1, support transcoding H264, H265, MJPEG

View File

@@ -7,8 +7,6 @@ import (
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/pkg/ffmpeg"
"github.com/rs/zerolog/log"
)
const (
@@ -130,19 +128,32 @@ func MakeHardware(args *ffmpeg.Args, engine string, defaults map[string]string)
case EngineRKMPP:
args.Codecs[i] = defaults[name+"/"+engine]
for j, filter := range args.Filters {
if strings.HasPrefix(filter, "scale=") {
args.Filters = append(args.Filters[:j], args.Filters[j+1:]...)
if !args.HasFilters("drawtext=") {
args.Input = "-hwaccel rkmpp -hwaccel_output_format drm_prime -afbc rga " + args.Input
width, height, _ := strings.Cut(filter[6:], ":")
if width != "-1" {
args.Codecs[i] += " -width " + width
for i, filter := range args.Filters {
if strings.HasPrefix(filter, "scale=") {
args.Filters[i] = "scale_rkrga=" + filter[6:] + ":force_original_aspect_ratio=0"
}
if height != "-1" {
args.Codecs[i] += " -height " + height
if strings.HasPrefix(filter, "transpose=") {
if filter == "transpose=1,transpose=1" { // 180 degrees half-turn
args.Filters[i] = "vpp_rkrga=transpose=4" // reversal
} else {
args.Filters[i] = "vpp_rkrga=transpose=" + filter[10:]
}
}
break
}
if len(args.Filters) > 0 {
// fix if input doesn't support hwaccel, do nothing when support
// insert as first filter before hardware scale and transpose
args.InsertFilter("format=drm_prime|nv12,hwupload")
}
} else {
// enable software pixel for drawtext, scale and transpose
args.Input = "-hwaccel rkmpp -hwaccel_output_format nv12 -afbc rga " + args.Input
args.AddFilter("hwupload")
}
}
}
@@ -152,7 +163,6 @@ 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
}

View File

@@ -1,3 +1,5 @@
//go:build freebsd || netbsd || openbsd || dragonfly
package hardware
import (

View File

@@ -1,3 +1,5 @@
//go:build darwin || ios
package hardware
import (

View File

@@ -1,3 +1,5 @@
//go:build unix && !darwin && !freebsd && !netbsd && !openbsd && !dragonfly
package hardware
import (
@@ -9,8 +11,9 @@ import (
const (
ProbeV4L2M2MH264 = "-f lavfi -i testsrc2 -t 1 -c h264_v4l2m2m -f null -"
ProbeV4L2M2MH265 = "-f lavfi -i testsrc2 -t 1 -c hevc_v4l2m2m -f null -"
ProbeRKMPPH264 = "-f lavfi -i testsrc2 -t 1 -c h264_rkmpp_encoder -f null -"
ProbeRKMPPH265 = "-f lavfi -i testsrc2 -t 1 -c hevc_rkmpp_encoder -f null -"
ProbeRKMPPH264 = "-f lavfi -i testsrc2 -t 1 -c h264_rkmpp -f null -"
ProbeRKMPPH265 = "-f lavfi -i testsrc2 -t 1 -c hevc_rkmpp -f null -"
ProbeRKMPPJPEG = "-f lavfi -i testsrc2 -t 1 -c mjpeg_rkmpp -f null -"
ProbeVAAPIH264 = "-init_hw_device vaapi -f lavfi -i testsrc2 -t 1 -vf format=nv12,hwupload -c h264_vaapi -f null -"
ProbeVAAPIH265 = "-init_hw_device vaapi -f lavfi -i testsrc2 -t 1 -vf format=nv12,hwupload -c hevc_vaapi -f null -"
ProbeVAAPIJPEG = "-init_hw_device vaapi -f lavfi -i testsrc2 -t 1 -vf format=nv12,hwupload -c mjpeg_vaapi -f null -"
@@ -37,6 +40,10 @@ func ProbeAll(bin string) []*api.Source {
Name: runToString(bin, ProbeRKMPPH265),
URL: "ffmpeg:...#video=h265#hardware=" + EngineRKMPP,
},
{
Name: runToString(bin, ProbeRKMPPJPEG),
URL: "ffmpeg:...#video=mjpeg#hardware=" + EngineRKMPP,
},
}
}
@@ -81,6 +88,10 @@ func ProbeHardware(bin, name string) string {
if run(bin, ProbeRKMPPH265) {
return EngineRKMPP
}
case "mjpeg":
if run(bin, ProbeRKMPPJPEG) {
return EngineRKMPP
}
}
return EngineSoftware

View File

@@ -1,3 +1,5 @@
//go:build windows
package hardware
import "github.com/AlexxIT/go2rtc/internal/api"

121
internal/ffmpeg/producer.go Normal file
View File

@@ -0,0 +1,121 @@
package ffmpeg
import (
"encoding/json"
"errors"
"net/url"
"strconv"
"strings"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/aac"
"github.com/AlexxIT/go2rtc/pkg/core"
)
type Producer struct {
core.Connection
url string
query url.Values
ffmpeg core.Producer
}
// NewProducer - FFmpeg producer with auto selection video/audio codec based on client capabilities
func NewProducer(url string) (core.Producer, error) {
p := &Producer{}
i := strings.IndexByte(url, '#')
p.url, p.query = url[:i], streams.ParseQuery(url[i+1:])
// ffmpeg.NewProducer support only one audio
if len(p.query["video"]) != 0 || len(p.query["audio"]) != 1 {
return nil, errors.New("ffmpeg: unsupported params: " + url[i:])
}
p.ID = core.NewID()
p.FormatName = "ffmpeg"
p.Medias = []*core.Media{
{
// we can support only audio, because don't know FmtpLine for H264 and PayloadType for MJPEG
Kind: core.KindAudio,
Direction: core.DirectionRecvonly,
// codecs in order from best to worst
Codecs: []*core.Codec{
// OPUS will always marked as OPUS/48000/2
{Name: core.CodecOpus, ClockRate: 48000, Channels: 2},
{Name: core.CodecPCML, ClockRate: 16000},
{Name: core.CodecPCM, ClockRate: 16000},
{Name: core.CodecPCMA, ClockRate: 16000},
{Name: core.CodecPCMU, ClockRate: 16000},
{Name: core.CodecPCM, ClockRate: 8000},
{Name: core.CodecPCMA, ClockRate: 8000},
{Name: core.CodecPCMU, ClockRate: 8000},
// AAC has unknown problems on Dahua two way
{Name: core.CodecAAC, ClockRate: 16000, FmtpLine: aac.FMTP + "1408"},
},
},
}
return p, nil
}
func (p *Producer) Start() error {
var err error
if p.ffmpeg, err = streams.GetProducer(p.newURL()); err != nil {
return err
}
for i, media := range p.ffmpeg.GetMedias() {
track, err := p.ffmpeg.GetTrack(media, media.Codecs[0])
if err != nil {
return err
}
p.Receivers[i].Replace(track)
}
return p.ffmpeg.Start()
}
func (p *Producer) Stop() error {
if p.ffmpeg == nil {
return nil
}
return p.ffmpeg.Stop()
}
func (p *Producer) MarshalJSON() ([]byte, error) {
if p.ffmpeg == nil {
return json.Marshal(p.Connection)
}
return json.Marshal(p.ffmpeg)
}
func (p *Producer) newURL() string {
s := p.url
// rewrite codecs in url from auto to known presets from defaults
for _, receiver := range p.Receivers {
codec := receiver.Codec
switch codec.Name {
case core.CodecOpus:
s += "#audio=opus"
case core.CodecAAC:
s += "#audio=aac/16000"
case core.CodecPCML:
s += "#audio=pcml/16000"
case core.CodecPCM:
s += "#audio=pcm/" + strconv.Itoa(int(codec.ClockRate))
case core.CodecPCMA:
s += "#audio=pcma/" + strconv.Itoa(int(codec.ClockRate))
case core.CodecPCMU:
s += "#audio=pcmu/" + strconv.Itoa(int(codec.ClockRate))
}
}
// add other params
for key, values := range p.query {
if key != "audio" {
for _, value := range values {
s += "#" + key + "=" + value
}
}
}
return s
}

View File

@@ -0,0 +1,46 @@
package ffmpeg
import (
"errors"
"os/exec"
"sync"
"github.com/AlexxIT/go2rtc/pkg/ffmpeg"
)
var verMu sync.Mutex
var verErr error
var verFF string
var verAV string
func Version() (string, error) {
verMu.Lock()
defer verMu.Unlock()
if verFF != "" {
return verFF, verErr
}
cmd := exec.Command(defaults["bin"], "-version")
b, err := cmd.Output()
if err != nil {
verFF = "-"
verErr = err
return verFF, verErr
}
verFF, verAV = ffmpeg.ParseVersion(b)
if verFF == "" {
verFF = "?"
}
// better to compare libavformat, because nightly/master builds
if verAV != "" && verAV < ffmpeg.Version50 {
verErr = errors.New("ffmpeg: unsupported version: " + verFF)
}
log.Debug().Str("version", verFF).Str("libavformat", verAV).Msgf("[ffmpeg] bin")
return verFF, verErr
}

View File

@@ -4,56 +4,76 @@ import (
"net/url"
)
func GetInput(src string) (string, error) {
func GetInput(src string) string {
query, err := url.ParseQuery(src)
if err != nil {
return "", err
return ""
}
// set defaults (using Add instead of Set)
query.Add("source", "testsrc")
query.Add("size", "1920x1080")
query.Add("decimals", "2")
input := "-re"
// https://ffmpeg.org/ffmpeg-filters.html
source := query.Get("source")
input := "-re -f lavfi -i " + source
for _, video := range query["video"] {
// https://ffmpeg.org/ffmpeg-filters.html
sep := "=" // first separator
sep := "=" // first separator
for key, values := range query {
value := values[0]
// https://ffmpeg.org/ffmpeg-utils.html#video-size-syntax
switch key {
case "color", "rate", "duration", "sar":
case "size":
switch value {
case "720":
value = "1280x720"
case "1080":
value = "1920x1080"
case "2K":
value = "2560x1440"
case "4K":
value = "3840x2160"
case "8K":
value = "7680x4230" // https://reolink.com/blog/8k-resolution/
}
case "decimals":
if source != "testsrc" {
continue
}
default:
continue
if video == "" {
video = "testsrc=decimals=2" // default video
sep = ":"
}
input += sep + key + "=" + value
sep = ":" // next separator
input += " -f lavfi -i " + video
// set defaults (using Add instead of Set)
query.Add("size", "1920x1080")
for key, values := range query {
value := values[0]
// https://ffmpeg.org/ffmpeg-utils.html#video-size-syntax
switch key {
case "color", "rate", "duration", "sar", "decimals":
case "size":
switch value {
case "720":
value = "1280x720" // crf=1 -> 12 Mbps
case "1080":
value = "1920x1080" // crf=1 -> 25 Mbps
case "2K":
value = "2560x1440" // crf=1 -> 43 Mbps
case "4K":
value = "3840x2160" // crf=1 -> 103 Mbps
case "8K":
value = "7680x4230" // https://reolink.com/blog/8k-resolution/
}
default:
continue
}
input += sep + key + "=" + value
sep = ":" // next separator
}
if s := query.Get("format"); s != "" {
input += ",format=" + s
}
}
if s := query.Get("format"); s != "" {
input += ",format=" + s
}
return input, nil
return input
}
func GetInputTTS(src string) string {
query, err := url.ParseQuery(src)
if err != nil {
return ""
}
input := `-re -f lavfi -i "flite=text='` + query.Get("text") + `'`
// ffmpeg -f lavfi -i flite=list_voices=1
// awb, kal, kal16, rms, slt
if voice := query.Get("voice"); voice != "" {
input += ":voice" + voice
}
return input + `"`
}

View File

@@ -0,0 +1,20 @@
package virtual
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestGetInput(t *testing.T) {
s := GetInput("video")
require.Equal(t, "-re -f lavfi -i testsrc=decimals=2:size=1920x1080", s)
s = GetInput("video=testsrc2&size=4K")
require.Equal(t, "-re -f lavfi -i testsrc2=size=3840x2160", s)
}
func TestGetInputTTS(t *testing.T) {
s := GetInputTTS("text=hello world&voice=slt")
require.Equal(t, `-re -f lavfi -i "flite=text='hello world':voiceslt"`, s)
}

View File

@@ -0,0 +1,10 @@
package flussonic
import (
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/flussonic"
)
func Init() {
streams.HandleFunc("flussonic", flussonic.Dial)
}

View File

@@ -10,15 +10,13 @@ import (
)
func Init() {
streams.HandleFunc("gopro", handleGoPro)
streams.HandleFunc("gopro", func(source string) (core.Producer, error) {
return gopro.Dial(source)
})
api.HandleFunc("api/gopro", apiGoPro)
}
func handleGoPro(rawURL string) (core.Producer, error) {
return gopro.Dial(rawURL)
}
func apiGoPro(w http.ResponseWriter, r *http.Request) {
var items []*api.Source

View File

@@ -63,7 +63,7 @@ func apiStream(w http.ResponseWriter, r *http.Request) {
return
}
s, err = webrtc.ExchangeSDP(stream, string(offer), "WebRTC/Hass sync", r.UserAgent())
s, err = webrtc.ExchangeSDP(stream, string(offer), "hass/webrtc", r.UserAgent())
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return

View File

@@ -21,7 +21,7 @@ import (
func Init() {
var conf struct {
API struct {
Listen string `json:"listen"`
Listen string `yaml:"listen"`
} `yaml:"api"`
Mod struct {
Config string `yaml:"config"`
@@ -45,19 +45,14 @@ func Init() {
return "", nil
})
streams.HandleFunc("hass", func(url string) (core.Producer, error) {
streams.HandleFunc("hass", func(source string) (core.Producer, error) {
// support hass://supervisor?entity_id=camera.driveway_doorbell
client, err := hass.NewClient(url)
if err != nil {
return nil, err
}
return client, nil
return hass.NewClient(source)
})
// load static entries from Hass config
if err := importConfig(conf.Mod.Config); err != nil {
log.Debug().Msgf("[hass] can't import config: %s", err)
log.Trace().Msgf("[hass] can't import config: %s", err)
api.HandleFunc("api/hass", func(w http.ResponseWriter, _ *http.Request) {
http.Error(w, "no hass config", http.StatusNotFound)

View File

@@ -12,7 +12,6 @@ import (
"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"
)
@@ -63,15 +62,13 @@ func handlerStream(w http.ResponseWriter, r *http.Request) {
medias := mp4.ParseQuery(r.URL.Query())
if medias != nil {
c := mp4.NewConsumer(medias)
c.Type = "HLS/fMP4 consumer"
c.RemoteAddr = tcp.RemoteAddr(r)
c.UserAgent = r.UserAgent()
c.FormatName = "hls/fmp4"
c.WithRequest(r)
cons = c
} else {
c := mpegts.NewConsumer()
c.Type = "HLS/TS consumer"
c.RemoteAddr = tcp.RemoteAddr(r)
c.UserAgent = r.UserAgent()
c.FormatName = "hls/mpegts"
c.WithRequest(r)
cons = c
}

View File

@@ -8,7 +8,6 @@ import (
"github.com/AlexxIT/go2rtc/internal/api/ws"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/mp4"
"github.com/AlexxIT/go2rtc/pkg/tcp"
)
func handlerWSHLS(tr *ws.Transport, msg *ws.Message) error {
@@ -20,9 +19,8 @@ func handlerWSHLS(tr *ws.Transport, msg *ws.Message) error {
codecs := msg.String()
medias := mp4.ParseCodecs(codecs, true)
cons := mp4.NewConsumer(medias)
cons.Type = "HLS/fMP4 consumer"
cons.RemoteAddr = tcp.RemoteAddr(tr.Request)
cons.UserAgent = tr.Request.UserAgent()
cons.FormatName = "hls/fmp4"
cons.WithRequest(tr.Request)
log.Trace().Msgf("[hls] new ws consumer codecs=%s", codecs)

View File

@@ -103,7 +103,7 @@ func apiPair(id, url string) error {
streams.New(id, conn.URL())
return app.PatchConfig(id, conn.URL(), "streams")
return app.PatchConfig([]string{"streams", id}, conn.URL())
}
func apiUnpair(id string) error {
@@ -112,7 +112,7 @@ func apiUnpair(id string) error {
return errors.New(api.StreamNotFound)
}
rawURL := findHomeKitURL(stream)
rawURL := findHomeKitURL(stream.Sources())
if rawURL == "" {
return errors.New("not homekit source")
}
@@ -123,15 +123,15 @@ func apiUnpair(id string) error {
streams.Delete(id)
return app.PatchConfig(id, nil, "streams")
return app.PatchConfig([]string{"streams", id}, nil)
}
func findHomeKitURLs() map[string]*url.URL {
urls := map[string]*url.URL{}
for id, stream := range streams.Streams() {
if rawURL := findHomeKitURL(stream); rawURL != "" {
for name, sources := range streams.GetAllSources() {
if rawURL := findHomeKitURL(sources); rawURL != "" {
if u, err := url.Parse(rawURL); err == nil {
urls[id] = u
urls[name] = u
}
}
}

View File

@@ -22,12 +22,11 @@ import (
func Init() {
var cfg struct {
Mod map[string]struct {
Pin string `json:"pin"`
Name string `json:"name"`
DeviceID string `json:"device_id"`
DevicePrivate string `json:"device_private"`
Pairings []string `json:"pairings"`
//Listen string `json:"listen"`
Pin string `yaml:"pin"`
Name string `yaml:"name"`
DeviceID string `yaml:"device_id"`
DevicePrivate string `yaml:"device_private"`
Pairings []string `yaml:"pairings"`
} `yaml:"homekit"`
}
app.LoadConfig(&cfg)
@@ -80,7 +79,7 @@ func Init() {
Handler: homekit.ServerHandler(srv),
}
if url := findHomeKitURL(stream); url != "" {
if url := findHomeKitURL(stream.Sources()); url != "" {
// 1. Act as transparent proxy for HomeKit camera
dial := func() (net.Conn, error) {
client, err := homekit.Dial(url, srtp.Server)
@@ -119,8 +118,8 @@ func Init() {
servers[host] = srv
}
api.HandleFunc(hap.PathPairSetup, hapPairSetup)
api.HandleFunc(hap.PathPairVerify, hapPairVerify)
api.HandleFunc(hap.PathPairSetup, hapHandler)
api.HandleFunc(hap.PathPairVerify, hapHandler)
log.Trace().Msgf("[homekit] mdns: %s", entries)
@@ -134,21 +133,34 @@ func Init() {
var log zerolog.Logger
var servers map[string]*server
func streamHandler(url string) (core.Producer, error) {
func streamHandler(rawURL string) (core.Producer, error) {
if srtp.Server == nil {
return nil, errors.New("homekit: can't work without SRTP server")
}
return homekit.Dial(url, srtp.Server)
}
func hapPairSetup(w http.ResponseWriter, r *http.Request) {
srv, ok := servers[r.Host]
if !ok {
log.Error().Msg("[homekit] unknown host: " + r.Host)
return
rawURL, rawQuery, _ := strings.Cut(rawURL, "#")
client, err := homekit.Dial(rawURL, srtp.Server)
if client != nil && rawQuery != "" {
query := streams.ParseQuery(rawQuery)
client.Bitrate = parseBitrate(query.Get("bitrate"))
}
return client, err
}
func resolve(host string) *server {
if len(servers) == 1 {
for _, srv := range servers {
return srv
}
}
if srv, ok := servers[host]; ok {
return srv
}
return nil
}
func hapHandler(w http.ResponseWriter, r *http.Request) {
conn, rw, err := w.(http.Hijacker).Hijack()
if err != nil {
return
@@ -156,32 +168,29 @@ func hapPairSetup(w http.ResponseWriter, r *http.Request) {
defer conn.Close()
if err = srv.hap.PairSetup(r, rw, conn); err != nil {
log.Error().Err(err).Caller().Send()
}
}
func hapPairVerify(w http.ResponseWriter, r *http.Request) {
srv, ok := servers[r.Host]
if !ok {
// Can support multiple HomeKit cameras on single port ONLY for Apple devices.
// Doesn't support Home Assistant and any other open source projects
// because they don't send the host header in requests.
srv := resolve(r.Host)
if srv == nil {
log.Error().Msg("[homekit] unknown host: " + r.Host)
_ = hap.WriteBackoff(rw)
return
}
conn, rw, err := w.(http.Hijacker).Hijack()
if err != nil {
return
switch r.RequestURI {
case hap.PathPairSetup:
err = srv.hap.PairSetup(r, rw, conn)
case hap.PathPairVerify:
err = srv.hap.PairVerify(r, rw, conn)
}
defer conn.Close()
if err = srv.hap.PairVerify(r, rw, conn); err != nil && err != io.EOF {
if err != nil && err != io.EOF {
log.Error().Err(err).Caller().Send()
}
}
func findHomeKitURL(stream *streams.Stream) string {
sources := stream.Sources()
func findHomeKitURL(sources []string) string {
if len(sources) == 0 {
return ""
}
@@ -200,3 +209,24 @@ func findHomeKitURL(stream *streams.Stream) string {
return ""
}
func parseBitrate(s string) int {
n := len(s)
if n == 0 {
return 0
}
var k int
switch n--; s[n] {
case 'K':
k = 1024
s = s[:n]
case 'M':
k = 1024 * 1024
s = s[:n]
default:
k = 1
}
return k * core.Atoi(s)
}

View File

@@ -87,7 +87,7 @@ func (s *server) SetCharacteristic(conn net.Conn, aid uint8, iid uint64, value a
switch char.Type {
case camera.TypeSetupEndpoints:
var offer camera.SetupEndpoints
if err := tlv8.UnmarshalBase64(value.(string), &offer); err != nil {
if err := tlv8.UnmarshalBase64(value, &offer); err != nil {
return
}
@@ -96,7 +96,7 @@ func (s *server) SetCharacteristic(conn net.Conn, aid uint8, iid uint64, value a
case camera.TypeSelectedStreamConfiguration:
var conf camera.SelectedStreamConfig
if err := tlv8.UnmarshalBase64(value.(string), &conf); err != nil {
if err := tlv8.UnmarshalBase64(value, &conf); err != nil {
return
}
@@ -222,7 +222,7 @@ func (s *server) DelPair(conn net.Conn, id string) {
}
func (s *server) PatchConfig() {
if err := app.PatchConfig("pairings", s.pairings, "homekit", s.stream); err != nil {
if err := app.PatchConfig([]string{"homekit", s.stream, "pairings"}, s.pairings); err != nil {
log.Error().Err(err).Msgf(
"[homekit] can't save %s pairings=%v", s.stream, s.pairings,
)

View File

@@ -11,9 +11,10 @@ import (
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/hls"
"github.com/AlexxIT/go2rtc/pkg/image"
"github.com/AlexxIT/go2rtc/pkg/magic"
"github.com/AlexxIT/go2rtc/pkg/mjpeg"
"github.com/AlexxIT/go2rtc/pkg/multipart"
"github.com/AlexxIT/go2rtc/pkg/mpjpeg"
"github.com/AlexxIT/go2rtc/pkg/pcm"
"github.com/AlexxIT/go2rtc/pkg/tcp"
)
@@ -45,6 +46,21 @@ func handleHTTP(rawURL string) (core.Producer, error) {
}
}
prod, err := do(req)
if err != nil {
return nil, err
}
if info, ok := prod.(core.Info); ok {
info.SetProtocol("http")
info.SetRemoteAddr(req.URL.Host) // TODO: rewrite to net.Conn
info.SetURL(rawURL)
}
return prod, nil
}
func do(req *http.Request) (core.Producer, error) {
res, err := tcp.Do(req)
if err != nil {
return nil, err
@@ -66,14 +82,15 @@ func handleHTTP(rawURL string) (core.Producer, error) {
}
switch {
case ct == "image/jpeg":
return mjpeg.NewClient(res), nil
case ct == "multipart/x-mixed-replace":
return multipart.Open(res.Body)
case ct == "application/vnd.apple.mpegurl" || ext == "m3u8":
return hls.OpenURL(req.URL, res.Body)
case ct == "image/jpeg":
return image.Open(res)
case ct == "multipart/x-mixed-replace":
return mpjpeg.Open(res.Body)
//https://www.iana.org/assignments/media-types/audio/basic
case ct == "audio/basic":
return pcm.Open(res.Body)
}
return magic.Open(res.Body)

View File

@@ -7,16 +7,7 @@ import (
)
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
streams.HandleFunc("isapi", func(source string) (core.Producer, error) {
return isapi.Dial(source)
})
}

View File

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

38
internal/mjpeg/README.md Normal file
View File

@@ -0,0 +1,38 @@
## Stream as ASCII to Terminal
[![](https://img.youtube.com/vi/sHj_3h_sX7M/mqdefault.jpg)](https://www.youtube.com/watch?v=sHj_3h_sX7M)
**Tips**
- this feature works only with MJPEG codec (use transcoding)
- choose a low frame rate (FPS)
- choose the width and height to fit in your terminal
- different terminals support different numbers of colours (8, 256, rgb)
- escape text param with urlencode
- you can stream any camera or file from a disc
**go2rtc.yaml** - transcoding to MJPEG, terminal size - 210x59 (16/9), fps - 10
```yaml
streams:
gamazda: ffmpeg:gamazda.mp4#video=mjpeg#hardware#width=210#height=59#raw=-r 10
```
**API params**
- `color` - foreground color, values: empty, `8`, `256`, `rgb`, [SGR](https://en.wikipedia.org/wiki/ANSI_escape_code)
- example: `30` (black), `37` (white), `38;5;226` (yellow)
- `back` - background color, values: empty, `8`, `256`, `rgb`, [SGR](https://en.wikipedia.org/wiki/ANSI_escape_code)
- example: `40` (black), `47` (white), `48;5;226` (yellow)
- `text` - character set, values: empty, one char, `block`, list of chars (in order of brightness)
- example: `%20` (space), `block` (keyword for block elements), `ox` (two chars)
**Examples**
```bash
% curl "http://192.168.1.123:1984/api/stream.ascii?src=gamazda"
% curl "http://192.168.1.123:1984/api/stream.ascii?src=gamazda&color=256"
% curl "http://192.168.1.123:1984/api/stream.ascii?src=gamazda&back=256&text=%20"
% curl "http://192.168.1.123:1984/api/stream.ascii?src=gamazda&back=8&text=%20%20"
% curl "http://192.168.1.123:1984/api/stream.ascii?src=gamazda&text=helloworld"
```

View File

@@ -5,26 +5,36 @@ import (
"io"
"net/http"
"strconv"
"strings"
"time"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/api/ws"
"github.com/AlexxIT/go2rtc/internal/app"
"github.com/AlexxIT/go2rtc/internal/ffmpeg"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/ascii"
"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"
"github.com/AlexxIT/go2rtc/pkg/mpjpeg"
"github.com/AlexxIT/go2rtc/pkg/y4m"
"github.com/rs/zerolog"
)
func Init() {
api.HandleFunc("api/frame.jpeg", handlerKeyframe)
api.HandleFunc("api/stream.mjpeg", handlerStream)
api.HandleFunc("api/stream.ascii", handlerStream)
api.HandleFunc("api/stream.y4m", apiStreamY4M)
ws.HandleFunc("mjpeg", handlerWS)
log = app.GetLogger("mjpeg")
}
var log zerolog.Logger
func handlerKeyframe(w http.ResponseWriter, r *http.Request) {
src := r.URL.Query().Get("src")
stream := streams.Get(src)
@@ -34,8 +44,7 @@ func handlerKeyframe(w http.ResponseWriter, r *http.Request) {
}
cons := magic.NewKeyframe()
cons.RemoteAddr = tcp.RemoteAddr(r)
cons.UserAgent = r.UserAgent()
cons.WithRequest(r)
if err := stream.AddConsumer(cons); err != nil {
log.Error().Err(err).Caller().Send()
@@ -90,8 +99,7 @@ func outputMjpeg(w http.ResponseWriter, r *http.Request) {
}
cons := mjpeg.NewConsumer()
cons.RemoteAddr = tcp.RemoteAddr(r)
cons.UserAgent = r.UserAgent()
cons.WithRequest(r)
if err := stream.AddConsumer(cons); err != nil {
log.Error().Err(err).Msg("[api.mjpeg] add consumer")
@@ -99,38 +107,22 @@ func outputMjpeg(w http.ResponseWriter, r *http.Request) {
}
h := w.Header()
h.Set("Content-Type", "multipart/x-mixed-replace; boundary=frame")
h.Set("Cache-Control", "no-cache")
h.Set("Connection", "close")
h.Set("Pragma", "no-cache")
wr := &writer{wr: w, buf: []byte(header)}
_, _ = cons.WriteTo(wr)
if strings.HasSuffix(r.URL.Path, "mjpeg") {
wr := mjpeg.NewWriter(w)
_, _ = cons.WriteTo(wr)
} else {
cons.FormatName = "ascii"
stream.RemoveConsumer(cons)
}
const header = "--frame\r\nContent-Type: image/jpeg\r\nContent-Length: "
type writer struct {
wr io.Writer
buf []byte
}
func (w *writer) Write(p []byte) (n int, err error) {
w.buf = w.buf[:len(header)]
w.buf = append(w.buf, strconv.Itoa(len(p))...)
w.buf = append(w.buf, "\r\n\r\n"...)
w.buf = append(w.buf, p...)
w.buf = append(w.buf, "\r\n"...)
// Chrome bug: mjpeg image always shows the second to last image
// https://bugs.chromium.org/p/chromium/issues/detail?id=527446
if n, err = w.wr.Write(w.buf); err == nil {
w.wr.(http.Flusher).Flush()
query := r.URL.Query()
wr := ascii.NewWriter(w, query.Get("color"), query.Get("back"), query.Get("text"))
_, _ = cons.WriteTo(wr)
}
return
stream.RemoveConsumer(cons)
}
func inputMjpeg(w http.ResponseWriter, r *http.Request) {
@@ -141,17 +133,16 @@ func inputMjpeg(w http.ResponseWriter, r *http.Request) {
return
}
res := &http.Response{Body: r.Body, Header: r.Header, Request: r}
res.Header.Set("Content-Type", "multipart/mixed;boundary=")
prod, _ := mpjpeg.Open(r.Body)
prod.WithRequest(r)
client := mjpeg.NewClient(res)
stream.AddProducer(client)
stream.AddProducer(prod)
if err := client.Start(); err != nil && err != io.EOF {
if err := prod.Start(); err != nil && err != io.EOF {
log.Warn().Err(err).Caller().Send()
}
stream.RemoveProducer(client)
stream.RemoveProducer(prod)
}
func handlerWS(tr *ws.Transport, _ *ws.Message) error {
@@ -161,8 +152,7 @@ func handlerWS(tr *ws.Transport, _ *ws.Message) error {
}
cons := mjpeg.NewConsumer()
cons.RemoteAddr = tcp.RemoteAddr(tr.Request)
cons.UserAgent = tr.Request.UserAgent()
cons.WithRequest(tr.Request)
if err := stream.AddConsumer(cons); err != nil {
log.Debug().Err(err).Msg("[mjpeg] add consumer")
@@ -179,3 +169,24 @@ func handlerWS(tr *ws.Transport, _ *ws.Message) error {
return nil
}
func apiStreamY4M(w http.ResponseWriter, r *http.Request) {
src := r.URL.Query().Get("src")
stream := streams.Get(src)
if stream == nil {
http.Error(w, api.StreamNotFound, http.StatusNotFound)
return
}
cons := y4m.NewConsumer()
cons.WithRequest(r)
if err := stream.AddConsumer(cons); err != nil {
log.Error().Err(err).Caller().Send()
return
}
_, _ = cons.WriteTo(w)
stream.RemoveConsumer(cons)
}

View File

@@ -1,6 +1,7 @@
package mp4
import (
"context"
"net/http"
"strconv"
"strings"
@@ -12,7 +13,6 @@ import (
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/mp4"
"github.com/AlexxIT/go2rtc/pkg/tcp"
"github.com/rs/zerolog"
)
@@ -99,9 +99,9 @@ func handlerMP4(w http.ResponseWriter, r *http.Request) {
medias := mp4.ParseQuery(r.URL.Query())
cons := mp4.NewConsumer(medias)
cons.Type = "MP4/HTTP active consumer"
cons.RemoteAddr = tcp.RemoteAddr(r)
cons.UserAgent = r.UserAgent()
cons.FormatName = "mp4"
cons.Protocol = "http"
cons.WithRequest(r)
if err := stream.AddConsumer(cons); err != nil {
log.Error().Err(err).Caller().Send()
@@ -127,20 +127,20 @@ func handlerMP4(w http.ResponseWriter, r *http.Request) {
header.Set("Content-Disposition", `attachment; filename="`+filename+`"`)
}
var duration *time.Timer
if s := query.Get("duration"); s != "" {
if i, _ := strconv.Atoi(s); i > 0 {
duration = time.AfterFunc(time.Second*time.Duration(i), func() {
_ = cons.Stop()
})
}
ctx := r.Context() // handle when the client drops the connection
if i := core.Atoi(query.Get("duration")); i > 0 {
timeout := time.Second * time.Duration(i)
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, timeout)
defer cancel()
}
go func() {
<-ctx.Done()
_ = cons.Stop()
stream.RemoveConsumer(cons)
}()
_, _ = cons.WriteTo(w)
stream.RemoveConsumer(cons)
if duration != nil {
duration.Stop()
}
}

View File

@@ -8,7 +8,6 @@ import (
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/mp4"
"github.com/AlexxIT/go2rtc/pkg/tcp"
)
func handlerWSMSE(tr *ws.Transport, msg *ws.Message) error {
@@ -24,9 +23,8 @@ func handlerWSMSE(tr *ws.Transport, msg *ws.Message) error {
}
cons := mp4.NewConsumer(medias)
cons.Type = "MSE/WebSocket active consumer"
cons.RemoteAddr = tcp.RemoteAddr(tr.Request)
cons.UserAgent = tr.Request.UserAgent()
cons.FormatName = "mse/fmp4"
cons.WithRequest(tr.Request)
if err := stream.AddConsumer(cons); err != nil {
log.Debug().Err(err).Msg("[mp4] add consumer")
@@ -57,9 +55,7 @@ func handlerWSMP4(tr *ws.Transport, msg *ws.Message) error {
}
cons := mp4.NewKeyframe(medias)
cons.Type = "MP4/WebSocket active consumer"
cons.RemoteAddr = tcp.RemoteAddr(tr.Request)
cons.UserAgent = tr.Request.UserAgent()
cons.WithRequest(tr.Request)
if err := stream.AddConsumer(cons); err != nil {
log.Error().Err(err).Caller().Send()

View File

@@ -6,8 +6,6 @@ import (
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/aac"
"github.com/AlexxIT/go2rtc/pkg/tcp"
"github.com/rs/zerolog/log"
)
func apiStreamAAC(w http.ResponseWriter, r *http.Request) {
@@ -19,11 +17,9 @@ func apiStreamAAC(w http.ResponseWriter, r *http.Request) {
}
cons := aac.NewConsumer()
cons.RemoteAddr = tcp.RemoteAddr(r)
cons.UserAgent = r.UserAgent()
cons.WithRequest(r)
if err := stream.AddConsumer(cons); err != nil {
log.Error().Err(err).Caller().Send()
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}

View File

@@ -6,8 +6,6 @@ import (
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/mpegts"
"github.com/AlexxIT/go2rtc/pkg/tcp"
"github.com/rs/zerolog/log"
)
func Init() {
@@ -32,11 +30,9 @@ func outputMpegTS(w http.ResponseWriter, r *http.Request) {
}
cons := mpegts.NewConsumer()
cons.RemoteAddr = tcp.RemoteAddr(r)
cons.UserAgent = r.UserAgent()
cons.WithRequest(r)
if err := stream.AddConsumer(cons); err != nil {
log.Error().Err(err).Caller().Send()
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}

View File

@@ -2,6 +2,7 @@ package nest
import (
"net/http"
"strings"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/streams"
@@ -10,19 +11,13 @@ import (
)
func Init() {
streams.HandleFunc("nest", streamNest)
streams.HandleFunc("nest", func(source string) (core.Producer, error) {
return nest.Dial(source)
})
api.HandleFunc("api/nest", apiNest)
}
func streamNest(url string) (core.Producer, error) {
client, err := nest.NewClient(url)
if err != nil {
return nil, err
}
return client, nil
}
func apiNest(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query()
cliendID := query.Get("client_id")
@@ -44,11 +39,12 @@ func apiNest(w http.ResponseWriter, r *http.Request) {
var items []*api.Source
for name, deviceID := range devices {
query.Set("device_id", deviceID)
for _, device := range devices {
query.Set("device_id", device.DeviceID)
query.Set("protocols", strings.Join(device.Protocols, ","))
items = append(items, &api.Source{
Name: name, URL: "nest:?" + query.Encode(),
Name: device.Name, URL: "nest:?" + query.Encode(),
})
}

View File

@@ -50,7 +50,7 @@ func Init() {
log.Info().Str("addr", address).Msg("[ngrok] add external candidate for WebRTC")
webrtc.AddCandidate(address, "tcp")
webrtc.AddCandidate("tcp", address)
}
}
})

25
internal/onvif/README.md Normal file
View File

@@ -0,0 +1,25 @@
# ONVIF
A regular camera has a single video source (`GetVideoSources`) and two profiles (`GetProfiles`).
Go2rtc has one video source and one profile per stream.
## Tested clients
Go2rtc works as ONVIF server:
- Happytime onvif client (windows)
- Home Assistant ONVIF integration (linux)
- Onvier (android)
- ONVIF Device Manager (windows)
PS. Support only TCP transport for RTSP protocol. UDP and HTTP transports - unsupported yet.
## Tested cameras
Go2rtc works as ONVIF client:
- Dahua IPC-K42
- OpenIPC
- Reolink RLC-520A
- TP-Link Tapo TC60

View File

@@ -55,49 +55,73 @@ func onvifDeviceService(w http.ResponseWriter, r *http.Request) {
return
}
action := onvif.GetRequestAction(b)
if action == "" {
operation := onvif.GetRequestAction(b)
if operation == "" {
http.Error(w, "malformed request body", http.StatusBadRequest)
return
}
log.Trace().Msgf("[onvif] %s", action)
log.Trace().Msgf("[onvif] server request %s %s:\n%s", r.Method, r.RequestURI, b)
var res string
switch operation {
case onvif.DeviceGetNetworkInterfaces, // important for Hass
onvif.DeviceGetSystemDateAndTime, // important for Hass
onvif.DeviceGetDiscoveryMode,
onvif.DeviceGetDNS,
onvif.DeviceGetHostname,
onvif.DeviceGetNetworkDefaultGateway,
onvif.DeviceGetNetworkProtocols,
onvif.DeviceGetNTP,
onvif.DeviceGetScopes,
onvif.MediaGetVideoEncoderConfigurations,
onvif.MediaGetAudioEncoderConfigurations,
onvif.MediaGetAudioSources,
onvif.MediaGetAudioSourceConfigurations:
b = onvif.StaticResponse(operation)
switch action {
case onvif.ActionGetCapabilities:
case onvif.DeviceGetCapabilities:
// important for Hass: Media section
res = onvif.GetCapabilitiesResponse(r.Host)
b = onvif.GetCapabilitiesResponse(r.Host)
case onvif.ActionGetSystemDateAndTime:
// important for Hass
res = onvif.GetSystemDateAndTimeResponse()
case onvif.DeviceGetServices:
b = onvif.GetServicesResponse(r.Host)
case onvif.ActionGetNetworkInterfaces:
// important for Hass: none
res = onvif.GetNetworkInterfacesResponse()
case onvif.ActionGetDeviceInformation:
case onvif.DeviceGetDeviceInformation:
// important for Hass: SerialNumber (unique server ID)
res = onvif.GetDeviceInformationResponse("", "go2rtc", app.Version, r.Host)
b = onvif.GetDeviceInformationResponse("", "go2rtc", app.Version, r.Host)
case onvif.ActionGetServiceCapabilities:
case onvif.ServiceGetServiceCapabilities:
// important for Hass
res = onvif.GetServiceCapabilitiesResponse()
// TODO: check path links to media
b = onvif.GetMediaServiceCapabilitiesResponse()
case onvif.ActionSystemReboot:
res = onvif.SystemRebootResponse()
case onvif.DeviceSystemReboot:
b = onvif.StaticResponse(operation)
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.MediaGetVideoSources:
b = onvif.GetVideoSourcesResponse(streams.GetAllNames())
case onvif.ActionGetStreamUri:
case onvif.MediaGetProfiles:
// important for Hass: H264 codec, width, height
b = onvif.GetProfilesResponse(streams.GetAllNames())
case onvif.MediaGetProfile:
token := onvif.FindTagValue(b, "ProfileToken")
b = onvif.GetProfileResponse(token)
case onvif.MediaGetVideoSourceConfigurations:
// important for Happytime Onvif Client
b = onvif.GetVideoSourceConfigurationsResponse(streams.GetAllNames())
case onvif.MediaGetVideoSourceConfiguration:
token := onvif.FindTagValue(b, "ConfigurationToken")
b = onvif.GetVideoSourceConfigurationResponse(token)
case onvif.MediaGetStreamUri:
host, _, err := net.SplitHostPort(r.Host)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
@@ -105,16 +129,23 @@ func onvifDeviceService(w http.ResponseWriter, r *http.Request) {
}
uri := "rtsp://" + host + ":" + rtsp.Port + "/" + onvif.FindTagValue(b, "ProfileToken")
res = onvif.GetStreamUriResponse(uri)
b = onvif.GetStreamUriResponse(uri)
case onvif.MediaGetSnapshotUri:
uri := "http://" + r.Host + "/api/frame.jpeg?src=" + onvif.FindTagValue(b, "ProfileToken")
b = onvif.GetSnapshotUriResponse(uri)
default:
http.Error(w, "unsupported action", http.StatusBadRequest)
http.Error(w, "unsupported operation", http.StatusBadRequest)
log.Warn().Msgf("[onvif] unsupported operation: %s", operation)
log.Debug().Msgf("[onvif] unsupported request:\n%s", b)
return
}
log.Trace().Msgf("[onvif] server response:\n%s", b)
w.Header().Set("Content-Type", "application/soap+xml; charset=utf-8")
if _, err = w.Write([]byte(res)); err != nil {
if _, err = w.Write(b); err != nil {
log.Error().Err(err).Caller().Send()
}
}
@@ -160,7 +191,7 @@ func apiOnvif(w http.ResponseWriter, r *http.Request) {
}
if l := log.Trace(); l.Enabled() {
b, _ := client.GetProfiles()
b, _ := client.MediaRequest(onvif.MediaGetProfiles)
l.Msgf("[onvif] src=%s profiles:\n%s", src, b)
}

102
internal/ring/ring.go Normal file
View File

@@ -0,0 +1,102 @@
package ring
import (
"encoding/json"
"net/http"
"net/url"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/ring"
)
func Init() {
streams.HandleFunc("ring", func(source string) (core.Producer, error) {
return ring.Dial(source)
})
api.HandleFunc("api/ring", apiRing)
}
func apiRing(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query()
var ringAPI *ring.RingRestClient
var err error
// Check auth method
if email := query.Get("email"); email != "" {
// Email/Password Flow
password := query.Get("password")
code := query.Get("code")
ringAPI, err = ring.NewRingRestClient(ring.EmailAuth{
Email: email,
Password: password,
}, nil)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// Try authentication (this will trigger 2FA if needed)
if _, err = ringAPI.GetAuth(code); err != nil {
if ringAPI.Using2FA {
// Return 2FA prompt
json.NewEncoder(w).Encode(map[string]interface{}{
"needs_2fa": true,
"prompt": ringAPI.PromptFor2FA,
})
return
}
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
} else {
// Refresh Token Flow
refreshToken := query.Get("refresh_token")
if refreshToken == "" {
http.Error(w, "either email/password or refresh_token is required", http.StatusBadRequest)
return
}
ringAPI, err = ring.NewRingRestClient(ring.RefreshTokenAuth{
RefreshToken: refreshToken,
}, nil)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
// Fetch devices
devices, err := ringAPI.FetchRingDevices()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// Create clean query with only required parameters
cleanQuery := url.Values{}
cleanQuery.Set("refresh_token", ringAPI.RefreshToken)
var items []*api.Source
for _, camera := range devices.AllCameras {
cleanQuery.Set("device_id", camera.DeviceID)
// Stream source
items = append(items, &api.Source{
Name: camera.Description,
URL: "ring:?" + cleanQuery.Encode(),
})
// Snapshot source
items = append(items, &api.Source{
Name: camera.Description + " Snapshot",
URL: "ring:?" + cleanQuery.Encode() + "&snapshot",
})
}
api.ResponseSources(w, items)
}

View File

@@ -11,22 +11,13 @@ import (
)
func Init() {
streams.HandleFunc("roborock", handle)
streams.HandleFunc("roborock", func(source string) (core.Producer, error) {
return roborock.Dial(source)
})
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"`

60
internal/rtmp/README.md Normal file
View File

@@ -0,0 +1,60 @@
## Tested client
| From | To | Comment |
|--------|---------------------------------|---------|
| go2rtc | Reolink RLC-520A fw. v3.1.0.801 | OK |
**go2rtc.yaml**
```yaml
streams:
rtmp-reolink1: rtmp://192.168.10.92/bcs/channel0_main.bcs?channel=0&stream=0&user=admin&password=password
rtmp-reolink2: rtmp://192.168.10.92/bcs/channel0_sub.bcs?channel=0&stream=1&user=admin&password=password
rtmp-reolink3: rtmp://192.168.10.92/bcs/channel0_ext.bcs?channel=0&stream=1&user=admin&password=password
```
## Tested server
| From | To | Comment |
|------------------------|--------|---------------------|
| OBS 31.0.2 | go2rtc | OK |
| OpenIPC 2.5.03.02-lite | go2rtc | OK |
| FFmpeg 6.1 | go2rtc | OK |
| GoPro Black 12 | go2rtc | OK, 1080p, 5000kbps |
**go2rtc.yaml**
```yaml
rtmp:
listen: :1935
streams:
tmp:
```
**OBS**
Settings > Stream:
- Service: Custom
- Server: rtmp://192.168.10.101/tmp
- Stream Key: <empty>
- Use auth: <disabled>
**OpenIPC**
WebUI > Majestic > Settings > Outgoing
- Enable
- Address: rtmp://192.168.10.101/tmp
- Save
- Restart
**FFmpeg**
```shell
ffmpeg -re -i bbb.mp4 -c copy -f flv rtmp://192.168.10.101/tmp
```
**GoPro**
GoPro Quik > Camera > Translation > Other

View File

@@ -12,7 +12,6 @@ import (
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/flv"
"github.com/AlexxIT/go2rtc/pkg/rtmp"
"github.com/AlexxIT/go2rtc/pkg/tcp"
"github.com/rs/zerolog"
)
@@ -128,17 +127,13 @@ func tcpHandle(netConn net.Conn) error {
var log zerolog.Logger
func streamsHandle(url string) (core.Producer, error) {
client, err := rtmp.DialPlay(url)
if err != nil {
return nil, err
}
return client, nil
return rtmp.DialPlay(url)
}
func streamsConsumerHandle(url string) (core.Consumer, func(), error) {
cons := flv.NewConsumer()
run := func() {
wr, err := rtmp.DialPublish(url)
wr, err := rtmp.DialPublish(url, cons)
if err != nil {
return
}
@@ -165,9 +160,7 @@ func outputFLV(w http.ResponseWriter, r *http.Request) {
}
cons := flv.NewConsumer()
cons.Type = "HTTP-FLV consumer"
cons.RemoteAddr = tcp.RemoteAddr(r)
cons.UserAgent = r.UserAgent()
cons.WithRequest(r)
if err := stream.AddConsumer(cons); err != nil {
log.Error().Err(err).Caller().Send()

View File

@@ -1,6 +1,7 @@
package rtsp
import (
"errors"
"io"
"net"
"net/url"
@@ -21,7 +22,7 @@ func Init() {
Username string `yaml:"username" json:"-"`
Password string `yaml:"password" json:"-"`
DefaultQuery string `yaml:"default_query" json:"default_query"`
PacketSize uint16 `yaml:"pkt_size"`
PacketSize uint16 `yaml:"pkt_size" json:"pkt_size,omitempty"`
} `yaml:"rtsp"`
}
@@ -147,6 +148,7 @@ func tcpHandler(conn *rtsp.Conn) {
var closer func()
trace := log.Trace().Enabled()
level := zerolog.WarnLevel
conn.Listen(func(msg any) {
if trace {
@@ -184,12 +186,38 @@ func tcpHandler(conn *rtsp.Conn) {
}
}
if query.Get("backchannel") == "1" {
conn.Medias = append(conn.Medias, &core.Media{
Kind: core.KindAudio,
Direction: core.DirectionRecvonly,
Codecs: []*core.Codec{
{Name: core.CodecOpus, ClockRate: 48000, Channels: 2},
{Name: core.CodecPCM, ClockRate: 16000},
{Name: core.CodecPCMA, ClockRate: 16000},
{Name: core.CodecPCMU, ClockRate: 16000},
{Name: core.CodecPCM, ClockRate: 8000},
{Name: core.CodecPCMA, ClockRate: 8000},
{Name: core.CodecPCMU, ClockRate: 8000},
},
})
}
if s := query.Get("pkt_size"); s != "" {
conn.PacketSize = uint16(core.Atoi(s))
}
// param name like ffmpeg style https://ffmpeg.org/ffmpeg-protocols.html
if s := query.Get("log_level"); s != "" {
if lvl, err := zerolog.ParseLevel(s); err == nil {
level = lvl
}
}
// will help to protect looping requests to same source
conn.Connection.Source = query.Get("source")
if err := stream.AddConsumer(conn); err != nil {
log.Warn().Err(err).Str("stream", name).Msg("[rtsp]")
log.WithLevel(level).Err(err).Str("stream", name).Msg("[rtsp]")
return
}
@@ -210,6 +238,11 @@ func tcpHandler(conn *rtsp.Conn) {
return
}
query := conn.URL.Query()
if s := query.Get("timeout"); s != "" {
conn.Timeout = core.Atoi(s)
}
log.Debug().Str("stream", name).Msg("[rtsp] new producer")
stream.AddProducer(conn)
@@ -221,8 +254,10 @@ func tcpHandler(conn *rtsp.Conn) {
})
if err := conn.Accept(); err != nil {
if err != io.EOF {
log.Warn().Err(err).Caller().Send()
if errors.Is(err, rtsp.FailedAuth) {
log.Warn().Str("remote_addr", conn.Connection.RemoteAddr).Msg("[rtsp] failed authentication")
} else if err != io.EOF {
log.WithLevel(level).Err(err).Caller().Send()
}
if closer != nil {
closer()

View File

@@ -0,0 +1,55 @@
## Examples
```yaml
streams:
# known RTSP sources
rtsp-dahua1: rtsp://admin:password@192.168.10.90/cam/realmonitor?channel=1&subtype=0&unicast=true&proto=Onvif
rtsp-dahua2: rtsp://admin:password@192.168.10.90/cam/realmonitor?channel=1&subtype=1
rtsp-tplink1: rtsp://admin:password@192.168.10.91/stream1
rtsp-tplink2: rtsp://admin:password@192.168.10.91/stream2
rtsp-reolink1: rtsp://admin:password@192.168.10.92/h264Preview_01_main
rtsp-reolink2: rtsp://admin:password@192.168.10.92/h264Preview_01_sub
rtsp-sonoff1: rtsp://admin:password@192.168.10.93/av_stream/ch0
rtsp-sonoff2: rtsp://admin:password@192.168.10.93/av_stream/ch1
# known RTMP sources
rtmp-reolink1: rtmp://192.168.10.92/bcs/channel0_main.bcs?channel=0&stream=0&user=admin&password=password
rtmp-reolink2: rtmp://192.168.10.92/bcs/channel0_sub.bcs?channel=0&stream=1&user=admin&password=password
rtmp-reolink3: rtmp://192.168.10.92/bcs/channel0_ext.bcs?channel=0&stream=1&user=admin&password=password
# known HTTP sources
http-reolink1: http://192.168.10.92/flv?port=1935&app=bcs&stream=channel0_main.bcs&user=admin&password=password
http-reolink2: http://192.168.10.92/flv?port=1935&app=bcs&stream=channel0_sub.bcs&user=admin&password=password
http-reolink3: http://192.168.10.92/flv?port=1935&app=bcs&stream=channel0_ext.bcs&user=admin&password=password
# known ONVIF sources
onvif-dahua1: onvif://admin:password@192.168.10.90?subtype=MediaProfile00000
onvif-dahua2: onvif://admin:password@192.168.10.90?subtype=MediaProfile00001
onvif-dahua3: onvif://admin:password@192.168.10.90?subtype=MediaProfile00000&snapshot
onvif-tplink1: onvif://admin:password@192.168.10.91:2020?subtype=profile_1
onvif-tplink2: onvif://admin:password@192.168.10.91:2020?subtype=profile_2
onvif-reolink1: onvif://admin:password@192.168.10.92:8000?subtype=000
onvif-reolink2: onvif://admin:password@192.168.10.92:8000?subtype=001
onvif-reolink3: onvif://admin:password@192.168.10.92:8000?subtype=000&snapshot
onvif-openipc1: onvif://admin:password@192.168.10.95:80?subtype=PROFILE_000
onvif-openipc2: onvif://admin:password@192.168.10.95:80?subtype=PROFILE_001
# some EXEC examples
exec-h264-pipe: exec:ffmpeg -re -i bbb.mp4 -c copy -f h264 -
exec-flv-pipe: exec:ffmpeg -re -i bbb.mp4 -c copy -f flv -
exec-mpegts-pipe: exec:ffmpeg -re -i bbb.mp4 -c copy -f mpegts -
exec-adts-pipe: exec:ffmpeg -re -i bbb.mp4 -c copy -f adts -
exec-mjpeg-pipe: exec:ffmpeg -re -i bbb.mp4 -c mjpeg -f mjpeg -
exec-hevc-pipe: exec:ffmpeg -re -i bbb.mp4 -c libx265 -preset superfast -tune zerolatency -f hevc -
exec-wav-pipe: exec:ffmpeg -re -i bbb.mp4 -c pcm_alaw -ar 8000 -ac 1 -f wav -
exec-y4m-pipe: exec:ffmpeg -re -i bbb.mp4 -c rawvideo -f yuv4mpegpipe -
exec-pcma-pipe: exec:ffmpeg -re -i numb.mp3 -c:a pcm_alaw -ar:a 8000 -ac:a 1 -f wav -
exec-pcmu-pipe: exec:ffmpeg -re -i numb.mp3 -c:a pcm_mulaw -ar:a 8000 -ac:a 1 -f wav -
exec-s16le-pipe: exec:ffmpeg -re -i numb.mp3 -c:a pcm_s16le -ar:a 16000 -ac:a 1 -f wav -
# some FFmpeg examples
ffmpeg-video-h264: ffmpeg:virtual?video#video=h264
ffmpeg-video-4K: ffmpeg:virtual?video&size=4K#video=h264
ffmpeg-video-10s: ffmpeg:virtual?video&duration=10#video=h264
ffmpeg-video-src2: ffmpeg:virtual?video=testsrc2&size=2K#video=h264
```

View File

@@ -22,6 +22,12 @@ func (s *Stream) AddConsumer(cons core.Consumer) (err error) {
producers:
for prodN, prod := range s.producers {
// check for loop request, ex. `camera1: ffmpeg:camera1`
if info, ok := cons.(core.Info); ok && prod.url == info.GetSource() {
log.Trace().Msgf("[streams] skip cons=%d prod=%d", consN, prodN)
continue
}
if prodErrors[prodN] != nil {
log.Trace().Msgf("[streams] skip cons=%d prod=%d", consN, prodN)
continue
@@ -129,7 +135,7 @@ func formatError(consMedias, prodMedias []*core.Media, prodErrors []error) error
for _, media := range prodMedias {
if media.Direction == core.DirectionRecvonly {
for _, codec := range media.Codecs {
prod = appendString(prod, codec.PrintName())
prod = appendString(prod, media.Kind+":"+codec.PrintName())
}
}
}
@@ -137,7 +143,7 @@ func formatError(consMedias, prodMedias []*core.Media, prodErrors []error) error
for _, media := range consMedias {
if media.Direction == core.DirectionSendonly {
for _, codec := range media.Codecs {
cons = appendString(cons, codec.PrintName())
cons = appendString(cons, media.Kind+":"+codec.PrintName())
}
}
}

124
internal/streams/api.go Normal file
View File

@@ -0,0 +1,124 @@
package streams
import (
"net/http"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/app"
"github.com/AlexxIT/go2rtc/pkg/probe"
)
func apiStreams(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" {
api.ResponseJSON(w, streams)
return
}
// Not sure about all this API. Should be rewrited...
switch r.Method {
case "GET":
stream := Get(src)
if stream == nil {
http.Error(w, "", http.StatusNotFound)
return
}
cons := probe.NewProbe(query)
if len(cons.Medias) != 0 {
cons.WithRequest(r)
if err := stream.AddConsumer(cons); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
api.ResponsePrettyJSON(w, stream)
stream.RemoveConsumer(cons)
} else {
api.ResponsePrettyJSON(w, streams[src])
}
case "PUT":
name := query.Get("name")
if name == "" {
name = src
}
if New(name, query["src"]...) == nil {
http.Error(w, "", http.StatusBadRequest)
return
}
if err := app.PatchConfig([]string{"streams", name}, query["src"]); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
}
case "PATCH":
name := query.Get("name")
if name == "" {
http.Error(w, "", http.StatusBadRequest)
return
}
// support {input} templates: https://github.com/AlexxIT/go2rtc#module-hass
if Patch(name, src) == nil {
http.Error(w, "", http.StatusBadRequest)
}
case "POST":
// with dst - redirect source to dst
if dst := query.Get("dst"); dst != "" {
if stream := Get(dst); stream != nil {
if err := Validate(src); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
} else if err = stream.Play(src); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
} else {
api.ResponseJSON(w, stream)
}
} else if stream = Get(src); stream != nil {
if err := Validate(dst); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
} else if err = stream.Publish(dst); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
} else {
http.Error(w, "", http.StatusNotFound)
}
} else {
http.Error(w, "", http.StatusBadRequest)
}
case "DELETE":
delete(streams, src)
if err := app.PatchConfig([]string{"streams", src}, nil); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
}
}
}
func apiStreamsDOT(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query()
dot := make([]byte, 0, 1024)
dot = append(dot, "digraph {\n"...)
if query.Has("src") {
for _, name := range query["src"] {
if stream := streams[name]; stream != nil {
dot = AppendDOT(dot, stream)
}
}
} else {
for _, stream := range streams {
dot = AppendDOT(dot, stream)
}
}
dot = append(dot, '}')
api.Response(w, dot, "text/vnd.graphviz")
}

176
internal/streams/dot.go Normal file
View File

@@ -0,0 +1,176 @@
package streams
import (
"encoding/json"
"fmt"
"strings"
)
func AppendDOT(dot []byte, stream *Stream) []byte {
for _, prod := range stream.producers {
if prod.conn == nil {
continue
}
c, err := marshalConn(prod.conn)
if err != nil {
continue
}
dot = c.appendDOT(dot, "producer")
}
for _, cons := range stream.consumers {
c, err := marshalConn(cons)
if err != nil {
continue
}
dot = c.appendDOT(dot, "consumer")
}
return dot
}
func marshalConn(v any) (*conn, error) {
b, err := json.Marshal(v)
if err != nil {
return nil, err
}
var c conn
if err = json.Unmarshal(b, &c); err != nil {
return nil, err
}
return &c, nil
}
const bytesK = "KMGTP"
func humanBytes(i int) string {
if i < 1000 {
return fmt.Sprintf("%d B", i)
}
f := float64(i) / 1000
var n uint8
for f >= 1000 && n < 5 {
f /= 1000
n++
}
return fmt.Sprintf("%.2f %cB", f, bytesK[n])
}
type node struct {
ID uint32 `json:"id"`
Codec map[string]any `json:"codec"`
Parent uint32 `json:"parent"`
Childs []uint32 `json:"childs"`
Bytes int `json:"bytes"`
//Packets uint32 `json:"packets"`
//Drops uint32 `json:"drops"`
}
var codecKeys = []string{"codec_name", "sample_rate", "channels", "profile", "level"}
func (n *node) name() string {
if name, ok := n.Codec["codec_name"].(string); ok {
return name
}
return "unknown"
}
func (n *node) codec() []byte {
b := make([]byte, 0, 128)
for _, k := range codecKeys {
if v := n.Codec[k]; v != nil {
b = fmt.Appendf(b, "%s=%v\n", k, v)
}
}
if l := len(b); l > 0 {
return b[:l-1]
}
return b
}
func (n *node) appendDOT(dot []byte, group string) []byte {
dot = fmt.Appendf(dot, "%d [group=%s, label=%q, title=%q];\n", n.ID, group, n.name(), n.codec())
//for _, sink := range n.Childs {
// dot = fmt.Appendf(dot, "%d -> %d;\n", n.ID, sink)
//}
return dot
}
type conn struct {
ID uint32 `json:"id"`
FormatName string `json:"format_name"`
Protocol string `json:"protocol"`
RemoteAddr string `json:"remote_addr"`
Source string `json:"source"`
URL string `json:"url"`
UserAgent string `json:"user_agent"`
Receivers []node `json:"receivers"`
Senders []node `json:"senders"`
BytesRecv int `json:"bytes_recv"`
BytesSend int `json:"bytes_send"`
}
func (c *conn) appendDOT(dot []byte, group string) []byte {
host := c.host()
dot = fmt.Appendf(dot, "%s [group=host];\n", host)
dot = fmt.Appendf(dot, "%d [group=%s, label=%q, title=%q];\n", c.ID, group, c.FormatName, c.label())
if group == "producer" {
dot = fmt.Appendf(dot, "%s -> %d [label=%q];\n", host, c.ID, humanBytes(c.BytesRecv))
} else {
dot = fmt.Appendf(dot, "%d -> %s [label=%q];\n", c.ID, host, humanBytes(c.BytesSend))
}
for _, recv := range c.Receivers {
dot = fmt.Appendf(dot, "%d -> %d [label=%q];\n", c.ID, recv.ID, humanBytes(recv.Bytes))
dot = recv.appendDOT(dot, "node")
}
for _, send := range c.Senders {
dot = fmt.Appendf(dot, "%d -> %d [label=%q];\n", send.Parent, c.ID, humanBytes(send.Bytes))
//dot = fmt.Appendf(dot, "%d -> %d [label=%q];\n", send.ID, c.ID, humanBytes(send.Bytes))
//dot = send.appendDOT(dot, "node")
}
return dot
}
func (c *conn) host() (s string) {
if c.Protocol == "pipe" {
return "127.0.0.1"
}
if s = c.RemoteAddr; s == "" {
return "unknown"
}
if i := strings.Index(s, "forwarded"); i > 0 {
s = s[i+10:]
}
if s[0] == '[' {
if i := strings.Index(s, "]"); i > 0 {
return s[1:i]
}
}
if i := strings.IndexAny(s, " ,:"); i > 0 {
return s[:i]
}
return
}
func (c *conn) label() string {
var sb strings.Builder
sb.WriteString("format_name=" + c.FormatName)
if c.Protocol != "" {
sb.WriteString("\nprotocol=" + c.Protocol)
}
if c.Source != "" {
sb.WriteString("\nsource=" + c.Source)
}
if c.URL != "" {
sb.WriteString("\nurl=" + c.URL)
}
if c.UserAgent != "" {
sb.WriteString("\nuser_agent=" + c.UserAgent)
}
// escape quotes https://github.com/AlexxIT/go2rtc/issues/1603
return strings.ReplaceAll(sb.String(), `"`, `'`)
}

View File

@@ -7,7 +7,7 @@ import (
"github.com/AlexxIT/go2rtc/pkg/core"
)
type Handler func(url string) (core.Producer, error)
type Handler func(source string) (core.Producer, error)
var handlers = map[string]Handler{}

View File

@@ -2,10 +2,12 @@ package streams
import (
"errors"
"time"
"github.com/AlexxIT/go2rtc/pkg/core"
)
func (s *Stream) Play(source string) error {
func (s *Stream) Play(urlOrProd any) error {
s.mu.Lock()
for _, producer := range s.producers {
if producer.state == stateInternal && producer.conn != nil {
@@ -14,12 +16,18 @@ func (s *Stream) Play(source string) error {
}
s.mu.Unlock()
if source == "" {
return nil
}
var source string
var src core.Producer
switch urlOrProd.(type) {
case string:
if source = urlOrProd.(string); source == "" {
return nil
}
case core.Producer:
src = urlOrProd.(core.Producer)
}
for _, producer := range s.producers {
if producer.conn == nil {
continue
@@ -80,18 +88,20 @@ func (s *Stream) Play(source string) error {
s.AddInternalProducer(src)
s.AddInternalConsumer(cons)
go func() {
_ = src.Start()
_ = dst.Stop()
s.RemoveProducer(src)
}()
go func() {
_ = dst.Start()
_ = src.Stop()
s.RemoveInternalConsumer(cons)
}()
go func() {
_ = src.Start()
// little timeout before stop dst, so the buffer can be transferred
time.Sleep(time.Second)
_ = dst.Stop()
s.RemoveProducer(src)
}()
return nil
}
@@ -99,7 +109,7 @@ func (s *Stream) Play(source string) error {
}
func (s *Stream) AddInternalProducer(conn core.Producer) {
producer := &Producer{conn: conn, state: stateInternal}
producer := &Producer{conn: conn, state: stateInternal, url: "internal"}
s.mu.Lock()
s.producers = append(s.producers, producer)
s.mu.Unlock()
@@ -136,10 +146,12 @@ func matchMedia(prod core.Producer, cons core.Consumer) bool {
track, err := prod.GetTrack(prodMedia, prodCodec)
if err != nil {
log.Warn().Err(err).Msg("[streams] can't get track")
continue
}
if err = cons.AddTrack(consMedia, consCodec, track); err != nil {
log.Warn().Err(err).Msg("[streams] can't add track")
continue
}

View File

@@ -132,11 +132,10 @@ func (p *Producer) AddTrack(media *core.Media, codec *core.Codec, track *core.Re
}
func (p *Producer) MarshalJSON() ([]byte, error) {
if p.conn != nil {
return json.Marshal(p.conn)
if conn := p.conn; conn != nil {
return json.Marshal(conn)
}
info := core.Info{URL: p.url}
info := map[string]string{"url": p.url}
return json.Marshal(info)
}
@@ -207,7 +206,7 @@ func (p *Producer) reconnect(workerID, retry int) {
for _, media := range conn.GetMedias() {
switch media.Direction {
case core.DirectionRecvonly:
for _, receiver := range p.receivers {
for i, receiver := range p.receivers {
codec := media.MatchCodec(receiver.Codec)
if codec == nil {
continue
@@ -219,6 +218,7 @@ func (p *Producer) reconnect(workerID, retry int) {
}
receiver.Replace(track)
p.receivers[i] = track
break
}
@@ -234,6 +234,9 @@ func (p *Producer) reconnect(workerID, retry int) {
}
}
// stop previous connection after moving tracks (fix ghost exec/ffmpeg)
_ = p.conn.Stop()
// swap connections
p.conn = conn
go p.worker(conn, workerID)

View File

@@ -21,6 +21,12 @@ func NewStream(source any) *Stream {
return &Stream{
producers: []*Producer{NewProducer(source)},
}
case []string:
s := new(Stream)
for _, str := range source {
s.producers = append(s.producers, NewProducer(str))
}
return s
case []any:
s := new(Stream)
for _, src := range source {
@@ -41,11 +47,12 @@ func NewStream(source any) *Stream {
}
}
func (s *Stream) Sources() (sources []string) {
func (s *Stream) Sources() []string {
sources := make([]string, 0, len(s.producers))
for _, prod := range s.producers {
sources = append(sources, prod.url)
}
return
return sources
}
func (s *Stream) SetSource(source string) {
@@ -70,7 +77,7 @@ func (s *Stream) RemoveConsumer(cons core.Consumer) {
}
func (s *Stream) AddProducer(prod core.Producer) {
producer := &Producer{conn: prod, state: stateExternal}
producer := &Producer{conn: prod, state: stateExternal, url: "external"}
s.mu.Lock()
s.producers = append(s.producers, producer)
s.mu.Unlock()
@@ -88,6 +95,11 @@ func (s *Stream) RemoveProducer(prod core.Producer) {
}
func (s *Stream) stopProducers() {
if s.pending.Load() > 0 {
log.Trace().Msg("[streams] skip stop pending producer")
return
}
s.mu.Lock()
producers:
for _, producer := range s.producers {
@@ -107,19 +119,12 @@ producers:
}
func (s *Stream) MarshalJSON() ([]byte, error) {
if !s.mu.TryLock() {
log.Warn().Msgf("[streams] json locked")
return json.Marshal(nil)
}
var info struct {
var info = struct {
Producers []*Producer `json:"producers"`
Consumers []core.Consumer `json:"consumers"`
}{
Producers: s.producers,
Consumers: s.consumers,
}
info.Producers = s.producers
info.Consumers = s.consumers
s.mu.Unlock()
return json.Marshal(info)
}

View File

@@ -1,7 +1,7 @@
package streams
import (
"net/http"
"errors"
"net/url"
"regexp"
"sync"
@@ -26,7 +26,8 @@ func Init() {
streams[name] = NewStream(item)
}
api.HandleFunc("api/streams", streamsHandler)
api.HandleFunc("api/streams", apiStreams)
api.HandleFunc("api/streams.dot", apiStreamsDOT)
if cfg.Publish == nil {
return
@@ -41,20 +42,29 @@ func Init() {
})
}
func Get(name string) *Stream {
return streams[name]
}
var sanitize = regexp.MustCompile(`\s`)
func New(name string, source string) *Stream {
// not allow creating dynamic streams with spaces in the source
// Validate - not allow creating dynamic streams with spaces in the source
func Validate(source string) error {
if sanitize.MatchString(source) {
return nil
return errors.New("streams: invalid dynamic source")
}
return nil
}
func New(name string, sources ...string) *Stream {
for _, source := range sources {
if Validate(source) != nil {
return nil
}
}
stream := NewStream(source)
stream := NewStream(sources)
streamsMu.Lock()
streams[name] = stream
streamsMu.Unlock()
return stream
}
@@ -87,6 +97,10 @@ func Patch(name string, source string) *Stream {
return nil
}
if Validate(source) != nil {
return nil
}
// check an existing stream with this name
if stream, ok := streams[name]; ok {
stream.SetSource(source)
@@ -94,7 +108,9 @@ func Patch(name string, source string) *Stream {
}
// create new stream with this name
return New(name, source)
stream := NewStream(source)
streams[name] = stream
return stream
}
func GetOrPatch(query url.Values) *Stream {
@@ -105,7 +121,7 @@ func GetOrPatch(query url.Values) *Stream {
}
// check if src is stream name
if stream, ok := streams[source]; ok {
if stream := Get(source); stream != nil {
return stream
}
@@ -120,92 +136,41 @@ func GetOrPatch(query url.Values) *Stream {
return Patch(source, source)
}
func GetAll() (names []string) {
var log zerolog.Logger
// streams map
var streams = map[string]*Stream{}
var streamsMu sync.Mutex
func Get(name string) *Stream {
streamsMu.Lock()
defer streamsMu.Unlock()
return streams[name]
}
func Delete(name string) {
streamsMu.Lock()
defer streamsMu.Unlock()
delete(streams, name)
}
func GetAllNames() []string {
streamsMu.Lock()
names := make([]string, 0, len(streams))
for name := range streams {
names = append(names, name)
}
return
streamsMu.Unlock()
return names
}
func Streams() map[string]*Stream {
return streams
}
func Delete(id string) {
delete(streams, id)
}
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" {
api.ResponseJSON(w, streams)
return
}
// Not sure about all this API. Should be rewrited...
switch r.Method {
case "GET":
api.ResponsePrettyJSON(w, streams[src])
case "PUT":
name := query.Get("name")
if name == "" {
name = src
}
if New(name, src) == nil {
http.Error(w, "", http.StatusBadRequest)
return
}
if err := app.PatchConfig(name, src, "streams"); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
}
case "PATCH":
name := query.Get("name")
if name == "" {
http.Error(w, "", http.StatusBadRequest)
return
}
// support {input} templates: https://github.com/AlexxIT/go2rtc#module-hass
if Patch(name, src) == nil {
http.Error(w, "", http.StatusBadRequest)
}
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 {
api.ResponseJSON(w, stream)
}
} else if stream = Get(src); stream != nil {
if err := stream.Publish(dst); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
} else {
http.Error(w, "", http.StatusNotFound)
}
} else {
http.Error(w, "", http.StatusBadRequest)
}
case "DELETE":
delete(streams, src)
if err := app.PatchConfig(src, nil, "streams"); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
}
func GetAllSources() map[string][]string {
streamsMu.Lock()
sources := make(map[string][]string, len(streams))
for name, stream := range streams {
sources[name] = stream.Sources()
}
streamsMu.Unlock()
return sources
}
var log zerolog.Logger
var streams = map[string]*Stream{}
var streamsMu sync.Mutex

View File

@@ -8,11 +8,15 @@ import (
)
func Init() {
streams.HandleFunc("kasa", func(url string) (core.Producer, error) {
return kasa.Dial(url)
streams.HandleFunc("kasa", func(source string) (core.Producer, error) {
return kasa.Dial(source)
})
streams.HandleFunc("tapo", func(url string) (core.Producer, error) {
return tapo.Dial(url)
streams.HandleFunc("tapo", func(source string) (core.Producer, error) {
return tapo.Dial(source)
})
streams.HandleFunc("vigi", func(source string) (core.Producer, error) {
return tapo.Dial(source)
})
}

39
internal/v4l2/README.md Normal file
View File

@@ -0,0 +1,39 @@
# V4L2
What you should to know about [V4L2](https://en.wikipedia.org/wiki/Video4Linux):
- V4L2 (Video for Linux API version 2) works only in Linux
- supports USB cameras and other similar devices
- one device can only be connected to one software simultaneously
- cameras support a fixed list of formats, resolutions and frame rates
- basic cameras supports only RAW (non-compressed) pixel formats
- regular cameras supports MJPEG format (series of JPEG frames)
- advances cameras support H264 format (MSE/MP4, WebRTC compatible)
- using MJPEG and H264 formats (if the camera supports them) won't cost you the CPU usage
- transcoding RAW format to MJPEG or H264 - will cost you a significant CPU usage
- H265 (HEVC) format is also supported (if the camera supports it)
Tests show that the basic Keenetic router with MIPS processor can broadcast three MJPEG cameras in the following resolutions: 1600х1200 + 640х480 + 640х480. The USB bus bandwidth is no more enough for larger resolutions. CPU consumption is no more than 5%.
Supported formats for your camera can be found here: **Go2rtc > WebUI > Add > V4L2**.
## RAW format
Example:
```yaml
streams:
camera1: v4l2:device?video=/dev/video0&input_format=yuyv422&video_size=1280x720&framerate=10
```
Go2rtc supports built-in transcoding of RAW to MJPEG format. This does not need to be additionally configured.
```
ffplay http://localhost:1984/api/stream.mjpeg?src=camera1
```
**Important.** You don't have to transcode the RAW format to transmit it over the network. You can stream it in `y4m` format, which is perfectly supported by ffmpeg. It won't cost you a CPU usage. But will require high network bandwidth.
```
ffplay http://localhost:1984/api/stream.y4m?src=camera1
```

7
internal/v4l2/v4l2.go Normal file
View File

@@ -0,0 +1,7 @@
//go:build !(linux && (386 || arm || mipsle || amd64 || arm64))
package v4l2
func Init() {
// not supported
}

View File

@@ -0,0 +1,91 @@
//go:build linux && (386 || arm || mipsle || amd64 || arm64)
package v4l2
import (
"encoding/binary"
"fmt"
"net/http"
"os"
"strings"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/v4l2"
"github.com/AlexxIT/go2rtc/pkg/v4l2/device"
)
func Init() {
streams.HandleFunc("v4l2", func(source string) (core.Producer, error) {
return v4l2.Open(source)
})
api.HandleFunc("api/v4l2", apiV4L2)
}
func apiV4L2(w http.ResponseWriter, r *http.Request) {
files, err := os.ReadDir("/dev")
if err != nil {
return
}
var sources []*api.Source
for _, file := range files {
if !strings.HasPrefix(file.Name(), core.KindVideo) {
continue
}
path := "/dev/" + file.Name()
dev, err := device.Open(path)
if err != nil {
continue
}
formats, _ := dev.ListFormats()
for _, fourCC := range formats {
name, ffmpeg := findFormat(fourCC)
source := &api.Source{Name: name}
sizes, _ := dev.ListSizes(fourCC)
for _, wh := range sizes {
if source.Info != "" {
source.Info += " "
}
source.Info += fmt.Sprintf("%dx%d", wh[0], wh[1])
frameRates, _ := dev.ListFrameRates(fourCC, wh[0], wh[1])
for _, fr := range frameRates {
source.Info += fmt.Sprintf("@%d", fr)
if source.URL == "" && ffmpeg != "" {
source.URL = fmt.Sprintf(
"v4l2:device?video=%s&input_format=%s&video_size=%dx%d&framerate=%d",
path, ffmpeg, wh[0], wh[1], fr,
)
}
}
}
if source.Info != "" {
sources = append(sources, source)
}
}
_ = dev.Close()
}
api.ResponseSources(w, sources)
}
func findFormat(fourCC uint32) (name, ffmpeg string) {
for _, format := range device.Formats {
if format.FourCC == fourCC {
return format.Name, format.FFmpeg
}
}
return string(binary.LittleEndian.AppendUint32(nil, fourCC)), ""
}

View File

@@ -1,13 +1,110 @@
What you should to know about WebRTC:
- It's almost always a **direct [peer-to-peer](https://en.wikipedia.org/wiki/Peer-to-peer) connection** from your browser to go2rtc app
- When you use Home Assistant, Frigate, Nginx, Nabu Casa, Cloudflare and other software - they are only **involved in establishing** the connection, but they are **not involved in transferring** media data
- WebRTC media cannot be transferred inside an HTTP connection
- Usually, WebRTC uses random UDP ports on client and server side to establish a connection
- Usually, WebRTC uses public [STUN](https://en.wikipedia.org/wiki/STUN) servers to establish a connection outside LAN, such servers are only needed to establish a connection and are not involved in data transfer
- Usually, WebRTC will automatically discover all of your local addresses and all of your public addresses and try to establish a connection
If an external connection via STUN is used:
- Uses [UDP hole punching](https://en.wikipedia.org/wiki/UDP_hole_punching) technology to bypass NAT even if you not open your server to the World
- For about 20% of users, the techology will not work because of the [Symmetric NAT](https://tomchen.github.io/symmetric-nat-test/)
- UDP is not suitable for transmitting 2K and 4K high bitrate video over open networks because of the high loss rate:
- https://habr.com/ru/companies/flashphoner/articles/480006/
- https://www.youtube.com/watch?v=FXVg2ckuKfs
## Default config
```yaml
webrtc:
listen: ":8555"
ice_servers:
- urls: [ "stun:stun.l.google.com:19302" ]
```
## Config
- supported TCP: fixed port (default), disabled
- supported UDP: random port (default), fixed port
**Important!** This example is not for copypasting!
| Config examples | TCP | UDP |
|-----------------------|-------|--------|
| `listen: ":8555/tcp"` | fixed | random |
| `listen: ":8555"` | fixed | fixed |
| `listen: ""` | no | random |
```yaml
webrtc:
# fix local TCP or UDP or both ports for WebRTC media
listen: ":8555" # address of your local server
# add additional host candidates manually
# order is important, the first will have a higher priority
candidates:
- 216.58.210.174:8555 # if you have static public IP-address
- stun:8555 # if you have dynamic public IP-address
- home.duckdns.org:8555 # if you have domain
# add custom STUN and TURN servers
# use `ice_servers: []` for remove defaults and leave empty
ice_servers:
- urls: [ stun:stun1.l.google.com:19302 ]
- urls: [ turn:123.123.123.123:3478 ]
username: your_user
credential: your_pass
# optional filter list for auto discovery logic
# some settings only make sense if you don't specify a fixed UDP port
filters:
# list of host candidates from auto discovery to be sent
# including candidates from the `listen` option
# use `candidates: []` to remove all auto discovery candidates
candidates: [ 192.168.1.123 ]
# enable localhost candidates
loopback: true
# list of network types to be used for connection
# including candidates from the `listen` option
networks: [ udp4, udp6, tcp4, tcp6 ]
# list of interfaces to be used for connection
# including interfaces from unspecified `listen` option (empty host)
interfaces: [ eno1 ]
# list of host IP-addresses to be used for connection
# including IPs from unspecified `listen` option (empty host)
ips: [ 192.168.1.123 ]
# range for random UDP ports [min, max] to be used for connection
# not related to the `listen` option
udp_ports: [ 50000, 50100 ]
```
By default go2rtc uses **fixed TCP** port and **fixed UDP** ports for each **direct** WebRTC connection - `listen: ":8555"`.
You can set **fixed TCP** and **random UDP** port for all connections - `listen: ":8555/tcp"`.
Don't know why, but you can disable TCP port and leave only random UDP ports - `listen: ""`.
## Config filters
**Importan!** By default go2rtc exclude all Docker-like candidates (`172.16.0.0/12`). This can not be disabled.
Filters allow you to exclude unnecessary candidates. Extra candidates don't make your connection worse or better. But the wrong filter settings can break everything. Skip this setting if you don't understand it.
For example, go2rtc is installed on the host system. And there are unnecessary interfaces. You can keep only the relevant via `interfaces` or `ips` options. You can also exclude IPv6 candidates if your server supports them but your home network does not.
```yaml
webrtc:
listen: ":8555/tcp" # use fixed TCP port and random UDP ports
filters:
ips: [ 192.168.1.2 ] # IP-address of your server
networks: [ udp4, tcp4 ] # skip IPv6, if it's not supported for you
```
For example, go2rtc inside closed docker container (ex. [Frigate](https://frigate.video/)). You shouldn't filter docker interfaces, otherwise go2rtc will not be able to connect anywhere. But you can filter the docker candidates because no one can connect to them.
```yaml
webrtc:
listen: ":8555" # use fixed TCP and UDP ports
candidates: [ 192.168.1.2:8555 ] # add manual host candidate (use docker port forwarding)
```
## Userful links

View File

@@ -2,57 +2,61 @@ package webrtc
import (
"net"
"strings"
"github.com/AlexxIT/go2rtc/internal/api/ws"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/webrtc"
"github.com/pion/sdp/v3"
"github.com/AlexxIT/go2rtc/pkg/xnet"
pion "github.com/pion/webrtc/v4"
)
type Address struct {
Host string
Port string
Network string
Offset int
host string
Port string
Network string
Priority uint32
}
func (a *Address) Marshal() string {
host := a.Host
if host == "stun" {
func (a *Address) Host() string {
if a.host == "stun" {
ip, err := webrtc.GetCachedPublicIP()
if err != nil {
return ""
}
host = ip.String()
return ip.String()
}
return a.host
}
switch a.Network {
case "udp":
return webrtc.CandidateManualHostUDP(host, a.Port, a.Offset)
case "tcp":
return webrtc.CandidateManualHostTCPPassive(host, a.Port, a.Offset)
func (a *Address) Marshal() string {
if host := a.Host(); host != "" {
return webrtc.CandidateICE(a.Network, host, a.Port, a.Priority)
}
return ""
}
var addresses []*Address
var filters webrtc.Filters
func AddCandidate(network, address string) {
if network == "" {
AddCandidate("tcp", address)
AddCandidate("udp", address)
return
}
func AddCandidate(address, network string) {
host, port, err := net.SplitHostPort(address)
if err != nil {
return
}
offset := -1 - len(addresses) // every next candidate will have a lower priority
// start from 1, so manual candidates will be lower than built-in
// and every next candidate will have a lower priority
candidateIndex := 1 + len(addresses)
switch network {
case "tcp", "udp":
addresses = append(addresses, &Address{host, port, network, offset})
default:
addresses = append(
addresses, &Address{host, port, "udp", offset}, &Address{host, port, "tcp", offset},
)
}
priority := webrtc.CandidateHostPriority(network, candidateIndex)
addresses = append(addresses, &Address{host, port, network, priority})
}
func GetCandidates() (candidates []string) {
@@ -64,6 +68,43 @@ func GetCandidates() (candidates []string) {
return
}
// FilterCandidate return true if candidate passed the check
func FilterCandidate(candidate *pion.ICECandidate) bool {
if candidate == nil {
return false
}
// remove any Docker-like IP from candidates
if ip := net.ParseIP(candidate.Address); ip != nil && xnet.Docker.Contains(ip) {
return false
}
// host candidate should be in the hosts list
if candidate.Typ == pion.ICECandidateTypeHost && filters.Candidates != nil {
if !core.Contains(filters.Candidates, candidate.Address) {
return false
}
}
if filters.Networks != nil {
networkType := NetworkType(candidate.Protocol.String(), candidate.Address)
if !core.Contains(filters.Networks, networkType) {
return false
}
}
return true
}
// NetworkType convert tcp/udp network to tcp4/tcp6/udp4/udp6
func NetworkType(network, host string) string {
if strings.IndexByte(host, ':') >= 0 {
return network + "6"
} else {
return network + "4"
}
}
func asyncCandidates(tr *ws.Transport, cons *webrtc.Conn) {
tr.WithContext(func(ctx map[any]any) {
if candidates, ok := ctx["candidate"].([]string); ok {
@@ -86,30 +127,6 @@ func asyncCandidates(tr *ws.Transport, cons *webrtc.Conn) {
}
}
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 *ws.Transport, msg *ws.Message) error {
// process incoming candidate in sync function
tr.WithContext(func(ctx map[any]any) {

View File

@@ -15,7 +15,7 @@ import (
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/webrtc"
"github.com/gorilla/websocket"
pion "github.com/pion/webrtc/v3"
pion "github.com/pion/webrtc/v4"
)
// streamsHandler supports:
@@ -41,9 +41,11 @@ func streamsHandler(rawURL string) (core.Producer, error) {
// https://aws.amazon.com/kinesis/video-streams/
// https://docs.aws.amazon.com/kinesisvideostreams-webrtc-dg/latest/devguide/what-is-kvswebrtc.html
// https://github.com/orgs/awslabs/repositories?q=kinesis+webrtc
return kinesisClient(rawURL, query, "WebRTC/Kinesis")
return kinesisClient(rawURL, query, "webrtc/kinesis", nil)
} else if format == "openipc" {
return openIPCClient(rawURL, query)
} else if format == "switchbot" {
return switchbotClient(rawURL, query)
} else {
return go2rtcClient(rawURL)
}
@@ -54,6 +56,8 @@ func streamsHandler(rawURL string) (core.Producer, error) {
} else if format == "wyze" {
// https://github.com/mrlt8/docker-wyze-bridge
return wyzeClient(rawURL)
} else if format == "creality" {
return crealityClient(rawURL)
} else {
return whepClient(rawURL)
}
@@ -77,17 +81,23 @@ func go2rtcClient(url string) (core.Producer, error) {
// 2. Create PeerConnection
pc, err := PeerConnection(true)
if err != nil {
log.Error().Err(err).Caller().Send()
return nil, err
}
defer func() {
if err != nil {
_ = pc.Close()
}
}()
// waiter will wait PC error or WS error or nil (connection OK)
var connState core.Waiter
var connMu sync.Mutex
prod := webrtc.NewConn(pc)
prod.Desc = "WebRTC/WebSocket async"
prod.Mode = core.ModeActiveProducer
prod.Protocol = "ws"
prod.URL = url
prod.Listen(func(msg any) {
switch msg := msg.(type) {
case *pion.ICECandidate:
@@ -132,7 +142,8 @@ func go2rtcClient(url string) (core.Producer, error) {
}
if msg.Type != "webrtc/answer" {
return nil, errors.New("wrong answer: " + msg.Type)
err = errors.New("wrong answer: " + msg.String())
return nil, err
}
answer := msg.String()
@@ -180,8 +191,9 @@ func whepClient(url string) (core.Producer, error) {
}
prod := webrtc.NewConn(pc)
prod.Desc = "WebRTC/WHEP sync"
prod.Mode = core.ModeActiveProducer
prod.Protocol = "http"
prod.URL = url
medias := []*core.Media{
{Kind: core.KindVideo, Direction: core.DirectionRecvonly},

View File

@@ -0,0 +1,152 @@
package webrtc
import (
"encoding/base64"
"encoding/json"
"io"
"net/http"
"strings"
"time"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/webrtc"
"github.com/pion/sdp/v3"
)
// https://github.com/AlexxIT/go2rtc/issues/1600
func crealityClient(url string) (core.Producer, error) {
pc, err := PeerConnection(true)
if err != nil {
return nil, err
}
prod := webrtc.NewConn(pc)
prod.FormatName = "webrtc/creality"
prod.Mode = core.ModeActiveProducer
prod.Protocol = "http"
prod.URL = url
medias := []*core.Media{
{Kind: core.KindVideo, Direction: core.DirectionRecvonly},
}
// TODO: return webrtc.SessionDescription
offer, err := prod.CreateCompleteOffer(medias)
if err != nil {
return nil, err
}
log.Trace().Msgf("[webrtc] offer:\n%s", offer)
body, err := offerToB64(offer)
if err != nil {
return nil, err
}
req, err := http.NewRequest("POST", url, body)
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "plain/text")
// TODO: change http.DefaultClient settings
client := http.Client{Timeout: time.Second * 5000}
defer client.CloseIdleConnections()
res, err := client.Do(req)
if err != nil {
return nil, err
}
answer, err := answerFromB64(res.Body)
if err != nil {
return nil, err
}
log.Trace().Msgf("[webrtc] answer:\n%s", answer)
if answer, err = fixCrealitySDP(answer); err != nil {
return nil, err
}
if err = prod.SetAnswer(answer); err != nil {
return nil, err
}
return prod, nil
}
func offerToB64(sdp string) (io.Reader, error) {
// JS object
v := map[string]string{
"type": "offer",
"sdp": sdp,
}
// bytes
b, err := json.Marshal(v)
if err != nil {
return nil, err
}
// base64, why? who knows...
s := base64.StdEncoding.EncodeToString(b)
return strings.NewReader(s), nil
}
func answerFromB64(r io.Reader) (string, error) {
// base64
b, err := io.ReadAll(r)
if err != nil {
return "", err
}
// bytes
if b, err = base64.StdEncoding.DecodeString(string(b)); err != nil {
return "", err
}
// JS object
var v map[string]string
if err = json.Unmarshal(b, &v); err != nil {
return "", err
}
// string "v=0..."
return v["sdp"], nil
}
func fixCrealitySDP(value string) (string, error) {
var sd sdp.SessionDescription
if err := sd.UnmarshalString(value); err != nil {
return "", err
}
md := sd.MediaDescriptions[0]
// important to skip first codec, because second codec will be used
skip := md.MediaName.Formats[0]
md.MediaName.Formats = md.MediaName.Formats[1:]
attrs := make([]sdp.Attribute, 0, len(md.Attributes))
for _, attr := range md.Attributes {
switch attr.Key {
case "fmtp", "rtpmap":
// important to skip fmtp with x-google, because this is second fmtp for same codec
// and pion library will fail parsing this SDP
if strings.HasPrefix(attr.Value, skip) || strings.Contains(attr.Value, "x-google") {
continue
}
}
attrs = append(attrs, attr)
}
md.Attributes = attrs
b, err := sd.Marshal()
if err != nil {
return "", err
}
return string(b), nil
}

View File

@@ -12,7 +12,7 @@ import (
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/webrtc"
"github.com/gorilla/websocket"
pion "github.com/pion/webrtc/v3"
pion "github.com/pion/webrtc/v4"
)
type kinesisRequest struct {
@@ -34,7 +34,10 @@ func (k kinesisResponse) String() string {
return fmt.Sprintf("type=%s, payload=%s", k.Type, k.Payload)
}
func kinesisClient(rawURL string, query url.Values, desc string) (core.Producer, error) {
func kinesisClient(
rawURL string, query url.Values, format string,
sdpOffer func(prod *webrtc.Conn, query url.Values) (any, error),
) (core.Producer, error) {
// 1. Connect to signalign server
conn, _, err := websocket.DefaultDialer.Dial(rawURL, nil)
if err != nil {
@@ -79,8 +82,10 @@ func kinesisClient(rawURL string, query url.Values, desc string) (core.Producer,
}
prod := webrtc.NewConn(pc)
prod.Desc = desc
prod.FormatName = format
prod.Mode = core.ModeActiveProducer
prod.Protocol = "ws"
prod.URL = rawURL
prod.Listen(func(msg any) {
switch msg := msg.(type) {
case *pion.ICECandidate:
@@ -106,23 +111,33 @@ func kinesisClient(rawURL string, query url.Values, desc string) (core.Producer,
}
})
medias := []*core.Media{
{Kind: core.KindVideo, Direction: core.DirectionRecvonly},
{Kind: core.KindAudio, Direction: core.DirectionRecvonly},
var payload any
if sdpOffer == nil {
medias := []*core.Media{
{Kind: core.KindVideo, Direction: core.DirectionRecvonly},
{Kind: core.KindAudio, Direction: core.DirectionRecvonly},
}
// 4. Create offer
var offer string
if offer, err = prod.CreateOffer(medias); err != nil {
return nil, err
}
// 5. Send offer
payload = pion.SessionDescription{
Type: pion.SDPTypeOffer,
SDP: offer,
}
} else {
if payload, err = sdpOffer(prod, query); err != nil {
return nil, err
}
}
// 4. Create offer
offer, err := prod.CreateOffer(medias)
if err != nil {
return nil, err
}
// 5. Send offer
req.Action = "SDP_OFFER"
req.Payload, _ = json.Marshal(pion.SessionDescription{
Type: pion.SDPTypeOffer,
SDP: offer,
})
req.Payload, _ = json.Marshal(payload)
if err = conn.WriteJSON(req); err != nil {
return nil, err
}
@@ -216,5 +231,5 @@ func wyzeClient(rawURL string) (core.Producer, error) {
"ice_servers": []string{string(kvs.Servers)},
}
return kinesisClient(kvs.URL, query, "WebRTC/Wyze")
return kinesisClient(kvs.URL, query, "webrtc/wyze", nil)
}

View File

@@ -12,7 +12,7 @@ import (
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/tcp"
"github.com/AlexxIT/go2rtc/pkg/webrtc"
pion "github.com/pion/webrtc/v3"
pion "github.com/pion/webrtc/v4"
)
// This package handles the Milestone WebRTC session lifecycle, including authentication,
@@ -193,8 +193,10 @@ func milestoneClient(rawURL string, query url.Values) (core.Producer, error) {
}
prod := webrtc.NewConn(pc)
prod.Desc = "WebRTC/Milestone"
prod.FormatName = "webrtc/milestone"
prod.Mode = core.ModeActiveProducer
prod.Protocol = "http"
prod.URL = rawURL
offer, err := mc.GetOffer()
if err != nil {

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