Compare commits

...

192 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
huynhquangtoan
066d559377 Merge branch 'AlexxIT:master' into master 2024-12-31 10:16:29 +07:00
Xiaokui Shu
261a936bb8 Add rtsp server failed auth logging 2024-12-25 17:09:23 -05: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
seydx
1797ff67c0 Merge branch 'master' into rtsp-backchannel 2024-12-14 11:30:37 +01:00
Rob van Oostenrijk
df4b5fc87d Merge branch 'master' into freebsd-builds 2024-12-01 07:19:40 +04: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
seydx
3ddf8b5922 Merge branch 'master' into rtsp-backchannel 2024-11-13 11:23:16 +01: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
Sergey Krashevich
23e8f7e0aa refactor(api): move port extraction logic to Init function for prevent data race 2024-07-28 05:34:49 +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
MarcA711
2b69eb2fd0 update FFmpeg presets for Rockchip boards 2024-06-18 17:56:04 +00: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
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
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
Rob van Oostenrijk
2b8ced9c59 Update build.yml 2024-05-06 08:06:08 +04:00
209 changed files with 9741 additions and 1808 deletions

View File

@@ -19,7 +19,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v5
with: { go-version: '1.22' }
with: { go-version: '1.24' }
- name: Build go2rtc_win64
env: { GOOS: windows, GOARCH: amd64 }
@@ -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:
@@ -156,6 +156,7 @@ jobs:
uses: docker/build-push-action@v5
with:
context: .
file: docker/Dockerfile
platforms: |
linux/amd64
linux/386
@@ -215,10 +216,65 @@ jobs:
uses: docker/build-push-action@v5
with:
context: .
file: hardware.Dockerfile
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
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/rockchip.Dockerfile
platforms: linux/arm64
push: ${{ github.event_name != 'pull_request' }}
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.22'
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

1
.gitignore vendored
View File

@@ -4,6 +4,7 @@
go2rtc.yaml
go2rtc.json
go2rtc_freebsd*
go2rtc_linux*
go2rtc_mac*
go2rtc_win*

View File

@@ -126,6 +126,8 @@ Download binary for your OS from [latest release](https://github.com/AlexxIT/go2
- `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` - 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.
@@ -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`.
@@ -680,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.
@@ -881,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:

View File

@@ -2,19 +2,10 @@
# 0. Prepare images
ARG PYTHON_VERSION="3.11"
ARG GO_VERSION="1.22"
ARG GO_VERSION="1.24"
# 1. Download ngrok binary (for support arm/v6)
FROM alpine AS ngrok
ARG TARGETARCH
ARG TARGETOS
ADD https://bin.equinox.io/c/bNyj1mQVY4c/ngrok-v3-stable-${TARGETOS}-${TARGETARCH}.tgz /
RUN tar -xzf /ngrok-v3-stable-${TARGETOS}-${TARGETARCH}.tgz -C /bin
# 2. Build go2rtc binary
# 1. Build go2rtc binary
FROM --platform=$BUILDPLATFORM golang:${GO_VERSION}-alpine AS build
ARG TARGETPLATFORM
ARG TARGETOS
@@ -35,14 +26,14 @@ COPY . .
RUN --mount=type=cache,target=/root/.cache/go-build CGO_ENABLED=0 go build -ldflags "-s -w" -trimpath
# 3. Final image
# 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
@@ -55,7 +46,6 @@ RUN if [ "${TARGETARCH}" = "amd64" ]; then apk add --no-cache libva-intel-driver
# RUN libva-vdpau-driver mesa-vdpau-gallium (+150MB total)
COPY --from=build /build/go2rtc /usr/local/bin/
COPY --from=ngrok /bin/ngrok /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.22-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

@@ -2,7 +2,6 @@ package main
import (
"log"
"net"
"net/url"
"os"
@@ -41,7 +40,13 @@ func main() {
onvif.DeviceGetSystemDateAndTime,
onvif.DeviceSystemReboot:
b, err = client.DeviceRequest(operation)
case onvif.MediaGetProfiles, onvif.MediaGetVideoSources:
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)
@@ -64,9 +69,7 @@ func main() {
log.Fatal(err)
}
host, _, _ := net.SplitHostPort(u.Host)
if err = os.WriteFile(host+"_"+operation+".xml", b, 0644); err != nil {
if err = os.WriteFile(u.Hostname()+"_"+operation+".xml", b, 0644); err != nil {
log.Printf("%s\n", err)
}
}

43
go.mod
View File

@@ -4,47 +4,46 @@ go 1.20
require (
github.com/asticode/go-astits v1.13.0
github.com/expr-lang/expr v1.16.9
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.62
github.com/pion/ice/v2 v2.3.37
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.10
github.com/pion/sdp/v3 v3.0.9
github.com/pion/srtp/v2 v2.0.20
github.com/pion/stun v0.6.1
github.com/pion/webrtc/v3 v3.3.5
github.com/rs/zerolog v1.33.0
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.10.0
github.com/tadglines/go-pkgs v0.0.0-20210623144937-b983b20f54f9
golang.org/x/crypto v0.31.0
golang.org/x/crypto v0.33.0
gopkg.in/yaml.v3 v3.0.1
)
require (
github.com/asticode/go-astikit v0.45.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.3.1 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/pion/datachannel v1.5.10 // indirect
github.com/pion/dtls/v2 v2.2.12 // indirect
github.com/pion/logging v0.2.2 // indirect
github.com/pion/mdns v0.0.12 // 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.35 // indirect
github.com/pion/transport/v2 v2.2.10 // indirect
github.com/pion/sctp v1.8.37 // indirect
github.com/pion/transport/v3 v3.0.7 // indirect
github.com/pion/turn/v2 v2.1.6 // indirect
github.com/pion/turn/v4 v4.0.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/wlynxg/anet v0.0.5 // indirect
golang.org/x/mod v0.20.0 // indirect
golang.org/x/net v0.33.0 // indirect
golang.org/x/sync v0.10.0 // indirect
golang.org/x/sys v0.28.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
)

162
go.sum
View File

@@ -1,6 +1,6 @@
github.com/asticode/go-astikit v0.30.0/go.mod h1:h4ly7idim1tNhaVkdVBeXQZEE3L0xblP7fCWbgwipF0=
github.com/asticode/go-astikit v0.45.0 h1:08to/jrbod9tchF2bJ9moW+RTDK7DBUxLdIeSE7v7Sw=
github.com/asticode/go-astikit v0.45.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=
@@ -8,71 +8,58 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3
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.9 h1:WUAzmR0JNI9JCiF0/ewwHB1gmcGw5wW7nWt8gc6PpCI=
github.com/expr-lang/expr v1.16.9/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4=
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.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
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 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
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.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ=
github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ=
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/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s=
github.com/pion/dtls/v2 v2.2.12 h1:KP7H5/c1EiVAAKUmXyCzPiQe5+bCJrpOeKg/L05dunk=
github.com/pion/dtls/v2 v2.2.12/go.mod h1:d9SYc9fch0CqK90mRk1dC7AkzzpwJj6u2GU3u+9pqFE=
github.com/pion/ice/v2 v2.3.37 h1:ObIdaNDu1rCo7hObhs34YSBcO7fjslJMZV0ux+uZWh0=
github.com/pion/ice/v2 v2.3.37/go.mod h1:mBF7lnigdqgtB+YHkaY/Y6s6tsyRyo4u4rPGRuOjUBQ=
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.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/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.15 h1:LZQi2JbdipLOj4eBjK4wlVoQWfrZbh3Q6eHtWtJBZBo=
github.com/pion/rtcp v1.2.15/go.mod h1:jlGuAjHMEXwMUHK78RgX0UmEJFV4zUKOFHR7OP+D3D0=
github.com/pion/rtp v1.8.3/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU=
github.com/pion/rtp v1.8.10 h1:puphjdbjPB+L+NFaVuZ5h6bt1g5q4kFIoI+r5q/g0CU=
github.com/pion/rtp v1.8.10/go.mod h1:8uMBJj32Pa1wwx8Fuv/AsFhn8jsgw+3rUC2PfoBZ8p4=
github.com/pion/sctp v1.8.35 h1:qwtKvNK1Wc5tHMIYgTDJhfZk7vATGVHhXbUDfHbYwzA=
github.com/pion/sctp v1.8.35/go.mod h1:EcXP8zCYVTRy3W9xtOF7wJm1L1aXfKRQzaM33SjQlzg=
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.20 h1:HNNny4s+OUmG280ETrCdgFndp4ufx3/uy85EawYEhTk=
github.com/pion/srtp/v2 v2.0.20/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.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.10 h1:ucLBLE8nuxiHfvkFKnkDQRYWYfp8ejf4YBOPfaQpw6Q=
github.com/pion/transport/v2 v2.2.10/go.mod h1:sq1kSLWs+cHW9E+2fJP95QudkzbK7wscs8yYgQToO5E=
github.com/pion/transport/v3 v3.0.1/go.mod h1:UY7kiITrlMv7/IKgd5eTUcaahZx5oUN3l9SzK5f5xE0=
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/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.3.5 h1:ZsSzaMz/i9nblPdiAkZoP+E6Kmjw+jnyq3bEmU3EtRg=
github.com/pion/webrtc/v3 v3.3.5/go.mod h1:liNa+E1iwyzyXqNUwvoMRNQ10x8h8FOeJKL8RkIbamE=
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=
@@ -80,105 +67,38 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
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.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8=
github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
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/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/wlynxg/anet v0.0.3/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU=
github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
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.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.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
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/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/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
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.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
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.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
golang.org/x/sync v0.10.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=
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.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.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.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.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/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.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.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA=
golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c=
golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg=
golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI=
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=
golang.org/x/tools v0.25.0 h1:oFU9pkj/iJgs+0DT+VMHrx+oBKs/LJMV+Uvg78sl+fE=
golang.org/x/tools v0.25.0/go.mod h1:/vtpO8WL1N9cQC3FN5zPqb//fRXskFHbLKk4OW1Q7rg=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
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

@@ -18,7 +18,7 @@ func LoadConfig(v any) {
}
}
func PatchConfig(key string, value any, path ...string) error {
func PatchConfig(path []string, value any) error {
if ConfigPath == "" {
return errors.New("config file disabled")
}
@@ -26,7 +26,7 @@ func PatchConfig(key string, value any, path ...string) error {
// empty config is OK
b, _ := os.ReadFile(ConfigPath)
b, err := yaml.Patch(b, key, value, path...)
b, err := yaml.Patch(b, path, value)
if err != nil {
return err
}

View File

@@ -3,12 +3,14 @@ package app
import (
"io"
"os"
"strings"
"sync"
"github.com/mattn/go-isatty"
"github.com/rs/zerolog"
)
var MemoryLog = newBuffer(16)
var MemoryLog = newBuffer()
func GetLogger(module string) zerolog.Logger {
if s, ok := modules[module]; ok {
@@ -38,11 +40,17 @@ func initLogger() {
var writer io.Writer
switch modules["output"] {
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"]
@@ -99,15 +107,19 @@ var modules = map[string]string{
"time": zerolog.TimeFormatUnixMs,
}
const chunkSize = 1 << 16
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
@@ -116,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
}
}
@@ -140,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

@@ -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,39 +0,0 @@
package exec
import (
"errors"
"net/url"
"os"
"os/exec"
"syscall"
"time"
"github.com/AlexxIT/go2rtc/pkg/core"
)
// closer support custom killsignal with custom killtimeout
type closer struct {
cmd *exec.Cmd
query url.Values
}
func (c *closer) Close() (err error) {
sig := os.Kill
if s := c.query.Get("killsignal"); s != "" {
sig = syscall.Signal(core.Atoi(s))
}
log.Trace().Msgf("[exec] kill with signal=%d", sig)
err = c.cmd.Process.Signal(sig)
if s := c.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)
_ = c.cmd.Process.Kill()
})
defer timer.Stop() // stop timer if Wait ends before timeout
}
return errors.Join(err, c.cmd.Wait())
}

View File

@@ -9,9 +9,9 @@ import (
"io"
"net/url"
"os"
"os/exec"
"strings"
"sync"
"syscall"
"time"
"github.com/AlexxIT/go2rtc/internal/app"
@@ -19,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"
)
@@ -49,7 +49,7 @@ 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)
@@ -67,39 +67,55 @@ func execHandle(rawURL string) (core.Producer, error) {
rawURL = rawURL[:i] + "rtsp://127.0.0.1:" + rtsp.Port + path + rawURL[i+8:]
}
args := shell.QuoteSplit(rawURL[5:]) // remove `exec:`
cmd := exec.Command(args[0], args[1:]...)
cmd := shell.NewCommand(rawURL[5:]) // remove `exec:`
cmd.Stderr = &logWriter{
buf: make([]byte, 512),
debug: log.Debug().Enabled(),
}
if query.Get("backchannel") == "1" {
return stdin.NewClient(cmd)
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)
}
}
cl := &closer{cmd: cmd, query: query}
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, cl)
prod, err = handlePipe(rawURL, cmd)
} else {
prod, err = handleRTSP(rawURL, cmd, path)
}
return handleRTSP(rawURL, cmd, cl, path)
if err != nil {
_ = cmd.Close()
}
return
}
func handlePipe(source string, cmd *exec.Cmd, cl io.Closer) (core.Producer, error) {
func handlePipe(source string, cmd *shell.Command) (core.Producer, error) {
stdout, err := cmd.StdoutPipe()
if err != nil {
return nil, err
}
rc := struct {
rd := struct {
io.Reader
io.Closer
}{
// add buffer for pipe reader to reduce syscall
bufio.NewReaderSize(stdout, core.BufferSize),
cl,
// stop cmd on close pipe call
cmd,
}
log.Debug().Strs("args", cmd.Args).Msg("[exec] run pipe")
@@ -110,9 +126,8 @@ func handlePipe(source string, cmd *exec.Cmd, cl io.Closer) (core.Producer, erro
return nil, err
}
prod, err := magic.Open(rc)
prod, err := magic.Open(rd)
if err != nil {
_ = rc.Close()
return nil, fmt.Errorf("exec/pipe: %w\n%s", err, cmd.Stderr)
}
@@ -126,7 +141,7 @@ func handlePipe(source string, cmd *exec.Cmd, cl io.Closer) (core.Producer, erro
return prod, nil
}
func handleRTSP(source string, cmd *exec.Cmd, cl io.Closer, path string) (core.Producer, error) {
func handleRTSP(source string, cmd *shell.Command, path string) (core.Producer, error) {
if log.Trace().Enabled() {
cmd.Stdout = os.Stdout
}
@@ -152,23 +167,22 @@ func handleRTSP(source string, cmd *exec.Cmd, cl io.Closer, path string) (core.P
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.Minute):
case <-timeout.C:
// haven't received data from app in timeout
log.Error().Str("source", source).Msg("[exec] timeout")
_ = cl.Close()
return nil, errors.New("exec: timeout")
case <-done:
// limit message size
case <-cmd.Done():
// app fail before we receive any data
return nil, fmt.Errorf("exec/rtsp\n%s", cmd.Stderr)
case prod := <-waiter:
// app started successfully
log.Debug().Stringer("launch", time.Since(ts)).Msg("[exec] run rtsp")
setRemoteInfo(prod, source, cmd.Args)
prod.OnClose = cl.Close
prod.OnClose = cmd.Close
return prod, nil
}
}

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

@@ -113,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
@@ -129,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

View File

@@ -251,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) {

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

@@ -128,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")
}
}
}

View File

@@ -11,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 -"
@@ -39,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,
},
}
}
@@ -83,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

@@ -42,6 +42,7 @@ func NewProducer(url string) (core.Producer, error) {
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},
@@ -97,6 +98,8 @@ func (p *Producer) newURL() string {
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:

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

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

@@ -79,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)
@@ -118,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)
@@ -148,13 +148,19 @@ func streamHandler(rawURL string) (core.Producer, error) {
return client, err
}
func hapPairSetup(w http.ResponseWriter, r *http.Request) {
srv, ok := servers[r.Host]
if !ok {
log.Error().Msg("[homekit] unknown host: " + r.Host)
return
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
@@ -162,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 ""
}

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

@@ -2,12 +2,9 @@ package ivideon
import (
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/ivideon"
)
func Init() {
streams.HandleFunc("ivideon", func(source string) (core.Producer, error) {
return ivideon.Dial(source)
})
streams.HandleFunc("ivideon", ivideon.Dial)
}

View File

@@ -2,6 +2,7 @@ package nest
import (
"net/http"
"strings"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/streams"
@@ -38,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

@@ -72,7 +72,11 @@ func onvifDeviceService(w http.ResponseWriter, r *http.Request) {
onvif.DeviceGetNetworkDefaultGateway,
onvif.DeviceGetNetworkProtocols,
onvif.DeviceGetNTP,
onvif.DeviceGetScopes:
onvif.DeviceGetScopes,
onvif.MediaGetVideoEncoderConfigurations,
onvif.MediaGetAudioEncoderConfigurations,
onvif.MediaGetAudioSources,
onvif.MediaGetAudioSourceConfigurations:
b = onvif.StaticResponse(operation)
case onvif.DeviceGetCapabilities:
@@ -99,16 +103,20 @@ func onvifDeviceService(w http.ResponseWriter, r *http.Request) {
})
case onvif.MediaGetVideoSources:
b = onvif.GetVideoSourcesResponse(streams.GetAll())
b = onvif.GetVideoSourcesResponse(streams.GetAllNames())
case onvif.MediaGetProfiles:
// important for Hass: H264 codec, width, height
b = onvif.GetProfilesResponse(streams.GetAll())
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)
@@ -129,6 +137,7 @@ func onvifDeviceService(w http.ResponseWriter, r *http.Request) {
default:
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
}

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

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

@@ -1,6 +1,7 @@
package rtsp
import (
"errors"
"io"
"net"
"net/url"
@@ -185,6 +186,22 @@ 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))
}
@@ -237,7 +254,9 @@ func tcpHandler(conn *rtsp.Conn) {
})
if err := conn.Accept(); err != nil {
if err != io.EOF {
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 {

View File

@@ -1,8 +1,55 @@
## Testing notes
## Examples
```yaml
streams:
test1-basic: ffmpeg:virtual?video#video=h264
test2-reconnect: ffmpeg:virtual?video&duration=10#video=h264
test3-execkill: exec:./examples/rtsp_client/rtsp_client/rtsp_client {output}
# 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

@@ -53,7 +53,7 @@ func apiStreams(w http.ResponseWriter, r *http.Request) {
return
}
if err := app.PatchConfig(name, query["src"], "streams"); err != nil {
if err := app.PatchConfig([]string{"streams", name}, query["src"]); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
}
@@ -96,7 +96,7 @@ func apiStreams(w http.ResponseWriter, r *http.Request) {
case "DELETE":
delete(streams, src)
if err := app.PatchConfig(src, nil, "streams"); err != nil {
if err := app.PatchConfig([]string{"streams", src}, nil); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
}
}

View File

@@ -171,5 +171,6 @@ func (c *conn) label() string {
if c.UserAgent != "" {
sb.WriteString("\nuser_agent=" + c.UserAgent)
}
return sb.String()
// 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"
)
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 {
@@ -16,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
@@ -140,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

@@ -47,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) {

View File

@@ -42,10 +42,6 @@ func Init() {
})
}
func Get(name string) *Stream {
return streams[name]
}
var sanitize = regexp.MustCompile(`\s`)
// Validate - not allow creating dynamic streams with spaces in the source
@@ -68,6 +64,7 @@ func New(name string, sources ...string) *Stream {
streamsMu.Lock()
streams[name] = stream
streamsMu.Unlock()
return stream
}
@@ -124,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
}
@@ -139,21 +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 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
}
func Delete(id string) {
delete(streams, id)
}
var log zerolog.Logger
var streams = map[string]*Stream{}
var streamsMu sync.Mutex

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

