Compare commits

..

251 Commits

Author SHA1 Message Date
Alex X 2dc0d58ba7 Update version to 1.9.12 2025-11-16 19:08:34 +03:00
Alex X cb22ae7833 Add security notes to readme 2025-11-16 19:07:03 +03:00
Alex X c98b0a83c4 Merge pull request #1939 from edenhaus/supportedSchemas
Add api endpoint to return supported schemas
2025-11-16 19:04:24 +03:00
Alex X 0bae158e41 Code refactoring for #1939 2025-11-16 19:03:19 +03:00
Robert Resch e2b63a4f6c Remove duplicate code 2025-11-16 16:40:04 +01:00
Robert Resch 3897f10a4d Add api endpoint to return supported schema 2025-11-16 16:33:09 +01:00
Alex X ac80f1470e Add errors output to streams API 2025-11-16 18:20:53 +03:00
Alex X 1fe602679e Update WebUI design 2025-11-15 21:08:00 +03:00
Alex X e2c7d06730 Add check for insecure uri from onvif source 2025-11-11 17:34:01 +03:00
Alex X 2133f5323c Add insecure sources logic 2025-11-11 17:33:15 +03:00
Alex X c10a06d199 Fix wrong log message for streams module 2025-11-11 17:29:10 +03:00
Alex X d053d88ce9 Add support maxwidth/maxheight settings for homekit source 2025-11-11 15:48:12 +03:00
Alex X 2ce38b4486 Add trace log for ignored api paths 2025-11-11 15:43:49 +03:00
Alex X 44d59b1696 Add config local_auth for api module 2025-11-11 15:22:28 +03:00
Alex X 15ec995ecc Add config for the list of modules to init 2025-11-11 15:10:24 +03:00
Alex X 231cab36b2 Add config allow_paths for api module 2025-11-11 15:01:27 +03:00
Alex X 640db3029e Add config allow_paths for exec module 2025-11-11 15:00:58 +03:00
Alex X 2836fdae13 Add config allow_paths for echo module 2025-11-11 14:59:05 +03:00
Alex X 964bb225fa Add support custom params for hass source 2025-11-11 10:54:13 +03:00
Alex X 5cc32197b8 Fix HomeKit proxy for hass source 2025-11-10 15:35:07 +03:00
Alex X bc1a4ac8e4 Fix API /api/homekit/accessories 2025-11-10 15:34:39 +03:00
Alex X 158f9d3a08 Code refactoring for HomeKit server 2025-11-09 21:58:44 +03:00
Alex X 81cfcf877a Fix HomeKit proxy EVENTs 2025-11-09 21:28:53 +03:00
Alex X 96919bf9e3 Add support uint64 to tlv8 2025-11-09 21:25:23 +03:00
Alex X e4359ac217 Rename HomeKit structures according to specs 2025-11-09 21:24:47 +03:00
Alex X ff18283d11 Improve homekit secure conn buffers 2025-10-26 15:46:11 +03:00
Alex X 994e0dc526 Improve homekit tlv8 parsing 2025-10-26 15:46:11 +03:00
Alex X 7254bd4fbc Code refactoring for tapo source 2025-10-24 17:54:55 +03:00
Alex X 9f407a754d Fix tapo source for some cameras #1918 2025-10-24 17:54:37 +03:00
Alex X cc97bc33c4 Restore simple onvif client logic 2025-10-24 17:28:49 +03:00
Alex X 6db4dda535 Fix onvif client for some cameras 2025-10-23 14:42:38 +03:00
Alex X be80eb1ac9 Update version to 1.9.11 2025-10-21 15:32:37 +03:00
Alex X a8d2312cb0 Update dependencies 2025-10-21 15:22:46 +03:00
Alex X 5bbc2aaf2e Move ngrok module docs to another page. 2025-10-21 15:21:33 +03:00
Alex X f8c88cfbe0 Merge pull request #1641 from felipecrs/fix-build.sh
Improve build.sh
2025-10-15 18:08:09 +03:00
Alex X cf4acd5a8d Code refactoring for #1641 2025-10-15 18:07:18 +03:00
Felipe Santos 19226df7d6 Add go2rtc.exe to gitignore 2025-10-15 09:44:20 -03:00
Felipe Santos c415c8f2af Remove GOTOOLCHAIN like done in CI 2025-10-15 09:44:11 -03:00
Felipe Santos 2751f41afb Merge branch 'master' of https://github.com/AlexxIT/go2rtc into fix-build.sh 2025-10-15 09:37:27 -03:00
Alex X d59cb99f0d Support RTSP redirects #1909 by @eddielth 2025-10-15 14:09:07 +03:00
Alex X 007e8dbc75 Add caution note to readme 2025-10-14 17:20:54 +03:00
Alex X dae396a1ed Merge pull request #1910 from felipecrs/go2rtc-gitignore
Add compiled go2rtc to gitignore
2025-10-14 11:15:48 +03:00
Felipe Santos d894483166 Add go2rtc to gitignore 2025-10-13 08:42:06 -03:00
Alex X 60ef52f44b Merge pull request #1752 from felipecrs/python3.13
Update Python to 3.13 in docker image
2025-10-11 11:00:59 +03:00
Alex X 240e1960f8 Merge branch 'master' into python3.13 2025-10-11 11:00:52 +03:00
Alex X a107d13e61 Merge pull request #1761 from felipecrs/fix-docker-workflow
Fix docker build and push job when running from a fork
2025-10-11 10:49:35 +03:00
Alex X f911936d79 Merge pull request #1823 from hsakoh/feature/addSwitchBotDoorbellSupport
Add Support for SwitchBot VideoDoorbell
2025-10-10 17:22:55 +03:00
Alex X ea23957f2a Code refactoring for #1823 2025-10-10 17:22:18 +03:00
Alex X f1971cec7c Merge pull request #1589 from seydx/onvif-client
fix: ONVIF client
2025-10-10 16:39:36 +03:00
Alex X 7291c03cea Code refactoring for #1589 2025-10-10 16:35:54 +03:00
Alex X fdb3116027 Added checks for corrupted data to the H265 handler 2025-10-10 11:51:53 +03:00
Alex X c87885be48 Merge pull request #1758 from seydx/rtsp-udp
Add RTSP UDP transport support
2025-10-10 11:30:28 +03:00
Alex X 98f88d037e Remove UDP example from readme 2025-10-10 11:11:29 +03:00
Alex X fde1fdc592 Code refactoring for #1758 2025-10-09 21:10:54 +03:00
Alex X cca216ace5 Merge pull request #1744 from seydx/secrets-file
Secrets Management
2025-10-07 15:23:02 +03:00
Alex X e953e949ef Merge branch 'master' into secrets-file 2025-10-07 15:22:44 +03:00
Alex X fe2cc4b525 Code refactoring for #1744 2025-10-07 15:15:04 +03:00
Alex X 6a67fc3712 Merge pull request #1895 from oeiber/master
Fix connection issues in conjunction with doorbird backchannel
2025-10-05 16:01:25 +03:00
Alex X 94b7c33485 Update backchannel.go
Code refactoring for #1895
2025-10-05 16:00:58 +03:00
Oliver Eiber 887f0f4890 fix connection handling in conjunction with doorbird backchannel 2025-10-04 21:37:19 +02:00
Alex X ec08dfee9c Fix stack API for new pion version 2025-10-04 19:19:01 +03:00
Alex X 54b95dced4 Fix probing after #1762 2025-10-04 19:18:36 +03:00
Alex X 37d7409e82 Merge pull request #1762 from seydx/preload
Preload Streams
2025-10-01 17:22:19 +03:00
Alex X 4dd1f73a18 Update readme for #1762 2025-10-01 17:03:32 +03:00
Alex X 22cc8ac2c4 Code refactoring for #1762 2025-10-01 16:57:39 +03:00
seydx a667acad07 Merge branch 'master' of https://github.com/AlexxIT/go2rtc into rtsp-udp 2025-09-30 19:20:21 +02:00
seydx c196b82a72 Merge branch 'master' of https://github.com/AlexxIT/go2rtc into preload 2025-09-30 19:13:36 +02:00
seydx 670370056c Refactor secrets management 2025-09-30 15:35:32 +02:00
seydx 0c5a2bf02b Remove NewSecret function and related import from helpers.go 2025-09-30 15:21:30 +02:00
seydx 8f9e78be0c Merge branch 'master' of https://github.com/AlexxIT/go2rtc into secrets-file 2025-09-30 14:58:40 +02:00
seydx 50d5fa93b6 Set Content-Type header to MimeJSON in ResponsePrettyJSON function 2025-09-30 14:50:57 +02:00
seydx 3a0e4078fd Dont redact hass entry title 2025-09-30 14:48:58 +02:00
Alex X 549da0257e Merge pull request #1745 from seydx/optimize-ring
Optimize ring
2025-09-30 13:36:42 +03:00
Alex X 2b5f9429a8 Update FFmpeg command for encoding H265 (fix profile and level) 2025-09-30 12:17:41 +03:00
Alex X c7119f4403 Fix RTP processing for H265 codec (restore VPS,SPS,PPS) 2025-09-30 12:14:41 +03:00
Alex X 7d9862202a Code refactoring for video-rtc.js 2025-09-30 12:12:29 +03:00
Alex X dd7130d2d4 Merge pull request #1644 from seydx/check-h265
Add support for H.265 codec verification
2025-09-29 18:23:05 +03:00
Alex X d697bdcf05 Code refactoring for #1644 2025-09-29 18:21:36 +03:00
seydx abd61919cf Merge branch 'master' of https://github.com/AlexxIT/go2rtc into secrets-file 2025-09-26 15:15:59 +02:00
seydx ad2383de80 Merge branch 'AlexxIT:master' into check-h265 2025-09-26 15:10:14 +02:00
seydx 9f3ff98951 Merge branch 'AlexxIT:master' into onvif-client 2025-09-26 15:09:11 +02:00
seydx d286980548 Merge branch 'AlexxIT:master' into optimize-ring 2025-09-26 15:09:01 +02:00
seydx 87533ac5a1 Merge branch 'AlexxIT:master' into preload 2025-09-26 15:08:52 +02:00
seydx d05451416d Merge branch 'AlexxIT:master' into rtsp-udp 2025-09-26 15:08:17 +02:00
Alex X df95ce39d0 Update version to 1.9.10 2025-09-24 16:34:54 +03:00
Alex X 26f16e392f Update go (build) version to 1.25 and related readme 2025-09-24 16:19:30 +03:00
Alex X fcc837e570 Merge pull request #1879 from mihailstoynov/patch-2
Update README.md
2025-09-24 11:46:36 +03:00
Alex X fd682306e7 Fix MultiUDPMuxDefault panic #1646 2025-09-22 18:11:47 +03:00
mihailstoynov e072842b7a Update README.md
example for VGA and HD streams for ease of use
2025-09-21 22:56:59 +03:00
Alex X 3b976c6812 Improve HomeKit TLV format parser 2025-09-19 15:29:24 +03:00
Alex X 40269328fb Fix insecure PINs for HomeKit server 2025-09-19 15:27:58 +03:00
Alex X 45cbbaf1cf Fixed a race condition when changing the config file 2025-09-19 15:26:54 +03:00
Alex X 3f542a642c Merge pull request #1841 from hugoaboud/master
Security Patch: Sanitize credentials on websocket error messages
2025-09-19 15:24:51 +03:00
Alex X 8b4df5f02c Code refactoring for #1841 2025-09-19 15:21:02 +03:00
Alex X 788afb7189 Fix HomeKit server support on iOS 26 #1843 2025-09-18 23:08:33 +03:00
Alex X cd7fa5d09c Fix RepairAVCC in some cases 2025-09-10 12:27:13 +03:00
Alex X beb82045ff Fix yet another broken Content-Base for RTSP #1852 2025-08-29 16:49:05 +03:00
Alex X 33f0fd5fe6 Add lightNVR project to readme 2025-08-29 16:48:53 +03:00
Alex X 850051a299 Merge pull request #1845 from kvikindi/patch-1
(DOC) Update Proxmox Helper Scripts link in README.md
2025-08-25 14:58:44 +03:00
Ragnar Petursson 8f7cbdf60a Update Proxmox Helper Scripts link in README.md
Changed link to current link, as previous repository is inactive as of November 2nd 2024.
2025-08-23 18:09:28 +01:00
Hugo Aboud 4577390130 Sanitize credentials on websocket error messages 2025-08-19 16:16:01 -03:00
Oliver Eiber f2242e31c8 impove connection timeout to prevent reconnections after 30 seconds 2025-08-19 07:53:10 +02:00
Oliver Eiber 975a43d392 reduce audio delay by lowering buffer size 2025-07-31 21:07:45 +02:00
Oliver Eiber 3d38e5e567 fix unexpected close of backchannel streams 2025-07-30 23:37:06 +02:00
Oliver Eiber 7d2ad92c4b fix app crashes
remove orphaned streams
2025-07-28 22:27:38 +02:00
hsakoh b82023bc32 Add Support for SwitchBot VideoDoorbell 2025-07-26 01:40:27 +09:00
Oliver Eiber a92e04b6e0 added audio mixing capability to avoid device overload when multiple backchannel audio streams are connected 2025-07-22 20:54:24 +02:00
Oliver Eiber 56e61a85ee proper error handling
cleanup files
2025-07-16 21:07:34 +02:00
seydx 7d83b0d6c8 Merge branch 'AlexxIT:master' into check-h265 2025-07-10 16:17:06 +02:00
seydx 708277230a Merge branch 'AlexxIT:master' into onvif-client 2025-07-10 16:16:11 +02:00
seydx 06e6e31443 Merge branch 'AlexxIT:master' into optimize-ring 2025-07-10 16:16:02 +02:00
seydx 8474f2f571 Merge branch 'AlexxIT:master' into preload 2025-07-10 16:15:53 +02:00
seydx 9ddea7d9d6 Merge branch 'AlexxIT:master' into rtsp-udp 2025-07-10 16:15:17 +02:00
seydx c2fbd372b3 Merge branch 'AlexxIT:master' into secrets-file 2025-07-10 16:15:07 +02:00
Alex X 34b103bbcb Update all dependencies and min go version to 1.23 2025-07-08 12:45:56 +03:00
Alex X 22fbd8bc9e Merge pull request #1773 from ehn/patch-1
Improve spelling and grammar in README.md
2025-07-07 19:21:21 +03:00
Alex X d175213369 Merge pull request #1782 from riker09/patch-1
Update schema.json
2025-07-07 17:28:43 +03:00
Oliver Eiber e00d211619 ensure that doorbird errors where shown in logs 2025-07-06 22:33:25 +02:00
Oliver Eiber c68e3cafe4 fixes doorbird backchannel audio:
- proper session handling
- honor http status codes
- prevent device from being flooded by limiting concurrent audio channels
2025-07-03 23:35:58 +02:00
Volker Thiel 5a34483513 Update schema.json
Add missing letter `r` in one of the examples
2025-06-23 20:08:54 +02:00
seydx 7bb0f0d2e6 readme 2025-06-19 10:29:56 +02:00
seydx 96d7066085 Merge branch 'AlexxIT:master' into secrets-file 2025-06-16 10:05:59 +02:00
seydx ae49946740 Merge branch 'AlexxIT:master' into optimize-ring 2025-06-16 10:05:31 +02:00
seydx fcc91c9b8a Merge branch 'AlexxIT:master' into onvif-client 2025-06-16 10:05:22 +02:00
seydx 994fd41826 Merge branch 'AlexxIT:master' into check-h265 2025-06-16 10:04:28 +02:00
seydx 3282b38900 Merge branch 'AlexxIT:master' into rtsp-udp 2025-06-16 10:03:19 +02:00
seydx 647b2acf48 cleanup 2025-06-16 09:58:55 +02:00
seydx ef318f663e fix preload queries 2025-06-16 09:32:07 +02:00
seydx 5771454400 use preload as format name 2025-06-16 00:50:48 +02:00
seydx 6732e726d5 update preload consumer to handle RTP packets 2025-06-16 00:33:16 +02:00
Andreas Ehn 230c80c70e Improve spelling and grammar in README.md 2025-06-14 11:24:12 +08:00
seydx 45503aa8c5 Merge branch 'AlexxIT:master' into preload 2025-06-14 00:40:06 +02:00
Alex X a4d7fd0d95 Add support yandex source 2025-06-12 16:52:05 +03:00
seydx b6579122d1 fix 2025-06-06 03:11:28 +02:00
seydx 42a67f8ad5 comments 2025-06-06 02:18:00 +02:00
seydx 91eeefec68 openapi: add preload endpoints 2025-06-05 16:01:49 +03:00
seydx 8ab7aeb8b2 update readme 2025-06-05 15:51:14 +03:00
seydx 493fa1ef6a add api endpoints and change config syntax 2025-06-05 11:33:03 +03:00
seydx 020549ef60 readme 2025-06-02 22:16:43 +03:00
seydx dfc1f45f97 support preloading streams 2025-06-02 22:06:47 +03:00
Felipe Santos 641e65ee95 Fix docker build and push job when running from a fork 2025-06-02 13:21:54 -03:00
seydx 24ca87e00d dont fallback to tcp if udp failes 2025-06-01 18:40:53 +03:00
seydx 859cd1cbe6 support rtsp udp transport 2025-06-01 01:44:01 +03:00
seydx 79656d1344 update readme 2025-05-26 23:10:57 +02:00
seydx 759f979182 dont redact config.env values 2025-05-26 22:23:24 +02:00
seydx 7c17e64090 format 2025-05-26 22:21:33 +02:00
seydx bf45f64a7e - refactor secrets
- add support for env in config
- redact sensitive information in logs/responses
2025-05-26 21:56:45 +02:00
Felipe Santos 72890d5983 Update Python to 3.13 in docker image 2025-05-26 14:01:26 -03:00
seydx e8e798d955 Merge branch 'AlexxIT:master' into secrets-file 2025-05-23 08:02:17 +02:00
seydx 8a8b379bfc Merge branch 'AlexxIT:master' into optimize-ring 2025-05-23 08:00:40 +02:00
seydx ca491def83 Merge branch 'AlexxIT:master' into onvif-client 2025-05-23 08:00:28 +02:00
seydx 5a597277a9 Merge branch 'AlexxIT:master' into check-h265 2025-05-23 07:59:11 +02:00
Alex X ae8145f266 Fix panic on AVCCToCodec #1652 2025-05-22 15:52:10 +03:00
seydx c90fcd1ce1 refactor 2025-05-21 13:16:49 +02:00
seydx e0687db9e2 add template parsing 2025-05-20 23:07:04 +02:00
seydx 24310e2f7a remove parse 2025-05-20 22:44:07 +02:00
seydx a1f0b86ab3 format 2025-05-20 22:29:27 +02:00
seydx 7f87c6e478 refactor 2025-05-20 21:40:33 +02:00
seydx a0145b4b24 revert handlers 2025-05-20 15:52:26 +02:00
seydx 2fcbb1d836 refactor 2025-05-20 15:51:15 +02:00
seydx a2beea1bbd refactor 2025-05-20 13:59:46 +02:00
seydx e5e55b7a50 improve secret vars and parse url with secrets 2025-05-20 13:05:11 +02:00
seydx 0830d8342e add secret management functions 2025-05-20 12:07:46 +02:00
seydx adb1b21e81 format 2025-05-17 16:37:12 +02:00
seydx edfa09bb9f ring: update peer connection state handling and pass sdo to producer 2025-05-10 19:04:47 +02:00
seydx 2eef7bdbd3 ring: implement session management and caching 2025-05-08 17:38:09 +02:00
seydx 124556f4db ring: skip refetching cameras to increase loading speed and refactor ring url 2025-05-08 16:09:04 +02:00
seydx d528e167db Merge branch 'AlexxIT:master' into onvif-client 2025-05-02 17:56:08 +02:00
seydx f151969fe1 Merge branch 'AlexxIT:master' into check-h265 2025-05-02 17:55:23 +02:00
Alex X 7107508286 Merge pull request #1669 from hnws/master
feat(nest): add retry logic for 429 and 409 errors with exponential backoff
2025-05-02 11:03:38 +03:00
hnws cd2f90a7a1 refactor: remove logging from nest package 2025-05-01 22:30:58 -04:00
Alex X e1577b5ad3 Remove unnecessary nil check 2025-05-01 15:31:18 +03:00
seydx 251686608a Merge branch 'AlexxIT:master' into onvif-client 2025-04-30 22:55:28 +02:00
seydx 993aa613fd Merge branch 'AlexxIT:master' into check-h265 2025-04-30 22:54:47 +02:00
Alex X 3c1f7e4181 Merge pull request #1716 from gudaja/fix/getScreenshotFromNonconfigurreCamera
Fix: Handle RTSP cameras requiring fragment-implied config in dynamic snapshot API
2025-04-29 20:15:15 +03:00
luki 2ed67648c3 get screenshot from nonconfigure camera 2025-04-29 18:01:25 +02:00
seydx 2fdfec6f21 Merge branch 'AlexxIT:master' into onvif-client 2025-04-26 00:24:59 +02:00
seydx a0d8f3ae81 Merge branch 'AlexxIT:master' into check-h265 2025-04-26 00:24:18 +02:00
Alex X 6d37cceb91 Improve readme for wyoming module 2025-04-25 14:52:11 +03:00
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
seydx fd5746a954 Merge branch 'AlexxIT:master' into onvif-client 2025-04-22 12:30:30 +02:00
seydx 2c3813deb9 Merge branch 'AlexxIT:master' into check-h265 2025-04-22 12:29:49 +02: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
seydx a8b51ad619 Merge branch 'AlexxIT:master' into onvif-client 2025-04-09 17:10:07 +02:00
seydx 49c4d45731 Merge branch 'AlexxIT:master' into check-h265 2025-04-09 17:09:19 +02: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
seydx 1282b23c57 Merge branch 'AlexxIT:master' into onvif-client 2025-04-01 22:12:55 +02:00
seydx 54afd0b50b Merge branch 'AlexxIT:master' into check-h265 2025-04-01 22:11:46 +02: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
seydx 6ee748a87a Merge branch 'AlexxIT:master' into onvif-client 2025-03-23 11:08:56 +01:00
seydx 0ebda76cc8 Merge branch 'AlexxIT:master' into check-h265 2025-03-23 11:08:06 +01: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
seydx 68b3dc6f37 Merge branch 'AlexxIT:master' into onvif-client 2025-03-22 14:56:10 +01:00
seydx 3c83102e91 Merge branch 'AlexxIT:master' into check-h265 2025-03-22 14:55:26 +01: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
hnws db2937d4a3 Merge branch 'AlexxIT:master' into master 2025-03-21 23:11:47 -04:00
hnws fe10a7e55f feat(nest): add retry logic for 429 and 409 errors with exponential backoff 2025-03-21 23:10:16 -04:00
seydx dd77c3e1f0 Merge branch 'AlexxIT:master' into onvif-client 2025-03-18 13:16:31 +01:00
seydx e5e1f6bd05 Merge branch 'AlexxIT:master' into check-h265 2025-03-18 13:15:45 +01:00
Alex X c52f3ebdd6 Fix wrong URL in hls.html example 2025-03-17 14:52:33 +03:00
seydx ac96b64c64 change codec priority handling for h265 2025-03-16 14:16:05 +01:00
seydx fa8fd60ac6 Merge branch 'AlexxIT:master' into onvif-client 2025-03-13 14:36:54 +01:00
seydx 9c9393e0cf Merge branch 'AlexxIT:master' into check-h265 2025-03-13 14:35:57 +01:00
Alex X 47f32a5f55 Fix support linux + riscv64 #1639 2025-03-13 15:33:23 +03:00
seydx 8b26f9309f Merge branch 'AlexxIT:master' into onvif-client 2025-03-12 22:19:50 +01:00
seydx 4027809f32 Merge branch 'AlexxIT:master' into check-h265 2025-03-12 22:19:17 +01: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
seydx b28ffa9543 indentation 2025-03-11 01:52:16 +01:00
seydx 7836f2e47f check h265 2025-03-11 01:50:41 +01:00
seydx 648873978c Merge branch 'AlexxIT:master' into onvif-client 2025-03-11 01:45:25 +01:00
Felipe Santos c51c13b4b8 Avoid export pollution 2025-03-10 19:49:04 -03:00
Felipe Santos 9a7c7d2a4b Fix GOTOOLCHAIN in build.sh 2025-03-10 19:45:13 -03:00
Felipe Santos 5f17474ff4 Fix build in linux amd64 2025-03-10 19:36:16 -03:00
Felipe Santos 3376bf8b99 Avoid ignoring errors 2025-03-10 19:34:18 -03:00
Felipe Santos ad1bea088e Fix check_command in build.sh 2025-03-10 19:18:04 -03:00
seydx d0ac99fc69 fix onvif client 2025-02-10 20:21:25 +01:00
MarcA711 2b69eb2fd0 update FFmpeg presets for Rockchip boards 2024-06-18 17:56:04 +00:00
213 changed files with 10029 additions and 3922 deletions
+67 -11
View File
@@ -19,7 +19,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v5
with: { go-version: '1.24' }
with: { go-version: '1.25' }
- name: Build go2rtc_win64
env: { GOOS: windows, GOARCH: amd64 }
@@ -29,7 +29,7 @@ jobs:
with: { name: go2rtc_win64, path: go2rtc.exe }
- name: Build go2rtc_win32
env: { GOOS: windows, GOARCH: 386, GOTOOLCHAIN: go1.20.14 }
env: { GOOS: windows, GOARCH: 386 }
run: go build -ldflags "-s -w" -trimpath
- name: Upload go2rtc_win32
uses: actions/upload-artifact@v4
@@ -85,7 +85,7 @@ jobs:
with: { name: go2rtc_linux_mipsel, path: go2rtc }
- name: Build go2rtc_mac_amd64
env: { GOOS: darwin, GOARCH: amd64, GOTOOLCHAIN: go1.20.14 }
env: { GOOS: darwin, GOARCH: amd64 }
run: go build -ldflags "-s -w" -trimpath
- name: Upload go2rtc_mac_amd64
uses: actions/upload-artifact@v4
@@ -124,7 +124,7 @@ jobs:
uses: docker/metadata-action@v5
with:
images: |
${{ github.repository }}
name=${{ github.repository }},enable=${{ github.event.repository.fork == false }}
ghcr.io/${{ github.repository }}
tags: |
type=ref,event=branch
@@ -138,14 +138,14 @@ jobs:
uses: docker/setup-buildx-action@v3
- name: Login to DockerHub
if: github.event_name != 'pull_request'
if: github.event_name == 'push' && github.event.repository.fork == false
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'
if: github.event_name == 'push'
uses: docker/login-action@v3
with:
registry: ghcr.io
@@ -156,6 +156,7 @@ jobs:
uses: docker/build-push-action@v5
with:
context: .
file: docker/Dockerfile
platforms: |
linux/amd64
linux/386
@@ -180,7 +181,7 @@ jobs:
uses: docker/metadata-action@v5
with:
images: |
${{ github.repository }}
name=${{ github.repository }},enable=${{ github.event.repository.fork == false }}
ghcr.io/${{ github.repository }}
flavor: |
suffix=-hardware,onlatest=true
@@ -197,14 +198,14 @@ jobs:
uses: docker/setup-buildx-action@v3
- name: Login to DockerHub
if: github.event_name != 'pull_request'
if: github.event_name == 'push' && github.event.repository.fork == false
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'
if: github.event_name == 'push'
uses: docker/login-action@v3
with:
registry: ghcr.io
@@ -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: |
name=${{ github.repository }},enable=${{ github.event.repository.fork == false }}
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 == 'push' && github.event.repository.fork == false
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry
if: github.event_name == 'push'
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
+2 -1
View File
@@ -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
+3
View File
@@ -9,6 +9,9 @@ go2rtc_linux*
go2rtc_mac*
go2rtc_win*
/go2rtc
/go2rtc.exe
0_test.go
.DS_Store
+216 -217
View File
@@ -8,7 +8,7 @@
[![goreport](https://goreportcard.com/badge/github.com/AlexxIT/go2rtc)](https://goreportcard.com/report/github.com/AlexxIT/go2rtc)
</h1>
Ultimate camera streaming application with support RTSP, WebRTC, HomeKit, FFmpeg, RTMP, etc.
Ultimate camera streaming application with support for RTSP, WebRTC, HomeKit, FFmpeg, RTMP, etc.
![](assets/go2rtc.png)
@@ -20,13 +20,12 @@ Ultimate camera streaming application with support RTSP, WebRTC, HomeKit, FFmpeg
- [publish](#publish-stream) any source to popular streaming services (YouTube, Telegram, etc.)
- first project in the World with support streaming from [HomeKit Cameras](#source-homekit)
- support H265 for WebRTC in browser (Safari only, [read more](https://github.com/AlexxIT/Blog/issues/5))
- on the fly transcoding for unsupported codecs via [FFmpeg](#source-ffmpeg)
- on-the-fly transcoding for unsupported codecs via [FFmpeg](#source-ffmpeg)
- play audio files and live streams on some cameras with [speaker](#stream-to-camera)
- multi-source 2-way [codecs negotiation](#codecs-negotiation)
- mixing tracks from different sources to single stream
- auto match client supported codecs
- auto-match client-supported codecs
- [2-way audio](#two-way-audio) for some cameras
- streaming from private networks via [ngrok](#module-ngrok)
- can be [integrated to](#module-api) any smart home platform or be used as [standalone app](#go2rtc-binary)
**Inspired by:**
@@ -39,6 +38,9 @@ Ultimate camera streaming application with support RTSP, WebRTC, HomeKit, FFmpeg
- HomeKit Accessory Protocol from [@brutella](https://github.com/brutella/hap)
- creator of the project's logo [@v_novoseltsev](https://www.instagram.com/v_novoseltsev)
> [!CAUTION]
> There is NO existing website for go2rtc project other than this GitHub repository. The website go2rtc[.]com is in no way associated with the authors of this project.
---
* [Fast start](#fast-start)
@@ -69,12 +71,14 @@ Ultimate camera streaming application with support RTSP, WebRTC, HomeKit, FFmpeg
* [Source: Hass](#source-hass)
* [Source: ISAPI](#source-isapi)
* [Source: Nest](#source-nest)
* [Source: Ring](#source-ring)
* [Source: Roborock](#source-roborock)
* [Source: WebRTC](#source-webrtc)
* [Source: WebTorrent](#source-webtorrent)
* [Incoming sources](#incoming-sources)
* [Stream to camera](#stream-to-camera)
* [Publish stream](#publish-stream)
* [Preload stream](#preload-stream)
* [Module: API](#module-api)
* [Module: RTSP](#module-rtsp)
* [Module: RTMP](#module-rtmp)
@@ -116,7 +120,7 @@ Ultimate camera streaming application with support RTSP, WebRTC, HomeKit, FFmpeg
Download binary for your OS from [latest release](https://github.com/AlexxIT/go2rtc/releases/):
- `go2rtc_win64.zip` - Windows 10+ 64-bit
- `go2rtc_win32.zip` - Windows 7+ 32-bit
- `go2rtc_win32.zip` - Windows 10+ 32-bit
- `go2rtc_win_arm64.zip` - Windows ARM 64-bit
- `go2rtc_linux_amd64` - Linux 64-bit
- `go2rtc_linux_i386` - Linux 32-bit
@@ -124,7 +128,7 @@ Download binary for your OS from [latest release](https://github.com/AlexxIT/go2
- `go2rtc_linux_arm` - Linux ARM 32-bit (ex. Raspberry 32-bit OS)
- `go2rtc_linux_armv6` - Linux ARMv6 (for old Raspberry 1 and Zero)
- `go2rtc_linux_mipsel` - Linux MIPS (ex. [Xiaomi Gateway 3](https://github.com/AlexxIT/XiaomiGateway3), [Wyze cameras](https://github.com/gtxaspec/wz_mini_hacks))
- `go2rtc_mac_amd64.zip` - macOS 10.13+ Intel 64-bit
- `go2rtc_mac_amd64.zip` - macOS 11+ 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
@@ -133,7 +137,7 @@ Don't forget to fix the rights `chmod +x go2rtc_xxx_xxx` on Linux and Mac.
### go2rtc: Docker
The Docker container [`alexxit/go2rtc`](https://hub.docker.com/r/alexxit/go2rtc) supports multiple architectures including `amd64`, `386`, `arm64`, and `arm`. This container offers the same functionality as the [Home Assistant Add-on](#go2rtc-home-assistant-add-on) but is designed to operate independently of Home Assistant. It comes preinstalled with [FFmpeg](#source-ffmpeg), [ngrok](#module-ngrok), and [Python](#source-echo).
The Docker container [`alexxit/go2rtc`](https://hub.docker.com/r/alexxit/go2rtc) supports multiple architectures including `amd64`, `386`, `arm64`, and `arm`. This container offers the same functionality as the [Home Assistant Add-on](#go2rtc-home-assistant-add-on) but is designed to operate independently of Home Assistant. It comes preinstalled with [FFmpeg](#source-ffmpeg) and [Python](#source-echo).
### go2rtc: Home Assistant Add-on
@@ -182,11 +186,11 @@ Available modules:
### Module: Streams
**go2rtc** support different stream source types. You can config one or multiple links of any type as stream source.
**go2rtc** supports different stream source types. You can config one or multiple links of any type as a stream source.
Available source types:
- [rtsp](#source-rtsp) - `RTSP` and `RTSPS` cameras with [two way audio](#two-way-audio) support
- [rtsp](#source-rtsp) - `RTSP` and `RTSPS` cameras with [two-way audio](#two-way-audio) support
- [rtmp](#source-rtmp) - `RTMP` streams
- [http](#source-http) - `HTTP-FLV`, `MPEG-TS`, `JPEG` (snapshots), `MJPEG` streams
- [onvif](#source-onvif) - get camera `RTSP` link and snapshot link using `ONVIF` protocol
@@ -199,20 +203,21 @@ Available source types:
- [bubble](#source-bubble) - streaming from ESeeCloud/dvr163 NVR
- [dvrip](#source-dvrip) - streaming from DVR-IP NVR
- [tapo](#source-tapo) - TP-Link Tapo cameras with [two way audio](#two-way-audio) support
- [ring](#source-ring) - Ring cameras with [two way audio](#two-way-audio) support
- [kasa](#source-tapo) - TP-Link Kasa cameras
- [gopro](#source-gopro) - GoPro cameras
- [ivideon](#source-ivideon) - public cameras from [Ivideon](https://tv.ivideon.com/) service
- [hass](#source-hass) - Home Assistant integration
- [isapi](#source-isapi) - two way audio for Hikvision (ISAPI) cameras
- [isapi](#source-isapi) - two-way audio for Hikvision (ISAPI) cameras
- [roborock](#source-roborock) - Roborock vacuums with cameras
- [webrtc](#source-webrtc) - WebRTC/WHEP sources
- [webtorrent](#source-webtorrent) - WebTorrent source from another go2rtc
Read more about [incoming sources](#incoming-sources)
#### Two way audio
#### Two-way audio
Supported for sources:
Supported sources:
- [RTSP cameras](#source-rtsp) with [ONVIF Profile T](https://www.onvif.org/specs/stream/ONVIF-Streaming-Spec.pdf) (back channel connection)
- [DVRIP](#source-dvrip) cameras
@@ -220,11 +225,12 @@ Supported for sources:
- [Hikvision ISAPI](#source-isapi) cameras
- [Roborock vacuums](#source-roborock) models with cameras
- [Exec](#source-exec) audio on server
- [Ring](#source-ring) cameras
- [Any Browser](#incoming-browser) as IP-camera
Two way audio can be used in browser with [WebRTC](#module-webrtc) technology. The browser will give access to the microphone only for HTTPS sites ([read more](https://stackoverflow.com/questions/52759992/how-to-access-camera-and-microphone-in-chrome-without-https)).
Two-way audio can be used in browser with [WebRTC](#module-webrtc) technology. The browser will give access to the microphone only for HTTPS sites ([read more](https://stackoverflow.com/questions/52759992/how-to-access-camera-and-microphone-in-chrome-without-https)).
go2rtc also support [play audio](#stream-to-camera) files and live streams on this cameras.
go2rtc also supports [play audio](#stream-to-camera) files and live streams on this cameras.
#### Source: RTSP
@@ -242,13 +248,13 @@ 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
- **Amcrest Doorbell** users may want to disable two-way audio, because with an active stream, you won't have a working call button. You need to add `#backchannel=0` to the end of your RTSP link in YAML config file
- **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
- **Reolink** users may want NOT to use RTSP protocol at all, some camera models have a very awful, unusable stream implementation
- **Ubiquiti UniFi** users may want to disable HTTPS verification. Use `rtspx://` prefix instead of `rtsps://`. And don't use `?enableSrtp` [suffix](https://github.com/AlexxIT/go2rtc/issues/81)
- **TP-Link Tapo** users may skip login and password, because go2rtc support login [without them](https://drmnsamoliu.github.io/video.html)
- If your camera has two RTSP links - you can add both of them as sources. This is useful when streams has different codecs, as example AAC audio with main stream and PCMU/PCMA audio with second stream
- If the stream from your camera is glitchy, try using [ffmpeg source](#source-ffmpeg). It will not add CPU load if you won't use transcoding
- If your camera has two RTSP links, you can add both as sources. This is useful when streams have different codecs, for example AAC audio with main stream and PCMU/PCMA audio with second stream
- If the stream from your camera is glitchy, try using [ffmpeg source](#source-ffmpeg). It will not add CPU load if you don't use transcoding
- If the stream from your camera is very glitchy, try to use transcoding with [ffmpeg source](#source-ffmpeg)
**Other options**
@@ -257,7 +263,7 @@ Format: `rtsp...#{param1}#{param2}#{param3}`
- Add custom timeout `#timeout=30` (in seconds)
- Ignore audio - `#media=video` or ignore video - `#media=audio`
- Ignore two way audio API `#backchannel=0` - important for some glitchy cameras
- Ignore two-way audio API `#backchannel=0` - important for some glitchy cameras
- Use WebSocket transport `#transport=ws...`
**RTSP over WebSocket**
@@ -272,7 +278,7 @@ streams:
#### Source: RTMP
You can get stream from RTMP server, for example [Nginx with nginx-rtmp-module](https://github.com/arut/nginx-rtmp-module).
You can get a stream from an RTMP server, for example [Nginx with nginx-rtmp-module](https://github.com/arut/nginx-rtmp-module).
```yaml
streams:
@@ -288,7 +294,7 @@ Support Content-Type:
- **HTTP-MJPEG** (`multipart/x`) - simple MJPEG stream over HTTP
- **MPEG-TS** (`video/mpeg`) - legacy [streaming format](https://en.wikipedia.org/wiki/MPEG_transport_stream)
Source also support HTTP and TCP streams with autodetection for different formats: **MJPEG**, **H.264/H.265 bitstream**, **MPEG-TS**.
Source also supports HTTP and TCP streams with autodetection for different formats: **MJPEG**, **H.264/H.265 bitstream**, **MPEG-TS**.
```yaml
streams:
@@ -308,7 +314,7 @@ streams:
custom_header: "https://mjpeg.sanford.io/count.mjpeg#header=Authorization: Bearer XXX"
```
**PS.** Dahua camera has bug: if you select MJPEG codec for RTSP second stream - snapshot won't work.
**PS.** Dahua camera has a bug: if you select MJPEG codec for RTSP second stream, snapshot won't work.
#### Source: ONVIF
@@ -316,7 +322,7 @@ streams:
The source is not very useful if you already know RTSP and snapshot links for your camera. But it can be useful if you don't.
**WebUI > Add** webpage support ONVIF autodiscovery. Your server must be on the same subnet as the camera. If you use docker, you must use "network host".
**WebUI > Add** webpage support ONVIF autodiscovery. Your server must be on the same subnet as the camera. If you use Docker, you must use "network host".
```yaml
streams:
@@ -327,7 +333,7 @@ streams:
#### Source: FFmpeg
You can get any stream or file or device via FFmpeg and push it to go2rtc. The app will automatically start FFmpeg with the proper arguments when someone starts watching the stream.
You can get any stream, file or device via FFmpeg and push it to go2rtc. The app will automatically start FFmpeg with the proper arguments when someone starts watching the stream.
- FFmpeg preistalled for **Docker** and **Hass Add-on** users
- **Hass Add-on** users can target files from [/media](https://www.home-assistant.io/more-info/local-media/setup-media/) folder
@@ -342,7 +348,7 @@ streams:
# [FILE] video will be transcoded to H264, audio will be skipped
file2: ffmpeg:/media/BigBuckBunny.mp4#video=h264
# [FILE] video will be copied, audio will be transcoded to pcmu
# [FILE] video will be copied, audio will be transcoded to PCMU
file3: ffmpeg:/media/BigBuckBunny.mp4#video=copy#audio=pcmu
# [HLS] video will be copied, audio will be skipped
@@ -355,9 +361,9 @@ streams:
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`.
All transcoding formats have [built-in templates](https://github.com/AlexxIT/go2rtc/blob/master/internal/ffmpeg/ffmpeg.go): `h264`, `h265`, `opus`, `pcmu`, `pcmu/16000`, `pcmu/48000`, `pcma`, `pcma/16000`, `pcma/48000`, `aac`, `aac/16000`.
But you can override them via YAML config. You can also add your own formats to config and use them with source params.
But you can override them via YAML config. You can also add your own formats to the config and use them with source params.
```yaml
ffmpeg:
@@ -385,12 +391,12 @@ Read more about [hardware acceleration](https://github.com/AlexxIT/go2rtc/wiki/H
#### Source: FFmpeg Device
You can get video from any USB-camera or Webcam as RTSP or WebRTC stream. This is part of FFmpeg integration.
You can get video from any USB camera or Webcam as RTSP or WebRTC stream. This is part of FFmpeg integration.
- check available devices in Web interface
- check available devices in web interface
- `video_size` and `framerate` must be supported by your camera!
- for Linux supported only video for now
- for macOS you can stream Facetime camera or whole Desktop!
- for macOS you can stream FaceTime camera or whole desktop!
- for macOS important to set right framerate
Format: `ffmpeg:device?{input-params}#{param1}#{param2}#{param3}`
@@ -408,7 +414,7 @@ streams:
Exec source can run any external application and expect data from it. Two transports are supported - **pipe** (*from [v1.5.0](https://github.com/AlexxIT/go2rtc/releases/tag/v1.5.0)*) and **RTSP**.
If you want to use **RTSP** transport - the command must contain the `{output}` argument in any place. On launch, it will be replaced by the local address of the RTSP server.
If you want to use **RTSP** transport, the command must contain the `{output}` argument in any place. On launch, it will be replaced by the local address of the RTSP server.
**pipe** reads data from app stdout in different formats: **MJPEG**, **H.264/H.265 bitstream**, **MPEG-TS**. Also pipe can write data to app stdin in two formats: **PCMA** and **PCM/48000**.
@@ -418,11 +424,11 @@ The source can be used with:
- [FFplay](https://ffmpeg.org/ffplay.html) - play audio on your server
- [GStreamer](https://gstreamer.freedesktop.org/)
- [Raspberry Pi Cameras](https://www.raspberrypi.com/documentation/computers/camera_software.html)
- any your own software
- any of your own software
Pipe commands support parameters (format: `exec:{command}#{param1}#{param2}`):
- `killsignal` - signal which will be send to stop the process (numeric form)
- `killsignal` - signal which will be sent to stop the process (numeric form)
- `killtimeout` - time in seconds for forced termination with sigkill
- `backchannel` - enable backchannel for two-way audio
@@ -439,7 +445,7 @@ streams:
#### Source: Echo
Some sources may have a dynamic link. And you will need to get it using a bash or python script. Your script should echo a link to the source. RTSP, FFmpeg or any of the [supported sources](#module-streams).
Some sources may have a dynamic link. And you will need to get it using a Bash or Python script. Your script should echo a link to the source. RTSP, FFmpeg or any of the [supported sources](#module-streams).
**Docker** and **Hass Add-on** users has preinstalled `python3`, `curl`, `jq`.
@@ -461,20 +467,20 @@ Like `echo` source, but uses the built-in [expr](https://github.com/antonmedv/ex
**Important:**
- You can use HomeKit Cameras **without Apple devices** (iPhone, iPad, etc.), it's just a yet another protocol
- HomeKit device can be paired with only one ecosystem. So, if you have paired it to an iPhone (Apple Home) - you can't pair it with Home Assistant or go2rtc. Or if you have paired it to go2rtc - you can't pair it with iPhone
- HomeKit device should be in same network with working [mDNS](https://en.wikipedia.org/wiki/Multicast_DNS) between device and go2rtc
- HomeKit device can be paired with only one ecosystem. So, if you have paired it to an iPhone (Apple Home), you can't pair it with Home Assistant or go2rtc. Or if you have paired it to go2rtc, you can't pair it with an iPhone
- HomeKit device should be on the same network with working [mDNS](https://en.wikipedia.org/wiki/Multicast_DNS) between the device and go2rtc
go2rtc support import paired HomeKit devices from [Home Assistant](#source-hass). So you can use HomeKit camera with Hass and go2rtc simultaneously. If you using Hass, I recommend pairing devices with it, it will give you more options.
go2rtc supports importing paired HomeKit devices from [Home Assistant](#source-hass). So you can use HomeKit camera with Hass and go2rtc simultaneously. If you are using Hass, I recommend pairing devices with it; it will give you more options.
You can pair device with go2rtc on the HomeKit page. If you can't see your devices - reload the page. Also try reboot your HomeKit device (power off). If you still can't see it - you have a problems with mDNS.
You can pair device with go2rtc on the HomeKit page. If you can't see your devices, reload the page. Also, try rebooting your HomeKit device (power off). If you still can't see it, you have a problem with mDNS.
If you see a device but it does not have a pair button - it is paired to some ecosystem (Apple Home, Home Assistant, HomeBridge etc). You need to delete device from that ecosystem, and it will be available for pairing. If you cannot unpair device, you will have to reset it.
If you see a device but it does not have a pairing button, it is paired to some ecosystem (Apple Home, Home Assistant, HomeBridge etc). You need to delete the device from that ecosystem, and it will be available for pairing. If you cannot unpair the device, you will have to reset it.
**Important:**
- HomeKit audio uses very non-standard **AAC-ELD** codec with very non-standard params and specification violation
- HomeKit audio uses very non-standard **AAC-ELD** codec with very non-standard params and specification violations
- Audio can't be played in `VLC` and probably any other player
- Audio should be transcoded for using with MSE, WebRTC, etc.
- Audio should be transcoded for use with MSE, WebRTC, etc.
Recommended settings for using HomeKit Camera with WebRTC, MSE, MP4, RTSP:
@@ -496,7 +502,7 @@ RTSP link with "normal" audio for any player: `rtsp://192.168.1.123:8554/aqara_g
Other names: [ESeeCloud](http://www.eseecloud.com/), [dvr163](http://help.dvr163.com/).
- you can skip `username`, `password`, `port`, `ch` and `stream` if they are default
- setup separate streams for different channels and streams
- set up separate streams for different channels and streams
```yaml
streams:
@@ -510,7 +516,7 @@ streams:
Other names: DVR-IP, NetSurveillance, Sofia protocol (NETsurveillance ActiveX plugin XMeye SDK).
- you can skip `username`, `password`, `port`, `channel` and `subtype` if they are default
- setup separate streams for different channels
- set up separate streams for different channels
- use `subtype=0` for Main stream, and `subtype=1` for Extra1 stream
- only the TCP protocol is supported
@@ -531,8 +537,8 @@ streams:
- stream quality is the same as [RTSP protocol](https://www.tapo.com/en/faq/34/)
- use the **cloud password**, this is not the RTSP password! you do not need to add a login!
- you can also use UPPERCASE MD5 hash from your cloud password with `admin` username
- some new camera firmwares requires SHA256 instead of MD5
- you can also use **UPPERCASE** MD5 hash from your cloud password with `admin` username
- some new camera firmwares require SHA256 instead of MD5
```yaml
streams:
@@ -542,6 +548,10 @@ streams:
camera2: tapo://admin:UPPERCASE-MD5@192.168.1.123
# admin username and UPPERCASE SHA256 cloud-password hash
camera3: tapo://admin:UPPERCASE-SHA256@192.168.1.123
# VGA stream (the so called substream, the lower resolution one)
camera4: tapo://cloud-password@192.168.1.123?subtype=1
# HD stream (default)
camera5: tapo://cloud-password@192.168.1.123?subtype=0
```
```bash
@@ -573,7 +583,7 @@ Support streaming from [GoPro](https://gopro.com/) cameras, connected via USB or
#### Source: Ivideon
Support public cameras from service [Ivideon](https://tv.ivideon.com/).
Support public cameras from the service [Ivideon](https://tv.ivideon.com/).
```yaml
streams:
@@ -591,7 +601,7 @@ Support import camera links from [Home Assistant](https://www.home-assistant.io/
```yaml
hass:
config: "/config" # skip this setting if you Hass Add-on user
config: "/config" # skip this setting if you Hass add-on user
streams:
generic_camera: hass:Camera1 # Settings > Integrations > Integration Name
@@ -600,9 +610,9 @@ streams:
**WebRTC Cameras** (*from [v1.6.0](https://github.com/AlexxIT/go2rtc/releases/tag/v1.6.0)*)
Any cameras in WebRTC format are supported. But at the moment Home Assistant only supports some [Nest](https://www.home-assistant.io/integrations/nest/) cameras in this fomat.
Any cameras in WebRTC format are supported. But at the moment Home Assistant only supports some [Nest](https://www.home-assistant.io/integrations/nest/) cameras in this format.
**Important.** The Nest API only allows you to get a link to a stream for 5 minutes. Do not use this with Frigate! If the stream expires, Frigate will consume all available ram on your machine within seconds. It's recommended to use [Nest source](#source-nest) - it supports extending the stream.
**Important.** The Nest API only allows you to get a link to a stream for 5 minutes. Do not use this with Frigate! If the stream expires, Frigate will consume all available RAM on your machine within seconds. It's recommended to use [Nest source](#source-nest) - it supports extending the stream.
```yaml
streams:
@@ -614,13 +624,13 @@ streams:
**RTSP Cameras**
By default, the Home Assistant API does not allow you to get dynamic RTSP link to a camera stream. So more cameras, like [Tuya](https://www.home-assistant.io/integrations/tuya/), and possibly others can also be imported by using [this method](https://github.com/felipecrs/hass-expose-camera-stream-source#importing-home-assistant-cameras-to-go2rtc-andor-frigate).
By default, the Home Assistant API does not allow you to get a dynamic RTSP link to a camera stream. So more cameras, like [Tuya](https://www.home-assistant.io/integrations/tuya/), and possibly others, can also be imported using [this method](https://github.com/felipecrs/hass-expose-camera-stream-source#importing-home-assistant-cameras-to-go2rtc-andor-frigate).
#### Source: ISAPI
*[New in v1.3.0](https://github.com/AlexxIT/go2rtc/releases/tag/v1.3.0)*
This source type support only backchannel audio for Hikvision ISAPI protocol. So it should be used as second source in addition to the RTSP protocol.
This source type supports only backchannel audio for the Hikvision ISAPI protocol. So it should be used as a second source in addition to the RTSP protocol.
```yaml
streams:
@@ -633,42 +643,52 @@ streams:
*[New in v1.6.0](https://github.com/AlexxIT/go2rtc/releases/tag/v1.6.0)*
Currently only WebRTC cameras are supported.
Currently, only WebRTC cameras are supported.
For simplicity, it is recommended to connect the Nest/WebRTC camera to the [Home Assistant](#source-hass). But if you can somehow get the below parameters - Nest/WebRTC source will work without Hass.
For simplicity, it is recommended to connect the Nest/WebRTC camera to the [Home Assistant](#source-hass). But if you can somehow get the below parameters, Nest/WebRTC source will work without Hass.
```yaml
streams:
nest-doorbell: nest:?client_id=***&client_secret=***&refresh_token=***&project_id=***&device_id=***
```
#### Source: Ring
This source type support Ring cameras with [two way audio](#two-way-audio) support. If you have a `refresh_token` and `device_id` - you can use it in `go2rtc.yaml` config file. Otherwise, you can use the go2rtc interface and add your ring account (WebUI > Add > Ring). Once added, it will list all your Ring cameras.
```yaml
streams:
ring: ring:?device_id=XXX&refresh_token=XXX
ring_snapshot: ring:?device_id=XXX&refresh_token=XXX&snapshot
```
#### Source: Roborock
*[New in v1.3.0](https://github.com/AlexxIT/go2rtc/releases/tag/v1.3.0)*
This source type support Roborock vacuums with cameras. Known working models:
This source type supports Roborock vacuums with cameras. Known working models:
- Roborock S6 MaxV - only video (the vacuum has no microphone)
- Roborock S7 MaxV - video and two way audio
- Roborock Qrevo MaxV - video and two way audio
- Roborock S7 MaxV - video and two-way audio
- Roborock Qrevo MaxV - video and two-way audio
Source support load Roborock credentials from Home Assistant [custom integration](https://github.com/humbertogontijo/homeassistant-roborock) or the [core integration](https://www.home-assistant.io/integrations/roborock). Otherwise, you need to log in to your Roborock account (MiHome account is not supported). Go to: go2rtc WebUI > Add webpage. Copy `roborock://...` source for your vacuum and paste it to `go2rtc.yaml` config.
Source supports loading Roborock credentials from Home Assistant [custom integration](https://github.com/humbertogontijo/homeassistant-roborock) or the [core integration](https://www.home-assistant.io/integrations/roborock). Otherwise, you need to log in to your Roborock account (MiHome account is not supported). Go to: go2rtc WebUI > Add webpage. Copy `roborock://...` source for your vacuum and paste it to `go2rtc.yaml` config.
If you have graphic pin for your vacuum - add it as numeric pin (lines: 123, 456, 789) to the end of the roborock-link.
If you have a graphic PIN for your vacuum, add it as a numeric PIN (lines: 123, 456, 789) to the end of the `roborock` link.
#### Source: WebRTC
*[New in v1.3.0](https://github.com/AlexxIT/go2rtc/releases/tag/v1.3.0)*
This source type support four connection formats.
This source type supports four connection formats.
**whep**
[WebRTC/WHEP](https://datatracker.ietf.org/doc/draft-murillo-whep/) - is replaced by [WebRTC/WISH](https://datatracker.ietf.org/doc/charter-ietf-wish/02/) standard for WebRTC video/audio viewers. But it may already be supported in some third-party software. It is supported in go2rtc.
[WebRTC/WHEP](https://datatracker.ietf.org/doc/draft-murillo-whep/) is replaced by [WebRTC/WISH](https://datatracker.ietf.org/doc/charter-ietf-wish/02/) standard for WebRTC video/audio viewers. But it may already be supported in some third-party software. It is supported in go2rtc.
**go2rtc**
This format is only supported in go2rtc. Unlike WHEP it supports asynchronous WebRTC connection and two way audio.
This format is only supported in go2rtc. Unlike WHEP, it supports asynchronous WebRTC connections and two-way audio.
**openipc** (*from [v1.7.0](https://github.com/AlexxIT/go2rtc/releases/tag/v1.7.0)*)
@@ -676,15 +696,15 @@ Support connection to [OpenIPC](https://openipc.org/) cameras.
**wyze** (*from [v1.6.1](https://github.com/AlexxIT/go2rtc/releases/tag/v1.6.1)*)
Supports connection to [Wyze](https://www.wyze.com/) cameras, using WebRTC protocol. You can use [docker-wyze-bridge](https://github.com/mrlt8/docker-wyze-bridge) project to get connection credentials.
Supports connection to [Wyze](https://www.wyze.com/) cameras, using WebRTC protocol. You can use the [docker-wyze-bridge](https://github.com/mrlt8/docker-wyze-bridge) project to get connection credentials.
**kinesis** (*from [v1.6.1](https://github.com/AlexxIT/go2rtc/releases/tag/v1.6.1)*)
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).
Supports [Amazon Kinesis Video Streams](https://aws.amazon.com/kinesis/video-streams/), using WebRTC protocol. You need to specify the 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.
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) and [Smart Video Doorbell](https://www.switchbot.jp/products/switchbot-smart-video-doorbell). `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:
@@ -693,10 +713,10 @@ streams:
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=[{...},{...}]
webrtc-switchbot: webrtc:wss://...amazonaws.com/?...#format=switchbot#resolution=hd#play_type=0#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.
**PS.** For `kinesis` sources, you can use [echo](#source-echo) to get connection params using `bash`, `python` or any other script language.
#### Source: WebTorrent
@@ -715,9 +735,9 @@ By default, go2rtc establishes a connection to the source when any client reques
- Go2rtc also can accepts incoming sources in [RTSP](#module-rtsp), [RTMP](#module-rtmp), [HTTP](#source-http) and **WebRTC/WHIP** formats
- Go2rtc won't stop such a source if it has no clients
- You can push data only to existing stream (create stream with empty source in config)
- You can push multiple incoming sources to same stream
- You can push data to non empty stream, so it will have additional codecs inside
- You can push data only to an existing stream (create a stream with empty source in config)
- You can push multiple incoming sources to the same stream
- You can push data to a non-empty stream, so it will have additional codecs inside
**Examples**
@@ -742,11 +762,11 @@ By default, go2rtc establishes a connection to the source when any client reques
*[New in v1.3.0](https://github.com/AlexxIT/go2rtc/releases/tag/v1.3.0)*
You can turn the browser of any PC or mobile into an IP-camera with support video and two way audio. Or even broadcast your PC screen:
You can turn the browser of any PC or mobile into an IP camera with support for video and two-way audio. Or even broadcast your PC screen:
1. Create empty stream in the `go2rtc.yaml`
2. Go to go2rtc WebUI
3. Open `links` page for you stream
3. Open `links` page for your stream
4. Select `camera+microphone` or `display+speaker` option
5. Open `webrtc` local page (your go2rtc **should work over HTTPS!**) or `share link` via [WebTorrent](#module-webtorrent) technology (work over HTTPS by default)
@@ -762,7 +782,7 @@ You can use **OBS Studio** or any other broadcast software with [WHIP](https://w
*[New in v1.3.0](https://github.com/AlexxIT/go2rtc/releases/tag/v1.3.0)*
go2rtc support play audio files (ex. music or [TTS](https://www.home-assistant.io/integrations/#text-to-speech)) and live streams (ex. radio) on cameras with [two way audio](#two-way-audio) support (RTSP/ONVIF cameras, TP-Link Tapo, Hikvision ISAPI, Roborock vacuums, any Browser).
go2rtc supports playing audio files (ex. music or [TTS](https://www.home-assistant.io/integrations/#text-to-speech)) and live streams (ex. radio) on cameras with [two-way audio](#two-way-audio) support (RTSP/ONVIF cameras, TP-Link Tapo, Hikvision ISAPI, Roborock vacuums, any Browser).
API example:
@@ -775,7 +795,7 @@ POST http://localhost:1984/api/streams?dst=camera1&src=ffmpeg:http://example.com
- you can check camera codecs on the go2rtc WebUI info page when the stream is active
- some cameras support only low quality `PCMA/8000` codec (ex. [Tapo](#source-tapo))
- it is recommended to choose higher quality formats if your camera supports them (ex. `PCMA/48000` for some Dahua cameras)
- if you play files over http-link, you need to add `#input=file` params for transcoding, so file will be transcoded and played in real time
- if you play files over `http` link, you need to add `#input=file` params for transcoding, so the file will be transcoded and played in real time
- if you play live streams, you should skip `#input` param, because it is already in real time
- you can stop active playback by calling the API with the empty `src` parameter
- you will see one active producer and one active consumer in go2rtc WebUI info page during streaming
@@ -787,10 +807,10 @@ POST http://localhost:1984/api/streams?dst=camera1&src=ffmpeg:http://example.com
You can publish any stream to streaming services (YouTube, Telegram, etc.) via RTMP/RTMPS. Important:
- Supported codecs: H264 for video and AAC for audio
- AAC audio is required for YouTube, videos without audio will not work
- AAC audio is required for YouTube; videos without audio will not work
- You don't need to enable [RTMP module](#module-rtmp) listening for this task
You can use API:
You can use the API:
```
POST http://localhost:1984/api/streams?src=camera1&dst=rtmps://...
@@ -818,11 +838,31 @@ streams:
- **Telegram Desktop App** > Any public or private channel or group (where you admin) > Live stream > Start with... > Start streaming.
- **YouTube** > Create > Go live > Stream latency: Ultra low-latency > Copy: Stream URL + Stream key.
### Preload stream
You can preload any stream on go2rtc start. This is useful for cameras that take a long time to start up.
```yaml
preload:
camera1: # default: video&audio = ANY
camera2: "video" # preload only video track
camera3: "video=h264&audio=opus" # preload H264 video and OPUS audio
streams:
camera1:
- rtsp://192.168.1.100/stream
camera2:
- rtsp://192.168.1.101/stream
camera3:
- rtsp://192.168.1.102/h265stream
- ffmpeg:camera3#video=h264#audio=opus#hardware
```
### Module: API
The HTTP API is the main part for interacting with the application. Default address: `http://localhost:1984/`.
**Important!** go2rtc passes requests from localhost and from unix socket without HTTP authorisation, even if you have it configured! It is your responsibility to set up secure external access to API. If not properly configured, an attacker can gain access to your cameras and even your server.
**Important!** go2rtc passes requests from localhost and from Unix sockets without HTTP authorisation, even if you have it configured! It is your responsibility to set up secure external access to the API. If not properly configured, an attacker can gain access to your cameras and even your server.
[API description](https://github.com/AlexxIT/go2rtc/tree/master/api).
@@ -830,7 +870,7 @@ The HTTP API is the main part for interacting with the application. Default addr
- you can disable HTTP API with `listen: ""` and use, for example, only RTSP client/server protocol
- you can enable HTTP API only on localhost with `listen: "127.0.0.1:1984"` setting
- you can change API `base_path` and host go2rtc on your main app webserver suburl
- you can change the API `base_path` and host go2rtc on your main app webserver suburl
- all files from `static_dir` hosted on root path: `/`
- you can use raw TLS cert/key content or path to files
@@ -839,7 +879,8 @@ api:
listen: ":1984" # default ":1984", HTTP API port ("" - disabled)
username: "admin" # default "", Basic auth for WebUI
password: "pass" # default "", Basic auth for WebUI
base_path: "/rtc" # default "", API prefix for serve on suburl (/api => /rtc/api)
local_auth: true # default false, Enable auth check for localhost requests
base_path: "/rtc" # default "", API prefix for serving on suburl (/api => /rtc/api)
static_dir: "www" # default "", folder for static files (custom web interface)
origin: "*" # default "", allow CORS requests (only * supported)
tls_listen: ":443" # default "", enable HTTPS server
@@ -863,7 +904,7 @@ api:
You can get any stream as RTSP-stream: `rtsp://192.168.1.123:8554/{stream_name}`
You can enable external password protection for your RTSP streams. Password protection always disabled for localhost calls (ex. FFmpeg or Hass on same server).
You can enable external password protection for your RTSP streams. Password protection is always disabled for localhost calls (ex. FFmpeg or Hass on the same server).
```yaml
rtsp:
@@ -888,7 +929,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 different problems with this format.
[Incoming stream](#incoming-sources) in RTMP format tested only with [OBS Studio](https://obsproject.com/) and a Dahua camera. Different FFmpeg versions have different problems with this format.
```yaml
rtmp:
@@ -897,12 +938,12 @@ rtmp:
### Module: WebRTC
In most cases [WebRTC](https://en.wikipedia.org/wiki/WebRTC) uses direct peer-to-peer connection from your browser to go2rtc and sends media data via UDP.
In most cases, [WebRTC](https://en.wikipedia.org/wiki/WebRTC) uses a direct peer-to-peer connection from your browser to go2rtc and sends media data via UDP.
It **can't pass** media data through your Nginx or Cloudflare or [Nabu Casa](https://www.nabucasa.com/) HTTP TCP connection!
It can automatically detects your external IP via public [STUN](https://en.wikipedia.org/wiki/STUN) server.
It can establish a external direct connection via [UDP hole punching](https://en.wikipedia.org/wiki/UDP_hole_punching) technology even if you not open your server to the World.
It can automatically detect your external IP via a public [STUN](https://en.wikipedia.org/wiki/STUN) server.
It can establish an external direct connection via [UDP hole punching](https://en.wikipedia.org/wiki/UDP_hole_punching) technology even if you do not open your server to the World.
But about 10-20% of users may need to configure additional settings for external access if **mobile phone** or **go2rtc server** behing [Symmetric NAT](https://tomchen.github.io/symmetric-nat-test/).
But about 10-20% of users may need to configure additional settings for external access if **mobile phone** or **go2rtc server** is behind [Symmetric NAT](https://tomchen.github.io/symmetric-nat-test/).
- by default, WebRTC uses both TCP and UDP on port 8555 for connections
- you can use this port for external access
@@ -915,39 +956,30 @@ webrtc:
**Static public IP**
- forward the port 8555 on your router (you can use same 8555 port or any other as external port)
- add your external IP-address and external port to YAML config
- forward the port 8555 on your router (you can use the same 8555 port or any other as external port)
- add your external IP address and external port to the YAML config
```yaml
webrtc:
candidates:
- 216.58.210.174:8555 # if you have static public IP-address
- 216.58.210.174:8555 # if you have a static public IP address
```
**Dynamic public IP**
- forward the port 8555 on your router (you can use same 8555 port or any other as the external port)
- forward the port 8555 on your router (you can use the same 8555 port or any other as the external port)
- add `stun` word and external port to YAML config
- go2rtc automatically detects your external address with STUN-server
- go2rtc automatically detects your external address with STUN server
```yaml
webrtc:
candidates:
- stun:8555 # if you have dynamic public IP-address
```
**Private IP**
- setup integration with [ngrok service](#module-ngrok)
```yaml
ngrok:
command: ...
- stun:8555 # if you have a dynamic public IP address
```
**Hard tech way 1. Own TCP-tunnel**
If you have personal [VPS](https://en.wikipedia.org/wiki/Virtual_private_server), you can create TCP-tunnel and setup in the same way as "Static public IP". But use your VPS IP-address in YAML config.
If you have a personal [VPS](https://en.wikipedia.org/wiki/Virtual_private_server), you can create a TCP tunnel and setup in the same way as "Static public IP". But use your VPS IP address in the YAML config.
**Hard tech way 2. Using TURN-server**
@@ -973,7 +1005,7 @@ HomeKit module can work in two modes:
**Important**
- HomeKit cameras supports only H264 video and OPUS audio
- HomeKit cameras support only H264 video and OPUS audio
**Minimal config**
@@ -1020,17 +1052,17 @@ homekit:
*[New in v1.3.0](https://github.com/AlexxIT/go2rtc/releases/tag/v1.3.0)*
This module support:
This module supports:
- Share any local stream via [WebTorrent](https://webtorrent.io/) technology
- Get any [incoming stream](#incoming-browser) from PC or mobile via [WebTorrent](https://webtorrent.io/) technology
- Get any remote [go2rtc source](#source-webtorrent) via [WebTorrent](https://webtorrent.io/) technology
Securely and free. You do not need to open a public access to the go2rtc server. But in some cases (Symmetric NAT) you may need to set up external access to [WebRTC module](#module-webrtc).
Securely and freely. You do not need to open a public access to the go2rtc server. But in some cases (Symmetric NAT), you may need to set up external access to [WebRTC module](#module-webrtc).
To generate sharing link or incoming link - goto go2rtc WebUI (stream links page). This link is **temporary** and will stop working after go2rtc is restarted!
To generate a sharing link or incoming link, go to the go2rtc WebUI (stream links page). This link is **temporary** and will stop working after go2rtc is restarted!
You can create permanent external links in go2rtc config:
You can create permanent external links in the go2rtc config:
```yaml
webtorrent:
@@ -1042,69 +1074,15 @@ webtorrent:
Link example: https://alexxit.github.io/go2rtc/#share=02SNtgjKXY&pwd=wznEQqznxW&media=video+audio
TODO: article how it works...
### Module: ngrok
With ngrok integration you can get external access to your streams in situations when you have Internet with private IP-address.
- ngrok is pre-installed for **Docker** and **Hass Add-on** users
- you may need external access for two different things:
- WebRTC stream, so you need tunnel WebRTC TCP port (ex. 8555)
- go2rtc web interface, so you need tunnel API HTTP port (ex. 1984)
- ngrok support authorization for your web interface
- ngrok automatically adds HTTPS to your web interface
The ngrok free subscription has the following limitations:
- You can reserve a free domain for serving the web interface, but the TCP address you get will always be random and change with each restart of the ngrok agent (not a problem for webrtc stream)
- You can forward multiple ports from a single agent, but you can only run one ngrok agent on the free plan
go2rtc will automatically get your external TCP address (if you enable it in ngrok config) and use it with WebRTC connection (if you enable it in webrtc config).
You need to manually download the [ngrok agent app](https://ngrok.com/download) for your OS and register with the [ngrok service](https://ngrok.com/signup).
**Tunnel for only WebRTC Stream**
You need to add your [ngrok authtoken](https://dashboard.ngrok.com/get-started/your-authtoken) and WebRTC TCP port to YAML:
```yaml
ngrok:
command: ngrok tcp 8555 --authtoken eW91IHNoYWxsIG5vdCBwYXNzCnlvdSBzaGFsbCBub3QgcGFzcw
```
**Tunnel for WebRTC and Web interface**
You need to create `ngrok.yaml` config file and add it to go2rtc config:
```yaml
ngrok:
command: ngrok start --all --config ngrok.yaml
```
ngrok config example:
```yaml
version: "2"
authtoken: eW91IHNoYWxsIG5vdCBwYXNzCnlvdSBzaGFsbCBub3QgcGFzcw
tunnels:
api:
addr: 1984 # use the same port as in go2rtc config
proto: http
basic_auth:
- admin:password # you can set login/pass for your web interface
webrtc:
addr: 8555 # use the same port as in go2rtc config
proto: tcp
```
See the [ngrok agent documentation](https://ngrok.com/docs/agent/config/) for more details on the ngrok configuration file.
With [ngrok](https://ngrok.com/) integration, you can get external access to your streams in situations when you have Internet with a private IP address ([read more](https://github.com/AlexxIT/go2rtc/blob/master/internal/ngrok/README.md)).
### Module: Hass
The best and easiest way to use go2rtc inside the Home Assistant is to install the custom integration [WebRTC Camera](#go2rtc-home-assistant-integration) and custom lovelace card.
The best and easiest way to use go2rtc inside Home Assistant is to install the custom integration [WebRTC Camera](#go2rtc-home-assistant-integration) and custom Lovelace card.
But go2rtc is also compatible and can be used with [RTSPtoWebRTC](https://www.home-assistant.io/integrations/rtsp_to_webrtc/) built-in integration.
But go2rtc is also compatible and can be used with the [RTSPtoWebRTC](https://www.home-assistant.io/integrations/rtsp_to_webrtc/) built-in integration.
You have several options on how to add a camera to Home Assistant:
@@ -1122,10 +1100,10 @@ You have several options on how to watch the stream from the cameras in Home Ass
- Install any [go2rtc](#fast-start)
- Hass > Settings > Integrations > Add Integration > [RTSPtoWebRTC](https://my.home-assistant.io/redirect/config_flow_start/?domain=rtsp_to_webrtc) > `http://127.0.0.1:1984/`
- RTSPtoWebRTC > Configure > STUN server: `stun.l.google.com:19302`
- Use Picture Entity or Picture Glance lovelace card
- Use Picture Entity or Picture Glance Lovelace card
3. `Camera Entity` or `Camera URL` => [WebRTC Camera](https://github.com/AlexxIT/WebRTC) => Technology: `WebRTC/MSE/MP4/MJPEG`, codecs: `H264/H265/AAC/PCMU/PCMA/OPUS`, best latency, best compatibility.
- Install and add [WebRTC Camera](https://github.com/AlexxIT/WebRTC) custom integration
- Use WebRTC Camera custom lovelace card
- Use WebRTC Camera custom Lovelace card
You can add camera `entity_id` to [go2rtc config](#configuration) if you need transcoding:
@@ -1134,7 +1112,7 @@ streams:
"camera.hall": ffmpeg:{input}#video=copy#audio=opus
```
**PS.** Default Home Assistant lovelace cards don't support 2-way audio. You can use 2-way audio from [Add-on Web UI](https://my.home-assistant.io/redirect/supervisor_addon/?addon=a889bffc_go2rtc&repository_url=https%3A%2F%2Fgithub.com%2FAlexxIT%2Fhassio-addons). But you need use HTTPS to access the microphone. This is a browser restriction and cannot be avoided.
**PS.** Default Home Assistant lovelace cards don't support two-way audio. You can use 2-way audio from [Add-on Web UI](https://my.home-assistant.io/redirect/supervisor_addon/?addon=a889bffc_go2rtc&repository_url=https%3A%2F%2Fgithub.com%2FAlexxIT%2Fhassio-addons), but you need to use HTTPS to access the microphone. This is a browser restriction and cannot be avoided.
**PS.** There is also another nice card with go2rtc support - [Frigate Lovelace Card](https://github.com/dermotduffy/frigate-hass-card).
@@ -1144,7 +1122,7 @@ Provides several features:
1. MSE stream (fMP4 over WebSocket)
2. Camera snapshots in MP4 format (single frame), can be sent to [Telegram](https://github.com/AlexxIT/go2rtc/wiki/Snapshot-to-Telegram)
3. HTTP progressive streaming (MP4 file stream) - bad format for streaming because of high start delay. This format doesn't work in all Safari browsers, but go2rtc will automatically redirect it to HLS/fMP4 it this case.
3. HTTP progressive streaming (MP4 file stream) - bad format for streaming because of high start delay. This format doesn't work in all Safari browsers, but go2rtc will automatically redirect it to HLS/fMP4 in this case.
API examples:
@@ -1178,13 +1156,13 @@ Read more about [codecs filters](#codecs-filters).
### Module: MJPEG
**Important.** For stream as MJPEG format, your source MUST contain the MJPEG codec. If your stream has a MJPEG codec - you can receive **MJPEG stream** or **JPEG snapshots** via API.
**Important.** For stream in MJPEG format, your source MUST contain the MJPEG codec. If your stream has an MJPEG codec, you can receive **MJPEG stream** or **JPEG snapshots** via API.
You can receive an MJPEG stream in several ways:
- some cameras support MJPEG codec inside [RTSP stream](#source-rtsp) (ex. second stream for Dahua cameras)
- some cameras has HTTP link with [MJPEG stream](#source-http)
- some cameras has HTTP link with snapshots - go2rtc can convert them to [MJPEG stream](#source-http)
- some cameras have an HTTP link with [MJPEG stream](#source-http)
- some cameras have an HTTP link with snapshots - go2rtc can convert them to [MJPEG stream](#source-http)
- you can convert H264/H265 stream from your camera via [FFmpeg integraion](#source-ffmpeg)
With this example, your stream will have both H264 and MJPEG codecs:
@@ -1217,7 +1195,6 @@ log:
level: info # default level
api: trace
exec: debug
ngrok: info
rtsp: warn
streams: error
webrtc: fatal
@@ -1225,7 +1202,28 @@ log:
## Security
By default `go2rtc` starts the Web interface on port `1984` and RTSP on port `8554`, as well as use port `8555` for WebRTC connections. The three ports are accessible from your local network. So anyone on your local network can watch video from your cameras without authorization. The same rule applies to the Home Assistant Add-on.
> [!IMPORTANT]
> If an attacker gains access to the API, you are in danger. Through the API, an attacker can use insecure sources such as echo and exec. And get full access to your server.
For maximum (paranoid) security, go2rtc has special settings:
```yaml
app:
# use only allowed modules
modules: [api, rtsp, webrtc, exec, ffmpeg, mjpeg]
api:
# use only allowed API paths
allow_paths: [/api, /api/streams, /api/webrtc, /api/frame.jpeg]
# enable auth for localhost (used together with username and password)
local_auth: true
exec:
# use only allowed exec paths
allow_paths: [ffmpeg]
```
By default, `go2rtc` starts the Web interface on port `1984` and RTSP on port `8554`, as well as uses port `8555` for WebRTC connections. The three ports are accessible from your local network. So anyone on your local network can watch video from your cameras without authorization. The same rule applies to the Home Assistant Add-on.
This is not a problem if you trust your local network as much as I do. But you can change this behaviour with a `go2rtc.yaml` config:
@@ -1241,13 +1239,13 @@ webrtc:
```
- local access to RTSP is not a problem for [FFmpeg](#source-ffmpeg) integration, because it runs locally on your server
- local access to API is not a problem for [Home Assistant Add-on](#go2rtc-home-assistant-add-on), because Hass runs locally on same server and Add-on Web UI protected with Hass authorization ([Ingress feature](https://www.home-assistant.io/blog/2019/04/15/hassio-ingress/))
- external access to WebRTC TCP port is not a problem, because it used only for transmit encrypted media data
- anyway you need to open this port to your local network and to the Internet in order for WebRTC to work
- local access to API is not a problem for the [Home Assistant add-on](#go2rtc-home-assistant-add-on), because Hass runs locally on the same server, and the add-on web UI is protected with Hass authorization ([Ingress feature](https://www.home-assistant.io/blog/2019/04/15/hassio-ingress/))
- external access to WebRTC TCP port is not a problem, because it is used only for transmitting encrypted media data
- anyway you need to open this port to your local network and to the Internet for WebRTC to work
If you need Web interface protection without Home Assistant Add-on - you need to use reverse proxy, like [Nginx](https://nginx.org/), [Caddy](https://caddyserver.com/), [ngrok](https://ngrok.com/), etc.
If you need web interface protection without the Home Assistant add-on, you need to use a reverse proxy, like [Nginx](https://nginx.org/), [Caddy](https://caddyserver.com/), etc.
PS. Additionally WebRTC will try to use the 8555 UDP port for transmit encrypted media. It works without problems on the local network. And sometimes also works for external access, even if you haven't opened this port on your router ([read more](https://en.wikipedia.org/wiki/UDP_hole_punching)). But for stable external WebRTC access, you need to open the 8555 port on your router for both TCP and UDP.
PS. Additionally, WebRTC will try to use the 8555 UDP port to transmit encrypted media. It works without problems on the local network, and sometimes also works for external access, even if you haven't opened this port on your router ([read more](https://en.wikipedia.org/wiki/UDP_hole_punching)). But for stable external WebRTC access, you need to open the 8555 port on your router for both TCP and UDP.
## Codecs filters
@@ -1270,11 +1268,11 @@ Some examples:
- `http://192.168.1.123:1984/api/stream.m3u8?src=camera1&mp4` - HLS stream with MP4 compatible codecs (HLS/fMP4)
- `http://192.168.1.123:1984/api/stream.m3u8?src=camera1&mp4=flac` - HLS stream with PCMA/PCMU/PCM audio support (HLS/fMP4), won't work on old devices
- `http://192.168.1.123:1984/api/stream.mp4?src=camera1&mp4=flac` - MP4 file with PCMA/PCMU/PCM audio support, won't work on old devices (ex. iOS 12)
- `http://192.168.1.123:1984/api/stream.mp4?src=camera1&mp4=all` - MP4 file with non standard audio codecs, won't work on some players
- `http://192.168.1.123:1984/api/stream.mp4?src=camera1&mp4=all` - MP4 file with non-standard audio codecs, won't work on some players
## Codecs madness
`AVC/H.264` video can be played almost anywhere. But `HEVC/H.265` has a lot of limitations in supporting with different devices and browsers. It's all about patents and money, you can't do anything about it.
`AVC/H.264` video can be played almost anywhere. But `HEVC/H.265` has many limitations in supporting different devices and browsers. It's all about patents and money; you can't do anything about it.
| Device | WebRTC | MSE | HTTP* | HLS |
|--------------------------------------------------------------------------|-----------------------------------------|-----------------------------------------|----------------------------------------------|-----------------------------|
@@ -1287,7 +1285,7 @@ Some examples:
[1]: https://apps.apple.com/app/home-assistant/id1099568401
`HTTP*` - HTTP Progressive Streaming, not related with [Progressive download](https://en.wikipedia.org/wiki/Progressive_download), because the file has no size and no end
`HTTP*` - HTTP Progressive Streaming, not related to [progressive download](https://en.wikipedia.org/wiki/Progressive_download), because the file has no size and no end
- Chrome H265: [read this](https://chromestatus.com/feature/5186511939567616) and [read this](https://github.com/StaZhu/enable-chromium-hevc-hardware-decoding)
- Edge H265: [read this](https://www.reddit.com/r/MicrosoftEdge/comments/v9iw8k/enable_hevc_support_in_edge/)
@@ -1298,7 +1296,7 @@ Some examples:
- Go2rtc support [automatic repack](#built-in-transcoding) `PCMA/PCMU/PCM` codecs to `FLAC` for MSE/MP4/HLS so they will work almost anywhere
- **WebRTC** audio codecs: `PCMU/8000`, `PCMA/8000`, `OPUS/48000/2`
- `OPUS` and `MP3` inside **MP4** is part of the standard, but some players do not support them anyway (especially Apple)
- `OPUS` and `MP3` inside **MP4** are part of the standard, but some players do not support them anyway (especially Apple)
**Apple devices**
@@ -1320,7 +1318,7 @@ Some examples:
There are no plans to embed complex transcoding algorithms inside go2rtc. [FFmpeg source](#source-ffmpeg) does a great job with this. Including [hardware acceleration](https://github.com/AlexxIT/go2rtc/wiki/Hardware-acceleration) support.
But go2rtc has some simple algorithms. They are turned on automatically, you do not need to set them up additionally.
But go2rtc has some simple algorithms. They are turned on automatically; you do not need to set them up additionally.
**PCM for MSE/MP4/HLS**
@@ -1332,7 +1330,7 @@ PCMA/PCMU => PCM => FLAC => MSE/MP4/HLS
**Resample PCMA/PCMU for WebRTC**
By default WebRTC support only `PCMA/8000` and `PCMU/8000`. But go2rtc can automatically resample PCMA and PCMU codec with with a different sample rate. Also go2rtc can transcode `PCM` codec to `PCMA/8000`, so WebRTC can play it:
By default WebRTC supports only `PCMA/8000` and `PCMU/8000`. But go2rtc can automatically resample PCMA and PCMU codecs with a different sample rate. Also, go2rtc can transcode `PCM` codec to `PCMA/8000`, so WebRTC can play it:
```
PCM/xxx => PCMA/8000 => WebRTC
@@ -1342,24 +1340,24 @@ PCMU/xxx => PCMU/8000 => WebRTC
**Important**
- FLAC codec not supported in a RTSP stream. If you using Frigate or Hass for recording MP4 files with PCMA/PCMU/PCM audio - you should setup transcoding to AAC codec.
- PCMA and PCMU are VERY low quality codecs. Them support only 256! different sounds. Use them only when you have no other options.
- FLAC codec not supported in an RTSP stream. If you are using Frigate or Hass for recording MP4 files with PCMA/PCMU/PCM audio, you should set up transcoding to the AAC codec.
- PCMA and PCMU are VERY low-quality codecs. They support only 256! different sounds. Use them only when you have no other options.
## Codecs negotiation
For example, you want to watch RTSP-stream from [Dahua IPC-K42](https://www.dahuasecurity.com/fr/products/All-Products/Network-Cameras/Wireless-Series/Wi-Fi-Series/4MP/IPC-K42) camera in your Chrome browser.
- this camera support 2-way audio standard **ONVIF Profile T**
- this camera support codecs **H264, H265** for send video, and you select `H264` in camera settings
- this camera support codecs **AAC, PCMU, PCMA** for send audio (from mic), and you select `AAC/16000` in camera settings
- this camera support codecs **AAC, PCMU, PCMA** for receive audio (to speaker), you don't need to select them
- your browser support codecs **H264, VP8, VP9, AV1** for receive video, you don't need to select them
- your browser support codecs **OPUS, PCMU, PCMA** for send and receive audio, you don't need to select them
- you can't get camera audio directly, because its audio codecs doesn't match with your browser codecs
- so you decide to use transcoding via FFmpeg and add this setting to config YAML file
- this camera supports two-way audio standard **ONVIF Profile T**
- this camera supports codecs **H264, H265** for send video, and you select `H264` in camera settings
- this camera supports codecs **AAC, PCMU, PCMA** for sending audio (from mic), and you select `AAC/16000` in camera settings
- this camera supports codecs **AAC, PCMU, PCMA** for receiving audio (to speaker), you don't need to select them
- your browser supports codecs **H264, VP8, VP9, AV1** for receiving video, you don't need to select them
- your browser supports codecs **OPUS, PCMU, PCMA** for sending and receiving audio, you don't need to select them
- you can't get camera audio directly, because its audio codecs don't match with your browser codecs
- so you decide to use transcoding via FFmpeg and add this setting to the config YAML file
- you have chosen `OPUS/48000/2` codec, because it is higher quality than the `PCMU/8000` or `PCMA/8000`
Now you have stream with two sources - **RTSP and FFmpeg**:
Now you have a stream with two sources - **RTSP and FFmpeg**:
```yaml
streams:
@@ -1368,22 +1366,23 @@ streams:
- ffmpeg:rtsp://admin:password@192.168.1.123/cam/realmonitor?channel=1&subtype=0#audio=opus
```
**go2rtc** automatically match codecs for you browser and all your stream sources. This called **multi-source 2-way codecs negotiation**. And this is one of the main features of this app.
**go2rtc** automatically matches codecs for your browser and all your stream sources. This is called **multi-source two-way codec negotiation**. And this is one of the main features of this app.
![](assets/codecs.svg)
**PS.** You can select `PCMU` or `PCMA` codec in camera setting and don't use transcoding at all. Or you can select `AAC` codec for main stream and `PCMU` codec for second stream and add both RTSP to YAML config, this also will work fine.
**PS.** You can select `PCMU` or `PCMA` codec in camera settings and not use transcoding at all. Or you can select `AAC` codec for main stream and `PCMU` codec for second stream and add both RTSP to YAML config, this also will work fine.
## Projects using go2rtc
- [Frigate 12+](https://frigate.video/) - open source NVR built around real-time AI object detection
- [Frigate](https://frigate.video/) 0.12+ - open-source NVR built around real-time AI object detection
- [Frigate Lovelace Card](https://github.com/dermotduffy/frigate-hass-card) - custom card for Home Assistant
- [OpenIPC](https://github.com/OpenIPC/firmware/tree/master/general/package/go2rtc) - Alternative IP Camera firmware from an open community
- [wz_mini_hacks](https://github.com/gtxaspec/wz_mini_hacks) - Custom firmware for Wyze cameras
- [EufyP2PStream](https://github.com/oischinger/eufyp2pstream) - A small project that provides a Video/Audio Stream from Eufy cameras that don't directly support RTSP
- [ioBroker.euSec](https://github.com/bropat/ioBroker.eusec) - [ioBroker](https://www.iobroker.net/) adapter for control Eufy security devices
- [MMM-go2rtc](https://github.com/Anonym-tsk/MMM-go2rtc) - MagicMirror² Module
- [ring-mqtt](https://github.com/tsightler/ring-mqtt) - Ring devices to MQTT Bridge
- [OpenIPC](https://github.com/OpenIPC/firmware/tree/master/general/package/go2rtc) - alternative IP camera firmware from an open community
- [wz_mini_hacks](https://github.com/gtxaspec/wz_mini_hacks) - custom firmware for Wyze cameras
- [EufyP2PStream](https://github.com/oischinger/eufyp2pstream) - a small project that provides a video/audio stream from Eufy cameras that don't directly support RTSP
- [ioBroker.euSec](https://github.com/bropat/ioBroker.eusec) - [ioBroker](https://www.iobroker.net/) adapter for controlling Eufy security devices
- [MMM-go2rtc](https://github.com/Anonym-tsk/MMM-go2rtc) - MagicMirror² module
- [ring-mqtt](https://github.com/tsightler/ring-mqtt) - Ring-to-MQTT bridge
- [lightNVR](https://github.com/opensensor/lightNVR)
**Distributions**
@@ -1391,20 +1390,20 @@ streams:
- [Arch User Repository](https://linux-packages.com/aur/package/go2rtc)
- [Gentoo](https://github.com/inode64/inode64-overlay/tree/main/media-video/go2rtc)
- [NixOS](https://search.nixos.org/packages?query=go2rtc)
- [Proxmox Helper Scripts](https://tteck.github.io/Proxmox/)
- [Proxmox Helper Scripts](https://github.com/community-scripts/ProxmoxVE/)
- [QNAP](https://www.myqnap.org/product/go2rtc/)
- [Synology NAS](https://synocommunity.com/package/go2rtc)
- [Unraid](https://unraid.net/community/apps?q=go2rtc)
## Cameras experience
## Camera experience
- [Dahua](https://www.dahuasecurity.com/) - reference implementation streaming protocols, a lot of settings, high stream quality, multiple streaming clients
- [EZVIZ](https://www.ezviz.com/) - awful RTSP protocol realisation, many bugs in SDP
- [EZVIZ](https://www.ezviz.com/) - awful RTSP protocol implementation, many bugs in SDP
- [Hikvision](https://www.hikvision.com/) - a lot of proprietary streaming technologies
- [Reolink](https://reolink.com/) - some models has awful unusable RTSP realisation and not best RTMP alternative (I recommend that you contact Reolink support for new firmware), few settings
- [Sonoff](https://sonoff.tech/) - very low stream quality, no settings, not best protocol implementation
- [Reolink](https://reolink.com/) - some models have an awful, unusable RTSP implementation and not the best RTMP alternative (I recommend that you contact Reolink support for new firmware), few settings
- [Sonoff](https://sonoff.tech/) - very low stream quality, no settings, not the best protocol implementation
- [TP-Link](https://www.tp-link.com/) - few streaming clients, packet loss?
- Chinese cheap noname cameras, Wyze Cams, Xiaomi cameras with hacks (usual has `/live/ch00_1` in RTSP URL) - awful but usable RTSP protocol realisation, low stream quality, few settings, packet loss?
- Chinese cheap noname cameras, Wyze Cams, Xiaomi cameras with hacks (usually have `/live/ch00_1` in RTSP URL) - awful but usable RTSP protocol implementation, low stream quality, few settings, packet loss?
## TIPS
@@ -1421,22 +1420,22 @@ streams:
**Q. What's the difference between go2rtc, WebRTC Camera and RTSPtoWebRTC?**
**go2rtc** is a new version of the server-side [WebRTC Camera](https://github.com/AlexxIT/WebRTC) integration, completely rewritten from scratch, with a number of fixes and a huge number of new features. It is compatible with native Home Assistant [RTSPtoWebRTC](https://www.home-assistant.io/integrations/rtsp_to_webrtc/) integration. So you [can use](#module-hass) default lovelace Picture Entity or Picture Glance.
**go2rtc** is a new version of the server-side [WebRTC Camera](https://github.com/AlexxIT/WebRTC) integration, completely rewritten from scratch, with a number of fixes and a huge number of new features. It is compatible with native Home Assistant [RTSPtoWebRTC](https://www.home-assistant.io/integrations/rtsp_to_webrtc/) integration. So you [can use](#module-hass) default Lovelace Picture Entity or Picture Glance.
**Q. Should I use go2rtc addon or WebRTC Camera integration?**
**Q. Should I use the go2rtc add-on or WebRTC Camera integration?**
**go2rtc** is more than just viewing your stream online with WebRTC/MSE/HLS/etc. You can use it all the time for your various tasks. But every time the Hass is rebooted - all integrations are also rebooted. So your streams may be interrupted if you use them in additional tasks.
**go2rtc** is more than just viewing your stream online with WebRTC/MSE/HLS/etc. You can use it all the time for your various tasks. But every time Hass is rebooted, all integrations are also rebooted. So your streams may be interrupted if you use them in additional tasks.
Basic users can use **WebRTC Camera** integration. Advanced users can use go2rtc addon or Frigate 12+ addon.
Basic users can use the **WebRTC Camera** integration. Advanced users can use the go2rtc add-on or the Frigate 0.12+ add-on.
**Q. Which RTSP link should I use inside Hass?**
You can use direct link to your cameras there (as you always do). **go2rtc** support zero-config feature. You may leave `streams` config section empty. And your streams will be created on the fly on first start from Hass. And your cameras will have multiple connections. Some from Hass directly and one from **go2rtc**.
You can use a direct link to your cameras there (as you always do). **go2rtc** supports zero-config feature. You may leave `streams` config section empty. And your streams will be created on the fly on first start from Hass. And your cameras will have multiple connections. Some from Hass directly and one from **go2rtc**.
Also you can specify your streams in **go2rtc** [config file](#configuration) and use RTSP links to this addon. With additional features: multi-source [codecs negotiation](#codecs-negotiation) or FFmpeg [transcoding](#source-ffmpeg) for unsupported codecs. Or use them as source for Frigate. And your cameras will have one connection from **go2rtc**. And **go2rtc** will have multiple connection - some from Hass via RTSP protocol, some from your browser via WebRTC/MSE/HLS protocols.
Also, you can specify your streams in **go2rtc** [config file](#configuration) and use RTSP links to this add-on with additional features: multi-source [codecs negotiation](#codecs-negotiation) or FFmpeg [transcoding](#source-ffmpeg) for unsupported codecs. Or use them as a source for Frigate. And your cameras will have one connection from **go2rtc**. And **go2rtc** will have multiple connections - some from Hass via RTSP protocol, some from your browser via WebRTC/MSE/HLS protocols.
Use any config what you like.
Use any config that you like.
**Q. What about lovelace card with support 2-way audio?**
**Q. What about Lovelace card with support for two-way audio?**
At this moment I am focused on improving stability and adding new features to **go2rtc**. Maybe someone could write such a card themselves. It's not difficult, I have [some sketches](https://github.com/AlexxIT/go2rtc/blob/master/www/webrtc.html).
At this moment, I am focused on improving stability and adding new features to **go2rtc**. Maybe someone could write such a card themselves. It's not difficult, I have [some sketches](https://github.com/AlexxIT/go2rtc/blob/master/www/webrtc.html).
+48
View File
@@ -237,6 +237,54 @@ paths:
/api/preload:
put:
summary: Preload new stream
tags: [ Streams list ]
parameters:
- name: src
in: query
description: Stream source (name)
required: true
schema: { type: string }
example: "camera1"
- name: video
in: query
description: Video codecs filter
required: false
schema: { type: string }
example: all,h264,h265,...
- name: audio
in: query
description: Audio codecs filter
required: false
schema: { type: string }
example: all,aac,opus,...
- name: microphone
in: query
description: Microphone codecs filter
required: false
schema: { type: string }
example: all,aac,opus,...
responses:
default:
description: Default response
delete:
summary: Delete preloaded stream
tags: [ Streams list ]
parameters:
- name: src
in: query
description: Stream source (name)
required: true
schema: { type: string }
example: "camera1"
responses:
default:
description: Default response
/api/streams?src={src}:
get:
summary: Get stream info in JSON format
+4 -14
View File
@@ -1,20 +1,11 @@
# syntax=docker/dockerfile:labs
# 0. Prepare images
ARG PYTHON_VERSION="3.11"
ARG GO_VERSION="1.24"
ARG PYTHON_VERSION="3.13"
ARG GO_VERSION="1.25"
# 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,7 +26,7 @@ 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),
@@ -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
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
```
@@ -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.24-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.25-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
+50
View File
@@ -0,0 +1,50 @@
# syntax=docker/dockerfile:labs
# 0. Prepare images
ARG PYTHON_VERSION="3.13-slim-bookworm"
ARG GO_VERSION="1.25-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"]
+1 -1
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",
+26 -27
View File
@@ -1,50 +1,49 @@
module github.com/AlexxIT/go2rtc
go 1.20
go 1.24.0
require (
github.com/asticode/go-astits v1.13.0
github.com/expr-lang/expr v1.16.9
github.com/expr-lang/expr v1.17.6
github.com/google/uuid v1.6.0
github.com/gorilla/websocket v1.5.3
github.com/mattn/go-isatty v0.0.20
github.com/miekg/dns v1.1.63
github.com/pion/ice/v2 v2.3.37
github.com/pion/interceptor v0.1.37
github.com/pion/rtcp v1.2.15
github.com/pion/rtp v1.8.11
github.com/pion/sdp/v3 v3.0.10
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/miekg/dns v1.1.68
github.com/pion/ice/v4 v4.0.10
github.com/pion/interceptor v0.1.41
github.com/pion/rtcp v1.2.16
github.com/pion/rtp v1.8.24
github.com/pion/sdp/v3 v3.0.16
github.com/pion/srtp/v3 v3.0.8
github.com/pion/stun/v3 v3.0.0
github.com/pion/webrtc/v4 v4.1.6
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/stretchr/testify v1.11.1
github.com/tadglines/go-pkgs v0.0.0-20210623144937-b983b20f54f9
golang.org/x/crypto v0.33.0
golang.org/x/crypto v0.43.0
gopkg.in/yaml.v3 v3.0.1
)
require (
github.com/asticode/go-astikit v0.52.0 // indirect
github.com/asticode/go-astikit v0.56.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/pion/datachannel v1.5.10 // indirect
github.com/pion/dtls/v2 v2.2.12 // indirect
github.com/pion/logging v0.2.3 // indirect
github.com/pion/mdns v0.0.12 // indirect
github.com/pion/dtls/v3 v3.0.7 // indirect
github.com/pion/logging v0.2.4 // indirect
github.com/pion/mdns/v2 v2.0.7 // indirect
github.com/pion/randutil v0.1.0 // indirect
github.com/pion/sctp v1.8.36 // indirect
github.com/pion/transport/v2 v2.2.10 // indirect
github.com/pion/transport/v3 v3.0.7 // indirect
github.com/pion/turn/v2 v2.1.6 // indirect
github.com/pion/sctp v1.8.40 // indirect
github.com/pion/transport/v3 v3.0.8 // indirect
github.com/pion/turn/v4 v4.1.1 // 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.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
golang.org/x/mod v0.29.0 // indirect
golang.org/x/net v0.46.0 // indirect
golang.org/x/sync v0.17.0 // indirect
golang.org/x/sys v0.37.0 // indirect
golang.org/x/tools v0.38.0 // indirect
)
+101 -100
View File
@@ -1,6 +1,8 @@
github.com/asticode/go-astikit v0.30.0/go.mod h1:h4ly7idim1tNhaVkdVBeXQZEE3L0xblP7fCWbgwipF0=
github.com/asticode/go-astikit v0.52.0 h1:kTl2XjgiVQhUl1H7kim7NhmTtCMwVBbPrXKqhQhbk8Y=
github.com/asticode/go-astikit v0.52.0/go.mod h1:fV43j20UZYfXzP9oBn33udkvCvDvCDhzjVqoLFuuYZE=
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-astikit v0.56.0 h1:DmD2p7YnvxiPdF0h+dRmos3bsejNEXbycENsY5JfBqw=
github.com/asticode/go-astikit v0.56.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,19 +10,19 @@ 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/expr-lang/expr v1.17.5 h1:i1WrMvcdLF249nSNlpQZN1S6NXuW9WaOfF5tPi3aw3k=
github.com/expr-lang/expr v1.17.5/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4=
github.com/expr-lang/expr v1.17.6 h1:1h6i8ONk9cexhDmowO/A64VPxHScu7qfSl2k8OlINec=
github.com/expr-lang/expr v1.17.6/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/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
@@ -32,49 +34,80 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/miekg/dns v1.1.63 h1:8M5aAw6OMZfFXTT7K5V0Eu5YiiL8l7nUAkyN6C9YwaY=
github.com/miekg/dns v1.1.63/go.mod h1:6NGHfjhpmr5lt3XPLuyfDJi5AXbNIPM9PY6H6sF1Nfs=
github.com/miekg/dns v1.1.66 h1:FeZXOS3VCVsKnEAd+wBkjMC3D2K+ww66Cq3VnCINuJE=
github.com/miekg/dns v1.1.66/go.mod h1:jGFzBsSNbJw6z1HYut1RKBKHA9PBdxeHrZG8J+gC2WE=
github.com/miekg/dns v1.1.68 h1:jsSRkNozw7G/mnmXULynzMNIsgY2dHC8LO6U6Ij2JEA=
github.com/miekg/dns v1.1.68/go.mod h1:fujopn7TB3Pu3JM69XaawiU0wqjpL9/8xGop5UrTPps=
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/dtls/v3 v3.0.7 h1:bItXtTYYhZwkPFk4t1n3Kkf5TDrfj6+4wG+CZR8uI9Q=
github.com/pion/dtls/v3 v3.0.7/go.mod h1:uDlH5VPrgOQIw59irKYkMudSFprY9IEFCqz/eTz16f8=
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/ice/v4 v4.0.10 h1:P59w1iauC/wPk9PdY8Vjl4fOFL5B+USq1+xbDcN6gT4=
github.com/pion/ice/v4 v4.0.10/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/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
github.com/pion/interceptor v0.1.40 h1:e0BjnPcGpr2CFQgKhrQisBU7V3GXK6wrfYrGYaU6Jq4=
github.com/pion/interceptor v0.1.40/go.mod h1:Z6kqH7M/FYirg3frjGJ21VLSRJGBXB/KqaTIrdqnOic=
github.com/pion/interceptor v0.1.41 h1:NpvX3HgWIukTf2yTBVjVGFXtpSpWgXjqz7IIpu7NsOw=
github.com/pion/interceptor v0.1.41/go.mod h1:nEt4187unvRXJFyjiw00GKo+kIuXMWQI9K89fsosDLY=
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 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.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8=
github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so=
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.11 h1:17xjnY5WO5hgO6SD3/NTIUPvSFw/PbLsIJyz1r1yNIk=
github.com/pion/rtp v1.8.11/go.mod h1:8uMBJj32Pa1wwx8Fuv/AsFhn8jsgw+3rUC2PfoBZ8p4=
github.com/pion/sctp v1.8.36 h1:owNudmnz1xmhfYje5L/FCav3V9wpPRePHle3Zi+P+M0=
github.com/pion/sctp v1.8.36/go.mod h1:cNiLdchXra8fHQwmIoqw0MbLLMs+f7uQ+dGMG2gWebE=
github.com/pion/sdp/v3 v3.0.10 h1:6MChLE/1xYB+CjumMw+gZ9ufp2DPApuVSnDT8t5MIgA=
github.com/pion/sdp/v3 v3.0.10/go.mod h1:88GMahN5xnScv1hIMTqLdu/cOcUkj6a9ytbncwMCq2E=
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/rtcp v1.2.16 h1:fk1B1dNW4hsI78XUCljZJlC4kZOPk67mNRuQ0fcEkSo=
github.com/pion/rtcp v1.2.16/go.mod h1:/as7VKfYbs5NIb4h6muQ35kQF/J0ZVNz2Z3xKoCBYOo=
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/rtp v1.8.20 h1:8zcyqohadZE8FCBeGdyEvHiclPIezcwRQH9zfapFyYI=
github.com/pion/rtp v1.8.20/go.mod h1:bAu2UFKScgzyFqvUKmbvzSdPr+NGbZtv6UB2hesqXBk=
github.com/pion/rtp v1.8.24 h1:+ICyZXUQDv95EsHN70RrA4XKJf5MGWyC6QQc1u6/ynI=
github.com/pion/rtp v1.8.24/go.mod h1:rF5nS1GqbR7H/TCpKwylzeq6yDM+MM6k+On5EgeThEM=
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/sctp v1.8.39 h1:PJma40vRHa3UTO3C4MyeJDQ+KIobVYRZQZ0Nt7SjQnE=
github.com/pion/sctp v1.8.39/go.mod h1:cNiLdchXra8fHQwmIoqw0MbLLMs+f7uQ+dGMG2gWebE=
github.com/pion/sctp v1.8.40 h1:bqbgWYOrUhsYItEnRObUYZuzvOMsVplS3oNgzedBlG8=
github.com/pion/sctp v1.8.40/go.mod h1:SPBBUENXE6ThkEksN5ZavfAhFYll+h+66ZiG6IZQuzo=
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/sdp/v3 v3.0.14 h1:1h7gBr9FhOWH5GjWWY5lcw/U85MtdcibTyt/o6RxRUI=
github.com/pion/sdp/v3 v3.0.14/go.mod h1:88GMahN5xnScv1hIMTqLdu/cOcUkj6a9ytbncwMCq2E=
github.com/pion/sdp/v3 v3.0.16 h1:0dKzYO6gTAvuLaAKQkC02eCPjMIi4NuAr/ibAwrGDCo=
github.com/pion/sdp/v3 v3.0.16/go.mod h1:9tyKzznud3qiweZcD86kS0ff1pGYB3VX+Bcsmkx6IXo=
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/srtp/v3 v3.0.6 h1:E2gyj1f5X10sB/qILUGIkL4C2CqK269Xq167PbGCc/4=
github.com/pion/srtp/v3 v3.0.6/go.mod h1:BxvziG3v/armJHAaJ87euvkhHqWe9I7iiOy50K2QkhY=
github.com/pion/srtp/v3 v3.0.8 h1:RjRrjcIeQsilPzxvdaElN0CpuQZdMvcl9VZ5UY9suUM=
github.com/pion/srtp/v3 v3.0.8/go.mod h1:2Sq6YnDH7/UDCvkSoHSDNDeyBcFgWL0sAVycVbAsXFg=
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/transport/v3 v3.0.8 h1:oI3myyYnTKUSTthu/NZZ8eu2I5sHbxbUNNFW62olaYc=
github.com/pion/transport/v3 v3.0.8/go.mod h1:+c2eewC5WJQHiAA46fkMMzoYZSuGzA/7E2FPrOYHctQ=
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/turn/v4 v4.0.2 h1:ZqgQ3+MjP32ug30xAbD6Mn+/K4Sxi3SdNOTFf+7mpps=
github.com/pion/turn/v4 v4.0.2/go.mod h1:pMMKP/ieNAG/fN5cZiN4SDuyKsXtNTr0ccN7IToA1zs=
github.com/pion/turn/v4 v4.1.1 h1:9UnY2HB99tpDyz3cVVZguSxcqkJ1DsTSZ+8TGruh4fc=
github.com/pion/turn/v4 v4.1.1/go.mod h1:2123tHk1O++vmjI5VSD0awT50NywDAq5A2NNNU4Jjs8=
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/pion/webrtc/v4 v4.1.3 h1:YZ67Boj9X/hk190jJZ8+HFGQ6DqSZ/fYP3sLAZv7c3c=
github.com/pion/webrtc/v4 v4.1.3/go.mod h1:rsq+zQ82ryfR9vbb0L1umPJ6Ogq7zm8mcn9fcGnxomM=
github.com/pion/webrtc/v4 v4.1.6 h1:srHH2HwvCGwPba25EYJgUzgLqCQoXl1VCUnrGQMSzUw=
github.com/pion/webrtc/v4 v4.1.6/go.mod h1:wKecGRlkl3ox/As/MYghJL+b/cVXMEhoPMJWPuGQFhU=
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=
@@ -82,96 +115,64 @@ 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/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.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
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.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
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.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0=
golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.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/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
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.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/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
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-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/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
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.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.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/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24=
golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
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
View File
@@ -0,0 +1,7 @@
//go:build !(linux && (386 || amd64 || arm || arm64 || mipsle))
package alsa
func Init() {
// not supported
}
+83
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
}
+17 -3
View File
@@ -7,6 +7,7 @@ import (
"net"
"net/http"
"os"
"slices"
"strconv"
"strings"
"sync"
@@ -23,6 +24,7 @@ func Init() {
Listen string `yaml:"listen"`
Username string `yaml:"username"`
Password string `yaml:"password"`
LocalAuth bool `yaml:"local_auth"`
BasePath string `yaml:"base_path"`
StaticDir string `yaml:"static_dir"`
Origin string `yaml:"origin"`
@@ -30,6 +32,8 @@ func Init() {
TLSCert string `yaml:"tls_cert"`
TLSKey string `yaml:"tls_key"`
UnixListen string `yaml:"unix_listen"`
AllowPaths []string `yaml:"allow_paths"`
} `yaml:"api"`
}
@@ -43,6 +47,7 @@ func Init() {
return
}
allowPaths = cfg.Mod.AllowPaths
basePath = cfg.Mod.BasePath
log = app.GetLogger("api")
@@ -61,7 +66,7 @@ func Init() {
}
if cfg.Mod.Username != "" {
Handler = middlewareAuth(cfg.Mod.Username, cfg.Mod.Password, Handler) // 2nd
Handler = middlewareAuth(cfg.Mod.Username, cfg.Mod.Password, cfg.Mod.LocalAuth, Handler) // 2nd
}
if log.Trace().Enabled() {
@@ -152,6 +157,10 @@ func HandleFunc(pattern string, handler http.HandlerFunc) {
if len(pattern) == 0 || pattern[0] != '/' {
pattern = basePath + "/" + pattern
}
if allowPaths != nil && !slices.Contains(allowPaths, pattern) {
log.Trace().Str("path", pattern).Msg("[api] ignore path not in allow_paths")
return
}
log.Trace().Str("path", pattern).Msg("[api] register path")
http.HandleFunc(pattern, handler)
}
@@ -185,6 +194,7 @@ func Response(w http.ResponseWriter, body any, contentType string) {
const StreamNotFound = "stream not found"
var allowPaths []string
var basePath string
var log zerolog.Logger
@@ -195,9 +205,13 @@ func middlewareLog(next http.Handler) http.Handler {
})
}
func middlewareAuth(username, password string, next http.Handler) http.Handler {
func isLoopback(remoteAddr string) bool {
return strings.HasPrefix(remoteAddr, "127.") || strings.HasPrefix(remoteAddr, "[::1]") || remoteAddr == "@"
}
func middlewareAuth(username, password string, localAuth bool, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !strings.HasPrefix(r.RemoteAddr, "127.") && !strings.HasPrefix(r.RemoteAddr, "[::1]") && r.RemoteAddr != "@" {
if localAuth || !isLoopback(r.RemoteAddr) {
user, pass, ok := r.BasicAuth()
if !ok || user != username || pass != password {
w.Header().Set("Www-Authenticate", `Basic realm="go2rtc"`)
+3 -1
View File
@@ -11,6 +11,7 @@ import (
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/app"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/gorilla/websocket"
"github.com/rs/zerolog"
)
@@ -132,7 +133,8 @@ func apiWS(w http.ResponseWriter, r *http.Request) {
if handler := wsHandlers[msg.Type]; handler != nil {
go func() {
if err = handler(tr, msg); err != nil {
tr.Write(&Message{Type: "error", Value: msg.Type + ": " + err.Error()})
errMsg := core.StripUserinfo(err.Error())
tr.Write(&Message{Type: "error", Value: msg.Type + ": " + errMsg})
}
}()
}
+11
View File
@@ -11,6 +11,7 @@ import (
var (
Version string
Modules []string
UserAgent string
ConfigPath string
Info = make(map[string]any)
@@ -76,6 +77,16 @@ func Init() {
if ConfigPath != "" {
Logger.Info().Str("path", ConfigPath).Msg("config")
}
var cfg struct {
Mod struct {
Modules []string `yaml:"modules"`
} `yaml:"app"`
}
LoadConfig(&cfg)
Modules = cfg.Mod.Modules
}
func readRevisionTime() (revision, vcsTime string) {
+10 -2
View File
@@ -5,8 +5,9 @@ import (
"os"
"path/filepath"
"strings"
"sync"
"github.com/AlexxIT/go2rtc/pkg/shell"
"github.com/AlexxIT/go2rtc/pkg/creds"
"github.com/AlexxIT/go2rtc/pkg/yaml"
)
@@ -18,11 +19,16 @@ func LoadConfig(v any) {
}
}
var configMu sync.Mutex
func PatchConfig(path []string, value any) error {
if ConfigPath == "" {
return errors.New("config file disabled")
}
configMu.Lock()
defer configMu.Unlock()
// empty config is OK
b, _ := os.ReadFile(ConfigPath)
@@ -65,13 +71,15 @@ func initConfig(confs flagConfig) {
// config as file
if ConfigPath == "" {
ConfigPath = conf
initStorage()
}
if data, _ = os.ReadFile(conf); data == nil {
continue
}
data = []byte(shell.ReplaceEnvVars(string(data)))
loadEnv(data)
data = creds.ReplaceVars(data)
configs = append(configs, data)
}
}
+3
View File
@@ -6,6 +6,7 @@ import (
"strings"
"sync"
"github.com/AlexxIT/go2rtc/pkg/creds"
"github.com/mattn/go-isatty"
"github.com/rs/zerolog"
)
@@ -88,6 +89,8 @@ func initLogger() {
writer = MemoryLog
}
writer = creds.SecretWriter(writer)
lvl, _ := zerolog.ParseLevel(modules["level"])
Logger = zerolog.New(writer).Level(lvl)
+56
View File
@@ -0,0 +1,56 @@
package app
import (
"sync"
"github.com/AlexxIT/go2rtc/pkg/creds"
"github.com/AlexxIT/go2rtc/pkg/yaml"
)
func initStorage() {
storage = &envStorage{data: make(map[string]string)}
creds.SetStorage(storage)
}
func loadEnv(data []byte) {
var cfg struct {
Env map[string]string `yaml:"env"`
}
if err := yaml.Unmarshal(data, &cfg); err != nil {
return
}
storage.mu.Lock()
for name, value := range cfg.Env {
storage.data[name] = value
creds.AddSecret(value)
}
storage.mu.Unlock()
}
var storage *envStorage
type envStorage struct {
data map[string]string
mu sync.Mutex
}
func (s *envStorage) SetValue(name, value string) error {
if err := PatchConfig([]string{"env", name}, value); err != nil {
return err
}
s.mu.Lock()
s.data[name] = value
s.mu.Unlock()
return nil
}
func (s *envStorage) GetValue(name string) (value string, ok bool) {
s.mu.Lock()
value, ok = s.data[name]
s.mu.Unlock()
return
}
+2 -2
View File
@@ -29,8 +29,8 @@ var stackSkip = [][]byte{
[]byte("created by github.com/AlexxIT/go2rtc/internal/homekit.Init"),
// webrtc/api.go
[]byte("created by github.com/pion/ice/v2.NewTCPMuxDefault"),
[]byte("created by github.com/pion/ice/v2.NewUDPMuxDefault"),
[]byte("created by github.com/pion/ice/v4.NewTCPMuxDefault"),
[]byte("created by github.com/pion/ice/v4.NewUDPMuxDefault"),
}
func stackHandler(w http.ResponseWriter, r *http.Request) {
+17
View File
@@ -2,7 +2,9 @@ package echo
import (
"bytes"
"errors"
"os/exec"
"slices"
"github.com/AlexxIT/go2rtc/internal/app"
"github.com/AlexxIT/go2rtc/internal/streams"
@@ -10,11 +12,25 @@ import (
)
func Init() {
var cfg struct {
Mod struct {
AllowPaths []string `yaml:"allow_paths"`
} `yaml:"echo"`
}
app.LoadConfig(&cfg)
allowPaths := cfg.Mod.AllowPaths
log := app.GetLogger("echo")
streams.RedirectFunc("echo", func(url string) (string, error) {
args := shell.QuoteSplit(url[5:])
if allowPaths != nil && !slices.Contains(allowPaths, args[0]) {
return "", errors.New("echo: bin not in allow_paths: " + args[0])
}
b, err := exec.Command(args[0], args[1:]...).Output()
if err != nil {
return "", err
@@ -26,4 +42,5 @@ func Init() {
return string(b), nil
})
streams.MarkInsecure("echo")
}
+10
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
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
```
+20 -2
View File
@@ -9,6 +9,7 @@ import (
"io"
"net/url"
"os"
"slices"
"strings"
"sync"
"syscall"
@@ -19,13 +20,23 @@ 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"
)
func Init() {
var cfg struct {
Mod struct {
AllowPaths []string `yaml:"allow_paths"`
} `yaml:"exec"`
}
app.LoadConfig(&cfg)
allowPaths = cfg.Mod.AllowPaths
rtsp.HandleFunc(func(conn *pkg.Conn) bool {
waitersMu.Lock()
waiter := waiters[conn.URL.Path]
@@ -45,10 +56,13 @@ func Init() {
})
streams.HandleFunc("exec", execHandle)
streams.MarkInsecure("exec")
log = app.GetLogger("exec")
}
var allowPaths []string
func execHandle(rawURL string) (prod core.Producer, err error) {
rawURL, rawQuery, _ := strings.Cut(rawURL, "#")
query := streams.ParseQuery(rawQuery)
@@ -73,6 +87,10 @@ func execHandle(rawURL string) (prod core.Producer, err error) {
debug: log.Debug().Enabled(),
}
if allowPaths != nil && !slices.Contains(allowPaths, cmd.Args[0]) {
return nil, errors.New("exec: bin not in allow_paths: " + cmd.Args[0])
}
if s := query.Get("killsignal"); s != "" {
sig := syscall.Signal(core.Atoi(s))
cmd.Cancel = func() error {
@@ -86,7 +104,7 @@ func execHandle(rawURL string) (prod core.Producer, err error) {
}
if query.Get("backchannel") == "1" {
return stdin.NewClient(cmd)
return pcm.NewBackchannel(cmd, query.Get("audio"))
}
if path == "" {
+2 -1
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
}
@@ -25,4 +25,5 @@ func Init() {
return url, nil
})
streams.MarkInsecure("expr")
}
+5 -3
View File
@@ -80,7 +80,7 @@ var defaults = map[string]string{
// `-profile high -level 4.1` - most used streaming profile
// `-pix_fmt:v yuv420p` - important for Telegram
"h264": "-c:v libx264 -g 50 -profile:v high -level:v 4.1 -preset:v superfast -tune:v zerolatency -pix_fmt:v yuv420p",
"h265": "-c:v libx265 -g 50 -profile:v main -level:v 5.1 -preset:v superfast -tune:v zerolatency -pix_fmt:v yuv420p",
"h265": "-c:v libx265 -g 50 -profile:v main -x265-params level=5.1:high-tier=0 -preset:v superfast -tune:v zerolatency -pix_fmt:v yuv420p",
"mjpeg": "-c:v mjpeg",
//"mjpeg": "-c:v mjpeg -force_duplicated_matrix:v 1 -huffman:v 0 -pix_fmt:v yuvj420p",
@@ -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
+27 -9
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) {
+106
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
+22 -9
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")
}
}
}
+11 -2
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
+3
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:
+10
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)
}
+2 -2
View File
@@ -30,10 +30,10 @@ func apiStream(w http.ResponseWriter, r *http.Request) {
// 1. link to go2rtc stream: rtsp://...:8554/{stream_name}
// 2. static link to Hass camera
// 3. dynamic link to Hass camera
if streams.Patch(v.Name, v.Channels.First.Url) != nil {
if _, err := streams.Patch(v.Name, v.Channels.First.Url); err == nil {
apiOK(w, r)
} else {
http.Error(w, "", http.StatusBadRequest)
http.Error(w, err.Error(), http.StatusBadRequest)
}
// /stream/{id}/channel/0/webrtc
+8 -2
View File
@@ -7,6 +7,7 @@ import (
"net/http"
"os"
"path"
"strings"
"sync"
"github.com/AlexxIT/go2rtc/internal/api"
@@ -37,8 +38,13 @@ func Init() {
api.HandleFunc("/streams", apiOK)
api.HandleFunc("/stream/", apiStream)
streams.RedirectFunc("hass", func(url string) (string, error) {
if location := entities[url[5:]]; location != "" {
streams.RedirectFunc("hass", func(rawURL string) (string, error) {
rawURL, rawQuery, _ := strings.Cut(rawURL, "#")
if location := entities[rawURL[5:]]; location != "" {
if rawQuery != "" {
return location + "#" + rawQuery, nil
}
return location, nil
}
+1 -1
View File
@@ -11,7 +11,7 @@ import (
)
func handlerWSHLS(tr *ws.Transport, msg *ws.Message) error {
stream := streams.GetOrPatch(tr.Request.URL.Query())
stream, _ := streams.GetOrPatch(tr.Request.URL.Query())
if stream == nil {
return errors.New(api.StreamNotFound)
}
+76 -38
View File
@@ -3,6 +3,7 @@ package homekit
import (
"errors"
"fmt"
"io"
"net/http"
"net/url"
"strings"
@@ -14,56 +15,93 @@ import (
"github.com/AlexxIT/go2rtc/pkg/mdns"
)
func apiHandler(w http.ResponseWriter, r *http.Request) {
func apiDiscovery(w http.ResponseWriter, r *http.Request) {
sources, err := discovery()
if err != nil {
api.Error(w, err)
return
}
urls := findHomeKitURLs()
for id, u := range urls {
deviceID := u.Query().Get("device_id")
for _, source := range sources {
if strings.Contains(source.URL, deviceID) {
source.Location = id
break
}
}
}
for _, source := range sources {
if source.Location == "" {
source.Location = " "
}
}
api.ResponseSources(w, sources)
}
func apiHomekit(w http.ResponseWriter, r *http.Request) {
if err := r.ParseForm(); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
switch r.Method {
case "GET":
sources, err := discovery()
if err != nil {
api.Error(w, err)
return
if id := r.Form.Get("id"); id != "" {
api.ResponsePrettyJSON(w, servers[id])
} else {
api.ResponsePrettyJSON(w, servers)
}
urls := findHomeKitURLs()
for id, u := range urls {
deviceID := u.Query().Get("device_id")
for _, source := range sources {
if strings.Contains(source.URL, deviceID) {
source.Location = id
break
}
}
}
for _, source := range sources {
if source.Location == "" {
source.Location = " "
}
}
api.ResponseSources(w, sources)
case "POST":
if err := r.ParseMultipartForm(1024); err != nil {
api.Error(w, err)
return
}
if err := apiPair(r.Form.Get("id"), r.Form.Get("url")); err != nil {
api.Error(w, err)
id := r.Form.Get("id")
rawURL := r.Form.Get("src") + "&pin=" + r.Form.Get("pin")
if err := apiPair(id, rawURL); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
case "DELETE":
if err := r.ParseMultipartForm(1024); err != nil {
api.Error(w, err)
return
}
if err := apiUnpair(r.Form.Get("id")); err != nil {
api.Error(w, err)
id := r.Form.Get("id")
if err := apiUnpair(id); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
}
func apiHomekitAccessories(w http.ResponseWriter, r *http.Request) {
id := r.URL.Query().Get("id")
stream := streams.Get(id)
if stream == nil {
http.Error(w, "", http.StatusNotFound)
return
}
rawURL := findHomeKitURL(stream.Sources())
if rawURL == "" {
http.Error(w, "", http.StatusBadRequest)
return
}
client, err := hap.Dial(rawURL)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer client.Close()
res, err := client.Get(hap.PathAccessories)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", api.MimeJSON)
_, _ = io.Copy(w, res.Body)
}
func discovery() ([]*api.Source, error) {
var sources []*api.Source
+28 -52
View File
@@ -2,8 +2,6 @@ package homekit
import (
"errors"
"io"
"net"
"net/http"
"strings"
@@ -35,12 +33,15 @@ func Init() {
streams.HandleFunc("homekit", streamHandler)
api.HandleFunc("api/homekit", apiHandler)
api.HandleFunc("api/homekit", apiHomekit)
api.HandleFunc("api/homekit/accessories", apiHomekitAccessories)
api.HandleFunc("api/discovery/homekit", apiDiscovery)
if cfg.Mod == nil {
return
}
hosts = map[string]*server{}
servers = map[string]*server{}
var entries []*mdns.ServiceEntry
@@ -66,33 +67,14 @@ func Init() {
srv := &server{
stream: id,
srtp: srtp.Server,
pairings: conf.Pairings,
}
srv.hap = &hap.Server{
Pin: pin,
DeviceID: deviceID,
DevicePrivate: calcDevicePrivate(conf.DevicePrivate, id),
GetPair: srv.GetPair,
AddPair: srv.AddPair,
Handler: homekit.ServerHandler(srv),
}
if url := findHomeKitURL(stream.Sources()); url != "" {
// 1. Act as transparent proxy for HomeKit camera
dial := func() (net.Conn, error) {
client, err := homekit.Dial(url, srtp.Server)
if err != nil {
return nil, err
}
return client.Conn(), nil
}
srv.hap.Handler = homekit.ProxyHandler(srv, dial)
} else {
// 2. Act as basic HomeKit camera
srv.accessory = camera.NewAccessory("AlexxIT", "go2rtc", name, "-", app.Version)
srv.hap.Handler = homekit.ServerHandler(srv)
Pin: pin,
DeviceID: deviceID,
DevicePrivate: calcDevicePrivate(conf.DevicePrivate, id),
GetClientPublic: srv.GetPair,
}
srv.mdns = &mdns.ServiceEntry{
@@ -114,15 +96,24 @@ func Init() {
srv.UpdateStatus()
if url := findHomeKitURL(stream.Sources()); url != "" {
// 1. Act as transparent proxy for HomeKit camera
srv.proxyURL = url
} else {
// 2. Act as basic HomeKit camera
srv.accessory = camera.NewAccessory("AlexxIT", "go2rtc", name, "-", app.Version)
}
host := srv.mdns.Host(mdns.ServiceHAP)
servers[host] = srv
hosts[host] = srv
servers[id] = srv
log.Trace().Msgf("[homekit] new server: %s", srv.mdns)
}
api.HandleFunc(hap.PathPairSetup, hapHandler)
api.HandleFunc(hap.PathPairVerify, hapHandler)
log.Trace().Msgf("[homekit] mdns: %s", entries)
go func() {
if err := mdns.Serve(mdns.ServiceHAP, entries); err != nil {
log.Error().Err(err).Caller().Send()
@@ -131,6 +122,7 @@ func Init() {
}
var log zerolog.Logger
var hosts map[string]*server
var servers map[string]*server
func streamHandler(rawURL string) (core.Producer, error) {
@@ -142,6 +134,8 @@ func streamHandler(rawURL string) (core.Producer, error) {
client, err := homekit.Dial(rawURL, srtp.Server)
if client != nil && rawQuery != "" {
query := streams.ParseQuery(rawQuery)
client.MaxWidth = core.Atoi(query.Get("maxwidth"))
client.MaxHeight = core.Atoi(query.Get("maxheight"))
client.Bitrate = parseBitrate(query.Get("bitrate"))
}
@@ -149,45 +143,27 @@ func streamHandler(rawURL string) (core.Producer, error) {
}
func resolve(host string) *server {
if len(servers) == 1 {
for _, srv := range servers {
if len(hosts) == 1 {
for _, srv := range hosts {
return srv
}
}
if srv, ok := servers[host]; ok {
if srv, ok := hosts[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
}
defer conn.Close()
// 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
}
switch r.RequestURI {
case hap.PathPairSetup:
err = srv.hap.PairSetup(r, rw, conn)
case hap.PathPairVerify:
err = srv.hap.PairVerify(r, rw, conn)
}
if err != nil && err != io.EOF {
log.Error().Err(err).Caller().Send()
}
srv.Handle(w, r)
}
func findHomeKitURL(sources []string) string {
@@ -203,7 +179,7 @@ func findHomeKitURL(sources []string) string {
if strings.HasPrefix(url, "hass") {
location, _ := streams.Location(url)
if strings.HasPrefix(location, "homekit") {
return url
return location
}
}
+210 -97
View File
@@ -4,10 +4,16 @@ import (
"crypto/ed25519"
"crypto/sha512"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
"net"
"net/http"
"net/url"
"slices"
"strings"
"sync"
"github.com/AlexxIT/go2rtc/internal/app"
"github.com/AlexxIT/go2rtc/internal/ffmpeg"
@@ -16,23 +22,133 @@ import (
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/hap"
"github.com/AlexxIT/go2rtc/pkg/hap/camera"
"github.com/AlexxIT/go2rtc/pkg/hap/hds"
"github.com/AlexxIT/go2rtc/pkg/hap/tlv8"
"github.com/AlexxIT/go2rtc/pkg/homekit"
"github.com/AlexxIT/go2rtc/pkg/magic"
"github.com/AlexxIT/go2rtc/pkg/mdns"
"github.com/AlexxIT/go2rtc/pkg/srtp"
)
type server struct {
stream string // stream name from YAML
hap *hap.Server // server for HAP connection and encryption
mdns *mdns.ServiceEntry
srtp *srtp.Server
accessory *hap.Accessory // HAP accessory
pairings []string // pairings list
hap *hap.Server // server for HAP connection and encryption
mdns *mdns.ServiceEntry
streams map[string]*homekit.Consumer
consumer *homekit.Consumer
pairings []string // pairings list
conns []any
mu sync.Mutex
accessory *hap.Accessory // HAP accessory
consumer *homekit.Consumer
proxyURL string
stream string // stream name from YAML
}
func (s *server) MarshalJSON() ([]byte, error) {
v := struct {
Name string `json:"name"`
DeviceID string `json:"device_id"`
Paired int `json:"paired"`
Conns []any `json:"connections"`
}{
Name: s.mdns.Name,
DeviceID: s.mdns.Info[hap.TXTDeviceID],
Paired: len(s.pairings),
Conns: s.conns,
}
return json.Marshal(v)
}
func (s *server) Handle(w http.ResponseWriter, r *http.Request) {
conn, rw, err := w.(http.Hijacker).Hijack()
if err != nil {
return
}
defer conn.Close()
// Fix reading from Body after Hijack.
r.Body = io.NopCloser(rw)
switch r.RequestURI {
case hap.PathPairSetup:
id, key, err := s.hap.PairSetup(r, rw)
if err != nil {
log.Error().Err(err).Caller().Send()
return
}
s.AddPair(id, key, hap.PermissionAdmin)
case hap.PathPairVerify:
id, key, err := s.hap.PairVerify(r, rw)
if err != nil {
log.Debug().Err(err).Caller().Send()
return
}
log.Debug().Str("stream", s.stream).Str("client_id", id).Msgf("[homekit] %s: new conn", conn.RemoteAddr())
controller, err := hap.NewConn(conn, rw, key, false)
if err != nil {
log.Error().Err(err).Caller().Send()
return
}
s.AddConn(controller)
defer s.DelConn(controller)
var handler homekit.HandlerFunc
switch {
case s.accessory != nil:
handler = homekit.ServerHandler(s)
case s.proxyURL != "":
client, err := hap.Dial(s.proxyURL)
if err != nil {
log.Error().Err(err).Caller().Send()
return
}
handler = homekit.ProxyHandler(s, client.Conn)
}
// If your iPhone goes to sleep, it will be an EOF error.
if err = handler(controller); err != nil && !errors.Is(err, io.EOF) {
log.Error().Err(err).Caller().Send()
return
}
}
}
type logger struct {
v any
}
func (l logger) String() string {
switch v := l.v.(type) {
case *hap.Conn:
return "hap " + v.RemoteAddr().String()
case *hds.Conn:
return "hds " + v.RemoteAddr().String()
case *homekit.Consumer:
return "rtp " + v.RemoteAddr
}
return "unknown"
}
func (s *server) AddConn(v any) {
log.Trace().Str("stream", s.stream).Msgf("[homekit] add conn %s", logger{v})
s.mu.Lock()
s.conns = append(s.conns, v)
s.mu.Unlock()
}
func (s *server) DelConn(v any) {
log.Trace().Str("stream", s.stream).Msgf("[homekit] del conn %s", logger{v})
s.mu.Lock()
if i := slices.Index(s.conns, v); i >= 0 {
s.conns = slices.Delete(s.conns, i, i+1)
}
s.mu.Unlock()
}
func (s *server) UpdateStatus() {
@@ -44,12 +160,68 @@ func (s *server) UpdateStatus() {
}
}
func (s *server) pairIndex(id string) int {
id = "client_id=" + id
for i, pairing := range s.pairings {
if strings.HasPrefix(pairing, id) {
return i
}
}
return -1
}
func (s *server) GetPair(id string) []byte {
s.mu.Lock()
defer s.mu.Unlock()
if i := s.pairIndex(id); i >= 0 {
query, _ := url.ParseQuery(s.pairings[i])
b, _ := hex.DecodeString(query.Get("client_public"))
return b
}
return nil
}
func (s *server) AddPair(id string, public []byte, permissions byte) {
log.Debug().Str("stream", s.stream).Msgf("[homekit] add pair id=%s public=%x perm=%d", id, public, permissions)
s.mu.Lock()
if s.pairIndex(id) < 0 {
s.pairings = append(s.pairings, fmt.Sprintf(
"client_id=%s&client_public=%x&permissions=%d", id, public, permissions,
))
s.UpdateStatus()
s.PatchConfig()
}
s.mu.Unlock()
}
func (s *server) DelPair(id string) {
log.Debug().Str("stream", s.stream).Msgf("[homekit] del pair id=%s", id)
s.mu.Lock()
if i := s.pairIndex(id); i >= 0 {
s.pairings = append(s.pairings[:i], s.pairings[i+1:]...)
s.UpdateStatus()
s.PatchConfig()
}
s.mu.Unlock()
}
func (s *server) PatchConfig() {
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,
)
}
}
func (s *server) GetAccessories(_ net.Conn) []*hap.Accessory {
return []*hap.Accessory{s.accessory}
}
func (s *server) GetCharacteristic(conn net.Conn, aid uint8, iid uint64) any {
log.Trace().Msgf("[homekit] %s: get char aid=%d iid=0x%x", conn.RemoteAddr(), aid, iid)
log.Trace().Str("stream", s.stream).Msgf("[homekit] get char aid=%d iid=0x%x", aid, iid)
char := s.accessory.GetCharacterByID(iid)
if char == nil {
@@ -59,11 +231,12 @@ func (s *server) GetCharacteristic(conn net.Conn, aid uint8, iid uint64) any {
switch char.Type {
case camera.TypeSetupEndpoints:
if s.consumer == nil {
consumer := s.consumer
if consumer == nil {
return nil
}
answer := s.consumer.GetAnswer()
answer := consumer.GetAnswer()
v, err := tlv8.MarshalBase64(answer)
if err != nil {
return nil
@@ -76,7 +249,7 @@ func (s *server) GetCharacteristic(conn net.Conn, aid uint8, iid uint64) any {
}
func (s *server) SetCharacteristic(conn net.Conn, aid uint8, iid uint64, value any) {
log.Trace().Msgf("[homekit] %s: set char aid=%d iid=0x%x value=%v", conn.RemoteAddr(), aid, iid, value)
log.Trace().Str("stream", s.stream).Msgf("[homekit] set char aid=%d iid=0x%x value=%v", aid, iid, value)
char := s.accessory.GetCharacterByID(iid)
if char == nil {
@@ -86,61 +259,64 @@ 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 {
var offer camera.SetupEndpointsRequest
if err := tlv8.UnmarshalBase64(value, &offer); err != nil {
return
}
s.consumer = homekit.NewConsumer(conn, srtp2.Server)
s.consumer.SetOffer(&offer)
consumer := homekit.NewConsumer(conn, srtp2.Server)
consumer.SetOffer(&offer)
s.consumer = consumer
case camera.TypeSelectedStreamConfiguration:
var conf camera.SelectedStreamConfig
if err := tlv8.UnmarshalBase64(value.(string), &conf); err != nil {
var conf camera.SelectedStreamConfiguration
if err := tlv8.UnmarshalBase64(value, &conf); err != nil {
return
}
log.Trace().Msgf("[homekit] %s stream id=%x cmd=%d", conn.RemoteAddr(), conf.Control.SessionID, conf.Control.Command)
log.Trace().Str("stream", s.stream).Msgf("[homekit] stream id=%x cmd=%d", conf.Control.SessionID, conf.Control.Command)
switch conf.Control.Command {
case camera.SessionCommandEnd:
if consumer := s.streams[conf.Control.SessionID]; consumer != nil {
_ = consumer.Stop()
for _, consumer := range s.conns {
if consumer, ok := consumer.(*homekit.Consumer); ok {
if consumer.SessionID() == conf.Control.SessionID {
_ = consumer.Stop()
return
}
}
}
case camera.SessionCommandStart:
if s.consumer == nil {
consumer := s.consumer
if consumer == nil {
return
}
if !s.consumer.SetConfig(&conf) {
if !consumer.SetConfig(&conf) {
log.Warn().Msgf("[homekit] wrong config")
return
}
if s.streams == nil {
s.streams = map[string]*homekit.Consumer{}
}
s.streams[conf.Control.SessionID] = s.consumer
s.AddConn(consumer)
stream := streams.Get(s.stream)
if err := stream.AddConsumer(s.consumer); err != nil {
if err := stream.AddConsumer(consumer); err != nil {
return
}
go func() {
_, _ = s.consumer.WriteTo(nil)
stream.RemoveConsumer(s.consumer)
_, _ = consumer.WriteTo(nil)
stream.RemoveConsumer(consumer)
delete(s.streams, conf.Control.SessionID)
s.DelConn(consumer)
}()
}
}
}
func (s *server) GetImage(conn net.Conn, width, height int) []byte {
log.Trace().Msgf("[homekit] %s: get image width=%d height=%d", conn.RemoteAddr(), width, height)
log.Trace().Str("stream", s.stream).Msgf("[homekit] get image width=%d height=%d", width, height)
stream := streams.Get(s.stream)
cons := magic.NewKeyframe()
@@ -166,69 +342,6 @@ func (s *server) GetImage(conn net.Conn, width, height int) []byte {
return b
}
func (s *server) GetPair(conn net.Conn, id string) []byte {
log.Trace().Msgf("[homekit] %s: get pair id=%s", conn.RemoteAddr(), id)
for _, pairing := range s.pairings {
if !strings.Contains(pairing, id) {
continue
}
query, err := url.ParseQuery(pairing)
if err != nil {
continue
}
if query.Get("client_id") != id {
continue
}
s := query.Get("client_public")
b, _ := hex.DecodeString(s)
return b
}
return nil
}
func (s *server) AddPair(conn net.Conn, id string, public []byte, permissions byte) {
log.Trace().Msgf("[homekit] %s: add pair id=%s public=%x perm=%d", conn.RemoteAddr(), id, public, permissions)
query := url.Values{
"client_id": []string{id},
"client_public": []string{hex.EncodeToString(public)},
"permissions": []string{string('0' + permissions)},
}
if s.GetPair(conn, id) == nil {
s.pairings = append(s.pairings, query.Encode())
s.UpdateStatus()
s.PatchConfig()
}
}
func (s *server) DelPair(conn net.Conn, id string) {
log.Trace().Msgf("[homekit] %s: del pair id=%s", conn.RemoteAddr(), id)
id = "client_id=" + id
for i, pairing := range s.pairings {
if !strings.Contains(pairing, id) {
continue
}
s.pairings = append(s.pairings[:i], s.pairings[i+1:]...)
s.UpdateStatus()
s.PatchConfig()
break
}
}
func (s *server) PatchConfig() {
if err := app.PatchConfig([]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,
)
}
}
func calcName(name, seed string) string {
if name != "" {
return name
+1 -4
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)
}
+2 -3
View File
@@ -36,8 +36,7 @@ func Init() {
var log zerolog.Logger
func handlerKeyframe(w http.ResponseWriter, r *http.Request) {
src := r.URL.Query().Get("src")
stream := streams.Get(src)
stream, _ := streams.GetOrPatch(r.URL.Query())
if stream == nil {
http.Error(w, api.StreamNotFound, http.StatusNotFound)
return
@@ -146,7 +145,7 @@ func inputMjpeg(w http.ResponseWriter, r *http.Request) {
}
func handlerWS(tr *ws.Transport, _ *ws.Message) error {
stream := streams.GetOrPatch(tr.Request.URL.Query())
stream, _ := streams.GetOrPatch(tr.Request.URL.Query())
if stream == nil {
return errors.New(api.StreamNotFound)
}
+1 -1
View File
@@ -91,7 +91,7 @@ func handlerMP4(w http.ResponseWriter, r *http.Request) {
return
}
stream := streams.GetOrPatch(query)
stream, _ := streams.GetOrPatch(query)
if stream == nil {
http.Error(w, api.StreamNotFound, http.StatusNotFound)
return
+2 -2
View File
@@ -11,7 +11,7 @@ import (
)
func handlerWSMSE(tr *ws.Transport, msg *ws.Message) error {
stream := streams.GetOrPatch(tr.Request.URL.Query())
stream, _ := streams.GetOrPatch(tr.Request.URL.Query())
if stream == nil {
return errors.New(api.StreamNotFound)
}
@@ -43,7 +43,7 @@ func handlerWSMSE(tr *ws.Transport, msg *ws.Message) error {
}
func handlerWSMP4(tr *ws.Transport, msg *ws.Message) error {
stream := streams.GetOrPatch(tr.Request.URL.Query())
stream, _ := streams.GetOrPatch(tr.Request.URL.Query())
if stream == nil {
return errors.New(api.StreamNotFound)
}
+52
View File
@@ -0,0 +1,52 @@
With ngrok integration, you can get external access to your streams in situations when you have Internet with a private IP address.
- you may need external access for two different things:
- WebRTC stream, so you need a tunnel WebRTC TCP port (ex. 8555)
- go2rtc web interface, so you need a tunnel API HTTP port (ex. 1984)
- ngrok supports authorization for your web interface
- ngrok automatically adds HTTPS to your web interface
The ngrok free subscription has the following limitations:
- You can reserve a free domain for serving the web interface, but the TCP address you get will always be random and change with each restart of the ngrok agent (not a problem for WebRTC stream)
- You can forward multiple ports from a single agent, but you can only run one ngrok agent on the free plan
go2rtc will automatically get your external TCP address (if you enable it in ngrok config) and use it with WebRTC connection (if you enable it in webrtc config).
You need to manually download the [ngrok agent app](https://ngrok.com/download) for your OS and register with the [ngrok service](https://ngrok.com/signup).
**Tunnel for only WebRTC Stream**
You need to add your [ngrok authtoken](https://dashboard.ngrok.com/get-started/your-authtoken) and WebRTC TCP port to YAML:
```yaml
ngrok:
command: ngrok tcp 8555 --authtoken eW91IHNoYWxsIG5vdCBwYXNzCnlvdSBzaGFsbCBub3QgcGFzcw
```
**Tunnel for WebRTC and Web interface**
You need to create `ngrok.yaml` config file and add it to the go2rtc config:
```yaml
ngrok:
command: ngrok start --all --config ngrok.yaml
```
ngrok config example:
```yaml
version: "2"
authtoken: eW91IHNoYWxsIG5vdCBwYXNzCnlvdSBzaGFsbCBub3QgcGFzcw
tunnels:
api:
addr: 1984 # use the same port as in the go2rtc config
proto: http
basic_auth:
- admin:password # you can set login/pass for your web interface
webrtc:
addr: 8555 # use the same port as in the go2rtc config
proto: tcp
```
See the [ngrok agent documentation](https://ngrok.com/docs/agent/config/) for more details on the ngrok configuration file.
+4
View File
@@ -45,6 +45,10 @@ func streamOnvif(rawURL string) (core.Producer, error) {
log.Debug().Msgf("[onvif] new uri=%s", uri)
if err = streams.Validate(uri); err != nil {
return nil, err
}
return streams.GetProducer(uri)
}
+14 -10
View File
@@ -1,10 +1,11 @@
package ring
import (
"encoding/json"
"net/http"
"net/url"
"fmt"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
@@ -21,8 +22,7 @@ func Init() {
func apiRing(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query()
var ringAPI *ring.RingRestClient
var err error
var ringAPI *ring.RingApi
// Check auth method
if email := query.Get("email"); email != "" {
@@ -30,7 +30,8 @@ func apiRing(w http.ResponseWriter, r *http.Request) {
password := query.Get("password")
code := query.Get("code")
ringAPI, err = ring.NewRingRestClient(ring.EmailAuth{
var err error
ringAPI, err = ring.NewRestClient(ring.EmailAuth{
Email: email,
Password: password,
}, nil)
@@ -44,7 +45,7 @@ func apiRing(w http.ResponseWriter, r *http.Request) {
if _, err = ringAPI.GetAuth(code); err != nil {
if ringAPI.Using2FA {
// Return 2FA prompt
json.NewEncoder(w).Encode(map[string]interface{}{
api.ResponseJSON(w, map[string]interface{}{
"needs_2fa": true,
"prompt": ringAPI.PromptFor2FA,
})
@@ -53,36 +54,39 @@ func apiRing(w http.ResponseWriter, r *http.Request) {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
} else {
} else if refreshToken := query.Get("refresh_token"); refreshToken != "" {
// 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{
var err error
ringAPI, err = ring.NewRestClient(ring.RefreshTokenAuth{
RefreshToken: refreshToken,
}, nil)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
} else {
http.Error(w, "either email/password or refresh token is required", http.StatusBadRequest)
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("camera_id", fmt.Sprint(camera.ID))
cleanQuery.Set("device_id", camera.DeviceID)
// Stream source
+63 -5
View File
@@ -5,10 +5,14 @@ import (
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/app"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/creds"
"github.com/AlexxIT/go2rtc/pkg/probe"
)
func apiStreams(w http.ResponseWriter, r *http.Request) {
w = creds.SecretResponse(w)
query := r.URL.Query()
src := query.Get("src")
@@ -27,7 +31,7 @@ func apiStreams(w http.ResponseWriter, r *http.Request) {
return
}
cons := probe.NewProbe(query)
cons := probe.Create("probe", query)
if len(cons.Medias) != 0 {
cons.WithRequest(r)
if err := stream.AddConsumer(cons); err != nil {
@@ -48,8 +52,8 @@ func apiStreams(w http.ResponseWriter, r *http.Request) {
name = src
}
if New(name, query["src"]...) == nil {
http.Error(w, "", http.StatusBadRequest)
if _, err := New(name, query["src"]...); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
@@ -65,8 +69,8 @@ func apiStreams(w http.ResponseWriter, r *http.Request) {
}
// support {input} templates: https://github.com/AlexxIT/go2rtc#module-hass
if Patch(name, src) == nil {
http.Error(w, "", http.StatusBadRequest)
if _, err := Patch(name, src); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
}
case "POST":
@@ -120,5 +124,59 @@ func apiStreamsDOT(w http.ResponseWriter, r *http.Request) {
}
dot = append(dot, '}')
dot = []byte(creds.SecretString(string(dot)))
api.Response(w, dot, "text/vnd.graphviz")
}
func apiPreload(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query()
src := query.Get("src")
// check if stream exists
stream := Get(src)
if stream == nil {
http.Error(w, "", http.StatusNotFound)
return
}
switch r.Method {
case "PUT":
// it's safe to delete from map while iterating
for k := range query {
switch k {
case core.KindVideo, core.KindAudio, "microphone":
default:
delete(query, k)
}
}
rawQuery := query.Encode()
if err := AddPreload(stream, rawQuery); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if err := app.PatchConfig([]string{"preload", src}, rawQuery); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
case "DELETE":
if err := DelPreload(stream); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if err := app.PatchConfig([]string{"preload", src}, nil); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
default:
http.Error(w, "", http.StatusMethodNotAllowed)
}
}
func apiSchemes(w http.ResponseWriter, r *http.Request) {
api.ResponseJSON(w, SupportedSchemes())
}
+66
View File
@@ -0,0 +1,66 @@
package streams
import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/stretchr/testify/require"
)
func TestApiSchemes(t *testing.T) {
// Setup: Register some test handlers and redirects
HandleFunc("rtsp", func(url string) (core.Producer, error) { return nil, nil })
HandleFunc("rtmp", func(url string) (core.Producer, error) { return nil, nil })
RedirectFunc("http", func(url string) (string, error) { return "", nil })
t.Run("GET request returns schemes", func(t *testing.T) {
req := httptest.NewRequest("GET", "/api/schemes", nil)
w := httptest.NewRecorder()
apiSchemes(w, req)
require.Equal(t, http.StatusOK, w.Code)
require.Equal(t, "application/json", w.Header().Get("Content-Type"))
var schemes []string
err := json.Unmarshal(w.Body.Bytes(), &schemes)
require.NoError(t, err)
require.NotEmpty(t, schemes)
// Check that our test schemes are in the response
require.Contains(t, schemes, "rtsp")
require.Contains(t, schemes, "rtmp")
require.Contains(t, schemes, "http")
})
}
func TestApiSchemesNoDuplicates(t *testing.T) {
// Setup: Register a scheme in both handlers and redirects
HandleFunc("duplicate", func(url string) (core.Producer, error) { return nil, nil })
RedirectFunc("duplicate", func(url string) (string, error) { return "", nil })
req := httptest.NewRequest("GET", "/api/schemes", nil)
w := httptest.NewRecorder()
apiSchemes(w, req)
require.Equal(t, http.StatusOK, w.Code)
var schemes []string
err := json.Unmarshal(w.Body.Bytes(), &schemes)
require.NoError(t, err)
// Count occurrences of "duplicate"
count := 0
for _, scheme := range schemes {
if scheme == "duplicate" {
count++
}
}
// Should only appear once
require.Equal(t, 1, count, "scheme 'duplicate' should appear exactly once")
}
+37
View File
@@ -2,6 +2,7 @@ package streams
import (
"errors"
"regexp"
"strings"
"github.com/AlexxIT/go2rtc/pkg/core"
@@ -15,6 +16,21 @@ func HandleFunc(scheme string, handler Handler) {
handlers[scheme] = handler
}
func SupportedSchemes() []string {
uniqueKeys := make(map[string]struct{}, len(handlers)+len(redirects))
for scheme := range handlers {
uniqueKeys[scheme] = struct{}{}
}
for scheme := range redirects {
uniqueKeys[scheme] = struct{}{}
}
resultKeys := make([]string, 0, len(uniqueKeys))
for key := range uniqueKeys {
resultKeys = append(resultKeys, key)
}
return resultKeys
}
func HasProducer(url string) bool {
if i := strings.IndexByte(url, ':'); i > 0 {
scheme := url[:i]
@@ -95,3 +111,24 @@ func GetConsumer(url string) (core.Consumer, func(), error) {
return nil, nil, errors.New("streams: unsupported scheme: " + url)
}
var insecure = map[string]bool{}
func MarkInsecure(scheme string) {
insecure[scheme] = true
}
var sanitize = regexp.MustCompile(`\s`)
func Validate(source string) error {
// TODO: Review the entire logic of insecure sources
if i := strings.IndexByte(source, ':'); i > 0 {
if insecure[source[:i]] {
return errors.New("streams: source from insecure producer")
}
}
if sanitize.MatchString(source) {
return errors.New("streams: source with spaces may be insecure")
}
return nil
}
+13 -5
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
}
+58
View File
@@ -0,0 +1,58 @@
package streams
import (
"errors"
"net/url"
"sync"
"github.com/AlexxIT/go2rtc/pkg/probe"
)
var preloads = map[*Stream]*probe.Probe{}
var preloadsMu sync.Mutex
func Preload(stream *Stream, rawQuery string) {
if err := AddPreload(stream, rawQuery); err != nil {
log.Error().Err(err).Caller().Send()
}
}
func AddPreload(stream *Stream, rawQuery string) error {
if rawQuery == "" {
rawQuery = "video&audio"
}
query, err := url.ParseQuery(rawQuery)
if err != nil {
return err
}
preloadsMu.Lock()
defer preloadsMu.Unlock()
if cons := preloads[stream]; cons != nil {
stream.RemoveConsumer(cons)
}
cons := probe.Create("preload", query)
if err = stream.AddConsumer(cons); err != nil {
return err
}
preloads[stream] = cons
return nil
}
func DelPreload(stream *Stream) error {
preloadsMu.Lock()
defer preloadsMu.Unlock()
if cons := preloads[stream]; cons != nil {
stream.RemoveConsumer(cons)
delete(preloads, stream)
return nil
}
return errors.New("streams: preload not found")
}
+31 -31
View File
@@ -3,7 +3,6 @@ package streams
import (
"errors"
"net/url"
"regexp"
"sync"
"time"
@@ -14,8 +13,9 @@ import (
func Init() {
var cfg struct {
Streams map[string]any `yaml:"streams"`
Publish map[string]any `yaml:"publish"`
Streams map[string]any `yaml:"streams"`
Publish map[string]any `yaml:"publish"`
Preload map[string]string `yaml:"preload"`
}
app.LoadConfig(&cfg)
@@ -28,34 +28,36 @@ func Init() {
api.HandleFunc("api/streams", apiStreams)
api.HandleFunc("api/streams.dot", apiStreamsDOT)
api.HandleFunc("api/preload", apiPreload)
api.HandleFunc("api/schemes", apiSchemes)
if cfg.Publish == nil {
if cfg.Publish == nil && cfg.Preload == nil {
return
}
time.AfterFunc(time.Second, func() {
// range for nil map is OK
for name, dst := range cfg.Publish {
if stream := Get(name); stream != nil {
Publish(stream, dst)
}
}
for name, rawQuery := range cfg.Preload {
if stream := Get(name); stream != nil {
Preload(stream, rawQuery)
}
}
})
}
var sanitize = regexp.MustCompile(`\s`)
// Validate - not allow creating dynamic streams with spaces in the source
func Validate(source string) error {
if sanitize.MatchString(source) {
return errors.New("streams: invalid dynamic source")
}
return nil
}
func New(name string, sources ...string) *Stream {
func New(name string, sources ...string) (*Stream, error) {
for _, source := range sources {
if Validate(source) != nil {
return nil
if !HasProducer(source) {
return nil, errors.New("streams: source not supported")
}
if err := Validate(source); err != nil {
return nil, err
}
}
@@ -65,10 +67,10 @@ func New(name string, sources ...string) *Stream {
streams[name] = stream
streamsMu.Unlock()
return stream
return stream, nil
}
func Patch(name string, source string) *Stream {
func Patch(name string, source string) (*Stream, error) {
streamsMu.Lock()
defer streamsMu.Unlock()
@@ -80,7 +82,7 @@ func Patch(name string, source string) *Stream {
// link (alias) streams[name] to streams[rtspName]
streams[name] = stream
}
return stream
return stream, nil
}
}
@@ -89,46 +91,44 @@ func Patch(name string, source string) *Stream {
// link (alias) streams[name] to streams[source]
streams[name] = stream
}
return stream
return stream, nil
}
// check if src has supported scheme
if !HasProducer(source) {
return nil
return nil, errors.New("streams: source not supported")
}
if Validate(source) != nil {
return nil
if err := Validate(source); err != nil {
return nil, err
}
// check an existing stream with this name
if stream, ok := streams[name]; ok {
stream.SetSource(source)
return stream
return stream, nil
}
// create new stream with this name
stream := NewStream(source)
streams[name] = stream
return stream
return stream, nil
}
func GetOrPatch(query url.Values) *Stream {
func GetOrPatch(query url.Values) (*Stream, error) {
// check if src param exists
source := query.Get("src")
if source == "" {
return nil
return nil, errors.New("streams: source empty")
}
// check if src is stream name
if stream := Get(source); stream != nil {
return stream
return stream, nil
}
// check if name param provided
if name := query.Get("name"); name != "" {
log.Info().Msgf("[streams] create new stream url=%s", source)
return Patch(name, source)
}
+1 -1
View File
@@ -1,4 +1,4 @@
//go:build !linux
//go:build !(linux && (386 || arm || mipsle || amd64 || arm64))
package v4l2
+2
View File
@@ -1,3 +1,5 @@
//go:build linux && (386 || arm || mipsle || amd64 || arm64)
package v4l2
import (
+1 -1
View File
@@ -8,7 +8,7 @@ import (
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/webrtc"
"github.com/AlexxIT/go2rtc/pkg/xnet"
pion "github.com/pion/webrtc/v3"
pion "github.com/pion/webrtc/v4"
)
type Address struct {
+1 -1
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:
+43 -1
View File
@@ -10,6 +10,7 @@ import (
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/webrtc"
"github.com/pion/sdp/v3"
)
// https://github.com/AlexxIT/go2rtc/issues/1600
@@ -27,7 +28,6 @@ func crealityClient(url string) (core.Producer, error) {
medias := []*core.Media{
{Kind: core.KindVideo, Direction: core.DirectionRecvonly},
{Kind: core.KindAudio, Direction: core.DirectionRecvonly},
}
// TODO: return webrtc.SessionDescription
@@ -36,6 +36,8 @@ func crealityClient(url string) (core.Producer, error) {
return nil, err
}
log.Trace().Msgf("[webrtc] offer:\n%s", offer)
body, err := offerToB64(offer)
if err != nil {
return nil, err
@@ -61,6 +63,12 @@ func crealityClient(url string) (core.Producer, error) {
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
}
@@ -108,3 +116,37 @@ func answerFromB64(r io.Reader) (string, error) {
// 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
}
+1 -1
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 {
+1 -1
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,
+1 -1
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) {
+1 -1
View File
@@ -13,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"
+4
View File
@@ -33,8 +33,12 @@ func switchbotClient(rawURL string, query url.Values) (core.Producer, error) {
v.Resolution = 0
case "sd":
v.Resolution = 1
case "auto":
v.Resolution = 2
}
v.PlayType = core.Atoi(query.Get("play_type")) // zero by default
return v, nil
})
}
+2 -2
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"
)
@@ -95,7 +95,7 @@ func asyncHandler(tr *ws.Transport, msg *ws.Message) (err error) {
query := tr.Request.URL.Query()
if name := query.Get("src"); name != "" {
stream = streams.GetOrPatch(query)
stream, _ = streams.GetOrPatch(query)
mode = core.ModePassiveConsumer
log.Debug().Str("src", name).Msg("[webrtc] new consumer")
} else if name = query.Get("dst"); name != "" {
+36 -1
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"))
}
+281
View File
@@ -0,0 +1,281 @@
# 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()
```
Supported functions and variables:
- `Detect()` - start the VAD and WAKE word detection process
- `Stream()` - start transmission of audio data to the client (Home Assistant)
- `Stop()` - stop and disconnect stream without disconnecting client (Home Assistant)
- `Pause()` - temporary pause of audio transfer, without disconnecting the stream
- `PlayAudio()` - playing the last audio that was sent from client (Home Assistant)
- `WriteEvent(type, data)` - send event to client (Home Assistant)
- `Sleep(duration)` - temporary script pause (ex. `Sleep('1.5s')`)
- `PlayFile(path)` - play audio from `wav` file
- `Type` - type (name) of event
- `Data` - event data in JSON format (ex. `{"text":"how are you"}`)
- also available other functions from [expr](https://github.com/AlexxIT/go2rtc/blob/master/internal/expr/README.md) module (ex. `fetch`)
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 external wyoming Microphone and Sound.
```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
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)
}
+22
View File
@@ -0,0 +1,22 @@
# Yandex
Source for receiving stream from new [Yandex IP camera](https://alice.yandex.ru/smart-home/security/ipcamera).
## Get Yandex token
1. Install HomeAssistant integration [YandexStation](https://github.com/AlexxIT/YandexStation).
2. Copy token from HomeAssistant config folder: `/config/.storage/core.config_entries`, key: `"x_token"`.
## Get device ID
1. Open this link in any browser: https://iot.quasar.yandex.ru/m/v3/user/devices
2. Copy ID of your camera, key: `"id"`.
## Config examples
```yaml
streams:
yandex_stream: yandex:?x_token=XXXX&device_id=XXXX
yandex_snapshot: yandex:?x_token=XXXX&device_id=XXXX&snapshot
yandex_snapshot_custom_size: yandex:?x_token=XXXX&device_id=XXXX&snapshot=h=540
```
+152
View File
@@ -0,0 +1,152 @@
package yandex
import (
"encoding/json"
"errors"
"fmt"
"time"
"github.com/AlexxIT/go2rtc/internal/webrtc"
"github.com/AlexxIT/go2rtc/pkg/core"
xwebrtc "github.com/AlexxIT/go2rtc/pkg/webrtc"
"github.com/google/uuid"
"github.com/gorilla/websocket"
pion "github.com/pion/webrtc/v4"
)
func goloomClient(serviceURL, serviceName, roomId, participantId, credentials string) (core.Producer, error) {
conn, _, err := websocket.DefaultDialer.Dial(serviceURL, nil)
if err != nil {
return nil, err
}
defer func() {
time.Sleep(time.Second)
_ = conn.Close()
}()
s := fmt.Sprintf(`{"hello": {
"credentials":"%s","participantId":"%s","roomId":"%s","serviceName":"%s","sdkInitializationId":"%s",
"capabilitiesOffer":{},"sendAudio":false,"sendSharing":false,"sendVideo":false,
"sdkInfo":{"hwConcurrency":4,"implementation":"browser","version":"5.4.0"},
"participantAttributes":{"description":"","name":"mike","role":"SPEAKER"},
"participantMeta":{"description":"","name":"mike","role":"SPEAKER","sendAudio":false,"sendVideo":false}
},"uid":"%s"}`,
credentials, participantId, roomId, serviceName,
uuid.NewString(), uuid.NewString(),
)
err = conn.WriteMessage(websocket.TextMessage, []byte(s))
if err != nil {
return nil, err
}
if _, _, err = conn.ReadMessage(); err != nil {
return nil, err
}
pc, err := webrtc.PeerConnection(true)
if err != nil {
return nil, err
}
prod := xwebrtc.NewConn(pc)
prod.FormatName = "yandex"
prod.Mode = core.ModeActiveProducer
prod.Protocol = "wss"
var connState core.Waiter
prod.Listen(func(msg any) {
switch msg := msg.(type) {
case pion.PeerConnectionState:
switch msg {
case pion.PeerConnectionStateConnecting:
case pion.PeerConnectionStateConnected:
connState.Done(nil)
default:
connState.Done(errors.New("webrtc: " + msg.String()))
}
}
})
go func() {
for {
var msg map[string]json.RawMessage
if err = conn.ReadJSON(&msg); err != nil {
return
}
for k, v := range msg {
switch k {
case "uid":
continue
case "serverHello":
case "subscriberSdpOffer":
var sdp subscriberSdp
if err = json.Unmarshal(v, &sdp); err != nil {
return
}
//log.Trace().Msgf("offer:\n%s", sdp.Sdp)
if err = prod.SetOffer(sdp.Sdp); err != nil {
return
}
if sdp.Sdp, err = prod.GetAnswer(); err != nil {
return
}
//log.Trace().Msgf("answer:\n%s", sdp.Sdp)
var raw []byte
if raw, err = json.Marshal(sdp); err != nil {
return
}
s = fmt.Sprintf(`{"uid":"%s","subscriberSdpAnswer":%s}`, uuid.NewString(), raw)
if err = conn.WriteMessage(websocket.TextMessage, []byte(s)); err != nil {
return
}
case "webrtcIceCandidate":
var candidate webrtcIceCandidate
if err = json.Unmarshal(v, &candidate); err != nil {
return
}
if err = prod.AddCandidate(candidate.Candidate); err != nil {
return
}
}
//log.Trace().Msgf("%s : %s", k, v)
}
if msg["ack"] != nil {
continue
}
s = fmt.Sprintf(`{"uid":%s,"ack":{"status":{"code":"OK"}}}`, msg["uid"])
if err = conn.WriteMessage(websocket.TextMessage, []byte(s)); err != nil {
return
}
}
}()
if err = connState.Wait(); err != nil {
return nil, err
}
s = fmt.Sprintf(`{"uid":"%s","setSlots":{"slots":[{"width":0,"height":0}],"audioSlotsCount":0,"key":1,"shutdownAllVideo":false,"withSelfView":false,"selfViewVisibility":"ON_LOADING_THEN_HIDE","gridConfig":{}}}`, uuid.NewString())
if err = conn.WriteMessage(websocket.TextMessage, []byte(s)); err != nil {
return nil, err
}
return prod, nil
}
type subscriberSdp struct {
PcSeq int `json:"pcSeq"`
Sdp string `json:"sdp"`
}
type webrtcIceCandidate struct {
PcSeq int `json:"pcSeq"`
Target string `json:"target"`
Candidate string `json:"candidate"`
SdpMid string `json:"sdpMid"`
SdpMlineIndex int `json:"sdpMlineIndex"`
}
+44
View File
@@ -0,0 +1,44 @@
package yandex
import (
"net/url"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/yandex"
)
func Init() {
streams.HandleFunc("yandex", func(source string) (core.Producer, error) {
u, err := url.Parse(source)
if err != nil {
return nil, err
}
query := u.Query()
token := query.Get("x_token")
session, err := yandex.GetSession(token)
if err != nil {
return nil, err
}
deviceID := query.Get("device_id")
if query.Has("snapshot") {
rawURL, err := session.GetSnapshotURL(deviceID)
if err != nil {
return nil, err
}
rawURL += "/current.jpg?" + query.Get("snapshot") + "#header=Cookie:" + session.GetCookieString(rawURL)
return streams.GetProducer(rawURL)
}
room, err := session.WebrtcCreateRoom(deviceID)
if err != nil {
return nil, err
}
return goloomClient(room.ServiceUrl, room.ServiceName, room.RoomId, room.ParticipantId, room.Credentials)
})
}
+65 -54
View File
@@ -1,6 +1,9 @@
package main
import (
"slices"
"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 +12,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"
@@ -35,67 +40,73 @@ import (
"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/internal/yandex"
"github.com/AlexxIT/go2rtc/pkg/shell"
)
func main() {
app.Version = "1.9.9"
app.Version = "1.9.12"
// 1. Core modules: app, api/ws, streams
type module struct {
name string
init func()
}
app.Init() // init config and logs
modules := []module{
{"", app.Init}, // init config and logs
{"api", api.Init}, // init API before all others
{"ws", ws.Init}, // init WS API endpoint
{"", streams.Init},
// Main sources and servers
{"http", http.Init}, // rtsp source, HTTP server
{"rtsp", rtsp.Init}, // rtsp source, RTSP server
{"webrtc", webrtc.Init}, // webrtc source, WebRTC server
// Main API
{"mp4", mp4.Init}, // MP4 API
{"hls", hls.Init}, // HLS API
{"mjpeg", mjpeg.Init}, // MJPEG API
// Other sources and servers
{"hass", hass.Init}, // hass source, Hass API server
{"homekit", homekit.Init}, // homekit source, HomeKit server
{"onvif", onvif.Init}, // onvif source, ONVIF API server
{"rtmp", rtmp.Init}, // rtmp source, RTMP server
{"webtorrent", webtorrent.Init}, // webtorrent source, WebTorrent module
{"wyoming", wyoming.Init},
// Exec and script sources
{"echo", echo.Init},
{"exec", exec.Init},
{"expr", expr.Init},
{"ffmpeg", ffmpeg.Init},
// Hardware sources
{"alsa", alsa.Init},
{"v4l2", v4l2.Init},
// Other sources
{"bubble", bubble.Init},
{"doorbird", doorbird.Init},
{"dvrip", dvrip.Init},
{"eseecloud", eseecloud.Init},
{"flussonic", flussonic.Init},
{"gopro", gopro.Init},
{"isapi", isapi.Init},
{"ivideon", ivideon.Init},
{"mpegts", mpegts.Init},
{"nest", nest.Init},
{"ring", ring.Init},
{"roborock", roborock.Init},
{"tapo", tapo.Init},
{"yandex", yandex.Init},
// Helper modules
{"debug", debug.Init},
{"ngrok", ngrok.Init},
{"srtp", srtp.Init},
}
api.Init() // init API before all others
ws.Init() // init WS API endpoint
streams.Init() // streams module
// 2. Main sources and servers
rtsp.Init() // rtsp source, RTSP server
webrtc.Init() // webrtc source, WebRTC server
// 3. Main API
mp4.Init() // MP4 API
hls.Init() // HLS API
mjpeg.Init() // MJPEG API
// 4. Other sources and servers
hass.Init() // hass source, Hass API server
onvif.Init() // onvif source, ONVIF API server
webtorrent.Init() // webtorrent source, WebTorrent module
// 5. Other sources
rtmp.Init() // rtmp source
exec.Init() // exec source
ffmpeg.Init() // ffmpeg source
echo.Init() // echo source
ivideon.Init() // ivideon source
http.Init() // http/tcp source
dvrip.Init() // dvrip source
tapo.Init() // tapo source
isapi.Init() // isapi source
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
// 6. Helper modules
ngrok.Init() // ngrok module
srtp.Init() // SRTP server
debug.Init() // debug API
// 7. Go
for _, m := range modules {
if app.Modules == nil || m.name == "" || slices.Contains(app.Modules, m.name) {
m.init()
}
}
shell.RunUntilSignal()
}
+1
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:` |
+1 -1
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
}
+2 -2
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
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
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)
}
+148
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
}
+148
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
}
+163
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;
}
+146
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
}
+231
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
}
+26
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
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)
}
+84
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()
}
+8 -2
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)
}
}
+34 -1
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
}
+14
View File
@@ -118,3 +118,17 @@ func TestName(t *testing.T) {
// stage3
_ = prod2.Stop()
}
func TestStripUserinfo(t *testing.T) {
s := `streams:
test:
- ffmpeg:rtsp://username:password@10.1.2.3:554/stream1
- ffmpeg:rtsp://10.1.2.3:554/stream1@#video=copy
`
s = StripUserinfo(s)
require.Equal(t, `streams:
test:
- ffmpeg:rtsp://***@10.1.2.3:554/stream1
- ffmpeg:rtsp://10.1.2.3:554/stream1@#video=copy
`, s)
}
+12
View File
@@ -2,6 +2,7 @@ package core
import (
"crypto/rand"
"regexp"
"runtime"
"strconv"
"strings"
@@ -77,3 +78,14 @@ func Caller() string {
_, file, line, _ := runtime.Caller(1)
return file + ":" + strconv.Itoa(line)
}
const (
unreserved = `A-Za-z0-9-._~`
subdelims = `!$&'()*+,;=`
userinfo = unreserved + subdelims + `%:`
)
func StripUserinfo(s string) string {
sanitizer := regexp.MustCompile(`://[` + userinfo + `]+@`)
return sanitizer.ReplaceAllString(s, `://***@`)
}
+1 -1
View File
@@ -139,7 +139,7 @@ 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)
+5 -5
View File
@@ -97,8 +97,8 @@ 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()
// unblock write to nil chan - OK, write to closed chan - panic
select {
case s.buf <- packet:
s.Bytes += len(packet.Payload)
@@ -139,13 +139,13 @@ func (s *Sender) Start() {
}
s.done = make(chan struct{})
go func() {
// for range on nil chan is OK
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() {
+7
View File
@@ -0,0 +1,7 @@
# Credentials
This module allows you to get variables:
- from custom storage (ex. config file)
- from [credential files](https://systemd.io/CREDENTIALS/)
- from environment variables
+79
View File
@@ -0,0 +1,79 @@
package creds
import (
"errors"
"os"
"path/filepath"
"regexp"
"strings"
)
type Storage interface {
SetValue(name, value string) error
GetValue(name string) (string, bool)
}
var storage Storage
func SetStorage(s Storage) {
storage = s
}
func SetValue(name, value string) error {
if storage == nil {
return errors.New("credentials: storage not initialized")
}
if err := storage.SetValue(name, value); err != nil {
return err
}
AddSecret(value)
return nil
}
func GetValue(name string) (value string, ok bool) {
value, ok = getValue(name)
AddSecret(value)
return
}
func getValue(name string) (string, bool) {
if storage != nil {
if value, ok := storage.GetValue(name); ok {
return value, true
}
}
if dir, ok := os.LookupEnv("CREDENTIALS_DIRECTORY"); ok {
if value, _ := os.ReadFile(filepath.Join(dir, name)); value != nil {
return strings.TrimSpace(string(value)), true
}
}
return os.LookupEnv(name)
}
// ReplaceVars - support format ${CAMERA_PASSWORD} and ${RTSP_USER:admin}
func ReplaceVars(data []byte) []byte {
re := regexp.MustCompile(`\${([^}{]+)}`)
return re.ReplaceAllFunc(data, func(match []byte) []byte {
key := string(match[2 : len(match)-1])
var def string
var defok bool
if i := strings.IndexByte(key, ':'); i > 0 {
key, def = key[:i], key[i+1:]
defok = true
}
if value, ok := GetValue(key); ok {
return []byte(value)
}
if defok {
return []byte(def)
}
return match
})
}
+83
View File
@@ -0,0 +1,83 @@
package creds
import (
"io"
"net/http"
"slices"
"strings"
"sync"
)
func AddSecret(value string) {
if value == "" {
return
}
secretsMu.Lock()
defer secretsMu.Unlock()
if slices.Contains(secrets, value) {
return
}
secrets = append(secrets, value)
secretsReplacer = nil
}
var secrets []string
var secretsMu sync.Mutex
var secretsReplacer *strings.Replacer
func getReplacer() *strings.Replacer {
secretsMu.Lock()
defer secretsMu.Unlock()
if secretsReplacer == nil {
oldnew := make([]string, 0, 2*len(secrets))
for _, s := range secrets {
oldnew = append(oldnew, s, "***")
}
secretsReplacer = strings.NewReplacer(oldnew...)
}
return secretsReplacer
}
func SecretString(s string) string {
re := getReplacer()
return re.Replace(s)
}
func SecretWriter(w io.Writer) io.Writer {
return &secretWriter{w}
}
type secretWriter struct {
w io.Writer
}
func (s *secretWriter) Write(b []byte) (int, error) {
re := getReplacer()
return re.WriteString(s.w, string(b))
}
type secretResponse struct {
w http.ResponseWriter
}
func (s *secretResponse) Header() http.Header {
return s.w.Header()
}
func (s *secretResponse) Write(b []byte) (int, error) {
re := getReplacer()
return re.WriteString(s.w, string(b))
}
func (s *secretResponse) WriteHeader(statusCode int) {
s.w.WriteHeader(statusCode)
}
func SecretResponse(w http.ResponseWriter) http.ResponseWriter {
return &secretResponse{w}
}
+15
View File
@@ -0,0 +1,15 @@
package creds
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestString(t *testing.T) {
AddSecret("admin")
AddSecret("pa$$word")
s := SecretString("rtsp://admin:pa$$word@192.168.1.123/stream1")
require.Equal(t, "rtsp://***:***@192.168.1.123/stream1", s)
}
+3 -1
View File
@@ -88,6 +88,8 @@ func (c *Client) AddTrack(media *core.Media, codec *core.Codec, track *core.Rece
}
func (c *Client) Start() (err error) {
_, err = c.conn.Read(nil)
// just block until c.conn closed
b := make([]byte, 1)
_, err = c.conn.Read(b)
return
}
+180
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
}
+22 -6
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)
}
+2 -2
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
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
}
}
}
+14 -5
View File
@@ -16,6 +16,11 @@ func RepairAVCC(codec *core.Codec, handler core.HandlerFunc) core.HandlerFunc {
ps := JoinNALU(sps, pps)
return func(packet *rtp.Packet) {
// this can happen for FLV from FFmpeg
if NALUType(packet.Payload) == NALUTypeSEI {
size := int(binary.BigEndian.Uint32(packet.Payload)) + 4
packet.Payload = packet.Payload[size:]
}
if NALUType(packet.Payload) == NALUTypeIFrame {
packet.Payload = Join(ps, packet.Payload)
}
@@ -82,7 +87,15 @@ func AVCCToCodec(avcc []byte) *core.Codec {
buf := bytes.NewBufferString("packetization-mode=1")
for {
n := len(avcc)
if n < 4 {
break
}
size := 4 + int(binary.BigEndian.Uint32(avcc))
if n < size {
break
}
switch NALUType(avcc) {
case NALUTypeSPS:
@@ -95,11 +108,7 @@ func AVCCToCodec(avcc []byte) *core.Codec {
buf.WriteString(base64.StdEncoding.EncodeToString(avcc[4:size]))
}
if size < len(avcc) {
avcc = avcc[size:]
} else {
break
}
avcc = avcc[size:]
}
return &core.Codec{
+17 -2
View File
@@ -5,7 +5,6 @@ import (
"encoding/hex"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -91,5 +90,21 @@ 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())
}
func TestAVCCToCodec(t *testing.T) {
s := "000000196764001fac2484014016ec0440000003004000000c23c60c920000000568ee32c8b0000000d365"
b, _ := hex.DecodeString(s)
codec := AVCCToCodec(b)
require.Equal(t, "packetization-mode=1;profile-level-id=64001f;sprop-parameter-sets=Z2QAH6wkhAFAFuwEQAAAAwBAAAAMI8YMkg==,aO4yyLA=", codec.FmtpLine)
}

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