@@ -11,13 +11,15 @@ 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
- 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/tcp"
listen: ":8555"
ice_servers:
- urls: [ "stun:stun.l.google.com:19302" ]
```
@@ -29,7 +31,7 @@ webrtc:
```yaml
webrtc:
# fix local TCP or UDP or both ports for WebRTC media
listen: ":8555/tcp" # address of your local server
listen: ":8555" # address of your local server
# add additional host candidates manually
# order is important, the first will have a higher priority
@@ -53,17 +55,20 @@ webrtc:
# 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
# not related to the `listen` option
# including interfaces from unspecified `listen` option (empty host)
interfaces: [ eno1 ]
# list of host IP-addresses to be used for connection
# not related to the `listen` option
# 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
@@ -71,14 +76,16 @@ webrtc:
udp_ports: [ 50000, 50100 ]
```
By default go2rtc uses **fixed TCP** port and multiple **random UDP** ports for each WebRTC connection - `listen: ":8555/tcp"`.
By default go2rtc uses **fixed TCP** port and **fixed UDP** ports for each **direct** WebRTC connection - `listen: ":8555"`.
You can set **fixed TCP** and **fixed UDP** port for all connections - `listen: ":8555"`. This may has lower performance, but it's your choice.
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.
@@ -97,8 +104,6 @@ For example, go2rtc inside closed docker container (ex. [Frigate](https://frigat
webrtc:
listen: ":8555" # use fixed TCP and UDP ports
candidates: [ 192.168.1.2:8555 ] # add manual host candidate (use docker port forwarding)
filters:
candidates: [] # skip all internal docker candidates
```
## Userful links

View File

@@ -7,7 +7,8 @@ import (
"github.com/AlexxIT/go2rtc/internal/api/ws"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/webrtc"
pion "github.com/pion/webrtc/v3"
"github.com/AlexxIT/go2rtc/pkg/xnet"
pion "github.com/pion/webrtc/v4"
)
type Address struct {
@@ -73,6 +74,11 @@ func FilterCandidate(candidate *pion.ICECandidate) bool {
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) {

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

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, format 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 {
@@ -108,23 +111,33 @@ func kinesisClient(rawURL string, query url.Values, format string) (core.Produce
}
})
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
}
@@ -218,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,

View File

@@ -9,7 +9,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"
)
func openIPCClient(rawURL string, query url.Values) (core.Producer, error) {

View File

@@ -1,6 +1,7 @@
package webrtc
import (
"encoding/base64"
"encoding/json"
"io"
"net/http"
@@ -12,7 +13,7 @@ import (
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/webrtc"
pion "github.com/pion/webrtc/v3"
pion "github.com/pion/webrtc/v4"
)
const MimeSDP = "application/sdp"
@@ -62,8 +63,8 @@ func syncHandler(w http.ResponseWriter, r *http.Request) {
// 2. application/sdp - receive/response SDP via WebRTC-HTTP Egress Protocol (WHEP)
// 3. other - receive/response raw SDP
func outputWebRTC(w http.ResponseWriter, r *http.Request) {
url := r.URL.Query().Get("src")
stream := streams.Get(url)
u := r.URL.Query().Get("src")
stream := streams.Get(u)
if stream == nil {
http.Error(w, api.StreamNotFound, http.StatusNotFound)
return
@@ -87,6 +88,21 @@ func outputWebRTC(w http.ResponseWriter, r *http.Request) {
}
offer = desc.SDP
case "application/x-www-form-urlencoded":
if err := r.ParseForm(); err != nil {
log.Error().Err(err).Caller().Send()
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
offerB64 := r.Form.Get("data")
b, err := base64.StdEncoding.DecodeString(offerB64)
if err != nil {
log.Error().Err(err).Caller().Send()
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
offer = string(b)
default:
body, err := io.ReadAll(r.Body)
if err != nil {
@@ -124,6 +140,11 @@ func outputWebRTC(w http.ResponseWriter, r *http.Request) {
}
err = json.NewEncoder(w).Encode(v)
case "application/x-www-form-urlencoded":
w.Header().Set("Content-Type", mediaType)
answerB64 := base64.StdEncoding.EncodeToString([]byte(answer))
_, err = w.Write([]byte(answerB64))
case MimeSDP:
w.Header().Set("Content-Type", mediaType)
w.WriteHeader(http.StatusCreated)

View File

@@ -0,0 +1,40 @@
package webrtc
import (
"net/url"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/webrtc"
)
func switchbotClient(rawURL string, query url.Values) (core.Producer, error) {
return kinesisClient(rawURL, query, "webrtc/switchbot", func(prod *webrtc.Conn, query url.Values) (any, error) {
medias := []*core.Media{
{Kind: core.KindVideo, Direction: core.DirectionRecvonly},
}
offer, err := prod.CreateOffer(medias)
if err != nil {
return nil, err
}
v := struct {
Type string `json:"type"`
SDP string `json:"sdp"`
Resolution int `json:"resolution"`
PlayType int `json:"play_type"`
}{
Type: "offer",
SDP: offer,
}
switch query.Get("resolution") {
case "hd":
v.Resolution = 0
case "sd":
v.Resolution = 1
}
return v, nil
})
}

View File

@@ -10,7 +10,7 @@ import (
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/webrtc"
pion "github.com/pion/webrtc/v3"
pion "github.com/pion/webrtc/v4"
"github.com/rs/zerolog"
)
@@ -24,7 +24,7 @@ func Init() {
} `yaml:"webrtc"`
}
cfg.Mod.Listen = ":8555/tcp"
cfg.Mod.Listen = ":8555"
cfg.Mod.IceServers = []pion.ICEServer{
{URLs: []string{"stun:stun.l.google.com:19302"}},
}

View File

@@ -2,10 +2,11 @@ package webrtc
import (
"encoding/json"
"strings"
"testing"
"github.com/AlexxIT/go2rtc/internal/api/ws"
pion "github.com/pion/webrtc/v3"
pion "github.com/pion/webrtc/v4"
"github.com/stretchr/testify/require"
)
@@ -36,3 +37,37 @@ func TestWebRTCAPIv2(t *testing.T) {
require.Equal(t, "v=0\n...", offer.SDP)
require.Equal(t, "stun:stun.l.google.com:19302", offer.ICEServers[0].URLs[0])
}
func TestCrealitySDP(t *testing.T) {
sdp := `v=0
o=- 1495799811084970 1495799811084970 IN IP4 0.0.0.0
s=-
t=0 0
a=msid-semantic:WMS *
a=group:BUNDLE 0
m=video 9 UDP/TLS/RTP/SAVPF 96 98
a=rtcp-fb:98 nack
a=rtcp-fb:98 nack pli
a=fmtp:96 profile-level-id=42e01f;level-asymmetry-allowed=1
a=fmtp:98 profile-level-id=42e01f;packetization-mode=1;level-asymmetry-allowed=1
a=fmtp:98 x-google-max-bitrate=6000;x-google-min-bitrate=2000;x-google-start-bitrate=4000
a=rtpmap:96 H264/90000
a=rtpmap:98 H264/90000
a=ssrc:1 cname:pear
c=IN IP4 0.0.0.0
a=sendonly
a=mid:0
a=rtcp-mux
a=ice-ufrag:7AVa
a=ice-pwd:T+F/5y05Paw+mtG5Jrd8N3
a=ice-options:trickle
a=fingerprint:sha-256 A5:AB:C0:4E:29:5B:BD:3B:7D:88:24:6C:56:6B:2A:79:A3:76:99:35:57:75:AD:C8:5A:A6:34:20:88:1B:68:EF
a=setup:passive
a=candidate:1 1 UDP 2015363327 172.22.233.10 48929 typ host
a=candidate:2 1 TCP 1015021823 172.22.233.10 0 typ host tcptype active
a=candidate:3 1 TCP 1010827519 172.22.233.10 60677 typ host tcptype passive
`
sdp, err := fixCrealitySDP(sdp)
require.Nil(t, err)
require.False(t, strings.Contains(sdp, "x-google-max-bitrate"))
}

View File

@@ -2,9 +2,10 @@ package webtorrent
import (
"fmt"
"net/http"
"github.com/AlexxIT/go2rtc/pkg/webtorrent"
"github.com/gorilla/websocket"
"net/http"
)
var upgrader *websocket.Upgrader

267
internal/wyoming/README.md Normal file
View File

@@ -0,0 +1,267 @@
# Wyoming
This module provide [Wyoming Protocol](https://www.home-assistant.io/integrations/wyoming/) support to create local voice assistants using [Home Assistant](https://www.home-assistant.io/).
- go2rtc can act as [Wyoming Satellite](https://github.com/rhasspy/wyoming-satellite)
- go2rtc can act as [Wyoming External Microphone](https://github.com/rhasspy/wyoming-mic-external)
- go2rtc can act as [Wyoming External Sound](https://github.com/rhasspy/wyoming-snd-external)
- any supported audio source with PCM codec can be used as audio input
- any supported two-way audio source with PCM codec can be used as audio output
- any desktop/server microphone/speaker can be used as two-way audio source
- supported any OS via FFmpeg or any similar software
- supported Linux via alsa source
- you can change the behavior using the built-in scripting engine
## Typical Voice Pipeline
1. Audio stream (MIC)
- any audio source with PCM codec support (include PCMA/PCMU)
2. Voice Activity Detector (VAD)
3. Wake Word (WAKE)
- [OpenWakeWord](https://www.home-assistant.io/voice_control/create_wake_word/)
4. Speech-to-Text (STT)
- [Whisper](https://github.com/home-assistant/addons/blob/master/whisper/README.md)
- [Vosk](https://github.com/rhasspy/hassio-addons/blob/master/vosk/README.md)
5. Conversation agent (INTENT)
- [Home Assistant](https://www.home-assistant.io/integrations/conversation/)
6. Text-to-speech (TTS)
- [Google Translate](https://www.home-assistant.io/integrations/google_translate/)
- [Piper](https://github.com/home-assistant/addons/blob/master/piper/README.md)
7. Audio stream (SND)
- any source with two-way audio (backchannel) and PCM codec support (include PCMA/PCMU)
You can use a large number of different projects for WAKE, STT, INTENT and TTS thanks to the Home Assistant.
And you can use a large number of different technologies for MIC and SND thanks to Go2rtc.
## Configuration
You can optionally specify WAKE service. So go2rtc will start transmitting audio to Home Assistant only after WAKE word. If the WAKE service cannot be connected to or not specified - go2rtc will pass all audio to Home Assistant. In this case WAKE service must be configured in your Voice Assistant pipeline.
You can optionally specify VAD threshold. So go2rtc will start transmitting audio to WAKE service only after some audio noise.
Your stream must support audio transmission in PCM codec (include PCMA/PCMU).
```yaml
wyoming:
stream_name_from_streams_section:
listen: :10700
name: "My Satellite" # optional name
wake_uri: tcp://192.168.1.23:10400 # optional WAKE service
vad_threshold: 1 # optional VAD threshold (from 0.1 to 3.5)
```
Home Assistant -> Settings -> Integrations -> Add -> Wyoming Protocol -> Host + Port from `go2rtc.yaml`
Select one or multiple wake words:
```yaml
wake_uri: tcp://192.168.1.23:10400?name=alexa_v0.1&name=hey_jarvis_v0.1&name=hey_mycroft_v0.1&name=hey_rhasspy_v0.1&name=ok_nabu_v0.1
```
## Events
You can add wyoming event handling using the [expr](https://github.com/AlexxIT/go2rtc/blob/master/internal/expr/README.md) language. For example, to pronounce TTS on some media player from HA.
Turn on the logs to see what kind of events happens.
This is what the default scripts look like:
```yaml
wyoming:
script_example:
event:
run-satellite: Detect()
pause-satellite: Stop()
voice-stopped: Pause()
audio-stop: PlayAudio() && WriteEvent("played") && Detect()
error: Detect()
internal-run: WriteEvent("run-pipeline", '{"start_stage":"wake","end_stage":"tts"}') && Stream()
internal-detection: WriteEvent("run-pipeline", '{"start_stage":"asr","end_stage":"tts"}') && Stream()
```
If you write a script for an event - the default action is no longer executed. You need to repeat the necessary steps yourself.
In addition to the standard events, there are two additional events:
- `internal-run` - called after `Detect()` when VAD detected, but WAKE service unavailable
- `internal-detection` - called after `Detect()` when WAKE word detected
**Example 1.** You want to play a sound file when a wake word detected (only `wav` supported):
- `PlayFile` and `PlayAudio` functions are executed synchronously, the following steps will be executed only after they are completed
```yaml
wyoming:
script_example:
event:
internal-detection: PlayFile('/media/beep.wav') && WriteEvent("run-pipeline", '{"start_stage":"asr","end_stage":"tts"}') && Stream()
```
**Example 2.** You want to play TTS on a Home Assistant media player:
Each event has a `Type` and `Data` in JSON format. You can use their values in scripts.
- in the `synthesize` step, we get the value of the `text` and call the HA REST API
- in the `audio-stop` step we get the duration of the TTS in seconds, wait for this time and start the pipeline again
```yaml
wyoming:
script_example:
event:
synthesize: |
let text = fromJSON(Data).text;
let token = 'eyJhbGci...';
fetch('http://localhost:8123/api/services/tts/speak', {
method: 'POST',
headers: {'Authorization': 'Bearer '+token,'Content-Type': 'application/json'},
body: toJSON({
entity_id: 'tts.google_translate_com',
media_player_entity_id: 'media_player.google_nest',
message: text,
language: 'en',
}),
}).ok
audio-stop: |
let timestamp = fromJSON(Data).timestamp;
let delay = string(timestamp)+'s';
Sleep(delay) && WriteEvent("played") && Detect()
```
## Config examples
Satellite on Windows server using FFmpeg and FFplay.
```yaml
streams:
satellite_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 -hide_banner -nodisp -probesize 32 -f s16le -ar 22050 -#backchannel=1#audio=s16le/22050
wyoming:
satellite_win:
listen: :10700
name: "Windows Satellite"
wake_uri: tcp://192.168.1.23:10400
vad_threshold: 1
```
Satellite on Dahua camera with two-way audio support.
```yaml
streams:
dahua_camera:
- rtsp://admin:password@192.168.1.123/cam/realmonitor?channel=1&subtype=1&unicast=true&proto=Onvif
wyoming:
dahua_camera:
listen: :10700
name: "Dahua Satellite"
wake_uri: tcp://192.168.1.23:10400
vad_threshold: 1
```
Satellite on Dahua camera with two-way audio support.
```yaml
streams:
wyoming_external:
- wyoming://192.168.1.23:10600 # wyoming-mic-external
- wyoming://192.168.1.23:10601?backchannel=1 # wyoming-snd-external
wyoming:
wyoming_external:
listen: :10700
name: "Wyoming Satellite"
wake_uri: tcp://192.168.1.23:10400
vad_threshold: 1
```
## Wyoming External Microphone and Sound
Advanced users, who want to enjoy the [Wyoming Satellite](https://github.com/rhasspy/wyoming-satellite) project, can use go2rtc as a [Wyoming External Microphone](https://github.com/rhasspy/wyoming-mic-external) or [Wyoming External Sound](https://github.com/rhasspy/wyoming-snd-external).
**go2rtc.yaml**
```yaml
streams:
wyoming_mic_external:
- exec:ffmpeg -hide_banner -f dshow -i "audio=Microphone (High Definition Audio Device)" -c pcm_s16le -ar 16000 -ac 1 -f wav -
wyoming_snd_external:
- exec:ffplay -hide_banner -nodisp -probesize 32 -f s16le -ar 22050 -#backchannel=1#audio=s16le/22050
wyoming:
wyoming_mic_external:
listen: :10600
mode: mic
wyoming_snd_external:
listen: :10601
mode: snd
```
**docker-compose.yml**
```yaml
version: "3.8"
services:
satellite:
build: wyoming-satellite # https://github.com/rhasspy/wyoming-satellite
ports:
- "10700:10700"
command:
- "--name"
- "my satellite"
- "--mic-uri"
- "tcp://192.168.1.23:10600"
- "--snd-uri"
- "tcp://192.168.1.23:10601"
- "--debug"
```
## Wyoming External Source
**go2rtc.yaml**
```yaml
streams:
wyoming_external:
- wyoming://192.168.1.23:10600
- wyoming://192.168.1.23:10601?backchannel=1
```
**docker-compose.yml**
```yaml
version: "3.8"
services:
microphone:
build: wyoming-mic-external # https://github.com/rhasspy/wyoming-mic-external
ports:
- "10600:10600"
devices:
- /dev/snd:/dev/snd
group_add:
- audio
command:
- "--device"
- "sysdefault"
- "--debug"
playback:
build: wyoming-snd-external # https://github.com/rhasspy/wyoming-snd-external
ports:
- "10601:10601"
devices:
- /dev/snd:/dev/snd
group_add:
- audio
command:
- "--device"
- "sysdefault"
- "--debug"
```
## Debug
```yaml
log:
wyoming: trace
```

106
internal/wyoming/wyoming.go Normal file
View File

@@ -0,0 +1,106 @@
package wyoming
import (
"net"
"github.com/AlexxIT/go2rtc/internal/app"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/wyoming"
"github.com/rs/zerolog"
)
func Init() {
streams.HandleFunc("wyoming", wyoming.Dial)
// server
var cfg struct {
Mod map[string]struct {
Listen string `yaml:"listen"`
Name string `yaml:"name"`
Mode string `yaml:"mode"`
Event map[string]string `yaml:"event"`
WakeURI string `yaml:"wake_uri"`
VADThreshold float32 `yaml:"vad_threshold"`
} `yaml:"wyoming"`
}
app.LoadConfig(&cfg)
log = app.GetLogger("wyoming")
for name, cfg := range cfg.Mod {
stream := streams.Get(name)
if stream == nil {
log.Warn().Msgf("[wyoming] missing stream: %s", name)
continue
}
if cfg.Name == "" {
cfg.Name = name
}
srv := &wyoming.Server{
Name: cfg.Name,
Event: cfg.Event,
VADThreshold: int16(1000 * cfg.VADThreshold), // 1.0 => 1000
WakeURI: cfg.WakeURI,
MicHandler: func(cons core.Consumer) error {
if err := stream.AddConsumer(cons); err != nil {
return err
}
// not best solution
if i, ok := cons.(interface{ OnClose(func()) }); ok {
i.OnClose(func() {
stream.RemoveConsumer(cons)
})
}
return nil
},
SndHandler: func(prod core.Producer) error {
return stream.Play(prod)
},
Trace: func(format string, v ...any) {
log.Trace().Msgf("[wyoming] "+format, v...)
},
Error: func(format string, v ...any) {
log.Error().Msgf("[wyoming] "+format, v...)
},
}
go serve(srv, cfg.Mode, cfg.Listen)
}
}
var log zerolog.Logger
func serve(srv *wyoming.Server, mode, address string) {
ln, err := net.Listen("tcp", address)
if err != nil {
log.Warn().Err(err).Msgf("[wyoming] listen")
}
for {
conn, err := ln.Accept()
if err != nil {
return
}
go handle(srv, mode, conn)
}
}
func handle(srv *wyoming.Server, mode string, conn net.Conn) {
addr := conn.RemoteAddr()
log.Trace().Msgf("[wyoming] %s connected", addr)
switch mode {
case "mic":
srv.HandleMic(conn)
case "snd":
srv.HandleSnd(conn)
default:
srv.Handle(conn)
}
log.Trace().Msgf("[wyoming] %s disconnected", addr)
}

14
main.go
View File

@@ -1,6 +1,7 @@
package main
import (
"github.com/AlexxIT/go2rtc/internal/alsa"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/api/ws"
"github.com/AlexxIT/go2rtc/internal/app"
@@ -9,9 +10,11 @@ import (
"github.com/AlexxIT/go2rtc/internal/doorbird"
"github.com/AlexxIT/go2rtc/internal/dvrip"
"github.com/AlexxIT/go2rtc/internal/echo"
"github.com/AlexxIT/go2rtc/internal/eseecloud"
"github.com/AlexxIT/go2rtc/internal/exec"
"github.com/AlexxIT/go2rtc/internal/expr"
"github.com/AlexxIT/go2rtc/internal/ffmpeg"
"github.com/AlexxIT/go2rtc/internal/flussonic"
"github.com/AlexxIT/go2rtc/internal/gopro"
"github.com/AlexxIT/go2rtc/internal/hass"
"github.com/AlexxIT/go2rtc/internal/hls"
@@ -25,19 +28,22 @@ import (
"github.com/AlexxIT/go2rtc/internal/nest"
"github.com/AlexxIT/go2rtc/internal/ngrok"
"github.com/AlexxIT/go2rtc/internal/onvif"
"github.com/AlexxIT/go2rtc/internal/ring"
"github.com/AlexxIT/go2rtc/internal/roborock"
"github.com/AlexxIT/go2rtc/internal/rtmp"
"github.com/AlexxIT/go2rtc/internal/rtsp"
"github.com/AlexxIT/go2rtc/internal/srtp"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/internal/tapo"
"github.com/AlexxIT/go2rtc/internal/v4l2"
"github.com/AlexxIT/go2rtc/internal/webrtc"
"github.com/AlexxIT/go2rtc/internal/webtorrent"
"github.com/AlexxIT/go2rtc/internal/wyoming"
"github.com/AlexxIT/go2rtc/pkg/shell"
)
func main() {
app.Version = "1.9.8"
app.Version = "1.9.9"
// 1. Core modules: app, api/ws, streams
@@ -64,6 +70,7 @@ func main() {
hass.Init() // hass source, Hass API server
onvif.Init() // onvif source, ONVIF API server
webtorrent.Init() // webtorrent source, WebTorrent module
wyoming.Init()
// 5. Other sources
@@ -79,11 +86,16 @@ func main() {
mpegts.Init() // mpegts passive source
roborock.Init() // roborock source
homekit.Init() // homekit source
ring.Init() // ring source
nest.Init() // nest source
bubble.Init() // bubble source
expr.Init() // expr source
gopro.Init() // gopro source
doorbird.Init() // doorbird source
v4l2.Init() // v4l2 source
alsa.Init() // alsa source
flussonic.Init()
eseecloud.Init()
// 6. Helper modules

View File

@@ -13,6 +13,7 @@ Some formats and protocols go2rtc supports exclusively. They have no equivalent
| Format | Source protocols | Ingress protocols | Recevers codecs | Senders codecs | Example |
|--------------|------------------|-------------------|------------------------------|--------------------|---------------|
| adts | http,tcp,pipe | http | aac | | `http:` |
| alsa | pipe | | | pcm | `alsa:` |
| bubble | http | | h264,hevc,pcm_alaw | | `bubble:` |
| dvrip | tcp | | h264,hevc,pcm_alaw,pcm_mulaw | pcm_alaw | `dvrip:` |
| flv | http,tcp,pipe | http | h264,aac | | `http:` |

View File

@@ -53,7 +53,7 @@ func ConfigToCodec(conf []byte) *core.Codec {
codec.ClockRate = rd.ReadBits(24)
}
codec.Channels = rd.ReadBits16(4)
codec.Channels = rd.ReadBits8(4)
return codec
}

View File

@@ -28,7 +28,7 @@ func ADTSToCodec(b []byte) *core.Codec {
objType := rd.ReadBits8(2) + 1 // Profile, the MPEG-4 Audio Object Type minus 1
sampleRateIdx := rd.ReadBits8(4) // MPEG-4 Sampling Frequency Index
_ = rd.ReadBit() // Private bit, guaranteed never to be used by MPEG, set to 0 when encoding, ignore when decoding
channels := rd.ReadBits16(3) // MPEG-4 Channel Configuration
channels := rd.ReadBits8(3) // MPEG-4 Channel Configuration
//_ = rd.ReadBit() // Originality, set to 1 to signal originality of the audio and 0 otherwise
//_ = rd.ReadBit() // Home, set to 1 to signal home usage of the audio and 0 otherwise
@@ -43,7 +43,7 @@ func ADTSToCodec(b []byte) *core.Codec {
wr := bits.NewWriter(nil)
wr.WriteBits8(objType, 5)
wr.WriteBits8(sampleRateIdx, 4)
wr.WriteBits16(channels, 4)
wr.WriteBits8(channels, 4)
conf := wr.Bytes()
codec := &core.Codec{

23
pkg/alsa/README.md Normal file
View File

@@ -0,0 +1,23 @@
## Build
```shell
x86_64-linux-gnu-gcc -w -static asound_arch.c -o asound_amd64
i686-linux-gnu-gcc -w -static asound_arch.c -o asound_i386
aarch64-linux-gnu-gcc -w -static asound_arch.c -o asound_arm64
arm-linux-gnueabihf-gcc -w -static asound_arch.c -o asound_arm
mipsel-linux-gnu-gcc -w -static asound_arch.c -o asound_mipsle -D_TIME_BITS=32
```
## Useful links
- https://github.com/torvalds/linux/blob/master/include/uapi/sound/asound.h
- https://github.com/yobert/alsa
- https://github.com/Narsil/alsa-go
- https://github.com/alsa-project/alsa-lib
- https://github.com/anisse/alsa
- https://github.com/tinyalsa/tinyalsa
**Broken pipe**
- https://stackoverflow.com/questions/26545139/alsa-cannot-recovery-from-underrun-prepare-failed-broken-pipe
- https://klipspringer.avadeaux.net/alsa-broken-pipe-errors/

90
pkg/alsa/capture_linux.go Normal file
View File

@@ -0,0 +1,90 @@
package alsa
import (
"github.com/AlexxIT/go2rtc/pkg/alsa/device"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/pcm"
"github.com/pion/rtp"
)
type Capture struct {
core.Connection
dev *device.Device
closed core.Waiter
}
func newCapture(dev *device.Device) (*Capture, error) {
medias := []*core.Media{
{
Kind: core.KindAudio,
Direction: core.DirectionRecvonly,
Codecs: []*core.Codec{
{Name: core.CodecPCML, ClockRate: 16000},
},
},
}
return &Capture{
Connection: core.Connection{
ID: core.NewID(),
FormatName: "alsa",
Medias: medias,
Transport: dev,
},
dev: dev,
}, nil
}
func (c *Capture) Start() error {
dst := c.Medias[0].Codecs[0]
src := &core.Codec{
Name: dst.Name,
ClockRate: c.dev.GetRateNear(dst.ClockRate),
Channels: c.dev.GetChannelsNear(dst.Channels),
}
if err := c.dev.SetHWParams(device.SNDRV_PCM_FORMAT_S16_LE, src.ClockRate, src.Channels); err != nil {
return err
}
transcode := transcodeFunc(dst, src)
frameBytes := int(pcm.BytesPerFrame(src))
var ts uint32
// readBufferSize for 20ms interval
readBufferSize := 20 * frameBytes * int(src.ClockRate) / 1000
b := make([]byte, readBufferSize)
for {
n, err := c.dev.Read(b)
if err != nil {
return err
}
c.Recv += n
if len(c.Receivers) == 0 {
continue
}
pkt := &rtp.Packet{
Header: rtp.Header{
Version: 2,
Marker: true,
Timestamp: ts,
},
Payload: transcode(b[:n]),
}
c.Receivers[0].WriteRTP(pkt)
ts += uint32(n / frameBytes)
}
}
func transcodeFunc(dst, src *core.Codec) func([]byte) []byte {
if dst.ClockRate == src.ClockRate && dst.Channels == src.Channels {
return func(b []byte) []byte {
return b
}
}
return pcm.Transcode(dst, src)
}

View File

@@ -0,0 +1,148 @@
//go:build 386 || arm
package device
type unsigned_char = byte
type signed_int = int32
type unsigned_int = uint32
type signed_long = int64
type unsigned_long = uint64
type __u32 = uint32
type void__user = uintptr
const (
SNDRV_PCM_STREAM_PLAYBACK = 0
SNDRV_PCM_STREAM_CAPTURE = 1
SNDRV_PCM_ACCESS_MMAP_INTERLEAVED = 0
SNDRV_PCM_ACCESS_MMAP_NONINTERLEAVED = 1
SNDRV_PCM_ACCESS_MMAP_COMPLEX = 2
SNDRV_PCM_ACCESS_RW_INTERLEAVED = 3
SNDRV_PCM_ACCESS_RW_NONINTERLEAVED = 4
SNDRV_PCM_FORMAT_S8 = 0
SNDRV_PCM_FORMAT_U8 = 1
SNDRV_PCM_FORMAT_S16_LE = 2
SNDRV_PCM_FORMAT_S16_BE = 3
SNDRV_PCM_FORMAT_U16_LE = 4
SNDRV_PCM_FORMAT_U16_BE = 5
SNDRV_PCM_FORMAT_S24_LE = 6
SNDRV_PCM_FORMAT_S24_BE = 7
SNDRV_PCM_FORMAT_U24_LE = 8
SNDRV_PCM_FORMAT_U24_BE = 9
SNDRV_PCM_FORMAT_S32_LE = 10
SNDRV_PCM_FORMAT_S32_BE = 11
SNDRV_PCM_FORMAT_U32_LE = 12
SNDRV_PCM_FORMAT_U32_BE = 13
SNDRV_PCM_FORMAT_FLOAT_LE = 14
SNDRV_PCM_FORMAT_FLOAT_BE = 15
SNDRV_PCM_FORMAT_FLOAT64_LE = 16
SNDRV_PCM_FORMAT_FLOAT64_BE = 17
SNDRV_PCM_FORMAT_MU_LAW = 20
SNDRV_PCM_FORMAT_A_LAW = 21
SNDRV_PCM_FORMAT_MPEG = 23
SNDRV_PCM_IOCTL_PVERSION = 0x80044100
SNDRV_PCM_IOCTL_INFO = 0x81204101
SNDRV_PCM_IOCTL_HW_REFINE = 0xc25c4110
SNDRV_PCM_IOCTL_HW_PARAMS = 0xc25c4111
SNDRV_PCM_IOCTL_SW_PARAMS = 0xc0684113
SNDRV_PCM_IOCTL_PREPARE = 0x00004140
SNDRV_PCM_IOCTL_WRITEI_FRAMES = 0x400c4150
SNDRV_PCM_IOCTL_READI_FRAMES = 0x800c4151
)
type snd_pcm_info struct { // size 288
device unsigned_int // offset 0, size 4
subdevice unsigned_int // offset 4, size 4
stream signed_int // offset 8, size 4
card signed_int // offset 12, size 4
id [64]unsigned_char // offset 16, size 64
name [80]unsigned_char // offset 80, size 80
subname [32]unsigned_char // offset 160, size 32
dev_class signed_int // offset 192, size 4
dev_subclass signed_int // offset 196, size 4
subdevices_count unsigned_int // offset 200, size 4
subdevices_avail unsigned_int // offset 204, size 4
pad1 [16]unsigned_char
reserved [64]unsigned_char // offset 224, size 64
}
type snd_pcm_uframes_t = unsigned_long
type snd_pcm_sframes_t = signed_long
type snd_xferi struct { // size 12
result snd_pcm_sframes_t // offset 0, size 4
buf void__user // offset 4, size 4
frames snd_pcm_uframes_t // offset 8, size 4
}
const (
SNDRV_PCM_HW_PARAM_ACCESS = 0
SNDRV_PCM_HW_PARAM_FORMAT = 1
SNDRV_PCM_HW_PARAM_SUBFORMAT = 2
SNDRV_PCM_HW_PARAM_FIRST_MASK = 0
SNDRV_PCM_HW_PARAM_LAST_MASK = 2
SNDRV_PCM_HW_PARAM_SAMPLE_BITS = 8
SNDRV_PCM_HW_PARAM_FRAME_BITS = 9
SNDRV_PCM_HW_PARAM_CHANNELS = 10
SNDRV_PCM_HW_PARAM_RATE = 11
SNDRV_PCM_HW_PARAM_PERIOD_TIME = 12
SNDRV_PCM_HW_PARAM_PERIOD_SIZE = 13
SNDRV_PCM_HW_PARAM_PERIOD_BYTES = 14
SNDRV_PCM_HW_PARAM_PERIODS = 15
SNDRV_PCM_HW_PARAM_BUFFER_TIME = 16
SNDRV_PCM_HW_PARAM_BUFFER_SIZE = 17
SNDRV_PCM_HW_PARAM_BUFFER_BYTES = 18
SNDRV_PCM_HW_PARAM_TICK_TIME = 19
SNDRV_PCM_HW_PARAM_FIRST_INTERVAL = 8
SNDRV_PCM_HW_PARAM_LAST_INTERVAL = 19
SNDRV_MASK_MAX = 256
SNDRV_PCM_TSTAMP_NONE = 0
SNDRV_PCM_TSTAMP_ENABLE = 1
)
type snd_mask struct { // size 32
bits [(SNDRV_MASK_MAX + 31) / 32]__u32 // offset 0, size 32
}
type snd_interval struct { // size 12
min unsigned_int // offset 0, size 4
max unsigned_int // offset 4, size 4
bit unsigned_int
}
type snd_pcm_hw_params struct { // size 604
flags unsigned_int // offset 0, size 4
masks [SNDRV_PCM_HW_PARAM_LAST_MASK - SNDRV_PCM_HW_PARAM_FIRST_MASK + 1]snd_mask // offset 4, size 96
mres [5]snd_mask // offset 100, size 160
intervals [SNDRV_PCM_HW_PARAM_LAST_INTERVAL - SNDRV_PCM_HW_PARAM_FIRST_INTERVAL + 1]snd_interval // offset 260, size 144
ires [9]snd_interval // offset 404, size 108
rmask unsigned_int // offset 512, size 4
cmask unsigned_int // offset 516, size 4
info unsigned_int // offset 520, size 4
msbits unsigned_int // offset 524, size 4
rate_num unsigned_int // offset 528, size 4
rate_den unsigned_int // offset 532, size 4
fifo_size snd_pcm_uframes_t // offset 536, size 4
reserved [64]unsigned_char // offset 540, size 64
}
type snd_pcm_sw_params struct { // size 104
tstamp_mode signed_int // offset 0, size 4
period_step unsigned_int // offset 4, size 4
sleep_min unsigned_int // offset 8, size 4
avail_min snd_pcm_uframes_t // offset 12, size 4
xfer_align snd_pcm_uframes_t // offset 16, size 4
start_threshold snd_pcm_uframes_t // offset 20, size 4
stop_threshold snd_pcm_uframes_t // offset 24, size 4
silence_threshold snd_pcm_uframes_t // offset 28, size 4
silence_size snd_pcm_uframes_t // offset 32, size 4
boundary snd_pcm_uframes_t // offset 36, size 4
proto unsigned_int // offset 40, size 4
tstamp_type unsigned_int // offset 44, size 4
reserved [56]unsigned_char // offset 48, size 56
}

View File

@@ -0,0 +1,148 @@
//go:build amd64 || arm64
package device
type unsigned_char = byte
type signed_int = int32
type unsigned_int = uint32
type signed_long = int64
type unsigned_long = uint64
type __u32 = uint32
type void__user = uintptr
const (
SNDRV_PCM_STREAM_PLAYBACK = 0
SNDRV_PCM_STREAM_CAPTURE = 1
SNDRV_PCM_ACCESS_MMAP_INTERLEAVED = 0
SNDRV_PCM_ACCESS_MMAP_NONINTERLEAVED = 1
SNDRV_PCM_ACCESS_MMAP_COMPLEX = 2
SNDRV_PCM_ACCESS_RW_INTERLEAVED = 3
SNDRV_PCM_ACCESS_RW_NONINTERLEAVED = 4
SNDRV_PCM_FORMAT_S8 = 0
SNDRV_PCM_FORMAT_U8 = 1
SNDRV_PCM_FORMAT_S16_LE = 2
SNDRV_PCM_FORMAT_S16_BE = 3
SNDRV_PCM_FORMAT_U16_LE = 4
SNDRV_PCM_FORMAT_U16_BE = 5
SNDRV_PCM_FORMAT_S24_LE = 6
SNDRV_PCM_FORMAT_S24_BE = 7
SNDRV_PCM_FORMAT_U24_LE = 8
SNDRV_PCM_FORMAT_U24_BE = 9
SNDRV_PCM_FORMAT_S32_LE = 10
SNDRV_PCM_FORMAT_S32_BE = 11
SNDRV_PCM_FORMAT_U32_LE = 12
SNDRV_PCM_FORMAT_U32_BE = 13
SNDRV_PCM_FORMAT_FLOAT_LE = 14
SNDRV_PCM_FORMAT_FLOAT_BE = 15
SNDRV_PCM_FORMAT_FLOAT64_LE = 16
SNDRV_PCM_FORMAT_FLOAT64_BE = 17
SNDRV_PCM_FORMAT_MU_LAW = 20
SNDRV_PCM_FORMAT_A_LAW = 21
SNDRV_PCM_FORMAT_MPEG = 23
SNDRV_PCM_IOCTL_PVERSION = 0x80044100
SNDRV_PCM_IOCTL_INFO = 0x81204101
SNDRV_PCM_IOCTL_HW_REFINE = 0xc2604110
SNDRV_PCM_IOCTL_HW_PARAMS = 0xc2604111
SNDRV_PCM_IOCTL_SW_PARAMS = 0xc0884113
SNDRV_PCM_IOCTL_PREPARE = 0x00004140
SNDRV_PCM_IOCTL_WRITEI_FRAMES = 0x40184150
SNDRV_PCM_IOCTL_READI_FRAMES = 0x80184151
)
type snd_pcm_info struct { // size 288
device unsigned_int // offset 0, size 4
subdevice unsigned_int // offset 4, size 4
stream signed_int // offset 8, size 4
card signed_int // offset 12, size 4
id [64]unsigned_char // offset 16, size 64
name [80]unsigned_char // offset 80, size 80
subname [32]unsigned_char // offset 160, size 32
dev_class signed_int // offset 192, size 4
dev_subclass signed_int // offset 196, size 4
subdevices_count unsigned_int // offset 200, size 4
subdevices_avail unsigned_int // offset 204, size 4
pad1 [16]unsigned_char
reserved [64]unsigned_char // offset 224, size 64
}
type snd_pcm_uframes_t = unsigned_long
type snd_pcm_sframes_t = signed_long
type snd_xferi struct { // size 24
result snd_pcm_sframes_t // offset 0, size 8
buf void__user // offset 8, size 8
frames snd_pcm_uframes_t // offset 16, size 8
}
const (
SNDRV_PCM_HW_PARAM_ACCESS = 0
SNDRV_PCM_HW_PARAM_FORMAT = 1
SNDRV_PCM_HW_PARAM_SUBFORMAT = 2
SNDRV_PCM_HW_PARAM_FIRST_MASK = 0
SNDRV_PCM_HW_PARAM_LAST_MASK = 2
SNDRV_PCM_HW_PARAM_SAMPLE_BITS = 8
SNDRV_PCM_HW_PARAM_FRAME_BITS = 9
SNDRV_PCM_HW_PARAM_CHANNELS = 10
SNDRV_PCM_HW_PARAM_RATE = 11
SNDRV_PCM_HW_PARAM_PERIOD_TIME = 12
SNDRV_PCM_HW_PARAM_PERIOD_SIZE = 13
SNDRV_PCM_HW_PARAM_PERIOD_BYTES = 14
SNDRV_PCM_HW_PARAM_PERIODS = 15
SNDRV_PCM_HW_PARAM_BUFFER_TIME = 16
SNDRV_PCM_HW_PARAM_BUFFER_SIZE = 17
SNDRV_PCM_HW_PARAM_BUFFER_BYTES = 18
SNDRV_PCM_HW_PARAM_TICK_TIME = 19
SNDRV_PCM_HW_PARAM_FIRST_INTERVAL = 8
SNDRV_PCM_HW_PARAM_LAST_INTERVAL = 19
SNDRV_MASK_MAX = 256
SNDRV_PCM_TSTAMP_NONE = 0
SNDRV_PCM_TSTAMP_ENABLE = 1
)
type snd_mask struct { // size 32
bits [(SNDRV_MASK_MAX + 31) / 32]__u32 // offset 0, size 32
}
type snd_interval struct { // size 12
min unsigned_int // offset 0, size 4
max unsigned_int // offset 4, size 4
bit unsigned_int
}
type snd_pcm_hw_params struct { // size 608
flags unsigned_int // offset 0, size 4
masks [SNDRV_PCM_HW_PARAM_LAST_MASK - SNDRV_PCM_HW_PARAM_FIRST_MASK + 1]snd_mask // offset 4, size 96
mres [5]snd_mask // offset 100, size 160
intervals [SNDRV_PCM_HW_PARAM_LAST_INTERVAL - SNDRV_PCM_HW_PARAM_FIRST_INTERVAL + 1]snd_interval // offset 260, size 144
ires [9]snd_interval // offset 404, size 108
rmask unsigned_int // offset 512, size 4
cmask unsigned_int // offset 516, size 4
info unsigned_int // offset 520, size 4
msbits unsigned_int // offset 524, size 4
rate_num unsigned_int // offset 528, size 4
rate_den unsigned_int // offset 532, size 4
fifo_size snd_pcm_uframes_t // offset 536, size 8
reserved [64]unsigned_char // offset 544, size 64
}
type snd_pcm_sw_params struct { // size 136
tstamp_mode signed_int // offset 0, size 4
period_step unsigned_int // offset 4, size 4
sleep_min unsigned_int // offset 8, size 4
avail_min snd_pcm_uframes_t // offset 16, size 8
xfer_align snd_pcm_uframes_t // offset 24, size 8
start_threshold snd_pcm_uframes_t // offset 32, size 8
stop_threshold snd_pcm_uframes_t // offset 40, size 8
silence_threshold snd_pcm_uframes_t // offset 48, size 8
silence_size snd_pcm_uframes_t // offset 56, size 8
boundary snd_pcm_uframes_t // offset 64, size 8
proto unsigned_int // offset 72, size 4
tstamp_type unsigned_int // offset 76, size 4
reserved [56]unsigned_char // offset 80, size 56
}

View File

@@ -0,0 +1,163 @@
#include <stdio.h>
#include <stddef.h>
#include <sys/ioctl.h>
#include <sound/asound.h>
#define print_line(text) printf("%s\n", text)
#define print_hex_const(name) printf("\t%s = 0x%08lx\n", #name, name)
#define print_int_const(con) printf("\t%s = %d\n", #con, con)
#define print_struct_header(str) printf("type %s struct { // size %lu\n", #str, sizeof(struct str))
#define print_struct_member(str, mem, typ) printf("\t%s %s // offset %lu, size %lu\n", #mem == "type" ? "typ" : #mem, typ, offsetof(struct str, mem), sizeof((struct str){0}.mem))
// https://github.com/torvalds/linux/blob/master/include/uapi/sound/asound.h
int main() {
print_line("package device\n");
print_line("type unsigned_char = byte");
print_line("type signed_int = int32");
print_line("type unsigned_int = uint32");
print_line("type signed_long = int64");
print_line("type unsigned_long = uint64");
print_line("type __u32 = uint32");
print_line("type void__user = uintptr\n");
print_line("const (");
print_int_const(SNDRV_PCM_STREAM_PLAYBACK);
print_int_const(SNDRV_PCM_STREAM_CAPTURE);
print_line("");
print_int_const(SNDRV_PCM_ACCESS_MMAP_INTERLEAVED);
print_int_const(SNDRV_PCM_ACCESS_MMAP_NONINTERLEAVED);
print_int_const(SNDRV_PCM_ACCESS_MMAP_COMPLEX);
print_int_const(SNDRV_PCM_ACCESS_RW_INTERLEAVED);
print_int_const(SNDRV_PCM_ACCESS_RW_NONINTERLEAVED);
print_line("");
print_int_const(SNDRV_PCM_FORMAT_S8);
print_int_const(SNDRV_PCM_FORMAT_U8);
print_int_const(SNDRV_PCM_FORMAT_S16_LE);
print_int_const(SNDRV_PCM_FORMAT_S16_BE);
print_int_const(SNDRV_PCM_FORMAT_U16_LE);
print_int_const(SNDRV_PCM_FORMAT_U16_BE);
print_int_const(SNDRV_PCM_FORMAT_S24_LE);
print_int_const(SNDRV_PCM_FORMAT_S24_BE);
print_int_const(SNDRV_PCM_FORMAT_U24_LE);
print_int_const(SNDRV_PCM_FORMAT_U24_BE);
print_int_const(SNDRV_PCM_FORMAT_S32_LE);
print_int_const(SNDRV_PCM_FORMAT_S32_BE);
print_int_const(SNDRV_PCM_FORMAT_U32_LE);
print_int_const(SNDRV_PCM_FORMAT_U32_BE);
print_int_const(SNDRV_PCM_FORMAT_FLOAT_LE);
print_int_const(SNDRV_PCM_FORMAT_FLOAT_BE);
print_int_const(SNDRV_PCM_FORMAT_FLOAT64_LE);
print_int_const(SNDRV_PCM_FORMAT_FLOAT64_BE);
print_int_const(SNDRV_PCM_FORMAT_MU_LAW);
print_int_const(SNDRV_PCM_FORMAT_A_LAW);
print_int_const(SNDRV_PCM_FORMAT_MPEG);
print_line("");
print_hex_const(SNDRV_PCM_IOCTL_PVERSION); // A 0x00
print_hex_const(SNDRV_PCM_IOCTL_INFO); // A 0x01
print_hex_const(SNDRV_PCM_IOCTL_HW_REFINE); // A 0x10
print_hex_const(SNDRV_PCM_IOCTL_HW_PARAMS); // A 0x11
print_hex_const(SNDRV_PCM_IOCTL_SW_PARAMS); // A 0x13
print_hex_const(SNDRV_PCM_IOCTL_PREPARE); // A 0x40
print_hex_const(SNDRV_PCM_IOCTL_WRITEI_FRAMES); // A 0x50
print_hex_const(SNDRV_PCM_IOCTL_READI_FRAMES); // A 0x51
print_line(")\n");
print_struct_header(snd_pcm_info);
print_struct_member(snd_pcm_info, device, "unsigned_int");
print_struct_member(snd_pcm_info, subdevice, "unsigned_int");
print_struct_member(snd_pcm_info, stream, "signed_int");
print_struct_member(snd_pcm_info, card, "signed_int");
print_struct_member(snd_pcm_info, id, "[64]unsigned_char");
print_struct_member(snd_pcm_info, name, "[80]unsigned_char");
print_struct_member(snd_pcm_info, subname, "[32]unsigned_char");
print_struct_member(snd_pcm_info, dev_class, "signed_int");
print_struct_member(snd_pcm_info, dev_subclass, "signed_int");
print_struct_member(snd_pcm_info, subdevices_count, "unsigned_int");
print_struct_member(snd_pcm_info, subdevices_avail, "unsigned_int");
print_line("\tpad1 [16]unsigned_char");
print_struct_member(snd_pcm_info, reserved, "[64]unsigned_char");
print_line("}\n");
print_line("type snd_pcm_uframes_t = unsigned_long");
print_line("type snd_pcm_sframes_t = signed_long\n");
print_struct_header(snd_xferi);
print_struct_member(snd_xferi, result, "snd_pcm_sframes_t");
print_struct_member(snd_xferi, buf, "void__user");
print_struct_member(snd_xferi, frames, "snd_pcm_uframes_t");
print_line("}\n");
print_line("const (");
print_int_const(SNDRV_PCM_HW_PARAM_ACCESS);
print_int_const(SNDRV_PCM_HW_PARAM_FORMAT);
print_int_const(SNDRV_PCM_HW_PARAM_SUBFORMAT);
print_int_const(SNDRV_PCM_HW_PARAM_FIRST_MASK);
print_int_const(SNDRV_PCM_HW_PARAM_LAST_MASK);
print_line("");
print_int_const(SNDRV_PCM_HW_PARAM_SAMPLE_BITS);
print_int_const(SNDRV_PCM_HW_PARAM_FRAME_BITS);
print_int_const(SNDRV_PCM_HW_PARAM_CHANNELS);
print_int_const(SNDRV_PCM_HW_PARAM_RATE);
print_int_const(SNDRV_PCM_HW_PARAM_PERIOD_TIME);
print_int_const(SNDRV_PCM_HW_PARAM_PERIOD_SIZE);
print_int_const(SNDRV_PCM_HW_PARAM_PERIOD_BYTES);
print_int_const(SNDRV_PCM_HW_PARAM_PERIODS);
print_int_const(SNDRV_PCM_HW_PARAM_BUFFER_TIME);
print_int_const(SNDRV_PCM_HW_PARAM_BUFFER_SIZE);
print_int_const(SNDRV_PCM_HW_PARAM_BUFFER_BYTES);
print_int_const(SNDRV_PCM_HW_PARAM_TICK_TIME);
print_int_const(SNDRV_PCM_HW_PARAM_FIRST_INTERVAL);
print_int_const(SNDRV_PCM_HW_PARAM_LAST_INTERVAL);
print_line("");
print_int_const(SNDRV_MASK_MAX);
print_line("");
print_int_const(SNDRV_PCM_TSTAMP_NONE);
print_int_const(SNDRV_PCM_TSTAMP_ENABLE);
print_line(")\n");
print_struct_header(snd_mask);
print_struct_member(snd_mask, bits, "[(SNDRV_MASK_MAX+31)/32]__u32");
print_line("}\n");
print_struct_header(snd_interval);
print_struct_member(snd_interval, min, "unsigned_int");
print_struct_member(snd_interval, max, "unsigned_int");
print_line("\tbit unsigned_int");
print_line("}\n");
print_struct_header(snd_pcm_hw_params);
print_struct_member(snd_pcm_hw_params, flags, "unsigned_int");
print_struct_member(snd_pcm_hw_params, masks, "[SNDRV_PCM_HW_PARAM_LAST_MASK-SNDRV_PCM_HW_PARAM_FIRST_MASK+1]snd_mask");
print_struct_member(snd_pcm_hw_params, mres, "[5]snd_mask");
print_struct_member(snd_pcm_hw_params, intervals, "[SNDRV_PCM_HW_PARAM_LAST_INTERVAL-SNDRV_PCM_HW_PARAM_FIRST_INTERVAL+1]snd_interval");
print_struct_member(snd_pcm_hw_params, ires, "[9]snd_interval");
print_struct_member(snd_pcm_hw_params, rmask, "unsigned_int");
print_struct_member(snd_pcm_hw_params, cmask, "unsigned_int");
print_struct_member(snd_pcm_hw_params, info, "unsigned_int");
print_struct_member(snd_pcm_hw_params, msbits, "unsigned_int");
print_struct_member(snd_pcm_hw_params, rate_num, "unsigned_int");
print_struct_member(snd_pcm_hw_params, rate_den, "unsigned_int");
print_struct_member(snd_pcm_hw_params, fifo_size, "snd_pcm_uframes_t");
print_struct_member(snd_pcm_hw_params, reserved, "[64]unsigned_char");
print_line("}\n");
print_struct_header(snd_pcm_sw_params);
print_struct_member(snd_pcm_sw_params, tstamp_mode, "signed_int");
print_struct_member(snd_pcm_sw_params, period_step, "unsigned_int");
print_struct_member(snd_pcm_sw_params, sleep_min, "unsigned_int");
print_struct_member(snd_pcm_sw_params, avail_min, "snd_pcm_uframes_t");
print_struct_member(snd_pcm_sw_params, xfer_align, "snd_pcm_uframes_t");
print_struct_member(snd_pcm_sw_params, start_threshold, "snd_pcm_uframes_t");
print_struct_member(snd_pcm_sw_params, stop_threshold, "snd_pcm_uframes_t");
print_struct_member(snd_pcm_sw_params, silence_threshold, "snd_pcm_uframes_t");
print_struct_member(snd_pcm_sw_params, silence_size, "snd_pcm_uframes_t");
print_struct_member(snd_pcm_sw_params, boundary, "snd_pcm_uframes_t");
print_struct_member(snd_pcm_sw_params, proto, "unsigned_int");
print_struct_member(snd_pcm_sw_params, tstamp_type, "unsigned_int");
print_struct_member(snd_pcm_sw_params, reserved, "[56]unsigned_char");
print_line("}\n");
return 0;
}

View File

@@ -0,0 +1,146 @@
package device
type unsigned_char = byte
type signed_int = int32
type unsigned_int = uint32
type signed_long = int64
type unsigned_long = uint64
type __u32 = uint32
type void__user = uintptr
const (
SNDRV_PCM_STREAM_PLAYBACK = 0
SNDRV_PCM_STREAM_CAPTURE = 1
SNDRV_PCM_ACCESS_MMAP_INTERLEAVED = 0
SNDRV_PCM_ACCESS_MMAP_NONINTERLEAVED = 1
SNDRV_PCM_ACCESS_MMAP_COMPLEX = 2
SNDRV_PCM_ACCESS_RW_INTERLEAVED = 3
SNDRV_PCM_ACCESS_RW_NONINTERLEAVED = 4
SNDRV_PCM_FORMAT_S8 = 0
SNDRV_PCM_FORMAT_U8 = 1
SNDRV_PCM_FORMAT_S16_LE = 2
SNDRV_PCM_FORMAT_S16_BE = 3
SNDRV_PCM_FORMAT_U16_LE = 4
SNDRV_PCM_FORMAT_U16_BE = 5
SNDRV_PCM_FORMAT_S24_LE = 6
SNDRV_PCM_FORMAT_S24_BE = 7
SNDRV_PCM_FORMAT_U24_LE = 8
SNDRV_PCM_FORMAT_U24_BE = 9
SNDRV_PCM_FORMAT_S32_LE = 10
SNDRV_PCM_FORMAT_S32_BE = 11
SNDRV_PCM_FORMAT_U32_LE = 12
SNDRV_PCM_FORMAT_U32_BE = 13
SNDRV_PCM_FORMAT_FLOAT_LE = 14
SNDRV_PCM_FORMAT_FLOAT_BE = 15
SNDRV_PCM_FORMAT_FLOAT64_LE = 16
SNDRV_PCM_FORMAT_FLOAT64_BE = 17
SNDRV_PCM_FORMAT_MU_LAW = 20
SNDRV_PCM_FORMAT_A_LAW = 21
SNDRV_PCM_FORMAT_MPEG = 23
SNDRV_PCM_IOCTL_PVERSION = 0x40044100
SNDRV_PCM_IOCTL_INFO = 0x41204101
SNDRV_PCM_IOCTL_HW_REFINE = 0xc25c4110
SNDRV_PCM_IOCTL_HW_PARAMS = 0xc25c4111
SNDRV_PCM_IOCTL_SW_PARAMS = 0xc0684113
SNDRV_PCM_IOCTL_PREPARE = 0x20004140
SNDRV_PCM_IOCTL_WRITEI_FRAMES = 0x800c4150
SNDRV_PCM_IOCTL_READI_FRAMES = 0x400c4151
)
type snd_pcm_info struct { // size 288
device unsigned_int // offset 0, size 4
subdevice unsigned_int // offset 4, size 4
stream signed_int // offset 8, size 4
card signed_int // offset 12, size 4
id [64]unsigned_char // offset 16, size 64
name [80]unsigned_char // offset 80, size 80
subname [32]unsigned_char // offset 160, size 32
dev_class signed_int // offset 192, size 4
dev_subclass signed_int // offset 196, size 4
subdevices_count unsigned_int // offset 200, size 4
subdevices_avail unsigned_int // offset 204, size 4
pad1 [16]unsigned_char
reserved [64]unsigned_char // offset 224, size 64
}
type snd_pcm_uframes_t = unsigned_long
type snd_pcm_sframes_t = signed_long
type snd_xferi struct { // size 12
result snd_pcm_sframes_t // offset 0, size 4
buf void__user // offset 4, size 4
frames snd_pcm_uframes_t // offset 8, size 4
}
const (
SNDRV_PCM_HW_PARAM_ACCESS = 0
SNDRV_PCM_HW_PARAM_FORMAT = 1
SNDRV_PCM_HW_PARAM_SUBFORMAT = 2
SNDRV_PCM_HW_PARAM_FIRST_MASK = 0
SNDRV_PCM_HW_PARAM_LAST_MASK = 2
SNDRV_PCM_HW_PARAM_SAMPLE_BITS = 8
SNDRV_PCM_HW_PARAM_FRAME_BITS = 9
SNDRV_PCM_HW_PARAM_CHANNELS = 10
SNDRV_PCM_HW_PARAM_RATE = 11
SNDRV_PCM_HW_PARAM_PERIOD_TIME = 12
SNDRV_PCM_HW_PARAM_PERIOD_SIZE = 13
SNDRV_PCM_HW_PARAM_PERIOD_BYTES = 14
SNDRV_PCM_HW_PARAM_PERIODS = 15
SNDRV_PCM_HW_PARAM_BUFFER_TIME = 16
SNDRV_PCM_HW_PARAM_BUFFER_SIZE = 17
SNDRV_PCM_HW_PARAM_BUFFER_BYTES = 18
SNDRV_PCM_HW_PARAM_TICK_TIME = 19
SNDRV_PCM_HW_PARAM_FIRST_INTERVAL = 8
SNDRV_PCM_HW_PARAM_LAST_INTERVAL = 19
SNDRV_MASK_MAX = 256
SNDRV_PCM_TSTAMP_NONE = 0
SNDRV_PCM_TSTAMP_ENABLE = 1
)
type snd_mask struct { // size 32
bits [(SNDRV_MASK_MAX + 31) / 32]__u32 // offset 0, size 32
}
type snd_interval struct { // size 12
min unsigned_int // offset 0, size 4
max unsigned_int // offset 4, size 4
bit unsigned_int
}
type snd_pcm_hw_params struct { // size 604
flags unsigned_int // offset 0, size 4
masks [SNDRV_PCM_HW_PARAM_LAST_MASK - SNDRV_PCM_HW_PARAM_FIRST_MASK + 1]snd_mask // offset 4, size 96
mres [5]snd_mask // offset 100, size 160
intervals [SNDRV_PCM_HW_PARAM_LAST_INTERVAL - SNDRV_PCM_HW_PARAM_FIRST_INTERVAL + 1]snd_interval // offset 260, size 144
ires [9]snd_interval // offset 404, size 108
rmask unsigned_int // offset 512, size 4
cmask unsigned_int // offset 516, size 4
info unsigned_int // offset 520, size 4
msbits unsigned_int // offset 524, size 4
rate_num unsigned_int // offset 528, size 4
rate_den unsigned_int // offset 532, size 4
fifo_size snd_pcm_uframes_t // offset 536, size 4
reserved [64]unsigned_char // offset 540, size 64
}
type snd_pcm_sw_params struct { // size 104
tstamp_mode signed_int // offset 0, size 4
period_step unsigned_int // offset 4, size 4
sleep_min unsigned_int // offset 8, size 4
avail_min snd_pcm_uframes_t // offset 12, size 4
xfer_align snd_pcm_uframes_t // offset 16, size 4
start_threshold snd_pcm_uframes_t // offset 20, size 4
stop_threshold snd_pcm_uframes_t // offset 24, size 4
silence_threshold snd_pcm_uframes_t // offset 28, size 4
silence_size snd_pcm_uframes_t // offset 32, size 4
boundary snd_pcm_uframes_t // offset 36, size 4
proto unsigned_int // offset 40, size 4
tstamp_type unsigned_int // offset 44, size 4
reserved [56]unsigned_char // offset 48, size 56
}

View File

@@ -0,0 +1,231 @@
package device
import (
"fmt"
"syscall"
"unsafe"
)
type Device struct {
fd uintptr
path string
hwparams snd_pcm_hw_params
frameBytes int // sample size * channels
}
func Open(path string) (*Device, error) {
// important to use nonblock because can get lock
fd, err := syscall.Open(path, syscall.O_RDWR|syscall.O_NONBLOCK, 0)
if err != nil {
return nil, err
}
// important to remove nonblock because better to handle reads and writes
if err = syscall.SetNonblock(fd, false); err != nil {
return nil, err
}
d := &Device{fd: uintptr(fd), path: path}
d.init()
// load all supported formats, channels, rates, etc.
if err = ioctl(d.fd, SNDRV_PCM_IOCTL_HW_REFINE, &d.hwparams); err != nil {
_ = d.Close()
return nil, err
}
d.setMask(SNDRV_PCM_HW_PARAM_ACCESS, SNDRV_PCM_ACCESS_RW_INTERLEAVED)
return d, nil
}
func (d *Device) Close() error {
return syscall.Close(int(d.fd))
}
func (d *Device) IsCapture() bool {
// path: /dev/snd/pcmC0D0c, where p - playback, c - capture
return d.path[len(d.path)-1] == 'c'
}
type Info struct {
Card int
Device int
SubDevice int
Stream int
ID string
Name string
SubName string
}
func (d *Device) Info() (*Info, error) {
var info snd_pcm_info
if err := ioctl(d.fd, SNDRV_PCM_IOCTL_INFO, &info); err != nil {
return nil, err
}
return &Info{
Card: int(info.card),
Device: int(info.device),
SubDevice: int(info.subdevice),
Stream: int(info.stream),
ID: str(info.id[:]),
Name: str(info.name[:]),
SubName: str(info.subname[:]),
}, nil
}
func (d *Device) CheckFormat(format byte) bool {
return d.checkMask(SNDRV_PCM_HW_PARAM_FORMAT, uint32(format))
}
func (d *Device) ListFormats() (formats []byte) {
for i := byte(0); i <= 28; i++ {
if d.CheckFormat(i) {
formats = append(formats, i)
}
}
return
}
func (d *Device) RangeRates() (uint32, uint32) {
return d.getInterval(SNDRV_PCM_HW_PARAM_RATE)
}
func (d *Device) RangeChannels() (byte, byte) {
minCh, maxCh := d.getInterval(SNDRV_PCM_HW_PARAM_CHANNELS)
return byte(minCh), byte(maxCh)
}
func (d *Device) GetRateNear(rate uint32) uint32 {
r1, r2 := d.RangeRates()
if rate < r1 {
return r1
}
if rate > r2 {
return r2
}
return rate
}
func (d *Device) GetChannelsNear(channels byte) byte {
c1, c2 := d.RangeChannels()
if channels < c1 {
return c1
}
if channels > c2 {
return c2
}
return channels
}
const bufferSize = 4096
func (d *Device) SetHWParams(format byte, rate uint32, channels byte) error {
d.setInterval(SNDRV_PCM_HW_PARAM_CHANNELS, uint32(channels))
d.setInterval(SNDRV_PCM_HW_PARAM_RATE, rate)
d.setMask(SNDRV_PCM_HW_PARAM_FORMAT, uint32(format))
//d.setMask(SNDRV_PCM_HW_PARAM_SUBFORMAT, 0)
// important for smooth playback
d.setInterval(SNDRV_PCM_HW_PARAM_BUFFER_SIZE, bufferSize)
//d.setInterval(SNDRV_PCM_HW_PARAM_PERIOD_SIZE, 2000)
if err := ioctl(d.fd, SNDRV_PCM_IOCTL_HW_PARAMS, &d.hwparams); err != nil {
return fmt.Errorf("[alsa] set hw_params: %w", err)
}
_, i := d.getInterval(SNDRV_PCM_HW_PARAM_FRAME_BITS)
d.frameBytes = int(i / 8)
_, periods := d.getInterval(SNDRV_PCM_HW_PARAM_PERIODS)
_, periodSize := d.getInterval(SNDRV_PCM_HW_PARAM_PERIOD_SIZE)
threshold := snd_pcm_uframes_t(periods * periodSize) // same as bufferSize
swparams := snd_pcm_sw_params{
//tstamp_mode: SNDRV_PCM_TSTAMP_ENABLE,
period_step: 1,
avail_min: 1, // start as soon as possible
stop_threshold: threshold,
}
if d.IsCapture() {
swparams.start_threshold = 1
} else {
swparams.start_threshold = threshold
}
if err := ioctl(d.fd, SNDRV_PCM_IOCTL_SW_PARAMS, &swparams); err != nil {
return fmt.Errorf("[alsa] set sw_params: %w", err)
}
if err := ioctl(d.fd, SNDRV_PCM_IOCTL_PREPARE, nil); err != nil {
return fmt.Errorf("[alsa] prepare: %w", err)
}
return nil
}
func (d *Device) Write(b []byte) (n int, err error) {
xfer := &snd_xferi{
buf: uintptr(unsafe.Pointer(&b[0])),
frames: snd_pcm_uframes_t(len(b) / d.frameBytes),
}
err = ioctl(d.fd, SNDRV_PCM_IOCTL_WRITEI_FRAMES, xfer)
if err == syscall.EPIPE {
// auto handle underrun state
// https://stackoverflow.com/questions/59396728/how-to-properly-handle-xrun-in-alsa-programming-when-playing-audio-with-snd-pcm
err = ioctl(d.fd, SNDRV_PCM_IOCTL_PREPARE, nil)
}
n = int(xfer.result) * d.frameBytes
return
}
func (d *Device) Read(b []byte) (n int, err error) {
xfer := &snd_xferi{
buf: uintptr(unsafe.Pointer(&b[0])),
frames: snd_pcm_uframes_t(len(b) / d.frameBytes),
}
err = ioctl(d.fd, SNDRV_PCM_IOCTL_READI_FRAMES, xfer)
n = int(xfer.result) * d.frameBytes
return
}
func (d *Device) init() {
for i := range d.hwparams.masks {
d.hwparams.masks[i].bits[0] = 0xFFFFFFFF
d.hwparams.masks[i].bits[1] = 0xFFFFFFFF
}
for i := range d.hwparams.intervals {
d.hwparams.intervals[i].max = 0xFFFFFFFF
}
d.hwparams.rmask = 0xFFFFFFFF
d.hwparams.cmask = 0
d.hwparams.info = 0xFFFFFFFF
}
func (d *Device) setInterval(param, val uint32) {
d.hwparams.intervals[param-SNDRV_PCM_HW_PARAM_FIRST_INTERVAL].min = val
d.hwparams.intervals[param-SNDRV_PCM_HW_PARAM_FIRST_INTERVAL].max = val
d.hwparams.intervals[param-SNDRV_PCM_HW_PARAM_FIRST_INTERVAL].bit = 0b0100 // integer
}
func (d *Device) setIntervalMin(param, val uint32) {
d.hwparams.intervals[param-SNDRV_PCM_HW_PARAM_FIRST_INTERVAL].min = val
}
func (d *Device) getInterval(param uint32) (uint32, uint32) {
return d.hwparams.intervals[param-SNDRV_PCM_HW_PARAM_FIRST_INTERVAL].min,
d.hwparams.intervals[param-SNDRV_PCM_HW_PARAM_FIRST_INTERVAL].max
}
func (d *Device) setMask(mask, val uint32) {
d.hwparams.masks[mask].bits[0] = 0
d.hwparams.masks[mask].bits[1] = 0
d.hwparams.masks[mask].bits[val>>5] = 1 << (val & 0x1F)
}
func (d *Device) checkMask(mask, val uint32) bool {
return d.hwparams.masks[mask].bits[val>>5]&(1<<(val&0x1F)) > 0
}

View File

@@ -0,0 +1,26 @@
package device
import (
"bytes"
"reflect"
"syscall"
)
func ioctl(fd, req uintptr, arg any) error {
var ptr uintptr
if arg != nil {
ptr = reflect.ValueOf(arg).Pointer()
}
_, _, err := syscall.Syscall(syscall.SYS_IOCTL, fd, req, ptr)
if err != 0 {
return err
}
return nil
}
func str(b []byte) string {
if i := bytes.IndexByte(b, 0); i >= 0 {
return string(b[:i])
}
return string(b)
}

44
pkg/alsa/open_linux.go Normal file
View File

@@ -0,0 +1,44 @@
package alsa
import (
"errors"
"fmt"
"net/url"
"github.com/AlexxIT/go2rtc/pkg/alsa/device"
"github.com/AlexxIT/go2rtc/pkg/core"
)
func Open(rawURL string) (core.Producer, error) {
// Example (ffmpeg source compatible):
// alsa:device?audio=/dev/snd/pcmC0D0p
// TODO: ?audio=default
// TODO: ?audio=hw:0,0
// TODO: &sample_rate=48000&channels=2
// TODO: &backchannel=1
u, err := url.Parse(rawURL)
if err != nil {
return nil, err
}
path := u.Query().Get("audio")
dev, err := device.Open(path)
if err != nil {
return nil, err
}
if !dev.CheckFormat(device.SNDRV_PCM_FORMAT_S16_LE) {
_ = dev.Close()
return nil, errors.New("alsa: format S16LE not supported")
}
switch path[len(path)-1] {
case 'p': // playback
return newPlayback(dev)
case 'c': // capture
return newCapture(dev)
}
_ = dev.Close()
return nil, fmt.Errorf("alsa: unknown path: %s", path)
}

View File

@@ -0,0 +1,84 @@
package alsa
import (
"fmt"
"github.com/AlexxIT/go2rtc/pkg/alsa/device"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/pcm"
"github.com/pion/rtp"
)
type Playback struct {
core.Connection
dev *device.Device
closed core.Waiter
}
func newPlayback(dev *device.Device) (*Playback, error) {
medias := []*core.Media{
{
Kind: core.KindAudio,
Direction: core.DirectionSendonly,
Codecs: []*core.Codec{
{Name: core.CodecPCML}, // support ffmpeg producer (auto transcode)
{Name: core.CodecPCMA, ClockRate: 8000}, // support webrtc producer
},
},
}
return &Playback{
Connection: core.Connection{
ID: core.NewID(),
FormatName: "alsa",
Medias: medias,
Transport: dev,
},
dev: dev,
}, nil
}
func (p *Playback) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) {
return nil, core.ErrCantGetTrack
}
func (p *Playback) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiver) error {
src := track.Codec
dst := &core.Codec{
Name: core.CodecPCML,
ClockRate: p.dev.GetRateNear(src.ClockRate),
Channels: p.dev.GetChannelsNear(src.Channels),
}
sender := core.NewSender(media, dst)
sender.Handler = func(pkt *rtp.Packet) {
if n, err := p.dev.Write(pkt.Payload); err == nil {
p.Send += n
}
}
if sender.Handler = pcm.TranscodeHandler(dst, src, sender.Handler); sender.Handler == nil {
return fmt.Errorf("alsa: can't convert %s to %s", src, dst)
}
// typical card support:
// - Formats: S16_LE, S32_LE
// - ClockRates: 8000 - 192000
// - Channels: 2 - 10
err := p.dev.SetHWParams(device.SNDRV_PCM_FORMAT_S16_LE, dst.ClockRate, byte(dst.Channels))
if err != nil {
return err
}
sender.HandleRTP(track)
p.Senders = append(p.Senders, sender)
return nil
}
func (p *Playback) Start() (err error) {
return p.closed.Wait()
}
func (p *Playback) Stop() error {
p.closed.Done(nil)
return p.Connection.Stop()
}

View File

@@ -89,6 +89,12 @@ func (r *Reader) ReadBits64(n byte) (res uint64) {
return
}
func (r *Reader) ReadFloat32() float64 {
i := r.ReadUint16()
f := r.ReadUint16()
return float64(i) + float64(f)/65536
}
func (r *Reader) ReadBytes(n int) (b []byte) {
if r.bits == 0 {
if r.pos+n > len(r.buf) {
@@ -122,9 +128,9 @@ func (r *Reader) ReadUEGolomb() uint32 {
// ReadSEGolomb - ReadSignedExponentialGolomb
func (r *Reader) ReadSEGolomb() int32 {
if b := r.ReadUEGolomb(); b%2 == 0 {
return -int32(b >> 1)
return -int32(b / 2)
} else {
return int32(b >> 1)
return int32((b + 1) / 2)
}
}

View File

@@ -13,7 +13,7 @@ import (
type Codec struct {
Name string // H264, PCMU, PCMA, opus...
ClockRate uint32 // 90000, 8000, 16000...
Channels uint16 // 0, 1, 2
Channels uint8 // 0, 1, 2
FmtpLine string
PayloadType uint8
}
@@ -249,3 +249,36 @@ func DecodeH264(fmtp string) (profile string, level byte) {
}
return
}
func ParseCodecString(s string) *Codec {
var codec Codec
ss := strings.Split(s, "/")
switch strings.ToLower(ss[0]) {
case "pcm_s16be", "s16be", "pcm":
codec.Name = CodecPCM
case "pcm_s16le", "s16le", "pcml":
codec.Name = CodecPCML
case "pcm_alaw", "alaw", "pcma":
codec.Name = CodecPCMA
case "pcm_mulaw", "mulaw", "pcmu":
codec.Name = CodecPCMU
case "aac", "mpeg4-generic":
codec.Name = CodecAAC
case "opus":
codec.Name = CodecOpus
case "flac":
codec.Name = CodecFLAC
default:
return nil
}
if len(ss) >= 2 {
codec.ClockRate = uint32(Atoi(ss[1]))
}
if len(ss) >= 3 {
codec.Channels = uint8(Atoi(ss[1]))
}
return &codec
}

View File

@@ -139,7 +139,11 @@ func MarshalSDP(name string, medias []*Media) ([]byte, error) {
Protos: []string{"RTP", "AVP"},
},
}
md.WithCodec(codec.PayloadType, name, codec.ClockRate, codec.Channels, codec.FmtpLine)
md.WithCodec(codec.PayloadType, name, codec.ClockRate, uint16(codec.Channels), codec.FmtpLine)
if media.Direction != "" {
md.WithPropertyAttribute(media.Direction)
}
if media.ID != "" {
md.WithValueAttribute("control", media.ID)

View File

@@ -97,13 +97,17 @@ func NewSender(media *Media, codec *Codec) *Sender {
buf: buf,
}
s.Input = func(packet *Packet) {
// writing to nil chan - OK, writing to closed chan - panic
s.mu.Lock()
select {
case s.buf <- packet:
s.Bytes += len(packet.Payload)
s.Packets++
default:
if s.buf != nil {
// unblocked write to channel
select {
case s.buf <- packet:
s.Bytes += len(packet.Payload)
s.Packets++
default:
s.Drops++
}
} else {
s.Drops++
}
s.mu.Unlock()
@@ -139,16 +143,17 @@ func (s *Sender) Start() {
}
s.done = make(chan struct{})
go func() {
for packet := range s.buf {
// pass buf directly so that it's impossible for buf to be nil
go func(buf chan *Packet) {
for packet := range buf {
s.Output(packet)
}
close(s.done)
}()
}(s.buf)
}
func (s *Sender) Wait() {
if done := s.done; s.done != nil {
if done := s.done; done != nil {
<-done
}
}
@@ -165,10 +170,12 @@ func (s *Sender) State() string {
func (s *Sender) Close() {
// close buffer if exists
if buf := s.buf; buf != nil {
s.buf = nil
defer close(buf)
s.mu.Lock()
if s.buf != nil {
close(s.buf) // exit from for range loop
s.buf = nil // prevent writing to closed chan
}
s.mu.Unlock()
s.Node.Close()
}

53
pkg/core/track_test.go Normal file
View File

@@ -0,0 +1,53 @@
package core
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestSenser(t *testing.T) {
recv := make(chan *Packet) // blocking receiver
sender := NewSender(nil, &Codec{})
sender.Output = func(packet *Packet) {
recv <- packet
}
require.Equal(t, "new", sender.State())
sender.Start()
require.Equal(t, "connected", sender.State())
sender.Input(&Packet{})
sender.Input(&Packet{})
require.Equal(t, 2, sender.Packets)
require.Equal(t, 0, sender.Drops)
// important to read one before close
// because goroutine in Start() can run with nil chan
// it's OK in real life, but bad for test
_, ok := <-recv
require.True(t, ok)
sender.Close()
require.Equal(t, "closed", sender.State())
sender.Input(&Packet{})
require.Equal(t, 2, sender.Packets)
require.Equal(t, 1, sender.Drops)
// read 2nd
_, ok = <-recv
require.True(t, ok)
// read 3rd
select {
case <-recv:
ok = true
default:
ok = false
}
require.False(t, ok)
}

180
pkg/eseecloud/eseecloud.go Normal file
View File

@@ -0,0 +1,180 @@
package eseecloud
import (
"bytes"
"encoding/binary"
"errors"
"io"
"net/http"
"regexp"
"strings"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/h264/annexb"
"github.com/pion/rtp"
)
type Producer struct {
core.Connection
rd *core.ReadBuffer
videoPT, audioPT uint8
}
func Dial(rawURL string) (core.Producer, error) {
rawURL, _ = strings.CutPrefix(rawURL, "eseecloud")
res, err := http.Get("http" + rawURL)
if err != nil {
return nil, err
}
prod, err := Open(res.Body)
if err != nil {
return nil, err
}
if info, ok := prod.(core.Info); ok {
info.SetProtocol("http")
info.SetURL(rawURL)
}
return prod, nil
}
func Open(r io.Reader) (core.Producer, error) {
prod := &Producer{
Connection: core.Connection{
ID: core.NewID(),
FormatName: "eseecloud",
Transport: r,
},
rd: core.NewReadBuffer(r),
}
if err := prod.probe(); err != nil {
return nil, err
}
return prod, nil
}
func (p *Producer) probe() error {
b, err := p.rd.Peek(1024)
if err != nil {
return err
}
i := bytes.Index(b, []byte("\r\n\r\n"))
if i == -1 {
return io.EOF
}
b = make([]byte, i+4)
_, _ = p.rd.Read(b)
re := regexp.MustCompile(`m=(video|audio) (\d+) (\w+)/(\d+)\S*`)
for _, item := range re.FindAllStringSubmatch(string(b), 2) {
p.SDP += item[0] + "\n"
switch item[3] {
case "H264", "H265":
p.Medias = append(p.Medias, &core.Media{
Kind: core.KindVideo,
Direction: core.DirectionRecvonly,
Codecs: []*core.Codec{
{
Name: item[3],
ClockRate: 90000,
PayloadType: core.PayloadTypeRAW,
},
},
})
p.videoPT = byte(core.Atoi(item[2]))
case "G711":
p.Medias = append(p.Medias, &core.Media{
Kind: core.KindAudio,
Direction: core.DirectionRecvonly,
Codecs: []*core.Codec{
{
Name: core.CodecPCMA,
ClockRate: 8000,
},
},
})
p.audioPT = byte(core.Atoi(item[2]))
}
}
return nil
}
func (p *Producer) Start() error {
receivers := make(map[uint8]*core.Receiver)
for _, receiver := range p.Receivers {
switch receiver.Codec.Kind() {
case core.KindVideo:
receivers[p.videoPT] = receiver
case core.KindAudio:
receivers[p.audioPT] = receiver
}
}
for {
pkt, err := p.readPacket()
if err != nil {
return err
}
if recv := receivers[pkt.PayloadType]; recv != nil {
switch recv.Codec.Name {
case core.CodecH264, core.CodecH265:
// timestamp = seconds x 1000000
pkt = &rtp.Packet{
Header: rtp.Header{
Timestamp: uint32(uint64(pkt.Timestamp) * 90000 / 1000000),
},
Payload: annexb.EncodeToAVCC(pkt.Payload),
}
case core.CodecPCMA:
pkt = &rtp.Packet{
Header: rtp.Header{
Version: 2,
SequenceNumber: pkt.SequenceNumber,
Timestamp: uint32(uint64(pkt.Timestamp) * 8000 / 1000000),
},
Payload: pkt.Payload,
}
}
recv.WriteRTP(pkt)
}
}
}
func (p *Producer) readPacket() (*core.Packet, error) {
b := make([]byte, 8)
if _, err := io.ReadFull(p.rd, b); err != nil {
return nil, err
}
if b[0] != '$' {
return nil, errors.New("eseecloud: wrong start byte")
}
size := binary.BigEndian.Uint32(b[4:])
b = make([]byte, size)
if _, err := io.ReadFull(p.rd, b); err != nil {
return nil, err
}
pkt := &core.Packet{}
if err := pkt.Unmarshal(b); err != nil {
return nil, err
}
p.Recv += int(size)
return pkt, nil
}

View File

@@ -6,17 +6,24 @@ import (
"io"
"net/http"
"regexp"
"strings"
"github.com/AlexxIT/go2rtc/pkg/tcp"
"github.com/expr-lang/expr"
"github.com/expr-lang/expr/vm"
)
func newRequest(method, url string, headers map[string]any) (*http.Request, error) {
func newRequest(method, url string, headers map[string]any, body string) (*http.Request, error) {
var rd io.Reader
if method == "" {
method = "GET"
}
if body != "" {
rd = strings.NewReader(body)
}
req, err := http.NewRequest(method, url, nil)
req, err := http.NewRequest(method, url, rd)
if err != nil {
return nil, err
}
@@ -55,7 +62,8 @@ var Options = []expr.Option{
options := params[1].(map[string]any)
method, _ := options["method"].(string)
headers, _ := options["headers"].(map[string]any)
req, err = newRequest(method, url, headers)
body, _ := options["body"].(string)
req, err = newRequest(method, url, headers, body)
} else {
req, err = http.NewRequest("GET", url, nil)
}
@@ -105,11 +113,19 @@ var Options = []expr.Option{
),
}
func Run(input string) (any, error) {
program, err := expr.Compile(input, Options...)
func Compile(input string) (*vm.Program, error) {
return expr.Compile(input, Options...)
}
func Eval(input string, env any) (any, error) {
program, err := Compile(input)
if err != nil {
return nil, err
}
return expr.Run(program, nil)
return expr.Run(program, env)
}
func Run(program *vm.Program, env any) (any, error) {
return vm.Run(program, env)
}

View File

@@ -7,11 +7,11 @@ import (
)
func TestMatchHost(t *testing.T) {
v, err := Run(`
v, err := Eval(`
let url = "rtsp://user:pass@192.168.1.123/cam/realmonitor?...";
let host = match(url, "//[^/]+")[0][2:];
host
`)
`, nil)
require.Nil(t, err)
require.Equal(t, "user:pass@192.168.1.123", v)
}

176
pkg/flussonic/flussonic.go Normal file
View File

@@ -0,0 +1,176 @@
package flussonic
import (
"strings"
"github.com/AlexxIT/go2rtc/pkg/aac"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/h264"
"github.com/AlexxIT/go2rtc/pkg/iso"
"github.com/gorilla/websocket"
"github.com/pion/rtp"
)
type Producer struct {
core.Connection
conn *websocket.Conn
videoTrackID, audioTrackID uint32
videoTimeScale, audioTimeScale float32
}
func Dial(source string) (core.Producer, error) {
url, _ := strings.CutPrefix(source, "flussonic:")
conn, _, err := websocket.DefaultDialer.Dial(url, nil)
if err != nil {
return nil, err
}
prod := &Producer{
Connection: core.Connection{
ID: core.NewID(),
FormatName: "flussonic",
Protocol: core.Before(url, ":"), // wss
RemoteAddr: conn.RemoteAddr().String(),
URL: url,
Transport: conn,
},
conn: conn,
}
if err = prod.probe(); err != nil {
_ = conn.Close()
return nil, err
}
return prod, nil
}
func (p *Producer) probe() error {
var init struct {
//Metadata struct {
// Tracks []struct {
// Width int `json:"width,omitempty"`
// Height int `json:"height,omitempty"`
// Fps int `json:"fps,omitempty"`
// Content string `json:"content"`
// TrackId string `json:"trackId"`
// Bitrate int `json:"bitrate"`
// } `json:"tracks"`
//} `json:"metadata"`
Tracks []struct {
Content string `json:"content"`
Id uint32 `json:"id"`
Payload []byte `json:"payload"`
} `json:"tracks"`
//Type string `json:"type"`
}
if err := p.conn.ReadJSON(&init); err != nil {
return err
}
var timeScale uint32
for _, track := range init.Tracks {
atoms, _ := iso.DecodeAtoms(track.Payload)
for _, atom := range atoms {
switch atom := atom.(type) {
case *iso.AtomMdhd:
timeScale = atom.TimeScale
case *iso.AtomVideo:
switch atom.Name {
case "avc1":
codec := h264.AVCCToCodec(atom.Config)
p.Medias = append(p.Medias, &core.Media{
Kind: core.KindVideo,
Direction: core.DirectionRecvonly,
Codecs: []*core.Codec{codec},
})
p.videoTrackID = track.Id
p.videoTimeScale = float32(codec.ClockRate) / float32(timeScale)
}
case *iso.AtomAudio:
switch atom.Name {
case "mp4a":
codec := aac.ConfigToCodec(atom.Config)
p.Medias = append(p.Medias, &core.Media{
Kind: core.KindAudio,
Direction: core.DirectionRecvonly,
Codecs: []*core.Codec{codec},
})
p.audioTrackID = track.Id
p.audioTimeScale = float32(codec.ClockRate) / float32(timeScale)
}
}
}
}
return nil
}
func (p *Producer) Start() error {
if err := p.conn.WriteMessage(websocket.TextMessage, []byte("resume")); err != nil {
return err
}
receivers := make(map[uint32]*core.Receiver)
timeScales := make(map[uint32]float32)
for _, receiver := range p.Receivers {
switch receiver.Codec.Kind() {
case core.KindVideo:
receivers[p.videoTrackID] = receiver
timeScales[p.videoTrackID] = p.videoTimeScale
case core.KindAudio:
receivers[p.audioTrackID] = receiver
timeScales[p.audioTrackID] = p.audioTimeScale
}
}
ch := make(chan []byte, 10)
defer close(ch)
go func() {
for b := range ch {
atoms, err := iso.DecodeAtoms(b)
if err != nil {
continue
}
var trackID uint32
var decodeTime uint64
for _, atom := range atoms {
switch atom := atom.(type) {
case *iso.AtomTfhd:
trackID = atom.TrackID
case *iso.AtomTfdt:
decodeTime = atom.DecodeTime
case *iso.AtomMdat:
b = atom.Data
}
}
if recv := receivers[trackID]; recv != nil {
timestamp := uint32(float32(decodeTime) * timeScales[trackID])
packet := &rtp.Packet{
Header: rtp.Header{Timestamp: timestamp},
Payload: b,
}
recv.WriteRTP(packet)
}
}
}()
for {
mType, b, err := p.conn.ReadMessage()
if err != nil {
return err
}
if mType == websocket.BinaryMessage {
p.Recv += len(b)
ch <- b
}
}
}

View File

@@ -141,7 +141,7 @@ func (c *Producer) probe() error {
// 2. MedaData without stereo key for AAC
// 3. Audio header after Video keyframe tag
// OpenIPC camera sends:
// OpenIPC camera (on old firmwares) sends:
// 1. Empty video/audio flag
// 2. No MetaData packet
// 3. Sends a video packet in more than 3 seconds

View File

@@ -83,3 +83,15 @@ func TestDahua(t *testing.T) {
n := naluTypes(b)
require.Equal(t, []byte{0x40, 0x42, 0x44, 0x26}, n)
}
func TestUSB(t *testing.T) {
s := "00 00 00 01 67 4D 00 1F 8D 8D 40 28 02 DD 37 01 01 01 40 00 01 C2 00 00 57 E4 01 00 00 00 01 68 EE 3C 80 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 65 88 80 00"
b := EncodeToAVCC(decode(s))
n := naluTypes(b)
require.Equal(t, []byte{0x67, 0x68, 0x65}, n)
s = "00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 41 9A 00 4C"
b = EncodeToAVCC(decode(s))
n = naluTypes(b)
require.Equal(t, []byte{0x41}, n)
}

View File

@@ -5,7 +5,6 @@ import (
"encoding/hex"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -91,5 +90,14 @@ func TestDecodeSPS2(t *testing.T) {
require.Nil(t, err)
sps := DecodeSPS(b)
assert.Nil(t, sps) // broken SPS?
require.Equal(t, uint16(928), sps.Width())
require.Equal(t, uint16(576), sps.Height())
s = "Z2QAHq2EAQwgCGEAQwgCGEAQwgCEO1BQF/yzcBAQFAAAD6AAAXcCEA==" // unknown
b, err = base64.StdEncoding.DecodeString(s)
require.Nil(t, err)
sps = DecodeSPS(b)
require.Equal(t, uint16(640), sps.Width())
require.Equal(t, uint16(360), sps.Height())
}

View File

@@ -22,7 +22,7 @@ func RTPDepay(codec *core.Codec, handler core.HandlerFunc) core.HandlerFunc {
buf := make([]byte, 0, 512*1024) // 512K
return func(packet *rtp.Packet) {
//log.Printf("[RTP] codec: %s, nalu: %2d, size: %6d, ts: %10d, pt: %2d, ssrc: %d, seq: %d, %v", track.Codec.Name, packet.Payload[0]&0x1F, len(packet.Payload), packet.Timestamp, packet.PayloadType, packet.SSRC, packet.SequenceNumber, packet.Marker)
//log.Printf("[RTP] codec: %s, nalu: %2d, size: %6d, ts: %10d, pt: %2d, ssrc: %d, seq: %d, %v", codec.Name, packet.Payload[0]&0x1F, len(packet.Payload), packet.Timestamp, packet.PayloadType, packet.SSRC, packet.SequenceNumber, packet.Marker)
payload, err := depack.Unmarshal(packet.Payload)
if len(payload) == 0 || err != nil {
@@ -68,6 +68,9 @@ func RTPDepay(codec *core.Codec, handler core.HandlerFunc) core.HandlerFunc {
payload = payload[i:]
continue
case NALUTypePFrame, NALUTypeSPS, NALUTypePPS: // pass
default:
return // skip any unknown NAL unit type
}
break
}

View File

@@ -88,6 +88,8 @@ func (s *SPS) Height() uint16 {
}
func DecodeSPS(sps []byte) *SPS {
// https://developer.ridgerun.com/wiki/index.php/H264_Analysis_Tools
// ffmpeg -i file.h264 -c copy -bsf:v trace_headers -f null -
r := bits.NewReader(sps)
hdr := r.ReadByte()

View File

@@ -3,6 +3,7 @@ package h265
import (
"encoding/base64"
"encoding/binary"
"github.com/AlexxIT/go2rtc/pkg/core"
)

View File

@@ -2,8 +2,9 @@ package h265
import (
"encoding/binary"
"github.com/AlexxIT/go2rtc/pkg/h264"
"math"
"github.com/AlexxIT/go2rtc/pkg/h264"
)
//

View File

@@ -2,12 +2,21 @@ package camera
import (
"encoding/base64"
"strings"
"testing"
"github.com/AlexxIT/go2rtc/pkg/hap"
"github.com/stretchr/testify/require"
)
func TestNilCharacter(t *testing.T) {
var res SetupEndpoints
char := &hap.Character{}
err := char.ReadTLV8(&res)
require.NotNil(t, err)
require.NotNil(t, strings.Contains(err.Error(), "can't read value"))
}
type testTLV8 struct {
name string
value string

View File

@@ -0,0 +1,17 @@
package camera
const TypeSetupDataStreamTransport = "131"
type SetupDataStreamRequest struct {
SessionCommandType byte `tlv8:"1"`
TransportType byte `tlv8:"2"`
ControllerKeySalt string `tlv8:"3"`
}
type SetupDataStreamResponse struct {
Status byte `tlv8:"1"`
TransportTypeSessionParameters struct {
TCPListeningPort uint16 `tlv8:"1"`
} `tlv8:"2"`
AccessoryKeySalt string `tlv8:"3"`
}

View File

@@ -3,6 +3,7 @@ package hap
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
@@ -126,11 +127,17 @@ func (c *Character) Write(v any) (err error) {
// ReadTLV8 value to right struct
func (c *Character) ReadTLV8(v any) (err error) {
return tlv8.UnmarshalBase64(c.Value.(string), v)
if s, ok := c.Value.(string); ok {
return tlv8.UnmarshalBase64(s, v)
}
return fmt.Errorf("hap: can't read value: %v", v)
}
func (c *Character) ReadBool() bool {
return c.Value.(bool)
func (c *Character) ReadBool() (bool, error) {
if v, ok := c.Value.(bool); ok {
return v, nil
}
return false, fmt.Errorf("hap: can't read value: %v", c.Value)
}
func (c *Character) String() string {

123
pkg/hap/hds/hds.go Normal file
View File

@@ -0,0 +1,123 @@
// Package hds - HomeKit Data Stream
package hds
import (
"bufio"
"encoding/binary"
"io"
"net"
"time"
"github.com/AlexxIT/go2rtc/pkg/hap/chacha20poly1305"
"github.com/AlexxIT/go2rtc/pkg/hap/hkdf"
"github.com/AlexxIT/go2rtc/pkg/hap/secure"
)
func Client(conn net.Conn, key []byte, salt string, controller bool) (*Conn, error) {
writeKey, err := hkdf.Sha512(key, salt, "HDS-Write-Encryption-Key")
if err != nil {
return nil, err
}
readKey, err := hkdf.Sha512(key, salt, "HDS-Read-Encryption-Key")
if err != nil {
return nil, err
}
c := &Conn{
conn: conn,
rd: bufio.NewReaderSize(conn, 32*1024),
wr: bufio.NewWriterSize(conn, 32*1024),
}
if controller {
c.decryptKey, c.encryptKey = readKey, writeKey
} else {
c.decryptKey, c.encryptKey = writeKey, readKey
}
return c, nil
}
type Conn struct {
conn net.Conn
rd *bufio.Reader
wr *bufio.Writer
decryptKey []byte
encryptKey []byte
decryptCnt uint64
encryptCnt uint64
}
func (c *Conn) Read(p []byte) (n int, err error) {
verify := make([]byte, 4)
if _, err = io.ReadFull(c.rd, verify); err != nil {
return
}
n = int(binary.BigEndian.Uint32(verify) & 0xFFFFFF)
ciphertext := make([]byte, n+secure.Overhead)
if _, err = io.ReadFull(c.rd, ciphertext); err != nil {
return
}
nonce := make([]byte, secure.NonceSize)
binary.LittleEndian.PutUint64(nonce, c.decryptCnt)
c.decryptCnt++
_, err = chacha20poly1305.DecryptAndVerify(c.decryptKey, p[:0], nonce, ciphertext, verify)
return
}
func (c *Conn) Write(b []byte) (n int, err error) {
n = len(b)
verify := make([]byte, 4)
binary.BigEndian.PutUint32(verify, 0x01000000|uint32(n))
if _, err = c.wr.Write(verify); err != nil {
return
}
nonce := make([]byte, secure.NonceSize)
binary.LittleEndian.PutUint64(nonce, c.encryptCnt)
c.encryptCnt++
buf := make([]byte, n+secure.Overhead)
if _, err = chacha20poly1305.EncryptAndSeal(c.encryptKey, buf[:0], nonce, b, verify); err != nil {
return
}
if _, err = c.wr.Write(buf); err != nil {
return
}
err = c.wr.Flush()
return
}
func (c *Conn) Close() error {
return c.conn.Close()
}
func (c *Conn) LocalAddr() net.Addr {
return c.conn.LocalAddr()
}
func (c *Conn) RemoteAddr() net.Addr {
return c.conn.RemoteAddr()
}
func (c *Conn) SetDeadline(t time.Time) error {
return c.conn.SetDeadline(t)
}
func (c *Conn) SetReadDeadline(t time.Time) error {
return c.conn.SetReadDeadline(t)
}
func (c *Conn) SetWriteDeadline(t time.Time) error {
return c.conn.SetWriteDeadline(t)
}

35
pkg/hap/hds/hds_test.go Normal file
View File

@@ -0,0 +1,35 @@
package hds
import (
"bufio"
"bytes"
"testing"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/stretchr/testify/require"
)
func TestEncryption(t *testing.T) {
key := []byte(core.RandString(16, 0))
salt := core.RandString(32, 0)
c, err := Client(nil, key, salt, true)
require.NoError(t, err)
buf := bytes.NewBuffer(nil)
c.wr = bufio.NewWriter(buf)
n, err := c.Write([]byte("test"))
require.NoError(t, err)
require.Equal(t, 4, n)
c, err = Client(nil, key, salt, false)
c.rd = bufio.NewReader(buf)
require.NoError(t, err)
b := make([]byte, 32)
n, err = c.Read(b)
require.NoError(t, err)
require.Equal(t, "test", string(b[:n]))
}

View File

@@ -64,10 +64,11 @@ type JSONCharacters struct {
}
type JSONCharacter struct {
AID uint8 `json:"aid"`
IID uint64 `json:"iid"`
Value any `json:"value,omitempty"`
Event any `json:"ev,omitempty"`
AID uint8 `json:"aid"`
IID uint64 `json:"iid"`
Status any `json:"status,omitempty"`
Value any `json:"value,omitempty"`
Event any `json:"ev,omitempty"`
}
func SanitizePin(pin string) (string, error) {

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