Compare commits

...

1001 Commits

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

The removal of a fixed width on the navigation bar (`nav`) style is aimed at enhancing responsiveness and flexibility in various screen sizes, promoting a better user experience across devices.

These changes contribute to a more cohesive and accessible dark mode theme, aligning with modern web design practices that prioritize user comfort and interface adaptability.
2024-04-09 10:00:41 +03:00
Sergey Krashevich
ef54b04ffc feat(docker): optimize hardware.Dockerfile by cleaning up apt cache
This commit optimizes the Docker image size for the hardware setup by including commands to clean up the APT cache after package installation. This change reduces the overall image size by removing unnecessary files and directories that are not needed in the final image, leading to faster download and deployment times.
2024-04-09 09:35:53 +03:00
Sergey Krashevich
51e20497ac fix(styles): implement flex layout for body element 2024-04-09 09:32:59 +03:00
Sergey Krashevich
4ddadc08cb feat(editor-theme): dynamically set editor theme based on dark mode preference 2024-04-09 09:18:43 +03:00
April MacDonald
801bb2d534 Add support for H200 hub and child devices (for example battery powered doorbell D230S1) 2024-04-01 19:39:10 +01:00
Sergey Krashevich
20dd16badf feat(dark-mode): improve contrast and visited link styles 2024-03-23 07:03:53 +03:00
Sergey Krashevich
31398a7e6b feat(dark-mode): implement dark mode and centralize CSS
Implemented a dark mode feature for the website, including a toggle button in the navigation bar that allows users to switch between light and dark themes. To support this feature, centralized common CSS styles (such as body, table, and button stylings) into main.js to ensure consistent application across all HTML pages. This change improves user experience by providing a visually comfortable alternative for low-light environments and centralizes styling rules for easier maintenance.

- Added dark mode styles for body, table, buttons, and navigation elements in main.js.
- Introduced a toggle mechanism in the navigation bar to switch between light and dark modes.
- Utilized JavaScript to detect system theme preference (`prefers-color-scheme`) and persist user's theme choice using localStorage.
- Removed duplicate and scattered CSS rules from individual HTML files (add.html, index.html, links.html, log.html) and centralized them in main.js to reduce redundancy and facilitate easier updates in the future.

This update enhances accessibility and user preference compliance by allowing users to select their desired theme while simplifying CSS management across the website.
2024-03-22 18:15:52 +03:00
Sergey Krashevich
de70b0a861 some fixes 2024-03-22 18:13:20 +03:00
Sergey Krashevich
a50c99b8e5 feat(log): introduce toggle for reversing log order
Added a button to the log page allowing users to toggle the order in which logs are displayed (normal or reversed). This feature enhances user experience by providing flexibility in viewing logs. The implementation involves a boolean flag `reverseOrder` to track the current state of log order and dynamically updates the button text to reflect the current mode. Additionally, the log fetching function now conditionally reverses the log array based on this flag, ensuring that the display order matches the user's preference. This change could significantly improve usability for users needing to analyze recent events without scrolling through the entire log history.
2024-03-22 18:10:45 +03:00
pabst2k
63de86a409 Update README.md
fix: Typo in url
2024-03-22 13:52:27 +01:00
Sergey Krashevich
9fc3d91a17 fix(streams): handle non-string elements in slice source for NewStream 2024-03-19 09:46:56 +03:00
f1d094
2ff7a20eba Modified func Close in pkg/isapi/client.go to call '/ISAPI/System/TwoWayAudio/channels/<channel id>/close' instead of '/ISAPI/System/TwoWayAudio/channels/<channel id/close/open'
Modified pkg/isapi/client.go to call 'close' before 'open' to prevent channel left open from prior connection blocking with 401 or 403 errors.
2024-02-24 18:26:43 -08:00
f1d094
3fa481bdfc Modified func Close in pkg/isapi/client.go to call '/ISAPI/System/TwoWayAudio/channels/<channel id>/close' instead of '/ISAPI/System/TwoWayAudio/channels/<channel id/close/open'
Modified pkg/isapi/client.go to call 'close' before 'open' to prevent channel left open from prior connection blocking with 401 or 403 errors.
2024-02-24 15:45:43 -08:00
Sergey Krashevich
9f7448d255 ci: upgrade GitHub Actions to newer versions
Updated various GitHub Actions used in the CI workflows (build.yml, gh-pages.yml, test.yml) to their latest major versions. This includes actions for checking out code, setting up Go, uploading artifacts, configuring Docker, and deploying to GitHub Pages. The update is part of routine maintenance to ensure compatibility with the latest features and improvements provided by these actions.
2024-02-24 13:14:51 +03:00
Sergey Krashevich
3afe8d7c1d fix(daemon-mode): handle '-daemon' argument correctly for background execution
This commit fixes the issue where the '-daemon' argument was not being properly handled when re-executing the program in daemon mode. The loop removes the '-daemon' flag from the arguments slice before the program is re-run in the background, ensuring that subsequent executions do not attempt to enter daemon mode again.

The change will prevent potential errors or unexpected behavior due to the presence of the '-daemon' argument in recursive calls, making the daemon mode feature more robust and reliable.
2024-02-24 13:04:18 +03:00
Sergey Krashevich
15c27e16cc feat(app): support daemon mode on non-Windows platforms
Added a new command-line flag `-daemon` to run the application in the background as a daemon. This option is only available for non-Windows operating systems due to platform-specific process handling. When enabled, the application restarts itself with the same arguments except for the `-daemon` flag, prints the PID of the background process, and then exits the current process.
2024-02-24 10:33:02 +03:00
Josip Janzic
14a9763c73 Fix crash with tapo cameras not returning 201 2024-02-23 16:39:29 +00:00
civita
6fbd141576 pkg/hap/camera/accessory.go 2024-02-16 20:18:53 -08:00
Sergey Krashevich
c0455a20aa fix grammar
Co-authored-by: Felipe Santos <felipecassiors@gmail.com>
2024-02-17 01:46:11 +03:00
Sergey Krashevich
6f9b8b732d Initial commit 2024-02-12 05:02:21 +00:00
Alex X
5fa31fe4d6 Fix reconnection issue 2024-02-10 08:49:47 +03:00
Alex X
f237119b9a Update docker hardware image to Debian 13 for FFmpeg 6.1 2024-02-06 15:29:39 +03:00
Alex X
b08b88357e Add mesa-va-drivers for docker hardware image 2024-01-31 11:30:16 +03:00
Rob van Oostenrijk
f73ee41d93 Updated FreeBSD ffmpeg integrations 2024-01-30 19:10:53 +04:00
Rob van Oostenrijk
93dad05bde Added FreeBSD Binaries (#2)
Co-authored-by: Rob van Oostenrijk <robvanoostenrijk@noreply.users.github.com>
2024-01-29 21:14:47 +04:00
Rob van Oostenrijk
b844722af1 Merge pull request #1 from robvanoostenrijk/freebsd-compile
Update build.yml
2024-01-29 14:21:11 +04:00
Rob van Oostenrijk
a4b212d906 Update build.yml 2024-01-29 14:20:27 +04:00
dadav
152719441e feat: Add signal related params to exec 2024-01-21 19:34:15 +01:00
Alex X
4b62a6e34f Fix double rtsp in the control field #830 2024-01-18 17:29:22 +03:00
Sergey Krashevich
48fabec431 fix(clipboard): fix copy to clipboard functionality
Added a `copyTextToClipboard` function to handle text copying across different browsers and fallback scenarios. This function utilizes the Clipboard API when available, providing an asynchronous method to copy text securely. For browsers where the Clipboard API is not available or the page is not served over a secure context, a fallback method using a temporary textarea element and `document.execCommand` is employed. Replaced direct use of `navigator.clipboard.writeText` with this function in the 'shareget' click event listener to enhance cross-browser support and error handling.
2024-01-13 18:07:07 +03:00
Sergey Krashevich
f8d9fccf74 fix(log-display): reverse log order to display newest first The
The applyLogStyling function in log.html has been updated to reverse the array of log lines. After parsing the JSON data, reversing the array ensures that the most recent logs appear at the top of the list. This change enhances the readability for users by displaying the logs in a descending chronological order.
2024-01-12 07:43:12 +03:00
Alex X
8793c36364 Add warning about secure access for API to docs 2024-01-11 14:26:51 +03:00
Alex X
59d25c10b3 Add unix socket example to docs 2024-01-11 14:26:04 +03:00
Alex X
3b3d5b033a Add sanitize from XSS to WebUI 2024-01-11 14:13:52 +03:00
Michael Reif
249ae49b43 execbc: Removed Buffered IO since it caused delay in the audio output 2024-01-07 10:14:23 +01:00
Michael Reif
33eafd5691 execbc: increased Buffer Size for IO Operation 2024-01-06 21:50:09 +01:00
Michael Reif
2b9247d630 execbc-source: Merged the dial function to the Client creation 2024-01-06 09:40:20 +01:00
Michael Reif
cc6b8277c9 Code Cleanup, rename outputbc to execbc, using buffered Writer 2024-01-06 09:32:47 +01:00
Michael Reif
f65b18842a Added support to stream backchannel to a command (outputbc) 2024-01-06 00:04:15 +01:00
René Simonsen
db190e69ed Updated README with more accurate information regarding nest integration. 2024-01-03 15:16:59 +01:00
René Simonsen
bc516bce7d Adds automatic extention of nest stream before it expires. 2024-01-03 15:08:21 +01:00
Alex X
ccec41a10f Update version to 1.8.5 2024-01-01 09:34:44 +03:00
Alex X
9feb98db3f Fix panic on reconnect #828 2024-01-01 09:30:40 +03:00
Alex X
a724c5f3ce Fix support Aqara G2H #793 2024-01-01 09:24:43 +03:00
Alex X
c60767c8b0 Add support H265 to FLV source #822 2023-12-31 21:06:41 +03:00
Alex X
ae13a72fde Fix mdns log message #843 2023-12-30 20:44:50 +03:00
Alex X
458d5e7d0d Add error for wrong homekit source #805 2023-12-30 20:43:40 +03:00
Alex X
89e15d9b57 Add support subtype for Tapo source #792 2023-12-30 13:04:53 +03:00
Alex X
0d2292c311 Add test for issue #825 2023-12-28 16:49:23 +03:00
Alex X
62343af009 Update dependencies 2023-12-28 16:49:09 +03:00
Alex X
c8c3b22d19 Fix memory allocation for HomeKit OPUS 2023-12-28 11:54:38 +03:00
Alex X
853e98879b Fix OPUS for HomeKit server #667 #843 2023-12-27 23:05:45 +03:00
Alex X
bf5cb33385 Add OpenIPC to readme 2023-12-22 11:33:53 +03:00
Alex X
7ad4d350f8 Fix hardware profiles for H265 templates #809 2023-12-17 18:07:57 +03:00
Alex X
c63fc6a2ad Fix exec source leaves zombie processes after fail #814 2023-12-17 17:59:41 +03:00
Alex X
7036d196be Fix H265 support from OpenIPC project 2023-12-15 12:42:07 +03:00
Alex X
d3bc18c369 Logs refactoring after #780 2023-12-11 18:07:38 +03:00
Alex X
1f3a32023f Merge pull request #780 from 'skrashevich/log-viewer' 2023-12-11 18:07:25 +03:00
Alex X
a46bad0522 Add support hardware resize for Rockchip 2023-12-10 15:58:54 +03:00
Alex X
d0dfa1d3dd Add support OPUS inside MPEG-TS 2023-12-10 15:56:53 +03:00
Sergey Krashevich
fc5b36acd3 actualise godoc comment for api.logHandler func 2023-12-05 17:54:53 +03:00
Sergey Krashevich
0a8ab9bbd1 Update app.go to remove the unused variable LogFilePath 2023-12-05 17:50:51 +03:00
Sergey Krashevich
b60000ac34 Refactor log handling to use in-memory Logger 2023-12-05 17:44:32 +03:00
Alex X
39d87625d7 Merge pull request #798 from MPTres/cors
Fix CORS support in WHEP/WHIP API
2023-12-05 16:16:07 +03:00
MPTres
0da8b46148 remove empty lines. 2023-12-05 13:52:34 +01:00
Alex X
8d9f87061c Add support hardware auto discovery for Rockchip 2023-12-05 15:43:50 +03:00
Alex X
4bdfa62039 Code refactoring for ffmpeg hardware linux 2023-12-05 15:43:14 +03:00
Alex X
67ea2d9d02 Fix support FFmpeg device on Windows 2023-12-05 15:40:16 +03:00
MPTres
39b614fb0f Remove X-PINGOTHER from allowed headers. 2023-12-05 13:37:05 +01:00
MPTres
84469dcd25 CORS. Add support for OPTIONS requests. 2023-12-04 17:14:18 +01:00
Alex X
eceb4a476f Add support Rockchip hardware transcoding 2023-12-04 16:54:50 +03:00
Alex X
051a4eabd7 Change example for publish 2023-11-30 15:52:10 +03:00
Alex X
e68a304698 Add about new tapo password to readme 2023-11-30 15:51:49 +03:00
Alex X
2e6c6b1d41 Add "new in version" to readme 2023-11-30 13:43:07 +03:00
Alex X
0def6f8de9 Merge pull request #785 from skrashevich/exit-code-check
Ensure exit code is within valid range
2023-11-29 10:26:02 +03:00
Sergey Krashevich
7ac5b4f114 Ensure exit code is within valid range
The exitHandler function now properly validates the exit code provided
in the query string. It checks for conversion errors and ensures the
code is within the valid range of 0 to 125. If the validation fails,
it responds with an HTTP 400 Bad Request error. This prevents potential
misuse of the exit endpoint by restricting the exit codes to expected
values.
2023-11-29 10:03:39 +03:00
Sergey Krashevich
ab47d5718f Refactor log handling and add UI auto-update toggle
This commit refactors the log handling in the API to use a switch statement for improved readability and maintainability. It also introduces error messages with more context when reading or truncating the log file fails.

On the frontend, a new auto-update toggle button has been added to the log viewer, allowing users to enable or disable automatic log updates. The button's appearance changes based on its state, providing a clear visual indication of whether auto-update is active. Additionally, the button styling has been updated to ensure consistency across the interface.
2023-11-28 22:55:50 +03:00
Alex X
94aced0fc0 Merge pull request #782 from miguelangel-nubla/patch-1
Typo in codec name
2023-11-28 18:45:55 +03:00
Miguel Angel Nubla
66a4c3d06e Typo in codec name 2023-11-28 12:37:27 +01:00
Sergey Krashevich
8d382afa0f Add log file handling and viewing capabilities
This commit introduces the ability to handle log files through the API and
provides a new log viewing page. The API now supports GET and DELETE methods
for log file operations, allowing retrieval and deletion of log contents.
A new log.html page has been added for viewing logs in the browser, with
automatic refresh every 5 seconds and styling based on log levels.

The app.go file has been updated to include a GetLogFilepath function that
retrieves or generates the log file path. The NewLogger function now accepts
a file parameter to enable file logging. The main.js file has been updated
to include a link to the new log.html page.

This enhancement improves the observability and management of the application
by providing real-time access to logs and the ability to clear them directly
from the web interface.
2023-11-26 23:21:57 +03:00
Alex X
051c5ff913 Fix buggy SDP from D-Link cameras #771 2023-11-22 17:42:41 +03:00
Alex X
a87dafbbec Update version to 1.8.4 2023-11-19 18:38:26 +03:00
Alex X
742cb7699b Improve magic producer about support mjpeg with trash on start 2023-11-18 17:08:57 +03:00
Alex X
43449e7b08 Fix api port for homekit module 2023-11-18 11:48:19 +03:00
Alex X
33512e73bd Add support ADTS to magic producer 2023-11-17 22:28:28 +03:00
Alex X
b367ffee6d Merge pull request #759 from russorat/ror/ngrok
fix: updating ngrok readme
2023-11-17 13:59:12 +03:00
Russ Savage
69447df6b3 fix: updating ngrok readme
Signed-off-by: Russ Savage <russorat@users.noreply.github.com>
2023-11-16 21:16:53 -08:00
Alex X
a6eac4ff02 Merge pull request #754 from inode64/master
Include support for Gentoo distribution
2023-11-15 21:20:46 +03:00
INODE64
1eaf879a76 Include support for Gentoo distribution 2023-11-15 17:45:36 +01:00
Alex X
c9ae6dcc03 Fix https source, again 2023-11-15 17:31:59 +03:00
Alex X
befa6bd356 Update version to 1.8.3 2023-11-15 12:20:47 +03:00
Alex X
100ab62ab4 Update dependencies 2023-11-15 12:16:34 +03:00
Alex X
a0f999d9c9 Add readme about gopro source 2023-11-15 12:10:16 +03:00
Alex X
9bda2f7e60 Add support gopro source 2023-11-15 11:41:42 +03:00
Alex X
54b19999c6 Fix support raw username/password for tapo source #748 2023-11-14 14:22:17 +03:00
Alex X
aa3c081352 Add support incoming H264 bitstream #745 2023-11-13 22:56:07 +03:00
Alex X
2d16ee8884 Code refactoring for mpegts input 2023-11-13 22:55:12 +03:00
Alex X
ec96a14807 Fix digest auth in some cases 2023-11-13 22:44:28 +03:00
Alex X
af72548a43 Fix panic for broken RTP with AAC #697 2023-11-13 21:56:35 +03:00
Alex X
6d85b36f47 Fix homekit source panic on stop producer #734 2023-11-13 21:51:52 +03:00
Alex X
28830a697d Add support unix socket for api module 2023-11-12 21:50:29 +03:00
Alex X
5d3953a948 Fix support Tapo C210 firmware v1.3.9 #733 2023-11-12 16:41:43 +03:00
Alex X
4d6432d38d Add about expr source to readme 2023-11-11 15:21:24 +03:00
Alex X
bcbebd5a36 Fix custom https client 2023-11-05 08:39:07 +03:00
Alex X
50e2a626a6 Update version to 1.8.2 2023-11-04 18:49:56 +03:00
Alex X
f4fe8c3769 Increase ProbeSize up to 5MB 2023-11-04 15:14:23 +03:00
Alex X
e42085a237 Remove lock on sender buffer processing 2023-11-04 15:14:04 +03:00
Alex X
a060b3447c Increase buffer for RTSP input 2023-11-04 15:13:39 +03:00
Alex X
d7784b24c6 Fix memory overflow on bad RTSP sources #675 2023-11-04 09:42:50 +03:00
Alex X
39645cb3d8 Remove unnecessary 0.0.0.0 from listeners 2023-11-03 12:45:40 +03:00
Alex X
36166caccc Fix raw conn for https client 2023-11-03 12:40:42 +03:00
Alex X
0f1dc73d55 Update WebRTC candidates logic 2023-11-03 11:13:54 +03:00
Alex X
6b29c37433 Update webrtc trace logs for local candidates 2023-11-02 14:58:30 +03:00
Alex X
535bacf9d6 Fix ngrok 2023-11-02 14:57:52 +03:00
Alex X
e6fb4081f7 Add drawtext tests for ffmpeg 2023-11-02 14:57:22 +03:00
Alex X
eb04fafaa4 Add more ffmpeg transcoding presets 2023-11-02 14:56:58 +03:00
Alex X
b4ed738d17 Add IPv6 support to WebRTC #721 2023-10-30 21:18:09 +03:00
Alex X
6a9ae93fa1 Update pixel format for h264 vaapi hardware 2023-10-30 19:06:56 +03:00
Alex X
2dd47654e6 Fix panic for HomeKit source without SRTP module #712 2023-10-27 17:08:07 +03:00
Alex X
c27e735c17 Fix wrong SDP for MERCURY camera #708 2023-10-27 14:37:12 +03:00
Alex X
8bc65e4c91 Update codecs table in readme 2023-10-27 07:41:00 +03:00
Alex X
0a476a74b3 Add QNAP to readme 2023-10-27 07:19:48 +03:00
Alex X
b5be4ce03b Add expr source 2023-10-26 21:07:48 +03:00
Alex X
f291f1d827 Rewrite shell cmd parser 2023-10-25 16:49:01 +03:00
Alex X
041ce885c7 Merge pull request #704 from testwill/map
chore: unnecessary guard around call to delete
2023-10-24 12:09:15 +04:00
Alex X
df16f28825 Update poster 2023-10-24 10:52:47 +03:00
Alex X
a8867bc3cb Add Synology NAS to readme 2023-10-24 10:34:55 +03:00
Alex X
b2b115ec9c Merge pull request #705 from skrashevich/openapi-add-restart-handler
add restart handler to openapi spec
2023-10-19 16:51:43 +03:00
Sergey Krashevich
95de3a1f3e Update openapi.yaml 2023-10-19 16:40:14 +03:00
guoguangwu
dd4376cd37 chore: unnecessary guard around call to delete 2023-10-19 21:21:09 +08:00
Alex X
20d45bff92 Update to version 1.8.1 2023-10-15 20:15:35 +03:00
Alex X
4ad67e9f6f Update external dependencies 2023-10-15 20:15:26 +03:00
Alex X
e367940bd9 Fix version in API 2023-10-15 09:37:24 +03:00
Alex X
6f2af78392 Update version to 1.8.0 2023-10-14 17:19:19 +03:00
Alex X
548d8133eb Update readme with new features 2023-10-14 17:17:13 +03:00
Alex X
36ee2b29fb Update TLS files handling 2023-10-14 15:51:43 +03:00
Alex X
05accb4555 Add support media config param for JS player 2023-10-14 11:41:45 +03:00
Alex X
f949a278da Fix HLS JS error on latest iOS 2023-10-14 11:40:49 +03:00
Alex X
bfae16f3a0 Improve SPS parser 2023-10-14 08:11:03 +03:00
Alex X
d09d21434b Panic if shell can't restart process 2023-10-14 08:06:09 +03:00
Alex X
2b9926cedb Support broken SPS for MP4 2023-10-14 08:04:20 +03:00
Alex X
af24fd67aa Fix snapshots for some streams 2023-10-13 14:46:24 +03:00
Alex X
e2cd34ffe3 Fix hap secure connection 2023-10-13 11:25:17 +03:00
Alex X
ecdf5ba271 Fix homekit proxy after events handler 2023-10-13 11:23:00 +03:00
Alex X
995ef5bb36 Add support RTMP from Dahua cameras 2023-10-12 17:55:03 +03:00
Alex X
8165adcab1 Rewrite hap secure connection 2023-10-12 17:03:58 +03:00
Alex X
91c4a3e7b5 Add ffmpeg test for DeckLink 2023-10-11 22:35:53 +03:00
Alex X
cb710ea2be Update dvrip source processing 2023-10-11 22:23:22 +03:00
Alex X
843a3ae9c9 Total rework DVRIP source + add two way audio #633 2023-10-11 19:47:26 +03:00
Alex X
de040fb160 Fix panic for homekit source (nil conn) #628 2023-10-11 14:34:01 +03:00
Alex X
acec8a76aa Fix panic from roborock source (iot.Dial error) #601 2023-10-11 14:26:02 +03:00
Alex X
6c07c59454 Fix panic on aac.RTPDepay #635 2023-10-11 14:21:56 +03:00
Alex X
4d708b5385 Fix send audio to RTSP (cuts out after 30 seconds) #659 2023-10-11 13:56:40 +03:00
Alex X
2e9f3181d4 Fix onvif source: invalid control character in URL #662 2023-10-11 13:43:45 +03:00
Alex X
3ae15d8f80 Add support TLS cert/key as file path #680 2023-10-11 11:50:30 +03:00
Alex X
d016529030 Merge pull request #632 from skrashevich/230911-fix-hap-pairing-dups
Fix: duplicate pairing strings in config
2023-10-11 11:33:05 +03:00
Alex X
09f1553e40 Fix SO_REUSEPORT for macOS #626 2023-10-11 11:31:37 +03:00
Alex X
52e4bf1b35 Add retry for publish 2023-10-11 07:14:43 +03:00
Alex X
bbe6ae0059 Update publish RTMP examples 2023-10-11 06:58:17 +03:00
Alex X
c02117e626 Add support incoming RTMP 2023-10-11 06:54:50 +03:00
Alex X
b8fb3acbab Fix Tapo error on setup 2023-10-11 06:52:39 +03:00
Alex X
d4d0064220 Change publish start time from 5 to 1 second 2023-10-11 06:52:23 +03:00
Alex X
855bbdeb60 Update ffmpeg tests 2023-10-10 12:25:07 +03:00
Alex X
05893c9203 Add fix for YCbCr range on hardware transcoding 2023-10-10 11:29:22 +03:00
Alex X
c9c8e73587 Add feature auto publish on app start 2023-10-09 23:09:28 +03:00
Alex X
c7b6eb5d5b Fix ffmpeg pix_fmt for H264 transcoding 2023-10-09 23:08:18 +03:00
Alex X
96bc88d8ce Add copyright for restart func 2023-10-09 17:37:53 +03:00
Alex X
9a2e9dd6d1 Add support /api/restart #652 2023-10-09 17:12:25 +03:00
Alex X
b252fcaaa1 Code refactoring after #661 2023-10-05 17:31:59 +03:00
Alex X
c582b932c7 Merge pull request #661 from skrashevich/230930-unpkg 2023-10-05 17:31:52 +03:00
Alex X
c3f26c4db8 Fix logger for rtmp 2023-10-05 16:37:58 +03:00
Sergey Krashevich
f27f7d28bb Update editor.html 2023-09-30 12:38:31 +03:00
Alex X
0424b1a92a Merge pull request #656 from skrashevich/230927-fix-whep-link
fix broken link in README
2023-09-27 14:35:09 +03:00
Sergey Krashevich
81fb8fc238 Update README.md 2023-09-27 14:11:03 +03:00
Alex X
037970a4ea Add support ManagedMediaSource for Safari 17 2023-09-26 13:25:50 +03:00
Alex X
3f6e83e87c Merge pull request #653 from skrashevich/230925-fix-openapi-spec
fix openapi specs
2023-09-25 10:33:05 +03:00
Sergey Krashevich
aa5b23fa80 fix openapi specs 2023-09-25 07:41:55 +03:00
Alex X
02bde2c8b7 Add RTMP publish to WebUI 2023-09-17 20:39:31 +03:00
Alex X
cb5e90cc3b Add RTMP server and publish to RMTP logic 2023-09-17 20:31:36 +03:00
Alex X
209fe09806 Add active publish logic to streams 2023-09-17 20:29:28 +03:00
Alex X
dca8279e0c Update AMF tests 2023-09-17 20:28:09 +03:00
Alex X
8163c7a520 Update tcp.Dial func 2023-09-17 20:28:09 +03:00
Alex X
4dffceaf7e Update FLV muxer 2023-09-17 14:07:55 +03:00
Alex X
9f1e33e0c6 Add output to HTTP-FLV 2023-09-16 11:14:56 +03:00
Alex X
9a7d7e68e2 Add tests to AMF reader 2023-09-16 11:12:51 +03:00
Alex X
ab18d5d1ca Improve magic bitstream producer 2023-09-16 11:11:23 +03:00
Alex X
6e53e74742 Add WriteEcmaArray func for AMF proto 2023-09-16 11:10:32 +03:00
Alex X
f910bd4fce Add auto Flush to core WriteBuffer 2023-09-16 11:09:46 +03:00
Alexey Khit
93e475f3a4 Fix support ONVIF client with line breaks #638 2023-09-13 18:08:57 +03:00
Alexey Khit
e5d8170037 Add HomeKit accessories parser 2023-09-12 21:04:55 +03:00
Alexey Khit
861632f92b Add support events for HomeKit client 2023-09-12 21:04:19 +03:00
Alexey Khit
9cf75565b5 Fix error for HAP server 2023-09-12 21:02:40 +03:00
Sergey Krashevich
9368a6b85e Add conditional check before adding a new pair in the server's AddPair function 2023-09-11 10:34:31 +03:00
Alexey Khit
c8ac6b2271 Update version to 1.7.1 2023-09-10 20:19:20 +03:00
Alexey Khit
28f5c2b974 Update dependencies 2023-09-10 20:01:39 +03:00
Alexey Khit
daa2522a52 Fix panic for HomeKit source 2023-09-10 16:10:00 +03:00
Alexey Khit
863f8ec19b Fix malformed HTTP version for HomeKit source #620 2023-09-10 16:08:06 +03:00
Alexey Khit
8f98fc4547 Fix after #614 fix 2023-09-10 16:03:49 +03:00
Alexey Khit
398afbe49f Update default deadline from 3 to 5 seconds 2023-09-10 16:03:08 +03:00
Alexey Khit
ad8c0ab2fb Fix HomeKit pairing for some cameras 2023-09-10 14:56:00 +03:00
Alexey Khit
37130576e9 Add support webrtc go2rtc source with auth #539 2023-09-10 07:58:20 +03:00
Alex X
486fea2227 Merge pull request #611 from felipecrs/patch-1
Clarify import from go2rtc to hass generic camera
2023-09-10 07:18:17 +03:00
Felipe Santos
6d7357b151 Update README.md 2023-09-09 15:16:58 -03:00
Alex X
452d7577f8 Merge pull request #614 from skrashevich/230905-fix-runtime-crash
Refactor LocalIP method to correctly handle non-TCP connections
2023-09-09 17:37:52 +03:00
Alexey Khit
124398115e Restore fix for Chinese buggy cameras 2023-09-06 13:15:04 +03:00
Sergey Krashevich
541a7b28a7 Refactor LocalIP method to correctly handle non-TCP connections 2023-09-05 10:27:12 +03:00
Felipe Santos
947b0970ad Update README.md 2023-09-04 13:23:16 -03:00
Felipe Santos
447fd5b3eb Clarify import from go2rtc to hass generic camera
The protocol is not set by default. According to my tests, only TCP works.
2023-09-04 13:22:11 -03:00
Alexey Khit
064ffef462 Add check config changes during WebUI 2023-09-04 12:05:17 +03:00
Alexey Khit
05360ac284 Fix patch YAML without new line on end of file 2023-09-04 11:52:13 +03:00
Alexey Khit
08dabc7331 Add support HomeKit doorbells 2023-09-02 20:34:39 +03:00
Alexey Khit
d724df7db2 Fix HomeKit PIN in docs 2023-09-02 19:25:48 +03:00
Alexey Khit
fc1b6af436 Update version to 1.7.0 2023-09-02 16:05:09 +03:00
Alexey Khit
88fb589d2e Update readme about new features 2023-09-02 16:04:53 +03:00
Alexey Khit
5c5357cd79 Fix default video codec for HomeKit source 2023-09-02 15:22:03 +03:00
Alexey Khit
5ffd60c429 Update HomeKit default PIN 2023-09-02 14:44:13 +03:00
Alexey Khit
5645c73613 Update FFmpeg device discovery for Linux 2023-09-02 14:40:27 +03:00
Alexey Khit
13a7957cf3 Update HomeKit pairing status 2023-09-02 11:05:02 +03:00
Alexey Khit
c0d5a7c01a Rename PCM codecs print name 2023-09-02 09:17:38 +03:00
Alexey Khit
d87cc9ddb6 Fix homekit pairing table 2023-09-02 09:14:09 +03:00
Alexey Khit
b1c4bcc508 Fix patching YAML in some cases 2023-09-02 08:43:46 +03:00
Alexey Khit
6288c2a57f Add MJPEG support to HomeKit client 2023-09-02 07:39:16 +03:00
Alexey Khit
1c569e690d Fix HomeKit client stat info 2023-09-02 07:38:49 +03:00
Alexey Khit
7fdc6b9472 Update SRTP server constructor 2023-09-02 07:37:49 +03:00
Alexey Khit
60d7d525f2 Update add consumer error message 2023-09-02 07:37:15 +03:00
Alexey Khit
f6f2998e85 Fix JPEG screen length 2023-09-02 07:36:26 +03:00
Alexey Khit
82d1f2cf0b Add new goroutine to debug stack 2023-09-02 07:35:58 +03:00
Alexey Khit
f00e646612 Add support HomeKit server 2023-09-02 06:35:04 +03:00
Alexey Khit
a101387b26 Add debug logger 2023-09-02 06:31:34 +03:00
Alexey Khit
af31ab604d Update FFmpeg preset for OPUS 2023-09-02 06:31:10 +03:00
Alexey Khit
ccdd6ed490 Update SPS parser 2023-09-02 06:30:30 +03:00
Alexey Khit
9f404d965f Rewrite HomeKit pairing API 2023-09-01 22:48:06 +03:00
Alexey Khit
0621b82aff Change response sources API 2023-09-01 22:32:49 +03:00
Alexey Khit
22787b979d Rewrite HomeKit client 2023-09-01 10:38:38 +03:00
Alexey Khit
7d65c60711 Add stream redirect handler 2023-09-01 10:18:50 +03:00
Alexey Khit
69da64a49c Rename streams to sources in the discovery API 2023-09-01 10:17:58 +03:00
Alexey Khit
66c858e00e Rewrite JPEG snapshot consumer 2023-08-30 05:57:00 +03:00
Alexey Khit
ef63cec7a8 Rewrite once buffer for keyframes 2023-08-29 18:04:02 +03:00
Alexey Khit
0ac505ba09 Simplify MJPEG consumer 2023-08-29 17:16:51 +03:00
Alexey Khit
d4444c6257 Code refactoring 2023-08-28 22:43:07 +03:00
Alexey Khit
c6d5bb4eeb Add kasa client and simplify multipart client 2023-08-28 22:31:52 +03:00
Alexey Khit
7f232c5cf2 Add insecure HTTPS requests to IP addresses 2023-08-28 22:29:12 +03:00
Alexey Khit
dc2ab5fcc0 Add support TP-Link Kasa Spot KC401 #545 2023-08-28 19:30:34 +03:00
Alex X
137b23da10 Fix config file validating 2023-08-26 07:13:59 +03:00
Alexey Khit
54e361e3b8 Update go.mod 2023-08-26 07:03:03 +03:00
Alexey Khit
c78da1a7a9 Add about Wyze cameras project to readme 2023-08-25 11:05:19 +03:00
Alex X
27673cb0c1 Merge pull request #592 from skrashevich/go1.21
Update Go version to 1.21 in workflows and Dockerfiles
2023-08-23 18:19:40 +03:00
Alex X
c040a02fa8 Merge pull request #593 from skrashevich/ace-1.24.1
Update ace version to 1.24.1
2023-08-23 18:17:11 +03:00
Alexey Khit
a664e3b838 Code refactoring 2023-08-23 18:14:49 +03:00
Alexey Khit
317b3b5eeb Add support OpenIPC WebRTC format 2023-08-23 18:11:01 +03:00
Sergey Krashevich
9f14b30aae Refactor CSS in editor.html for better readability and remove duplicate body rule 2023-08-23 17:24:17 +03:00
Sergey Krashevich
065a6f4f46 Update Debian and Go versions to bookworm-slim and 1.21-bookworm respectively in hardware.Dockerfile 2023-08-23 17:06:21 +03:00
Alexey Khit
9f9dc7e844 Add support custom timeout for RTSP source 2023-08-23 14:08:15 +03:00
Alexey Khit
b1c0a28366 Update readme about artifacts 2023-08-23 13:27:49 +03:00
Alexey Khit
fc963dfe5c Fix H264 profile parsing for OpenIPC project 2023-08-23 13:26:57 +03:00
Sergey Krashevich
6f5ba2ade6 Update ace library version to 1.24.1 and fix code syntax in editor.html 2023-08-23 12:59:05 +03:00
Alexey Khit
ea708bb606 Add responses on RTSP OPTIONS pings 2023-08-23 10:14:58 +03:00
Sergey Krashevich
0822326900 Update Go version to 1.21 in build and test workflows and Dockerfiles 2023-08-23 10:09:53 +03:00
Alexey Khit
79fc0cd395 Update headers handling for http source 2023-08-23 07:46:34 +03:00
Alex X
357e7c1b18 Merge pull request #557 from h0nIg/patch-1
fix known problem of wrong profile declaration capabilities
2023-08-23 07:01:24 +03:00
Alexey Khit
71f1e445e1 Fix 400 response on PLAY for Reolink Doorbell #562 2023-08-23 06:52:33 +03:00
Alexey Khit
20efe22e60 Update readme about wyze-bridge #588 2023-08-23 06:08:11 +03:00
Alexey Khit
75a3dad745 Fix redirect for rtspx source #565 2023-08-23 06:07:23 +03:00
Alexey Khit
f5cca50830 Add check for empty H265 packet #589 2023-08-22 16:08:02 +03:00
Alexey Khit
8cd977f7ad Add support B-frames for MP4 consumer 2023-08-22 15:55:20 +03:00
Alexey Khit
90f2a9e106 Fix some audio in RTSP server 2023-08-21 20:54:45 +03:00
Alexey Khit
e0ad358aa9 Update timestamp processing for MPEG-TS 2023-08-21 20:34:18 +03:00
Alexey Khit
3db4002420 Support hass source without hass config #541 2023-08-21 16:56:58 +03:00
Alexey Khit
bf248c49c3 Add support two channel PCM family audio #580 2023-08-21 15:35:23 +03:00
Alexey Khit
69a3a30a0e Add media filter for RTSP source #198 2023-08-21 14:07:07 +03:00
Alexey Khit
f80f179e4c Fix MP4 consumer with only audio 2023-08-21 07:31:21 +03:00
Alexey Khit
c1c1d84cef Add AAC consumer 2023-08-21 07:12:30 +03:00
Alexey Khit
c431d888f0 Add AAC raw codec to MPEG-TS consumer 2023-08-21 07:03:40 +03:00
Alexey Khit
2ebb791eb7 Remove old code 2023-08-21 07:00:46 +03:00
Alex X
00b818b4d7 Add support custom headers for HTTP source 2023-08-21 06:30:05 +03:00
Alex X
ce1b0d442c Remove old unnecessary file 2023-08-21 06:29:19 +03:00
Alexey Khit
5283c9781c Update readme about dev version 2023-08-21 00:04:08 +03:00
Alexey Khit
279d8bf799 Rewrite GitHub actions 2023-08-20 23:41:39 +03:00
Alexey Khit
7114d63ba6 Update readme about Reolink cameras 2023-08-20 21:46:12 +03:00
Alexey Khit
120ae89578 Update dependencies 2023-08-20 21:45:08 +03:00
Alexey Khit
d1eb623fd6 Add buffer for RTSP output 2023-08-20 21:25:45 +03:00
Alexey Khit
873cf65317 Increase buffer for RTSP input 2023-08-20 21:24:55 +03:00
Alexey Khit
2091dead3f Refactoring for MP4 file handler 2023-08-20 18:43:42 +03:00
Alexey Khit
2ffd859f0e Update MPEG-TS consumer compatibility 2023-08-20 18:43:12 +03:00
Alexey Khit
da02a97a00 Fix close for HLS source 2023-08-20 18:40:19 +03:00
Alexey Khit
fb51dc781d Improve HLS reader 2023-08-20 16:35:09 +03:00
Alexey Khit
32bf64028d Fix ADTStoRTP parser 2023-08-20 16:33:57 +03:00
Alexey Khit
2e4e75e386 Rewrite MP4, HLS, MPEG-TS consumers 2023-08-20 09:57:46 +03:00
Alexey Khit
f67f6e5b9f Rewrite mpegts producer and consumer 2023-08-19 16:37:52 +03:00
Alexey Khit
24039218a1 Add multipart source to magic source 2023-08-19 16:14:46 +03:00
Alexey Khit
1f447ef73c Rewrite multipart source 2023-08-19 16:14:36 +03:00
Alexey Khit
4509198eef Rewrite FLV source 2023-08-19 16:13:43 +03:00
Alexey Khit
bc60cbefb8 Rewrite magic source 2023-08-19 15:19:09 +03:00
Alexey Khit
a9118562a9 Rewrite MJPEG consumer 2023-08-19 06:09:05 +03:00
Alexey Khit
24637be7c2 Fix panic for multipart client 2023-08-19 06:05:34 +03:00
Alexey Khit
d74be47696 Improve bits reader and writer 2023-08-19 06:04:31 +03:00
Alexey Khit
76a00031cd Code refactoring 2023-08-18 15:53:46 +03:00
Alexey Khit
063a192699 Add support RTMPS source 2023-08-17 08:00:02 +03:00
Alexey Khit
b016b7dc2a Refactoring for RTMP source 2023-08-17 07:59:21 +03:00
Alexey Khit
42f6441512 Fix mpegts reader for tapo client 2023-08-17 07:13:41 +03:00
Alexey Khit
dd066ba040 Add HLS client 2023-08-17 06:55:59 +03:00
Alexey Khit
b3def6cfa2 Rewrite support MPEG-TS client 2023-08-17 05:45:45 +03:00
Alexey Khit
4a82eb3503 Rewrite magic client 2023-08-16 20:13:42 +03:00
Alexey Khit
c3ba8db660 Rewrite FLV/RTMP clients 2023-08-16 19:35:13 +03:00
Alexey Khit
4e1a0e1ab9 Rewrite magic client 2023-08-16 17:15:27 +03:00
Alexey Khit
1dd3dbbcd8 Rewrite AnnexB/AVCC parsers 2023-08-16 16:50:55 +03:00
Alexey Khit
e1be2d9e48 Add buffer to pipe reader 2023-08-16 16:32:09 +03:00
Alexey Khit
8fbfccd024 Add MP4 atoms reader 2023-08-14 14:49:16 +03:00
Alexey Khit
de6bb33f01 Add SPS parser and AVC/HVC conf encoders 2023-08-14 11:55:08 +03:00
Alexey Khit
3a40515a90 Remove old source files 2023-08-14 11:35:34 +03:00
Alexey Khit
5d533338d0 Fix incoming FLV source 2023-08-14 06:49:37 +03:00
Alexey Khit
f412852d50 Remove old HTTP-FLV client 2023-08-14 06:49:37 +03:00
Alexey Khit
5fbec487e2 Add ffplay to links page 2023-08-14 06:49:37 +03:00
Alexey Khit
19c61e20c0 Total rework RTMP client 2023-08-14 06:49:37 +03:00
Alexey Khit
0b6fda2af5 Total rework FLV client 2023-08-14 06:49:37 +03:00
Alexey Khit
e9795e7521 Add goreportcard to readme 2023-08-13 15:44:29 +03:00
Alex X
3b8413a9dd Merge pull request #567 from awatuna/awatuna-patch-1
Update helpers.go
2023-08-07 06:49:32 +04:00
awatuna
b2f9ad7efb Update helpers.go
more tplink ipcams
2023-08-07 06:25:16 +08:00
Alexey Khit
4baa3f5588 Fix rare error with ws.close() 2023-08-04 16:31:13 +04:00
Alex X
9c5ae3260c Merge pull request #561 from dbuezas/fix/another-h265-mediaCode
Add 83 (0x53) to h265 mediaCode
2023-08-04 13:21:00 +04:00
David Buezas
b7baef0a48 Add 83 (0x53) to h265 mediaCode 2023-08-04 11:07:20 +02:00
Alexey Khit
8778d7c9ab Add support http/mixed video/audio #545 2023-08-02 17:57:33 +04:00
Hans-Joachim Kliemeck
d275997e54 fix known problem of wrong profile declaration capabilities 2023-08-01 22:18:45 +02:00
Alexey Khit
2faea1bb69 Fix bug with esp32-cam-webserver #545 2023-07-31 20:55:46 +03:00
Alexey Khit
ba6c96412b Add YAML pkg with Patch function 2023-07-25 18:05:50 +03:00
Alexey Khit
ed38122752 Code refactoring 2023-07-25 18:05:29 +03:00
Alexey Khit
922587ed2e Fix WebUI background color for dark mode browser 2023-07-24 21:57:10 +03:00
Alexey Khit
8e7c9d19e4 Fix H265 codec for bubble source 2023-07-24 14:11:28 +03:00
Alexey Khit
0f33ef0fc5 Add support MJPEG codec for HomeKit cameras 2023-07-23 22:35:53 +03:00
Alexey Khit
a14c87ad60 Code refactoring for MJPEG source 2023-07-23 22:35:31 +03:00
Alexey Khit
6d82b1ce89 Total rework HAP pkg and HomeKit source 2023-07-23 22:22:36 +03:00
Alexey Khit
d73e9f6bcf Fix custom OPUS params inside MP4 2023-07-23 22:19:35 +03:00
Alexey Khit
e6a87fbd69 Add RTSP SDP to stream info JSON 2023-07-23 22:18:54 +03:00
Alexey Khit
3defbd60db Add deadline handler for SRTP server 2023-07-23 17:20:58 +03:00
Alexey Khit
6e9574a1bd Fix receive SRTP with empty sessions 2023-07-23 17:08:14 +03:00
Alexey Khit
7005cd08f2 Improve mDNS handler 2023-07-23 17:07:12 +03:00
Alexey Khit
e94f338b77 Add error msg for producer empty medias 2023-07-23 17:04:19 +03:00
Alexey Khit
d6172587b3 Fix readme about first project in the World 2023-07-21 09:45:19 +03:00
Alexey Khit
f196d83a14 Update version to 1.6.2 2023-07-20 23:40:43 +03:00
Alexey Khit
9d4f4e1509 Fix syscalls on different archs 2023-07-20 23:29:27 +03:00
Alexey Khit
7308652f6e Fix PATCH stream with same name and src 2023-07-20 22:29:59 +03:00
Alexey Khit
870e9c3688 Fix creating stream on the fly #534 2023-07-20 22:09:11 +03:00
Alexey Khit
189f142fae Restore IPv6 support for API and RTSP #532 2023-07-20 22:09:11 +03:00
Alexey Khit
6c0918662e Improve HomeKit source start time 2023-07-20 21:46:06 +03:00
Alexey Khit
2bc01c143a Adds mDNS examples file 2023-07-20 21:34:38 +03:00
Alexey Khit
f310b85ee6 Improve mDNS package 2023-07-20 21:34:16 +03:00
Alexey Khit
97fef36f2f Move cmd to examples 2023-07-20 21:33:29 +03:00
Alexey Khit
a8526ae4eb Update version to 1.6.1 2023-07-20 08:12:10 +03:00
Alexey Khit
966fbe7d61 Update readme about webrtc wyze and kinesis sources 2023-07-20 08:11:26 +03:00
Alexey Khit
a77c2ef71f Update readme about new bubble source 2023-07-20 08:06:04 +03:00
Alexey Khit
61a194e396 Update readme about JPEG snapshot query params 2023-07-20 08:05:42 +03:00
Alexey Khit
ae25784d72 Update readme about MP4 stream query params 2023-07-20 08:04:58 +03:00
Alexey Khit
3343c78699 Add WebRTC sources for Amazon Kinesis and Wyze 2023-07-19 23:36:31 +03:00
Alexey Khit
7928f54a95 Fix handling bubble source 2023-07-17 18:28:21 +03:00
Alexey Khit
e4b68518e5 Remove all listeners from IPv6 interface 2023-07-17 18:28:15 +03:00
Alexey Khit
14ed1cdee8 Add restriction on symbols in dynamic source 2023-07-17 18:28:06 +03:00
Alexey Khit
72f159be88 Update Windows USB audio default settings 2023-07-16 18:40:54 +03:00
Alexey Khit
144954b979 Add default params to Linux ALSA 2023-07-16 14:09:13 +03:00
Alexey Khit
9e15391471 Code refactoring after #517 2023-07-16 13:43:27 +03:00
Alexey Khit
d62b1e445a Merge pull request #517 from skrashevich/230711-jpg-resize 2023-07-16 07:01:40 +03:00
Alexey Khit
ade4c035b7 Fix resample to G711 for WebRTC 2023-07-16 06:36:26 +03:00
Alexey Khit
13ca991c37 Add support pcm_s16le audio 2023-07-15 15:06:49 +03:00
Alexey Khit
e48459f49d Add channels and sample rate params to ALSA 2023-07-15 14:43:47 +03:00
Alexey Khit
facf18e0df Code refactoring for source bubble 2023-07-15 14:43:47 +03:00
Alexey Khit
5c93dc62bd Add support source Bubble (Eseenet/dvr163) 2023-07-15 11:46:10 +03:00
Alexey Khit
d272d4b6c3 Fix FLAC mime type for Chrome 2023-07-15 11:42:50 +03:00
Alexey Khit
1b41edfc7e Fix empty SPS/PPS for HLS/TS 2023-07-15 11:42:12 +03:00
Alexey Khit
d55270bd64 Fix tests 2023-07-13 23:49:17 +03:00
Alexey Khit
85225917f5 Rewritten streams creation 2023-07-13 23:32:01 +03:00
Alexey Khit
eaef62a775 Update RTSPtoWebRTC errors output 2023-07-13 22:52:03 +03:00
Alexey Khit
f6c8d63658 Another fix for OPUS audio quality 2023-07-13 20:31:59 +03:00
Alexey Khit
ea82d7ec2b Add support rotate and scale to MP4 stream 2023-07-13 19:32:55 +03:00
Alexey Khit
e8a7ba056c Add Wyze project to readme 2023-07-13 18:38:08 +03:00
Alexey Khit
9fd40467f2 Update codecs detection for Safari browsers 2023-07-13 16:16:37 +03:00
Alexey Khit
c81e29fe54 Fix FLAC mime type for HLS 2023-07-13 16:14:50 +03:00
Alexey Khit
b9b7bb5489 Adds README for API 2023-07-13 16:10:23 +03:00
Alexey Khit
8036278e29 Fix complex Content-Type for image/jpeg #278 2023-07-11 15:57:21 +03:00
Alexey Khit
39c25215ba Update readme 2023-07-11 15:03:27 +03:00
Sergey Krashevich
490a48cd50 Refactored code to resize JPEG snapshot if "h" parameter exists in the URL query 2023-07-11 10:35:53 +03:00
Alexey Khit
b5d40caffc Update version to 1.6.0 2023-07-11 07:48:51 +03:00
Alexey Khit
1e0952be86 Fix duplicates in mDNS from Hass docker 2023-07-11 07:37:06 +03:00
Alexey Khit
d5fa933772 Update external libraries 2023-07-11 00:49:49 +03:00
Alexey Khit
73bf96e123 Rewrite mDNS processing 2023-07-11 00:44:27 +03:00
Alexey Khit
4ea5a22eda Code refactoring after #510 2023-07-10 11:37:52 +03:00
Alexey Khit
a79fe6041d Merge pull request #510 from horttorrell32/master 2023-07-10 10:58:11 +03:00
Alexey Khit
07440f359e Update MP4 codecs detection 2023-07-08 09:34:00 +03:00
Alexey Khit
01ef67153e Update HLS stream processing 2023-07-08 09:33:31 +03:00
Alexey Khit
fded87aa33 Update stream info for MP4/MSE/HLS 2023-07-08 09:32:54 +03:00
Alexey Khit
52a4fc329c Clear html video resources on disconnect 2023-07-08 09:31:05 +03:00
Alexey Khit
ce61d5759c Fix html video autoplay in some cases 2023-07-08 09:30:41 +03:00
Alexey Khit
39cc4610e3 Add ESLinter and fix JS lint problems 2023-07-08 09:30:02 +03:00
agalindo
67b25015df Update de ffmpeg test after sync 2023-07-07 17:18:01 +02:00
Galindo, Alex
f0d627fa55 Revert "Add framerate parameter option to HTTPs"
This reverts commit f94cd16cb7.
2023-07-07 14:03:53 +02:00
agalindo
9809f41117 Merge branch 'master' of https://github.com/horttorrell32/go2rtc 2023-07-07 07:39:51 +02:00
Galindo, Alex
2ce72dbcca Add more ffmpeg Test 2023-07-06 16:27:23 +02:00
Alexey Khit
ddfeb6fae6 Fix default bin for ffmpeg transcoding to jpeg 2023-07-06 15:22:07 +03:00
Alex X
19130a4858 Merge pull request #414 from skrashevich/230504-patch-dockerfile.hardware
Update hardware.Dockerfile for multi-platform support
2023-07-06 15:18:46 +03:00
Alexey Khit
51b494b193 Add support webrtc/tcp mode to video player 2023-07-06 15:02:39 +03:00
Alexey Khit
fd3b3c9bf1 Replace MP4 stream mode to HLS mode 2023-07-06 15:02:39 +03:00
Alexey Khit
fa763399c2 Improve HLS processing 2023-07-06 15:02:39 +03:00
Alexey Khit
af2398c072 Move mp4 parse codecs func to pkg 2023-07-06 15:02:39 +03:00
Alexey Khit
19b0bc5f44 Update scripts readme 2023-07-06 15:02:39 +03:00
Galindo, Alex
f94cd16cb7 Add framerate parameter option to HTTPs
Some HTTP (a.g. JPEG or MJPEG) needs set the input framerate explicitly.
2023-07-06 12:08:49 +02:00
Alexey Khit
3246e7284c Update API description about WebRTC 2023-07-06 11:36:42 +03:00
Alexey Khit
9339957c13 Add Ezviz to cameras experience 2023-07-06 11:29:46 +03:00
Alexey Khit
4ca397da3d Update OpenAPI link 2023-07-06 11:28:02 +03:00
Galindo, Alex
f6936f7cee Allow add Input param without codecs changes.
Correct the Input Template to delete the 'input' parameter to allow set the copy codecs option, otherwise the '-vn -an' codecs option is selected.
2023-07-06 10:25:09 +02:00
Alexey Khit
bdafaef7dc Add api.html webpage 2023-07-06 11:21:52 +03:00
Alexey Khit
209d7b47d9 Rewrite FFmpeg devices and add support ALSA for Linux 2023-07-04 16:47:07 +03:00
Alexey Khit
4283ae1022 Add RepackG711 func for WebRTC (fix PCMA/PCMU audio) 2023-07-04 16:47:00 +03:00
Alexey Khit
c2a398211c Rewrite repack G711 func (for RTSP backchannel) 2023-07-04 16:46:54 +03:00
Alexey Khit
6c2f883f9e Fix OPUS transcoding quality 2023-07-04 10:37:40 +03:00
Alexey Khit
c34f9ae2b7 Support FFmpeg drawtext param #487 2023-07-03 00:46:35 +03:00
Alexey Khit
c29dd8c4e3 Support templates for FFmpeg raw param #487 2023-07-03 00:44:25 +03:00
Alexey Khit
9e65f18e08 Add interactive OpenAPI to readme 2023-07-02 20:51:40 +03:00
Alexey Khit
db3fb72ac8 Add OpenAPI 2023-07-02 20:47:31 +03:00
Alexey Khit
90cdfafcf5 Add Content-Type to WebRTC API 2023-07-02 20:46:56 +03:00
Alexey Khit
fa8d4e4807 Remove on the fly stream creation for security reason 2023-06-29 22:52:59 +03:00
Alexey Khit
37abe2ce0d Code refactoring after #274 2023-06-29 22:08:17 +03:00
Alexey Khit
1c3835f2a8 Merge pull request #274 from skrashevich/fix-exit-code-on-config-save 2023-06-29 22:04:08 +03:00
Alexey Khit
bc6e4f40bf Code refactoring after #352 2023-06-29 21:39:31 +03:00
Alexey Khit
ac5bcda492 Merge pull request #352 from skrashevich/patch-listen-tls 2023-06-29 21:35:19 +03:00
Alexey Khit
7bd42eb55f Fix onvif discovery close port 2023-06-29 20:29:35 +03:00
Alexey Khit
e4c7ffd1b4 Code refactoring after #462 2023-06-29 17:17:45 +03:00
Alexey Khit
d31cf5521b Merge pull request #462 from dbuezas/dvrip/discovery 2023-06-29 17:14:09 +03:00
Alexey Khit
9de980a63c Fix config tab showing byte string instead of text #479 2023-06-29 16:16:08 +03:00
Alexey Khit
74cef13479 Fix panic after RTSP reconnect feature #433 2023-06-29 16:09:17 +03:00
Alexey Khit
887a491077 Fix panic on processing RTCP from HomeKit cameras #287 2023-06-29 11:13:27 +03:00
Alexey Khit
253fc4c915 Code refactoring for SRTP 2023-06-29 11:13:00 +03:00
Alexey Khit
3a51fa2397 Fix panic with only audio for MP4/MSE #404 2023-06-29 10:55:43 +03:00
Alexey Khit
306451f94f Fix race on pcm pack backchannel #432 2023-06-28 20:25:40 +03:00
Alexey Khit
39811d121b Fix panic on empty path in RTSP link #474 2023-06-28 20:03:09 +03:00
Alexey Khit
99b962e7bb Fix panic on empty RTSP medias #481 2023-06-28 19:59:36 +03:00
Alex X
3dd14a826c Merge pull request #461 from dbuezas/master
For dvrip video codec, only compare least significant 4 bits
2023-06-16 22:48:04 +03:00
David Buezas
a99d7097b9 Revert ignoring high 4 bits and add 0x43 as an h265 code 2023-06-16 21:45:24 +02:00
Alexey Khit
4f97e119ac Update selectall checkbox on index page 2023-06-16 15:18:22 +03:00
Alex X
44ee0066a5 Merge pull request #466 from Vipas-ana/patch-1
Update index.html
2023-06-16 15:14:28 +03:00
Alexey Khit
e5e899450f Fix ws service not load origin from config #469 2023-06-16 15:07:38 +03:00
Alexey Khit
05a2f53b67 Update projects using section in readme 2023-06-16 15:01:42 +03:00
Alexey Khit
63bcaa836a Merge remote-tracking branch 'origin/master' 2023-06-16 15:00:10 +03:00
Alex X
ba68bcb89e Merge pull request #413 from skrashevich/230504-patch-hwdetect-darwin
Refactor video toolbox probe commands to use SVGA resolution in hardw…
2023-06-15 16:52:00 +03:00
Alex X
4a162c9a55 Merge pull request #471 from makuser/patch-1
Fix camera brand typo in README.md
2023-06-12 13:10:41 +03:00
Marc Kolly
c2f5f37f40 Fix camera brand typo in README.md 2023-06-12 10:56:53 +02:00
Vipas-ana
11201790d2 Update index.html
Don't add blank "src" because of the checked "selectall" box.
2023-06-07 10:04:00 -04:00
David Buezas
64804cbc87 Simplify code and improve error handling 2023-06-04 22:37:29 +02:00
David Buezas
75818d6967 Add dvrip discovery 2023-06-04 02:25:22 +02:00
David Buezas
14bb4b40f7 For dvrip video codec, only compare less significant byte 2023-06-03 12:00:35 +02:00
Alex X
0fdb0b128b Merge pull request #460 from dbuezas/master
Add mediaCode 0x12 to CodecH264 identifiers ini DVIRIP stream
2023-06-03 08:07:04 +03:00
David Buezas
fe28c32400 Add mediaCode 0x12 to CodecH264 identifiers ini DVIRIP stream 2023-06-01 21:22:37 +02:00
Alexey Khit
888159d2b6 Update API response mime type 2023-05-31 14:41:57 +03:00
Alexey Khit
397eb0b6ee Fix tests 2023-05-31 14:41:17 +03:00
Alexey Khit
ffeb473918 Remove broken tests 2023-05-31 14:36:00 +03:00
Alexey Khit
966bedd38c Fix MP4 with PCM on Android Telegram 2023-05-30 22:03:20 +03:00
Alexey Khit
0e270081fe Add content-type to API responses 2023-05-30 22:02:16 +03:00
Alexey Khit
1612f9c81e Fix AAC inside MP4 2023-05-25 06:49:04 +03:00
Alexey Khit
bff9b06d5d Add support filename query param for mp4 files 2023-05-25 06:47:51 +03:00
Alexey Khit
59555cfe1d Move WS API to separate module 2023-05-23 14:21:39 +03:00
Sergey Krashevich
c94d1e237d Merge remote-tracking branch 'remotes/upstream/master' into patch-listen-tls 2023-05-21 23:29:08 +03:00
Alexey Khit
82a8e07b66 Rewrite shell signal handling 2023-05-20 06:29:29 +03:00
Alexey Khit
e29307125c Add Nest source for WebRTC cameras 2023-05-20 06:28:33 +03:00
Alexey Khit
1eaacdb217 Add Hass API source for WebRTC cameras 2023-05-20 06:26:05 +03:00
Alexey Khit
c09438d3d0 Set prefer_tcp flag for ffmpeg 2023-05-16 18:39:39 +03:00
Alexey Khit
8b126c0d37 Add support RTSP over WebSocket 2023-05-06 14:31:46 +03:00
Alexey Khit
3139189975 Move ParseQuery from ffmpeg to streams module 2023-05-06 14:29:35 +03:00
Alexey Khit
4fe078c7c0 ONVIF code refactoring 2023-05-05 10:07:14 +03:00
Alexey Khit
083ec127fd Fix video timestamp accuracy 2023-05-05 09:45:55 +03:00
Alexey Khit
bcb9756aca Update readme about ONVIF 2023-05-05 09:08:13 +03:00
Sergey Krashevich
981974eac9 Update hardware.Dockerfile 2023-05-04 22:45:39 +03:00
Sergey Krashevich
5b29306d4f Refactor video toolbox probe commands to use SVGA resolution in hardware_darwin.go 2023-05-04 22:22:00 +03:00
Alexey Khit
e89c5cb429 Add bages to readme 2023-05-04 17:45:17 +03:00
Alexey Khit
04f263aa15 Add binary for old Raspberry 1 and Zero 2023-05-04 17:40:54 +03:00
Sergey Krashevich
af717b2172 add tls support 2023-04-14 18:28:03 +03:00
Sergey Krashevich
91a7b5be27 Update editor.html 2023-02-27 05:37:17 +03:00
469 changed files with 37299 additions and 11799 deletions

280
.github/workflows/build.yml vendored Normal file
View File

@@ -0,0 +1,280 @@
name: Build and Push
on:
workflow_dispatch:
push:
branches:
- 'master'
tags:
- 'v*'
jobs:
build-binaries:
name: Build binaries
runs-on: ubuntu-latest
env: { CGO_ENABLED: 0 }
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Go
uses: actions/setup-go@v5
with: { go-version: '1.25' }
- name: Build go2rtc_win64
env: { GOOS: windows, GOARCH: amd64 }
run: go build -ldflags "-s -w" -trimpath
- name: Upload go2rtc_win64
uses: actions/upload-artifact@v4
with: { name: go2rtc_win64, path: go2rtc.exe }
- name: Build go2rtc_win32
env: { GOOS: windows, GOARCH: 386 }
run: go build -ldflags "-s -w" -trimpath
- name: Upload go2rtc_win32
uses: actions/upload-artifact@v4
with: { name: go2rtc_win32, path: go2rtc.exe }
- name: Build go2rtc_win_arm64
env: { GOOS: windows, GOARCH: arm64 }
run: go build -ldflags "-s -w" -trimpath
- name: Upload go2rtc_win_arm64
uses: actions/upload-artifact@v4
with: { name: go2rtc_win_arm64, path: go2rtc.exe }
- name: Build go2rtc_linux_amd64
env: { GOOS: linux, GOARCH: amd64 }
run: go build -ldflags "-s -w" -trimpath
- name: Upload go2rtc_linux_amd64
uses: actions/upload-artifact@v4
with: { name: go2rtc_linux_amd64, path: go2rtc }
- name: Build go2rtc_linux_i386
env: { GOOS: linux, GOARCH: 386 }
run: go build -ldflags "-s -w" -trimpath
- name: Upload go2rtc_linux_i386
uses: actions/upload-artifact@v4
with: { name: go2rtc_linux_i386, path: go2rtc }
- name: Build go2rtc_linux_arm64
env: { GOOS: linux, GOARCH: arm64 }
run: go build -ldflags "-s -w" -trimpath
- name: Upload go2rtc_linux_arm64
uses: actions/upload-artifact@v4
with: { name: go2rtc_linux_arm64, path: go2rtc }
- name: Build go2rtc_linux_arm
env: { GOOS: linux, GOARCH: arm, GOARM: 7 }
run: go build -ldflags "-s -w" -trimpath
- name: Upload go2rtc_linux_arm
uses: actions/upload-artifact@v4
with: { name: go2rtc_linux_arm, path: go2rtc }
- name: Build go2rtc_linux_armv6
env: { GOOS: linux, GOARCH: arm, GOARM: 6 }
run: go build -ldflags "-s -w" -trimpath
- name: Upload go2rtc_linux_armv6
uses: actions/upload-artifact@v4
with: { name: go2rtc_linux_armv6, path: go2rtc }
- name: Build go2rtc_linux_mipsel
env: { GOOS: linux, GOARCH: mipsle }
run: go build -ldflags "-s -w" -trimpath
- name: Upload go2rtc_linux_mipsel
uses: actions/upload-artifact@v4
with: { name: go2rtc_linux_mipsel, path: go2rtc }
- name: Build go2rtc_mac_amd64
env: { GOOS: darwin, GOARCH: amd64 }
run: go build -ldflags "-s -w" -trimpath
- name: Upload go2rtc_mac_amd64
uses: actions/upload-artifact@v4
with: { name: go2rtc_mac_amd64, path: go2rtc }
- name: Build go2rtc_mac_arm64
env: { GOOS: darwin, GOARCH: arm64 }
run: go build -ldflags "-s -w" -trimpath
- name: Upload go2rtc_mac_arm64
uses: actions/upload-artifact@v4
with: { name: go2rtc_mac_arm64, path: go2rtc }
- name: Build go2rtc_freebsd_amd64
env: { GOOS: freebsd, GOARCH: amd64 }
run: go build -ldflags "-s -w" -trimpath
- name: Upload go2rtc_freebsd_amd64
uses: actions/upload-artifact@v4
with: { name: go2rtc_freebsd_amd64, path: go2rtc }
- name: Build go2rtc_freebsd_arm64
env: { GOOS: freebsd, GOARCH: arm64 }
run: go build -ldflags "-s -w" -trimpath
- name: Upload go2rtc_freebsd_arm64
uses: actions/upload-artifact@v4
with: { name: go2rtc_freebsd_arm64, path: go2rtc }
docker-master:
name: Build docker master
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: |
${{ github.repository }}
ghcr.io/${{ github.repository }}
tags: |
type=ref,event=branch
type=semver,pattern={{version}},enable=false
type=match,pattern=v(.*),group=1
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to DockerHub
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
file: docker/Dockerfile
platforms: |
linux/amd64
linux/386
linux/arm/v6
linux/arm/v7
linux/arm64/v8
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
docker-hardware:
name: Build docker hardware
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Docker meta
id: meta-hw
uses: docker/metadata-action@v5
with:
images: |
${{ github.repository }}
ghcr.io/${{ github.repository }}
flavor: |
suffix=-hardware,onlatest=true
latest=auto
tags: |
type=ref,event=branch
type=semver,pattern={{version}},enable=false
type=match,pattern=v(.*),group=1
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to DockerHub
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
file: docker/hardware.Dockerfile
platforms: linux/amd64
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta-hw.outputs.tags }}
labels: ${{ steps.meta-hw.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
docker-rockchip:
name: Build docker rockchip
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Docker meta
id: meta-rk
uses: docker/metadata-action@v5
with:
images: |
${{ github.repository }}
ghcr.io/${{ github.repository }}
flavor: |
suffix=-rockchip,onlatest=true
latest=auto
tags: |
type=ref,event=branch
type=semver,pattern={{version}},enable=false
type=match,pattern=v(.*),group=1
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to DockerHub
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
file: docker/rockchip.Dockerfile
platforms: linux/arm64
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta-rk.outputs.tags }}
labels: ${{ steps.meta-rk.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max

View File

@@ -1,78 +0,0 @@
name: docker
on:
workflow_dispatch:
push:
branches:
- 'master'
tags:
- 'v*'
jobs:
build-and-push:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Docker meta
id: meta
uses: docker/metadata-action@v4
with:
images: ${{ github.repository }}
tags: |
type=ref,event=branch
type=semver,pattern={{version}},enable=false
type=match,pattern=v(.*),group=1
- name: Docker meta Hardware
id: meta-hw
uses: docker/metadata-action@v4
with:
images: ${{ github.repository }}
flavor: |
suffix=-hardware
latest=false
tags: |
type=ref,event=branch
type=semver,pattern={{version}},enable=false
type=match,pattern=v(.*),group=1
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to DockerHub
if: github.event_name != 'pull_request'
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v4
with:
context: .
platforms: |
linux/amd64
linux/386
linux/arm/v7
linux/arm64/v8
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Build and push Hardware
uses: docker/build-push-action@v4
with:
context: .
file: hardware.Dockerfile
platforms: linux/amd64
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta-hw.outputs.tags }}
labels: ${{ steps.meta-hw.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max

View File

@@ -25,13 +25,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Setup Pages
uses: actions/configure-pages@v3
uses: actions/configure-pages@v4
- name: Upload artifact
uses: actions/upload-pages-artifact@v1
uses: actions/upload-pages-artifact@v3
with:
path: './website'
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v2
uses: actions/deploy-pages@v4

View File

@@ -1,99 +0,0 @@
name: release
on:
workflow_dispatch:
# push:
# tags:
# - 'v*'
jobs:
build-and-release:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Generate changelog
run: |
echo -e "$(git log $(git describe --tags --abbrev=0)..HEAD --oneline | awk '{print "- "$0}')" > CHANGELOG.md
- name: install lipo
run: |
curl -L -o /tmp/lipo https://github.com/konoui/lipo/releases/latest/download/lipo_Linux_amd64
chmod +x /tmp/lipo
mv /tmp/lipo /usr/local/bin
- name: Build Go binaries
run: |
#!/bin/bash
export CGO_ENABLED=0
mkdir -p artifacts
export GOOS=windows
export GOARCH=amd64
export FILENAME=artifacts/go2rtc_win64.zip
go build -ldflags "-s -w" -trimpath && 7z a -mx9 -sdel "$FILENAME" go2rtc.exe
export GOOS=windows
export GOARCH=386
export FILENAME=artifacts/go2rtc_win32.zip
go build -ldflags "-s -w" -trimpath && 7z a -mx9 -sdel "$FILENAME" go2rtc.exe
export GOOS=windows
export GOARCH=arm64
export FILENAME=artifacts/go2rtc_win_arm64.zip
go build -ldflags "-s -w" -trimpath && 7z a -mx9 -sdel "$FILENAME" go2rtc.exe
export GOOS=linux
export GOARCH=amd64
export FILENAME=artifacts/go2rtc_linux_amd64
go build -ldflags "-s -w" -trimpath -o "$FILENAME"
export GOOS=linux
export GOARCH=386
export FILENAME=artifacts/go2rtc_linux_i386
go build -ldflags "-s -w" -trimpath -o "$FILENAME"
export GOOS=linux
export GOARCH=arm64
export FILENAME=artifacts/go2rtc_linux_arm64
go build -ldflags "-s -w" -trimpath -o "$FILENAME"
export GOOS=linux
export GOARCH=arm
export GOARM=7
export FILENAME=artifacts/go2rtc_linux_arm
go build -ldflags "-s -w" -trimpath -o "$FILENAME"
export GOOS=linux
export GOARCH=mipsle
export FILENAME=artifacts/go2rtc_linux_mipsel
go build -ldflags "-s -w" -trimpath -o "$FILENAME"
export GOOS=darwin
export GOARCH=amd64
go build -ldflags "-s -w" -trimpath -o go2rtc.amd64
export GOOS=darwin
export GOARCH=arm64
go build -ldflags "-s -w" -trimpath -o go2rtc.arm64
export FILENAME=artifacts/go2rtc_mac_universal.zip
lipo -output go2rtc -create go2rtc.arm64 go2rtc.amd64 && 7z a -mx9 -sdel "$FILENAME" go2rtc
parallel --jobs $(nproc) "upx {}" ::: artifacts/go2rtc_linux_*
- name: Setup tmate session
uses: mxschmitt/action-tmate@v3
if: ${{ failure() }}
- name: Set env
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
- name: Create GitHub release
uses: softprops/action-gh-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
files: artifacts/*
generate_release_notes: true
name: Release ${{ env.RELEASE_VERSION }}
body_path: CHANGELOG.md
draft: false
prerelease: false

View File

@@ -1,11 +1,11 @@
name: Test Build and Run
on:
push:
branches:
- '*'
pull_request:
merge_group:
# push:
# branches:
# - '*'
# pull_request:
# merge_group:
workflow_dispatch:
jobs:
@@ -21,12 +21,12 @@ jobs:
GOARCH: ${{ matrix.arch }}
steps:
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v4
- name: Setup Go
uses: actions/setup-go@v2
uses: actions/setup-go@v5
with:
go-version: '1.20'
go-version: '1.24'
- name: Build Go binary
run: go build -ldflags "-s -w" -trimpath -o ./go2rtc
@@ -70,15 +70,16 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
uses: docker/setup-buildx-action@v3
- name: Build and push
uses: docker/build-push-action@v4
uses: docker/build-push-action@v5
with:
context: .
file: docker/Dockerfile
platforms: linux/${{ matrix.platform }}
push: false
load: true
@@ -89,10 +90,10 @@ jobs:
- name: Build and push Hardware
if: matrix.platform == 'amd64'
uses: docker/build-push-action@v4
uses: docker/build-push-action@v5
with:
context: .
file: hardware.Dockerfile
file: docker/hardware.Dockerfile
platforms: linux/amd64
push: false
load: true

12
.gitignore vendored
View File

@@ -1,8 +1,14 @@
.idea/
.tmp/
go2rtc.yaml
go2rtc.json
go2rtc_freebsd*
go2rtc_linux*
go2rtc_mac*
go2rtc_win*
0_test.go
.DS_Store

792
README.md

File diff suppressed because it is too large Load Diff

117
api/README.md Normal file
View File

@@ -0,0 +1,117 @@
# API
Fill free to make any API design proposals.
## HTTP API
Interactive [OpenAPI](https://alexxit.github.io/go2rtc/api/).
`www/stream.html` - universal viewer with support params in URL:
- multiple streams on page `src=camera1&src=camera2...`
- stream technology autoselection `mode=webrtc,webrtc/tcp,mse,hls,mp4,mjpeg`
- stream technology comparison `src=camera1&mode=webrtc&mode=mse&mode=mp4`
- player width setting in pixels `width=320px` or percents `width=50%`
`www/webrtc.html` - WebRTC viewer with support two way audio and params in URL:
- `media=video+audio` - simple viewer
- `media=video+audio+microphone` - two way audio from camera
- `media=camera+microphone` - stream from browser
- `media=display+speaker` - stream from desktop
## JavaScript API
- You can write your viewer from the scratch
- You can extend the built-in viewer - `www/video-rtc.js`
- Check example - `www/video-stream.js`
- Check example - https://github.com/AlexxIT/WebRTC
`video-rtc.js` features:
- support technologies:
- WebRTC over UDP or TCP
- MSE or HLS or MP4 or MJPEG over WebSocket
- automatic selection best technology according on:
- codecs inside your stream
- current browser capabilities
- current network configuration
- automatic stop stream while browser or page not active
- automatic stop stream while player not inside page viewport
- automatic reconnection
Technology selection based on priorities:
1. Video and Audio better than just Video
2. H265 better than H264
3. WebRTC better than MSE, than HLS, than MJPEG
## WebSocket API
Endpoint: `/api/ws`
Query parameters:
- `src` (required) - Stream name
### WebRTC
Request SDP:
```json
{"type":"webrtc/offer","value":"v=0\r\n..."}
```
Response SDP:
```json
{"type":"webrtc/answer","value":"v=0\r\n..."}
```
Request/response candidate:
- empty value also allowed and optional
```json
{"type":"webrtc/candidate","value":"candidate:3277516026 1 udp 2130706431 192.168.1.123 54321 typ host"}
```
### MSE
Request:
- codecs list optional
```json
{"type":"mse","value":"avc1.640029,avc1.64002A,avc1.640033,hvc1.1.6.L153.B0,mp4a.40.2,mp4a.40.5,flac,opus"}
```
Response:
```json
{"type":"mse","value":"video/mp4; codecs=\"avc1.64001F,mp4a.40.2\""}
```
### HLS
Request:
```json
{"type":"hls","value":"avc1.640029,avc1.64002A,avc1.640033,hvc1.1.6.L153.B0,mp4a.40.2,mp4a.40.5,flac"}
```
Response:
- you MUST rewrite full HTTP path to `http://192.168.1.123:1984/api/hls/playlist.m3u8`
```json
{"type":"hls","value":"#EXTM3U\n#EXT-X-STREAM-INF:BANDWIDTH=1000000,CODECS=\"avc1.64001F,mp4a.40.2\"\nhls/playlist.m3u8?id=DvmHdd9w"}
```
### MJPEG
Request/response:
```json
{"type":"mjpeg"}
```

537
api/openapi.yaml Normal file
View File

@@ -0,0 +1,537 @@
openapi: 3.1.0
info:
title: go2rtc
license: { name: MIT,url: https://opensource.org/licenses/MIT }
version: 1.0.0
contact: { url: https://github.com/AlexxIT/go2rtc }
description: |
Ultimate camera streaming application with support RTSP, RTMP, HTTP-FLV, WebRTC, MSE, HLS, MP4, MJPEG, HomeKit, FFmpeg, etc.
servers:
- url: http://localhost:1984
components:
parameters:
stream_src_path:
name: src
in: path
description: Source stream name
required: true
schema: { type: string }
example: camera1
stream_dst_path:
name: dst
in: path
description: Destination stream name
required: true
schema: { type: string }
example: camera1
stream_src_query:
name: src
in: query
description: Source stream name
required: true
schema: { type: string }
example: camera1
mp4_filter:
name: mp4
in: query
description: MP4 codecs filter
required: false
schema:
type: string
enum: [ "", flac, all ]
example: flac
video_filter:
name: video
in: query
description: Video codecs filter
schema:
type: string
enum: [ "", all, h264, h265, mjpeg ]
example: h264,h265
audio_filter:
name: audio
in: query
description: Audio codecs filter
schema:
type: string
enum: [ "", all, aac, opus, pcm, pcmu, pcma ]
example: aac
responses:
discovery:
description: ""
content:
application/json:
example: { streams: [ { "name": "Camera 1","url": "..." } ] }
webtorrent:
description: ""
content:
application/json:
example: { share: AKDypPy4zz, pwd: H0Km1HLTTP }
tags:
- name: Application
description: "[Module: API](https://github.com/AlexxIT/go2rtc#module-api)"
- name: Config
description: "[Configuration](https://github.com/AlexxIT/go2rtc#configuration)"
- name: Streams list
description: "[Module: Streams](https://github.com/AlexxIT/go2rtc#module-streams)"
- name: Consume stream
- name: Snapshot
- name: Produce stream
- name: Discovery
- name: ONVIF
- name: RTSPtoWebRTC
- name: WebTorrent
description: "[Module: WebTorrent](https://github.com/AlexxIT/go2rtc#module-webtorrent)"
- name: Debug
paths:
/api:
get:
summary: Get application info
tags: [ Application ]
responses:
"200":
description: ""
content:
application/json:
example: { config_path: "/config/go2rtc.yaml",host: "192.168.1.123:1984",rtsp: { listen: ":8554",default_query: "video&audio" },version: "1.5.0" }
/api/exit:
post:
summary: Close application
tags: [ Application ]
parameters:
- name: code
in: query
description: Application exit code
required: false
schema: { type: integer }
example: 100
responses:
default:
description: Default response
/api/restart:
post:
summary: Restart Daemon
description: Restarts the daemon.
tags: [ Application ]
responses:
default:
description: Default response
/api/config:
get:
summary: Get main config file content
tags: [ Config ]
responses:
"200":
description: ""
content:
application/yaml: { example: "streams:..." }
post:
summary: Rewrite main config file
tags: [ Config ]
requestBody:
content:
"*/*": { example: "streams:..." }
responses:
default:
description: Default response
patch:
summary: Merge changes to main config file
tags: [ Config ]
requestBody:
content:
"*/*": { example: "streams:..." }
responses:
default:
description: Default response
/api/streams:
get:
summary: Get all streams info
tags: [ Streams list ]
responses:
"200":
description: ""
content:
application/json: { example: { camera1: { producers: [ ],consumers: [ ] } } }
put:
summary: Create new stream
tags: [ Streams list ]
parameters:
- name: src
in: query
description: Stream source (URI)
required: true
schema: { type: string }
example: "rtsp://rtsp:12345678@192.168.1.123/av_stream/ch0"
- name: name
in: query
description: Stream name
required: false
schema: { type: string }
example: camera1
responses:
default:
description: Default response
patch:
summary: Update stream source
tags: [ Streams list ]
parameters:
- name: src
in: query
description: Stream source (URI)
required: true
schema: { type: string }
example: "rtsp://rtsp:12345678@192.168.1.123/av_stream/ch0"
- name: name
in: query
description: Stream name
required: true
schema: { type: string }
example: camera1
responses:
default:
description: Default response
delete:
summary: Delete stream
tags: [ Streams list ]
parameters:
- name: src
in: query
description: Stream name
required: true
schema: { type: string }
example: camera1
responses:
default:
description: Default response
post:
summary: Send stream from source to destination
description: "[Stream to camera](https://github.com/AlexxIT/go2rtc#stream-to-camera)"
tags: [ Streams list ]
parameters:
- name: src
in: query
description: Stream source (URI)
required: true
schema: { type: string }
example: "ffmpeg:http://example.com/song.mp3#audio=pcma#input=file"
- name: dst
in: query
description: Destination stream 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
tags: [ Consume stream ]
parameters:
- $ref: "#/components/parameters/stream_src_path"
responses:
"200":
description: ""
content:
application/json:
example: { producers: [ { url: "rtsp://rtsp:12345678@192.168.1.123/av_stream/ch0" } ], consumers: [ ] }
/api/webrtc?src={src}:
post:
summary: Get stream in WebRTC format (WHEP)
description: "[Module: WebRTC](https://github.com/AlexxIT/go2rtc#module-webrtc)"
tags: [ Consume stream ]
parameters:
- $ref: "#/components/parameters/stream_src_path"
requestBody:
description: |
Support:
- JSON format (`Content-Type: application/json`)
- WHEP standard (`Content-Type: application/sdp`)
- raw SDP (`Content-Type: anything`)
required: true
content:
application/json: { example: { type: offer, sdp: "v=0..." } }
"application/sdp": { example: "v=0..." }
"*/*": { example: "v=0..." }
responses:
"200":
description: "Response on JSON or raw SDP"
content:
application/json: { example: { type: answer, sdp: "v=0..." } }
application/sdp: { example: "v=0..." }
"201":
description: "Response on `Content-Type: application/sdp`"
content:
application/sdp: { example: "v=0..." }
/api/stream.mp4?src={src}:
get:
summary: Get stream in MP4 format (HTTP progressive)
description: "[Module: MP4](https://github.com/AlexxIT/go2rtc#module-mp4)"
tags: [ Consume stream ]
parameters:
- $ref: "#/components/parameters/stream_src_path"
- name: duration
in: query
description: Limit the length of the stream in seconds
required: false
schema: { type: string }
example: 15
- name: filename
in: query
description: Download as a file with this name
required: false
schema: { type: string }
example: camera1.mp4
- $ref: "#/components/parameters/mp4_filter"
- $ref: "#/components/parameters/video_filter"
- $ref: "#/components/parameters/audio_filter"
responses:
200:
description: ""
content: { video/mp4: { example: "" } }
/api/stream.m3u8?src={src}:
get:
summary: Get stream in HLS format
description: "[Module: HLS](https://github.com/AlexxIT/go2rtc#module-hls)"
tags: [ Consume stream ]
parameters:
- $ref: "#/components/parameters/stream_src_path"
- $ref: "#/components/parameters/mp4_filter"
- $ref: "#/components/parameters/video_filter"
- $ref: "#/components/parameters/audio_filter"
responses:
200:
description: ""
content: { application/vnd.apple.mpegurl: { example: "" } }
/api/stream.mjpeg?src={src}:
get:
summary: Get stream in MJPEG format
description: "[Module: MJPEG](https://github.com/AlexxIT/go2rtc#module-mjpeg)"
tags: [ Consume stream ]
parameters:
- $ref: "#/components/parameters/stream_src_path"
responses:
200:
description: ""
content: { multipart/x-mixed-replace: { example: "" } }
/api/frame.jpeg?src={src}:
get:
summary: Get snapshot in JPEG format
description: "[Module: MJPEG](https://github.com/AlexxIT/go2rtc#module-mjpeg)"
tags: [ Snapshot ]
parameters:
- $ref: "#/components/parameters/stream_src_path"
responses:
200:
description: ""
content: { image/jpeg: { example: "" } }
/api/frame.mp4?src={src}:
get:
summary: Get snapshot in MP4 format
description: "[Module: MP4](https://github.com/AlexxIT/go2rtc#module-mp4)"
tags: [ Snapshot ]
parameters:
- $ref: "#/components/parameters/stream_src_path"
responses:
200:
description: ""
content: { video/mp4: { example: "" } }
/api/webrtc?dst={dst}:
post:
summary: Post stream in WebRTC format
description: "[Incoming: WebRTC/WHIP](https://github.com/AlexxIT/go2rtc#incoming-webrtcwhip)"
tags: [ Produce stream ]
parameters:
- $ref: "#/components/parameters/stream_dst_path"
responses:
default:
description: Default response
/api/stream.flv?dst={dst}:
post:
summary: Post stream in FLV format
description: "[Incoming sources](https://github.com/AlexxIT/go2rtc#incoming-sources)"
tags: [ Produce stream ]
parameters:
- $ref: "#/components/parameters/stream_dst_path"
responses:
default:
description: Default response
/api/stream.ts?dst={dst}:
post:
summary: Post stream in MPEG-TS format
description: "[Incoming sources](https://github.com/AlexxIT/go2rtc#incoming-sources)"
tags: [ Produce stream ]
parameters:
- $ref: "#/components/parameters/stream_dst_path"
responses:
default:
description: Default response
/api/stream.mjpeg?dst={dst}:
post:
summary: Post stream in MJPEG format
description: "[Incoming sources](https://github.com/AlexxIT/go2rtc#incoming-sources)"
tags: [ Produce stream ]
parameters:
- $ref: "#/components/parameters/stream_dst_path"
responses:
default:
description: Default response
/api/dvrip:
get:
summary: DVRIP cameras discovery
description: "[Source: DVRIP](https://github.com/AlexxIT/go2rtc#source-dvrip)"
tags: [ Discovery ]
responses:
default:
description: Default response
/api/ffmpeg/devices:
get:
summary: FFmpeg USB devices discovery
description: "[Source: FFmpeg Device](https://github.com/AlexxIT/go2rtc#source-ffmpeg-device)"
tags: [ Discovery ]
responses:
default:
description: Default response
/api/ffmpeg/hardware:
get:
summary: FFmpeg hardware transcoding discovery
description: "[Hardware acceleration](https://github.com/AlexxIT/go2rtc/wiki/Hardware-acceleration)"
tags: [ Discovery ]
responses:
default:
description: Default response
/api/hass:
get:
summary: Home Assistant cameras discovery
description: "[Source: Hass](https://github.com/AlexxIT/go2rtc#source-hass)"
tags: [ Discovery ]
responses:
default:
description: Default response
/api/homekit:
get:
summary: HomeKit cameras discovery
description: "[Source: HomeKit](https://github.com/AlexxIT/go2rtc#source-homekit)"
tags: [ Discovery ]
responses:
default:
description: Default response
/api/nest:
get:
summary: Nest cameras discovery
tags: [ Discovery ]
responses:
default:
description: Default response
/api/onvif:
get:
summary: ONVIF cameras discovery
description: "[Source: ONVIF](https://github.com/AlexxIT/go2rtc#source-onvif)"
tags: [ Discovery ]
responses:
default:
description: Default response
/api/roborock:
get:
summary: Roborock vacuums discovery
description: "[Source: Roborock](https://github.com/AlexxIT/go2rtc#source-roborock)"
tags: [ Discovery ]
responses:
default:
description: Default response
/onvif/:
get:
summary: ONVIF server implementation
description: Simple realisation of the ONVIF protocol. Accepts any suburl requests
tags: [ ONVIF ]
responses:
default:
description: Default response
/stream/:
get:
summary: RTSPtoWebRTC server implementation
description: Simple API for support [RTSPtoWebRTC](https://www.home-assistant.io/integrations/rtsp_to_webrtc/) integration
tags: [ RTSPtoWebRTC ]
responses:
default:
description: Default response
/api/webtorrent?src={src}:
get:
summary: Get WebTorrent share info
tags: [ WebTorrent ]
parameters:
- $ref: "#/components/parameters/stream_src_path"
responses:
200: { $ref: "#/components/responses/webtorrent" }
post:
summary: Add WebTorrent share
tags: [ WebTorrent ]
parameters:
- $ref: "#/components/parameters/stream_src_path"
responses:
200: { $ref: "#/components/responses/webtorrent" }
delete:
summary: Delete WebTorrent share
tags: [ WebTorrent ]
parameters:
- $ref: "#/components/parameters/stream_src_path"
responses:
default:
description: Default response
/api/webtorrent:
get:
summary: Get all WebTorrent shares info
tags: [ WebTorrent ]
responses:
200: { $ref: "#/components/responses/discovery" }
/api/stack:
get:
summary: Show list unknown goroutines
tags: [ Debug ]
responses:
200:
description: ""
content: { text/plain: { example: "" } }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 295 KiB

After

Width:  |  Height:  |  Size: 154 KiB

BIN
assets/logo.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 KiB

BIN
assets/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

View File

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

50
docker/README.md Normal file
View File

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

View File

@@ -1,18 +1,20 @@
# syntax=docker/dockerfile:labs
# 0. Prepare images
# only debian 12 (bookworm) has latest ffmpeg
ARG DEBIAN_VERSION="bookworm-slim"
ARG GO_VERSION="1.20-buster"
ARG NGROK_VERSION="3"
FROM debian:${DEBIAN_VERSION} AS base
FROM golang:${GO_VERSION} AS go
FROM ngrok/ngrok:${NGROK_VERSION} AS ngrok
# only debian 13 (trixie) has latest ffmpeg
# https://packages.debian.org/trixie/ffmpeg
ARG DEBIAN_VERSION="trixie-slim"
ARG GO_VERSION="1.25-bookworm"
# 1. Build go2rtc binary
FROM go AS build
FROM --platform=$BUILDPLATFORM golang:${GO_VERSION} AS build
ARG TARGETPLATFORM
ARG TARGETOS
ARG TARGETARCH
ENV GOOS=${TARGETOS}
ENV GOARCH=${TARGETARCH}
WORKDIR /build
@@ -24,31 +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 bookworm non-free' > /etc/apt/sources.list.d/debian-non-free.list && \
apt-get -y update && apt-get -y install tini ffmpeg \
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 ffmpeg tini \
python3 curl jq \
intel-media-va-driver-non-free \
libasound2-plugins
COPY --link --from=rootfs / /
mesa-va-drivers \
libasound2-plugins && \
apt-get clean && rm -rf /var/lib/apt/lists/*
COPY --from=build /build/go2rtc /usr/local/bin/
ENTRYPOINT ["/usr/bin/tini", "--"]
VOLUME /config

View File

@@ -0,0 +1,50 @@
# syntax=docker/dockerfile:labs
# 0. Prepare images
ARG PYTHON_VERSION="3.13-slim-bookworm"
ARG GO_VERSION="1.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"]

View File

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

View File

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

View File

@@ -4,9 +4,7 @@ import (
"github.com/AlexxIT/go2rtc/internal/app"
"github.com/AlexxIT/go2rtc/internal/rtsp"
"github.com/AlexxIT/go2rtc/internal/streams"
"os"
"os/signal"
"syscall"
"github.com/AlexxIT/go2rtc/pkg/shell"
)
func main() {
@@ -15,9 +13,5 @@ func main() {
rtsp.Init()
sigs := make(chan os.Signal, 1)
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
<-sigs
println("exit OK")
shell.RunUntilSignal()
}

View File

@@ -0,0 +1,123 @@
package main
import (
"encoding/json"
"os"
"github.com/AlexxIT/go2rtc/pkg/hap"
)
var servs = map[string]string{
"3E": "Accessory Information",
"7E": "Security System",
"85": "Motion Sensor",
"96": "Battery",
"A2": "Protocol Information",
"110": "Camera RTP Stream Management",
"112": "Microphone",
"113": "Speaker",
"121": "Doorbell",
"129": "Data Stream Transport Management",
"204": "Camera Recording Management",
"21A": "Camera Operating Mode",
"22A": "Wi-Fi Transport",
"239": "Accessory Runtime Information",
}
var chars = map[string]string{
"14": "Identify",
"20": "Manufacturer",
"21": "Model",
"23": "Name",
"30": "Serial Number",
"52": "Firmware Revision",
"53": "Hardware Revision",
"220": "Product Data",
"A6": "Accessory Flags",
"22": "Motion Detected",
"75": "Status Active",
"11A": "Mute",
"119": "Volume",
"B0": "Active",
"209": "Selected Camera Recording Configuration",
"207": "Supported Audio Recording Configuration",
"205": "Supported Camera Recording Configuration",
"206": "Supported Video Recording Configuration",
"226": "Recording Audio Active",
"223": "Event Snapshots Active",
"225": "Periodic Snapshots Active",
"21B": "HomeKit Camera Active",
"21C": "Third Party Camera Active",
"21D": "Camera Operating Mode Indicator",
"11B": "Night Vision",
//"129": "Supported Data Stream Transport Configuration",
"37": "Version",
"131": "Setup Data Stream Transport",
"130": "Supported Data Stream Transport Configuration",
"120": "Streaming Status",
"115": "Supported Audio Stream Configuration",
"116": "Supported RTP Configuration",
"114": "Supported Video Stream Configuration",
"117": "Selected RTP Stream Configuration",
"118": "Setup Endpoints",
"22B": "Current Transport",
"22C": "Wi-Fi Capabilities",
"22D": "Wi-Fi Configuration Control",
"23C": "Ping",
"68": "Battery Level",
"79": "Status Low Battery",
"8F": "Charging State",
"73": "Programmable Switch Event",
"232": "Operating State Response",
"66": "Security System Current State",
"67": "Security System Target State",
}
func main() {
src := os.Args[1]
dst := os.Args[2]
f, err := os.Open(src)
if err != nil {
panic(err)
}
var v hap.JSONAccessories
if err = json.NewDecoder(f).Decode(&v); err != nil {
panic(err)
}
for _, acc := range v.Value {
for _, srv := range acc.Services {
if srv.Desc == "" {
srv.Desc = servs[srv.Type]
}
for _, chr := range srv.Characters {
if chr.Desc == "" {
chr.Desc = chars[chr.Type]
}
}
}
}
f, err = os.Create(dst)
if err != nil {
panic(err)
}
enc := json.NewEncoder(f)
enc.SetIndent("", " ")
if err = enc.Encode(v); err != nil {
panic(err)
}
}

39
examples/mdns/main.go Normal file
View File

@@ -0,0 +1,39 @@
package main
import (
"log"
"os"
"github.com/AlexxIT/go2rtc/pkg/mdns"
)
func main() {
var service = mdns.ServiceHAP
if len(os.Args) >= 2 {
service = os.Args[1]
}
onentry := func(entry *mdns.ServiceEntry) bool {
log.Printf("name=%s, addr=%s, info=%s\n", entry.Name, entry.Addr(), entry.Info)
return false
}
var err error
if len(os.Args) >= 3 {
host := os.Args[2]
log.Printf("run discovery service=%s host=%s\n", service, host)
err = mdns.QueryOrDiscovery(host, service, onentry)
} else {
log.Printf("run discovery service=%s\n", service)
err = mdns.Discovery(service, onentry)
}
if err != nil {
log.Println(err)
}
}

View File

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

View File

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

View File

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

82
go.mod
View File

@@ -1,61 +1,49 @@
module github.com/AlexxIT/go2rtc
go 1.20
go 1.23.0
require (
github.com/brutella/hap v0.0.17
github.com/deepch/vdk v0.0.19
github.com/gorilla/websocket v1.5.0
github.com/hashicorp/mdns v1.0.5
github.com/pion/ice/v2 v2.3.1
github.com/pion/interceptor v0.1.12
github.com/pion/rtcp v1.2.10
github.com/pion/rtp v1.7.13
github.com/pion/sdp/v3 v3.0.6
github.com/pion/srtp/v2 v2.0.12
github.com/pion/stun v0.4.0
github.com/pion/webrtc/v3 v3.1.58
github.com/rs/zerolog v1.29.0
github.com/sigurn/crc16 v0.0.0-20211026045750-20ab5afb07e3
github.com/asticode/go-astits v1.13.0
github.com/expr-lang/expr v1.17.5
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.66
github.com/pion/ice/v4 v4.0.10
github.com/pion/interceptor v0.1.40
github.com/pion/rtcp v1.2.15
github.com/pion/rtp v1.8.20
github.com/pion/sdp/v3 v3.0.14
github.com/pion/srtp/v3 v3.0.6
github.com/pion/stun/v3 v3.0.0
github.com/pion/webrtc/v4 v4.1.3
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.8.2
github.com/stretchr/testify v1.10.0
github.com/tadglines/go-pkgs v0.0.0-20210623144937-b983b20f54f9
golang.org/x/crypto v0.39.0
gopkg.in/yaml.v3 v3.0.1
)
require (
github.com/brutella/dnssd v1.2.5 // indirect
github.com/asticode/go-astikit v0.56.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/go-chi/chi v1.5.4 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/kr/pretty v0.2.1 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.17 // indirect
github.com/miekg/dns v1.1.52 // indirect
github.com/pion/datachannel v1.5.5 // indirect
github.com/pion/dtls/v2 v2.2.6 // indirect
github.com/pion/logging v0.2.2 // indirect
github.com/pion/mdns v0.0.7 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/pion/datachannel v1.5.10 // indirect
github.com/pion/dtls/v3 v3.0.6 // indirect
github.com/pion/logging v0.2.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.6 // indirect
github.com/pion/transport/v2 v2.0.2 // indirect
github.com/pion/turn/v2 v2.1.0 // indirect
github.com/pion/udp/v2 v2.0.1 // indirect
github.com/pion/sctp v1.8.39 // indirect
github.com/pion/transport/v3 v3.0.7 // indirect
github.com/pion/turn/v4 v4.0.2 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/xiam/to v0.0.0-20200126224905-d60d31e03561 // indirect
golang.org/x/crypto v0.7.0 // indirect
golang.org/x/mod v0.9.0 // indirect
golang.org/x/net v0.8.0 // indirect
golang.org/x/sys v0.6.0 // indirect
golang.org/x/text v0.8.0 // indirect
golang.org/x/tools v0.7.0 // indirect
)
replace (
// windows support: https://github.com/brutella/dnssd/pull/35
github.com/brutella/dnssd v1.2.2 => github.com/rblenkinsopp/dnssd v1.2.3-0.20220516082132-0923f3c787a1
// RTP tlv8 fix
github.com/brutella/hap v0.0.17 => github.com/AlexxIT/hap v0.0.15-0.20221108133010-d8a45b7a7045
// fix reading AAC config bytes
github.com/deepch/vdk v0.0.19 => github.com/AlexxIT/vdk v0.0.18-0.20221108193131-6168555b4f92
github.com/wlynxg/anet v0.0.5 // indirect
golang.org/x/mod v0.25.0 // indirect
golang.org/x/net v0.41.0 // indirect
golang.org/x/sync v0.15.0 // indirect
golang.org/x/sys v0.33.0 // indirect
golang.org/x/tools v0.34.0 // indirect
)

331
go.sum
View File

@@ -1,239 +1,140 @@
github.com/AlexxIT/hap v0.0.15-0.20221108133010-d8a45b7a7045 h1:xJf3FxQJReJSDyYXJfI1NUWv8tUEAGNV9xigLqNtmrI=
github.com/AlexxIT/hap v0.0.15-0.20221108133010-d8a45b7a7045/go.mod h1:QNA3sm16zE5uUyC8+E/gNkMvQWjqQLuxQKkU5PMi8N4=
github.com/AlexxIT/vdk v0.0.18-0.20221108193131-6168555b4f92 h1:cIeYMGaAirSZnrKRDTb5VgZDDYqPLhYiczElMg4sQW0=
github.com/AlexxIT/vdk v0.0.18-0.20221108193131-6168555b4f92/go.mod h1:7ydHfSkflMZxBXfWR79dMjrT54xzvLxnPaByOa9Jpzg=
github.com/brutella/dnssd v1.2.3/go.mod h1:JoW2sJUrmVIef25G6lrLj7HS6Xdwh6q8WUIvMkkBYXs=
github.com/brutella/dnssd v1.2.5 h1:b8syhho41/5ikw3X2X4baR9NWEBSlpZnfQgujsv7bk4=
github.com/brutella/dnssd v1.2.5/go.mod h1:JoW2sJUrmVIef25G6lrLj7HS6Xdwh6q8WUIvMkkBYXs=
github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/asticode/go-astikit v0.30.0/go.mod h1:h4ly7idim1tNhaVkdVBeXQZEE3L0xblP7fCWbgwipF0=
github.com/asticode/go-astikit v0.54.0 h1:uq9eurgisdkYwJU9vSWIQaPH4MH0cac82sQH00kmSNQ=
github.com/asticode/go-astikit v0.54.0/go.mod h1:fV43j20UZYfXzP9oBn33udkvCvDvCDhzjVqoLFuuYZE=
github.com/asticode/go-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=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/go-chi/chi v1.5.4 h1:QHdzF2szwjqVV4wmByUnTcsbIg7UGaQ0tPF2t5GcAIs=
github.com/go-chi/chi v1.5.4/go.mod h1:uaf8YgoFazUOkPBG7fxPftUylNumIev9awIWOENIuEg=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
github.com/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/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hashicorp/mdns v1.0.5 h1:1M5hW1cunYeoXOqHwEb/GBDDHAFo0Yqb/uz/beC6LbE=
github.com/hashicorp/mdns v1.0.5/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
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.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI=
github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME=
github.com/miekg/dns v1.1.52 h1:Bmlc/qsNNULOe6bpXcUTsuOajd0DzRHwup6D9k1An0c=
github.com/miekg/dns v1.1.52/go.mod h1:uInx36IzPl7FYnDcMeVWxj9byh7DutNykX4G9Sj60FY=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
github.com/pion/datachannel v1.5.5 h1:10ef4kwdjije+M9d7Xm9im2Y3O6A6ccQb0zcqZcJew8=
github.com/pion/datachannel v1.5.5/go.mod h1:iMz+lECmfdCMqFRhXhcA/219B0SQlbpoR2V118yimL0=
github.com/pion/dtls/v2 v2.2.6 h1:yXMxKr0Skd+Ub6A8UqXTRLSywskx93ooMRHsQUtd+Z4=
github.com/pion/dtls/v2 v2.2.6/go.mod h1:t8fWJCIquY5rlQZwA2yWxUS1+OCrAdXrhVKXB5oD/wY=
github.com/pion/ice/v2 v2.3.1 h1:FQCmUfZe2Jpe7LYStVBOP6z1DiSzbIateih3TztgTjc=
github.com/pion/ice/v2 v2.3.1/go.mod h1:aq2kc6MtYNcn4XmMhobAv6hTNJiHzvD0yXRz80+bnP8=
github.com/pion/interceptor v0.1.12 h1:CslaNriCFUItiXS5o+hh5lpL0t0ytQkFnUcbbCs2Zq8=
github.com/pion/interceptor v0.1.12/go.mod h1:bDtgAD9dRkBZpWHGKaoKb42FhDHTG2rX8Ii9LRALLVA=
github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
github.com/pion/mdns v0.0.7 h1:P0UB4Sr6xDWEox0kTVxF0LmQihtCbSAdW0H2nEgkA3U=
github.com/pion/mdns v0.0.7/go.mod h1:4iP2UbeFhLI/vWju/bw6ZfwjJzk0z8DNValjGxR/dD8=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/miekg/dns v1.1.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/pion/datachannel v1.5.10 h1:ly0Q26K1i6ZkGf42W7D4hQYR90pZwzFOjTq5AuCKk4o=
github.com/pion/datachannel v1.5.10/go.mod h1:p/jJfC9arb29W7WrxyKbepTU20CFgyx5oLo8Rs4Py/M=
github.com/pion/dtls/v3 v3.0.6 h1:7Hkd8WhAJNbRgq9RgdNh1aaWlZlGpYTzdqjy9x9sK2E=
github.com/pion/dtls/v3 v3.0.6/go.mod h1:iJxNQ3Uhn1NZWOMWlLxEEHAN5yX7GyPvvKw04v9bzYU=
github.com/pion/ice/v4 v4.0.9 h1:VKgU4MwA2LUDVLq+WBkpEHTcAb8c5iCvFMECeuPOZNk=
github.com/pion/ice/v4 v4.0.9/go.mod h1:y3M18aPhIxLlcO/4dn9X8LzLLSma84cx6emMSu14FGw=
github.com/pion/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/interceptor v0.1.40 h1:e0BjnPcGpr2CFQgKhrQisBU7V3GXK6wrfYrGYaU6Jq4=
github.com/pion/interceptor v0.1.40/go.mod h1:Z6kqH7M/FYirg3frjGJ21VLSRJGBXB/KqaTIrdqnOic=
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/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.10 h1:nkr3uj+8Sp97zyItdN60tE/S6vk4al5CPRR6Gejsdjc=
github.com/pion/rtcp v1.2.10/go.mod h1:ztfEwXZNLGyF1oQDttz/ZKIBaeeg/oWbRYqzBM9TL1I=
github.com/pion/rtp v1.7.13 h1:qcHwlmtiI50t1XivvoawdCGTP4Uiypzfrsap+bijcoA=
github.com/pion/rtp v1.7.13/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko=
github.com/pion/sctp v1.8.5/go.mod h1:SUFFfDpViyKejTAdwD1d/HQsCu+V/40cCs2nZIvC3s0=
github.com/pion/sctp v1.8.6 h1:CUex11Vkt9YS++VhLf8b55O3VqKrWL6W3SDwX4jAqsI=
github.com/pion/sctp v1.8.6/go.mod h1:SUFFfDpViyKejTAdwD1d/HQsCu+V/40cCs2nZIvC3s0=
github.com/pion/sdp/v3 v3.0.6 h1:WuDLhtuFUUVpTfus9ILC4HRyHsW6TdugjEX/QY9OiUw=
github.com/pion/sdp/v3 v3.0.6/go.mod h1:iiFWFpQO8Fy3S5ldclBkpXqmWy02ns78NOKoLLL0YQw=
github.com/pion/srtp/v2 v2.0.12 h1:WrmiVCubGMOAObBU1vwWjG0H3VSyQHawKeer2PVA5rY=
github.com/pion/srtp/v2 v2.0.12/go.mod h1:C3Ep44hlOo2qEYaq4ddsmK5dL63eLehXFbHaZ9F5V9Y=
github.com/pion/stun v0.4.0 h1:vgRrbBE2htWHy7l3Zsxckk7rkjnjOsSM7PHZnBwo8rk=
github.com/pion/stun v0.4.0/go.mod h1:QPsh1/SbXASntw3zkkrIk3ZJVKz4saBY2G7S10P3wCw=
github.com/pion/transport v0.14.1 h1:XSM6olwW+o8J4SCmOBb/BpwZypkHeyM0PGFCxNQBr40=
github.com/pion/transport v0.14.1/go.mod h1:4tGmbk00NeYA3rUa9+n+dzCCoKkcy3YlYb99Jn2fNnI=
github.com/pion/transport/v2 v2.0.0/go.mod h1:HS2MEBJTwD+1ZI2eSXSvHJx/HnzQqRy2/LXxt6eVMHc=
github.com/pion/transport/v2 v2.0.2 h1:St+8o+1PEzPT51O9bv+tH/KYYLMNR5Vwm5Z3Qkjsywg=
github.com/pion/transport/v2 v2.0.2/go.mod h1:vrz6bUbFr/cjdwbnxq8OdDDzHf7JJfGsIRkxfpZoTA0=
github.com/pion/turn/v2 v2.1.0 h1:5wGHSgGhJhP/RpabkUb/T9PdsAjkGLS6toYz5HNzoSI=
github.com/pion/turn/v2 v2.1.0/go.mod h1:yrT5XbXSGX1VFSF31A3c1kCNB5bBZgk/uu5LET162qs=
github.com/pion/udp/v2 v2.0.1 h1:xP0z6WNux1zWEjhC7onRA3EwwSliXqu1ElUZAQhUP54=
github.com/pion/udp/v2 v2.0.1/go.mod h1:B7uvTMP00lzWdyMr/1PVZXtV3wpPIxBRd4Wl6AksXn8=
github.com/pion/webrtc/v3 v3.1.58 h1:husXqiKQuk6gbOqJlPHs185OskAyxUW6iAEgHghgCrc=
github.com/pion/webrtc/v3 v3.1.58/go.mod h1:jJdqoqGBlZiE3y8Z1tg1fjSkyEDCZLL+foypUBn0Lhk=
github.com/pion/rtcp v1.2.15 h1:LZQi2JbdipLOj4eBjK4wlVoQWfrZbh3Q6eHtWtJBZBo=
github.com/pion/rtcp v1.2.15/go.mod h1:jlGuAjHMEXwMUHK78RgX0UmEJFV4zUKOFHR7OP+D3D0=
github.com/pion/rtp v1.8.13 h1:8uSUPpjSL4OlwZI8Ygqu7+h2p9NPFB+yAZ461Xn5sNg=
github.com/pion/rtp v1.8.13/go.mod h1:8uMBJj32Pa1wwx8Fuv/AsFhn8jsgw+3rUC2PfoBZ8p4=
github.com/pion/rtp v1.8.20 h1:8zcyqohadZE8FCBeGdyEvHiclPIezcwRQH9zfapFyYI=
github.com/pion/rtp v1.8.20/go.mod h1:bAu2UFKScgzyFqvUKmbvzSdPr+NGbZtv6UB2hesqXBk=
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/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/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/stun/v3 v3.0.0 h1:4h1gwhWLWuZWOJIJR9s2ferRO+W3zA/b6ijOI6mKzUw=
github.com/pion/stun/v3 v3.0.0/go.mod h1:HvCN8txt8mwi4FBvS3EmDghW6aQJ24T+y+1TKjB5jyU=
github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0=
github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo=
github.com/pion/turn/v4 v4.0.0 h1:qxplo3Rxa9Yg1xXDxxH8xaqcyGUtbHYw4QSCvmFWvhM=
github.com/pion/turn/v4 v4.0.0/go.mod h1:MuPDkm15nYSklKpN8vWJ9W2M0PlyQZqYt1McGuxG7mA=
github.com/pion/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/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/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/profile v1.4.0/go.mod h1:NWz/XGvpEW1FyYQ7fCx4dqYBLlfTcE+A9FLAkNKqjFE=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.29.0 h1:Zes4hju04hjbvkVkOhdl2HpZa+0PmVwigmo8XoORE5w=
github.com/rs/zerolog v1.29.0/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6usyD0=
github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw=
github.com/sigurn/crc16 v0.0.0-20211026045750-20ab5afb07e3 h1:aQKxg3+2p+IFXXg97McgDGT5zcMrQoi0EICZs8Pgchs=
github.com/sigurn/crc16 v0.0.0-20211026045750-20ab5afb07e3/go.mod h1:9/etS5gpQq9BJsJMWg1wpLbfuSnkm8dPF6FdW2JXVhA=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
github.com/sigurn/crc16 v0.0.0-20240131213347-83fcde1e29d1 h1:NVK+OqnavpyFmUiKfUMHrpvbCi2VFoWTrcpI7aDaJ2I=
github.com/sigurn/crc16 v0.0.0-20240131213347-83fcde1e29d1/go.mod h1:9/etS5gpQq9BJsJMWg1wpLbfuSnkm8dPF6FdW2JXVhA=
github.com/sigurn/crc8 v0.0.0-20220107193325-2243fe600f9f h1:1R9KdKjCNSd7F8iGTxIpoID9prlYH8nuNYKt0XvweHA=
github.com/sigurn/crc8 v0.0.0-20220107193325-2243fe600f9f/go.mod h1:vQhwQ4meQEDfahT5kd61wLAF5AAeh5ZPLVI4JJ/tYo8=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tadglines/go-pkgs v0.0.0-20210623144937-b983b20f54f9 h1:aeN+ghOV0b2VCmKKO3gqnDQ8mLbpABZgRR2FVYx4ouI=
github.com/tadglines/go-pkgs v0.0.0-20210623144937-b983b20f54f9/go.mod h1:roo6cZ/uqpwKMuvPG0YmzI5+AmUiMWfjCBZpGXqbTxE=
github.com/xiam/to v0.0.0-20200126224905-d60d31e03561 h1:SVoNK97S6JlaYlHcaC+79tg3JUlQABcc0dH2VQ4Y+9s=
github.com/xiam/to v0.0.0-20200126224905-d60d31e03561/go.mod h1:cqbG7phSzrbdg3aj+Kn63bpVruzwDZi58CpxlZkjwzw=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU=
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs=
golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8=
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU=
github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
golang.org/x/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/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0=
golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
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/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4=
golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
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/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24=
golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ=
golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
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/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

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

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

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

View File

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

4
internal/api/README.md Normal file
View File

@@ -0,0 +1,4 @@
## Exit codes
- https://tldp.org/LDP/abs/html/exitcodes.html
- https://komodor.com/learn/exit-codes-in-containers-and-kubernetes-the-complete-guide/

View File

@@ -1,26 +1,35 @@
package api
import (
"crypto/tls"
"encoding/json"
"github.com/AlexxIT/go2rtc/internal/app"
"github.com/rs/zerolog"
"fmt"
"net"
"net/http"
"os"
"strconv"
"strings"
"sync"
"syscall"
"time"
"github.com/AlexxIT/go2rtc/internal/app"
"github.com/rs/zerolog"
)
func Init() {
var cfg struct {
Mod struct {
Listen string `yaml:"listen"`
Username string `yaml:"username"`
Password string `yaml:"password"`
BasePath string `yaml:"base_path"`
StaticDir string `yaml:"static_dir"`
Origin string `yaml:"origin"`
Listen string `yaml:"listen"`
Username string `yaml:"username"`
Password string `yaml:"password"`
BasePath string `yaml:"base_path"`
StaticDir string `yaml:"static_dir"`
Origin string `yaml:"origin"`
TLSListen string `yaml:"tls_listen"`
TLSCert string `yaml:"tls_cert"`
TLSKey string `yaml:"tls_key"`
UnixListen string `yaml:"unix_listen"`
} `yaml:"api"`
}
@@ -30,7 +39,7 @@ func Init() {
// load config from YAML
app.LoadConfig(&cfg)
if cfg.Mod.Listen == "" {
if cfg.Mod.Listen == "" && cfg.Mod.UnixListen == "" && cfg.Mod.TLSListen == "" {
return
}
@@ -38,21 +47,12 @@ func Init() {
log = app.GetLogger("api")
initStatic(cfg.Mod.StaticDir)
initWS(cfg.Mod.Origin)
HandleFunc("api", apiHandler)
HandleFunc("api/config", configHandler)
HandleFunc("api/exit", exitHandler)
HandleFunc("api/ws", apiWS)
// ensure we can listen without errors
listener, err := net.Listen("tcp", cfg.Mod.Listen)
if err != nil {
log.Fatal().Err(err).Msg("[api] listen")
return
}
log.Info().Str("addr", cfg.Mod.Listen).Msg("[api] listen")
HandleFunc("api/restart", restartHandler)
HandleFunc("api/log", logHandler)
Handler = http.DefaultServeMux // 4th
@@ -68,15 +68,81 @@ func Init() {
Handler = middlewareLog(Handler) // 1st
}
go func() {
s := http.Server{}
s.Handler = Handler
if err = s.Serve(listener); err != nil {
log.Fatal().Err(err).Msg("[api] serve")
}
}()
if cfg.Mod.Listen != "" {
_, port, _ := net.SplitHostPort(cfg.Mod.Listen)
Port, _ = strconv.Atoi(port)
go listen("tcp", cfg.Mod.Listen)
}
if cfg.Mod.UnixListen != "" {
_ = syscall.Unlink(cfg.Mod.UnixListen)
go listen("unix", cfg.Mod.UnixListen)
}
// Initialize the HTTPS server
if cfg.Mod.TLSListen != "" && cfg.Mod.TLSCert != "" && cfg.Mod.TLSKey != "" {
go tlsListen("tcp", cfg.Mod.TLSListen, cfg.Mod.TLSCert, cfg.Mod.TLSKey)
}
}
func listen(network, address string) {
ln, err := net.Listen(network, address)
if err != nil {
log.Error().Err(err).Msg("[api] listen")
return
}
log.Info().Str("addr", address).Msg("[api] listen")
server := http.Server{
Handler: Handler,
ReadHeaderTimeout: 5 * time.Second, // Example: Set to 5 seconds
}
if err = server.Serve(ln); err != nil {
log.Fatal().Err(err).Msg("[api] serve")
}
}
func tlsListen(network, address, certFile, keyFile string) {
var cert tls.Certificate
var err error
if strings.IndexByte(certFile, '\n') < 0 && strings.IndexByte(keyFile, '\n') < 0 {
// check if file path
cert, err = tls.LoadX509KeyPair(certFile, keyFile)
} else {
// if text file content
cert, err = tls.X509KeyPair([]byte(certFile), []byte(keyFile))
}
if err != nil {
log.Error().Err(err).Caller().Send()
return
}
ln, err := net.Listen(network, address)
if err != nil {
log.Error().Err(err).Msg("[api] tls listen")
return
}
log.Info().Str("addr", address).Msg("[api] tls listen")
server := &http.Server{
Handler: Handler,
TLSConfig: &tls.Config{Certificates: []tls.Certificate{cert}},
ReadHeaderTimeout: 5 * time.Second,
}
if err = server.ServeTLS(ln, "", ""); err != nil {
log.Fatal().Err(err).Msg("[api] tls serve")
}
}
var Port int
const (
MimeJSON = "application/json"
MimeText = "text/plain"
)
var Handler http.Handler
// HandleFunc handle pattern with relative path:
@@ -90,6 +156,33 @@ func HandleFunc(pattern string, handler http.HandlerFunc) {
http.HandleFunc(pattern, handler)
}
// ResponseJSON important always add Content-Type
// so go won't need to call http.DetectContentType
func ResponseJSON(w http.ResponseWriter, v any) {
w.Header().Set("Content-Type", MimeJSON)
_ = json.NewEncoder(w).Encode(v)
}
func ResponsePrettyJSON(w http.ResponseWriter, v any) {
w.Header().Set("Content-Type", MimeJSON)
enc := json.NewEncoder(w)
enc.SetIndent("", " ")
_ = enc.Encode(v)
}
func Response(w http.ResponseWriter, body any, contentType string) {
w.Header().Set("Content-Type", contentType)
switch v := body.(type) {
case []byte:
_, _ = w.Write(v)
case string:
_, _ = w.Write([]byte(v))
default:
_, _ = fmt.Fprint(w, body)
}
}
const StreamNotFound = "stream not found"
var basePath string
@@ -104,7 +197,7 @@ func middlewareLog(next http.Handler) http.Handler {
func middlewareAuth(username, password string, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !strings.HasPrefix(r.RemoteAddr, "127.") && !strings.HasPrefix(r.RemoteAddr, "[::1]") {
if !strings.HasPrefix(r.RemoteAddr, "127.") && !strings.HasPrefix(r.RemoteAddr, "[::1]") && r.RemoteAddr != "@" {
user, pass, ok := r.BasicAuth()
if !ok || user != username || pass != password {
w.Header().Set("Www-Authenticate", `Basic realm="go2rtc"`)
@@ -121,7 +214,7 @@ func middlewareCORS(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Authorization")
w.Header().Set("Access-Control-Allow-Headers", "Authorization, Content-Type")
next.ServeHTTP(w, r)
})
}
@@ -133,9 +226,7 @@ func apiHandler(w http.ResponseWriter, r *http.Request) {
app.Info["host"] = r.Host
mu.Unlock()
if err := json.NewEncoder(w).Encode(app.Info); err != nil {
log.Warn().Err(err).Caller().Send()
}
ResponseJSON(w, app.Info)
}
func exitHandler(w http.ResponseWriter, r *http.Request) {
@@ -145,26 +236,72 @@ func exitHandler(w http.ResponseWriter, r *http.Request) {
}
s := r.URL.Query().Get("code")
code, _ := strconv.Atoi(s)
os.Exit(code)
}
code, err := strconv.Atoi(s)
type Stream struct {
Name string `json:"name"`
URL string `json:"url"`
}
func ResponseStreams(w http.ResponseWriter, streams []Stream) {
if len(streams) == 0 {
http.Error(w, "no streams", http.StatusNotFound)
// https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_08_02
if err != nil || code < 0 || code > 125 {
http.Error(w, "Code must be in the range [0, 125]", http.StatusBadRequest)
return
}
var response struct {
Streams []Stream `json:"streams"`
os.Exit(code)
}
func restartHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
http.Error(w, "", http.StatusBadRequest)
return
}
response.Streams = streams
if err := json.NewEncoder(w).Encode(response); err != nil {
path, err := os.Executable()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
log.Debug().Msgf("[api] restart %s", path)
go syscall.Exec(path, os.Args, os.Environ())
}
func logHandler(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case "GET":
// Send current state of the log file immediately
w.Header().Set("Content-Type", "application/jsonlines")
_, _ = app.MemoryLog.WriteTo(w)
case "DELETE":
app.MemoryLog.Reset()
Response(w, "OK", "text/plain")
default:
http.Error(w, "Method not allowed", http.StatusBadRequest)
}
}
type Source struct {
ID string `json:"id,omitempty"`
Name string `json:"name,omitempty"`
Info string `json:"info,omitempty"`
URL string `json:"url,omitempty"`
Location string `json:"location,omitempty"`
}
func ResponseSources(w http.ResponseWriter, sources []*Source) {
if len(sources) == 0 {
http.Error(w, "no sources", http.StatusNotFound)
return
}
var response = struct {
Sources []*Source `json:"sources"`
}{
Sources: sources,
}
ResponseJSON(w, response)
}
func Error(w http.ResponseWriter, err error) {
log.Error().Err(err).Caller(1).Send()
http.Error(w, err.Error(), http.StatusInsufficientStorage)
}

View File

@@ -1,11 +1,12 @@
package api
import (
"github.com/AlexxIT/go2rtc/internal/app"
"gopkg.in/yaml.v3"
"io"
"net/http"
"os"
"github.com/AlexxIT/go2rtc/internal/app"
"gopkg.in/yaml.v3"
)
func configHandler(w http.ResponseWriter, r *http.Request) {
@@ -21,9 +22,8 @@ func configHandler(w http.ResponseWriter, r *http.Request) {
http.Error(w, "", http.StatusNotFound)
return
}
if _, err = w.Write(data); err != nil {
log.Warn().Err(err).Caller().Send()
}
// https://www.ietf.org/archive/id/draft-ietf-httpapi-yaml-mediatypes-00.html
Response(w, data, "application/yaml")
case "POST", "PATCH":
data, err := io.ReadAll(r.Body)
@@ -41,8 +41,7 @@ func configHandler(w http.ResponseWriter, r *http.Request) {
}
} else {
// validate config
var tmp struct{}
if err = yaml.Unmarshal(data, &tmp); err != nil {
if err = yaml.Unmarshal(data, map[string]any{}); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}

View File

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

View File

@@ -1,39 +1,63 @@
package api
package ws
import (
"github.com/gorilla/websocket"
"encoding/json"
"io"
"net/http"
"net/url"
"strings"
"sync"
"time"
"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"
)
func Init() {
var cfg struct {
Mod struct {
Origin string `yaml:"origin"`
} `yaml:"api"`
}
app.LoadConfig(&cfg)
log = app.GetLogger("api")
initWS(cfg.Mod.Origin)
api.HandleFunc("api/ws", apiWS)
}
var log zerolog.Logger
// Message - struct for data exchange in Web API
type Message struct {
Type string `json:"type"`
Value any `json:"value,omitempty"`
}
func (m *Message) String() string {
func (m *Message) String() (value string) {
if s, ok := m.Value.(string); ok {
return s
}
return ""
return
}
func (m *Message) GetString(key string) string {
if v, ok := m.Value.(map[string]any); ok {
if s, ok := v[key].(string); ok {
return s
}
func (m *Message) Unmarshal(v any) error {
b, err := json.Marshal(m.Value)
if err != nil {
return err
}
return ""
return json.Unmarshal(b, v)
}
type WSHandler func(tr *Transport, msg *Message) error
func HandleWS(msgType string, handler WSHandler) {
func HandleFunc(msgType string, handler WSHandler) {
wsHandlers[msgType] = handler
}
@@ -60,7 +84,7 @@ func initWS(origin string) {
if o.Host == r.Host {
return true
}
log.Trace().Msgf("[api.ws] origin=%s, host=%s", o.Host, r.Host)
log.Trace().Msgf("[api] ws origin=%s, host=%s", o.Host, r.Host)
// https://github.com/AlexxIT/go2rtc/issues/118
if i := strings.IndexByte(o.Host, ':'); i > 0 {
return o.Host[:i] == r.Host
@@ -84,13 +108,13 @@ func apiWS(w http.ResponseWriter, r *http.Request) {
}
tr := &Transport{Request: r}
tr.OnWrite(func(msg any) {
tr.OnWrite(func(msg any) error {
_ = ws.SetWriteDeadline(time.Now().Add(time.Second * 5))
if data, ok := msg.([]byte); ok {
_ = ws.WriteMessage(websocket.BinaryMessage, data)
return ws.WriteMessage(websocket.BinaryMessage, data)
} else {
_ = ws.WriteJSON(msg)
return ws.WriteJSON(msg)
}
})
@@ -104,12 +128,13 @@ func apiWS(w http.ResponseWriter, r *http.Request) {
break
}
log.Trace().Str("type", msg.Type).Msg("[api.ws] msg")
log.Trace().Str("type", msg.Type).Msg("[api] ws msg")
if handler := wsHandlers[msg.Type]; handler != nil {
go func() {
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})
}
}()
}
@@ -130,11 +155,11 @@ type Transport struct {
wrmx sync.Mutex
onChange func()
onWrite func(msg any)
onWrite func(msg any) error
onClose []func()
}
func (t *Transport) OnWrite(f func(msg any)) {
func (t *Transport) OnWrite(f func(msg any) error) {
t.mx.Lock()
if t.onChange != nil {
t.onChange()
@@ -145,7 +170,7 @@ func (t *Transport) OnWrite(f func(msg any)) {
func (t *Transport) Write(msg any) {
t.wrmx.Lock()
t.onWrite(msg)
_ = t.onWrite(msg)
t.wrmx.Unlock()
}
@@ -183,3 +208,20 @@ func (t *Transport) WithContext(f func(ctx map[any]any)) {
f(t.ctx)
t.mx.Unlock()
}
func (t *Transport) Writer() io.Writer {
return &writer{t: t}
}
type writer struct {
t *Transport
}
func (w *writer) Write(p []byte) (n int, err error) {
w.t.wrmx.Lock()
if err = w.t.onWrite(p); err == nil {
n = len(p)
}
w.t.wrmx.Unlock()
return
}

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

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

View File

@@ -3,140 +3,99 @@ package app
import (
"flag"
"fmt"
"io"
"os"
"path/filepath"
"os/exec"
"runtime"
"strings"
"time"
"github.com/AlexxIT/go2rtc/pkg/shell"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"gopkg.in/yaml.v3"
"runtime/debug"
)
var Version = "1.5.0"
var UserAgent = "go2rtc/" + Version
var (
Version string
UserAgent string
ConfigPath string
Info = make(map[string]any)
)
var ConfigPath string
var Info = map[string]any{
"version": Version,
}
const usage = `Usage of go2rtc:
-c, --config Path to config file or config string as YAML or JSON, support multiple
-d, --daemon Run in background
-v, --version Print version and exit
`
func Init() {
var confs Config
var config flagConfig
var daemon bool
var version bool
flag.Var(&confs, "config", "go2rtc config (path to file or raw text), support multiple")
flag.BoolVar(&version, "version", false, "Print the version of the application and exit")
flag.Var(&config, "config", "")
flag.Var(&config, "c", "")
flag.BoolVar(&daemon, "daemon", false, "")
flag.BoolVar(&daemon, "d", false, "")
flag.BoolVar(&version, "version", false, "")
flag.BoolVar(&version, "v", false, "")
flag.Usage = func() { fmt.Print(usage) }
flag.Parse()
revision, vcsTime := readRevisionTime()
if version {
fmt.Println("Current version: ", Version)
fmt.Printf("go2rtc version %s (%s) %s/%s\n", Version, revision, runtime.GOOS, runtime.GOARCH)
os.Exit(0)
}
if confs == nil {
confs = []string{"go2rtc.yaml"}
}
for _, conf := range confs {
if conf[0] != '{' {
// config as file
if ConfigPath == "" {
ConfigPath = conf
}
data, _ := os.ReadFile(conf)
if data == nil {
continue
}
data = []byte(shell.ReplaceEnvVars(string(data)))
configs = append(configs, data)
} else {
// config as raw YAML
configs = append(configs, []byte(conf))
if daemon && os.Getppid() != 1 {
if runtime.GOOS == "windows" {
fmt.Println("Daemon mode is not supported on Windows")
os.Exit(1)
}
// Re-run the program in background and exit
cmd := exec.Command(os.Args[0], os.Args[1:]...)
if err := cmd.Start(); err != nil {
fmt.Println("Failed to start daemon:", err)
os.Exit(1)
}
fmt.Println("Running in daemon mode with PID:", cmd.Process.Pid)
os.Exit(0)
}
UserAgent = "go2rtc/" + Version
Info["version"] = Version
Info["revision"] = revision
initConfig(config)
initLogger()
platform := fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH)
Logger.Info().Str("version", Version).Str("platform", platform).Str("revision", revision).Msg("go2rtc")
Logger.Debug().Str("version", runtime.Version()).Str("vcs.time", vcsTime).Msg("build")
if ConfigPath != "" {
if !filepath.IsAbs(ConfigPath) {
if cwd, err := os.Getwd(); err == nil {
ConfigPath = filepath.Join(cwd, ConfigPath)
Logger.Info().Str("path", ConfigPath).Msg("config")
}
}
func readRevisionTime() (revision, vcsTime string) {
if info, ok := debug.ReadBuildInfo(); ok {
for _, setting := range info.Settings {
switch setting.Key {
case "vcs.revision":
if len(setting.Value) > 7 {
revision = setting.Value[:7]
} else {
revision = setting.Value
}
case "vcs.time":
vcsTime = setting.Value
case "vcs.modified":
if setting.Value == "true" {
revision = "mod." + revision
}
}
}
Info["config_path"] = ConfigPath
}
var cfg struct {
Mod map[string]string `yaml:"log"`
}
LoadConfig(&cfg)
log.Logger = NewLogger(cfg.Mod["format"], cfg.Mod["level"])
modules = cfg.Mod
log.Info().Msgf("go2rtc version %s %s/%s", Version, runtime.GOOS, runtime.GOARCH)
return
}
func NewLogger(format string, level string) zerolog.Logger {
var writer io.Writer = os.Stdout
if format != "json" {
writer = zerolog.ConsoleWriter{
Out: writer, TimeFormat: "15:04:05.000",
NoColor: writer != os.Stdout || format == "text",
}
}
zerolog.TimeFieldFormat = time.RFC3339Nano
lvl, err := zerolog.ParseLevel(level)
if err != nil || lvl == zerolog.NoLevel {
lvl = zerolog.InfoLevel
}
return zerolog.New(writer).With().Timestamp().Logger().Level(lvl)
}
func LoadConfig(v any) {
for _, data := range configs {
if err := yaml.Unmarshal(data, v); err != nil {
log.Warn().Err(err).Msg("[app] read config")
}
}
}
func GetLogger(module string) zerolog.Logger {
if s, ok := modules[module]; ok {
lvl, err := zerolog.ParseLevel(s)
if err == nil {
return log.Level(lvl)
}
log.Warn().Err(err).Caller().Send()
}
return log.Logger
}
// internal
type Config []string
func (c *Config) String() string {
return strings.Join(*c, " ")
}
func (c *Config) Set(value string) error {
*c = append(*c, value)
return nil
}
var configs [][]byte
// modules log levels
var modules map[string]string

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

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

186
internal/app/log.go Normal file
View File

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

View File

@@ -1,61 +0,0 @@
package store
import (
"encoding/json"
"github.com/rs/zerolog/log"
"os"
)
const name = "go2rtc.json"
var store map[string]any
func load() {
data, _ := os.ReadFile(name)
if data != nil {
if err := json.Unmarshal(data, &store); err != nil {
// TODO: log
log.Warn().Err(err).Msg("[app] read storage")
}
}
if store == nil {
store = make(map[string]any)
}
}
func save() error {
data, err := json.Marshal(store)
if err != nil {
return err
}
return os.WriteFile(name, data, 0644)
}
func GetRaw(key string) any {
if store == nil {
load()
}
return store[key]
}
func GetDict(key string) map[string]any {
raw := GetRaw(key)
if raw != nil {
return raw.(map[string]any)
}
return make(map[string]any)
}
func Set(key string, v any) error {
if store == nil {
load()
}
store[key] = v
return save()
}

13
internal/bubble/bubble.go Normal file
View File

@@ -0,0 +1,13 @@
package bubble
import (
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/bubble"
"github.com/AlexxIT/go2rtc/pkg/core"
)
func Init() {
streams.HandleFunc("bubble", func(source string) (core.Producer, error) {
return bubble.Dial(source)
})
}

View File

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

View File

@@ -5,6 +5,8 @@ import (
"fmt"
"net/http"
"runtime"
"github.com/AlexxIT/go2rtc/internal/api"
)
var stackSkip = [][]byte{
@@ -23,6 +25,9 @@ var stackSkip = [][]byte{
[]byte("created by github.com/AlexxIT/go2rtc/internal/rtsp.Init"),
[]byte("created by github.com/AlexxIT/go2rtc/internal/srtp.Init"),
// homekit
[]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"),
@@ -51,7 +56,5 @@ func stackHandler(w http.ResponseWriter, r *http.Request) {
"Total: %d, Skipped: %d", runtime.NumGoroutine(), skipped),
)
if _, err := w.Write(buf[:i]); err != nil {
panic(err)
}
api.Response(w, buf[:i], api.MimeText)
}

View File

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

View File

@@ -1,25 +1,161 @@
package dvrip
import (
"encoding/hex"
"encoding/json"
"fmt"
"net"
"net/http"
"time"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/dvrip"
)
func Init() {
streams.HandleFunc("dvrip", handle)
streams.HandleFunc("dvrip", dvrip.Dial)
// DVRIP client autodiscovery
api.HandleFunc("api/dvrip", apiDvrip)
}
func handle(url string) (core.Producer, error) {
conn := dvrip.NewClient(url)
if err := conn.Dial(); err != nil {
return nil, err
const Port = 34569 // UDP port number for dvrip discovery
func apiDvrip(w http.ResponseWriter, r *http.Request) {
items, err := discover()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if err := conn.Play(); err != nil {
return nil, err
}
if err := conn.Handle(); err != nil {
return nil, err
}
return conn, nil
api.ResponseSources(w, items)
}
func discover() ([]*api.Source, error) {
addr := &net.UDPAddr{
Port: Port,
IP: net.IP{239, 255, 255, 250},
}
conn, err := net.ListenUDP("udp4", addr)
if err != nil {
return nil, err
}
defer conn.Close()
go sendBroadcasts(conn)
var items []*api.Source
for _, info := range getResponses(conn) {
if info.HostIP == "" || info.HostName == "" {
continue
}
host, err := hexToDecimalBytes(info.HostIP)
if err != nil {
continue
}
items = append(items, &api.Source{
Name: info.HostName,
URL: "dvrip://user:pass@" + host + "?channel=0&subtype=0",
})
}
return items, nil
}
func sendBroadcasts(conn *net.UDPConn) {
// broadcasting the same multiple times because the devies some times don't answer
data, err := hex.DecodeString("ff00000000000000000000000000fa0500000000")
if err != nil {
return
}
addr := &net.UDPAddr{
Port: Port,
IP: net.IP{255, 255, 255, 255},
}
for i := 0; i < 3; i++ {
time.Sleep(100 * time.Millisecond)
_, _ = conn.WriteToUDP(data, addr)
}
}
type Message struct {
NetCommon NetCommon `json:"NetWork.NetCommon"`
Ret int `json:"Ret"`
SessionID string `json:"SessionID"`
}
type NetCommon struct {
BuildDate string `json:"BuildDate"`
ChannelNum int `json:"ChannelNum"`
DeviceType int `json:"DeviceType"`
GateWay string `json:"GateWay"`
HostIP string `json:"HostIP"`
HostName string `json:"HostName"`
HttpPort int `json:"HttpPort"`
MAC string `json:"MAC"`
MonMode string `json:"MonMode"`
NetConnectState int `json:"NetConnectState"`
OtherFunction string `json:"OtherFunction"`
SN string `json:"SN"`
SSLPort int `json:"SSLPort"`
Submask string `json:"Submask"`
TCPMaxConn int `json:"TCPMaxConn"`
TCPPort int `json:"TCPPort"`
UDPPort int `json:"UDPPort"`
UseHSDownLoad bool `json:"UseHSDownLoad"`
Version string `json:"Version"`
}
func getResponses(conn *net.UDPConn) (infos []*NetCommon) {
if err := conn.SetReadDeadline(time.Now().Add(time.Second * 2)); err != nil {
return
}
var ips []net.IP // processed IPs
b := make([]byte, 4096)
loop:
for {
n, addr, err := conn.ReadFromUDP(b)
if err != nil {
break
}
for _, ip := range ips {
if ip.Equal(addr.IP) {
continue loop
}
}
if n <= 20+1 {
continue
}
var msg Message
if err = json.Unmarshal(b[20:n-1], &msg); err != nil {
continue
}
infos = append(infos, &msg.NetCommon)
ips = append(ips, addr.IP)
}
return
}
func hexToDecimalBytes(hexIP string) (string, error) {
b, err := hex.DecodeString(hexIP[2:]) // remove the '0x' prefix
if err != nil {
return "", err
}
return fmt.Sprintf("%d.%d.%d.%d", b[3], b[2], b[1], b[0]), nil
}

View File

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

View File

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

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

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

View File

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

View File

@@ -1,26 +0,0 @@
package exec
import (
"github.com/AlexxIT/go2rtc/pkg/core"
"io"
"os/exec"
)
// PipeCloser - return StdoutPipe that Kill cmd on Close call
func PipeCloser(cmd *exec.Cmd) (io.ReadCloser, error) {
stdout, err := cmd.StdoutPipe()
if err != nil {
return nil, err
}
return pipeCloser{stdout, cmd}, nil
}
type pipeCloser struct {
io.ReadCloser
cmd *exec.Cmd
}
func (p pipeCloser) Close() error {
return core.Any(p.ReadCloser.Close(), p.cmd.Process.Kill(), p.cmd.Wait())
}

91
internal/expr/README.md Normal file
View File

@@ -0,0 +1,91 @@
# Expr
[Expr](https://github.com/antonmedv/expr) - expression language and expression evaluation for Go.
- [language definition](https://expr.medv.io/docs/Language-Definition) - takes best from JS, Python, Jinja2 syntax
- your expression should return a link of any supported source
- expression supports multiple operation, but:
- all operations must be separated by a semicolon
- all operations, except the last one, must declare a new variable (`let s = "abc";`)
- the last operation should return a string
- go2rtc supports additional functions:
- `fetch` - JS-like HTTP requests
- `match` - JS-like RegExp queries
## Examples
**Two way audio for Dahua VTO**
```yaml
streams:
dahua_vto: |
expr: let host = "admin:password@192.168.1.123";
fetch("http://"+host+"/cgi-bin/configManager.cgi?action=setConfig&Encode[0].MainFormat[0].Audio.Compression=G.711A&Encode[0].MainFormat[0].Audio.Frequency=8000").ok
? "rtsp://"+host+"/cam/realmonitor?channel=1&subtype=0&unicast=true&proto=Onvif" : ""
```
**dom.ru**
You can get credentials via:
- https://github.com/alexmorbo/domru (file `/share/domru/accounts`)
- https://github.com/ad/domru
```yaml
streams:
dom_ru: |
expr: let camera = "99999999"; let token = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"; let operator = 99;
fetch("https://myhome.novotelecom.ru/rest/v1/forpost/cameras/"+camera+"/video", {
headers: {Authorization: "Bearer "+token, Operator: operator}
}).json().data.URL
```
**Parse HLS files from Apple**
Same example in two languages - python and expr.
```yaml
streams:
example_python: |
echo:python -c 'from urllib.request import urlopen; import re
# url1 = "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_16x9/bipbop_16x9_variant.m3u8"
html1 = urlopen("https://developer.apple.com/streaming/examples/basic-stream-osx-ios5.html").read().decode("utf-8")
url1 = re.search(r"https.+?m3u8", html1)[0]
# url2 = "gear1/prog_index.m3u8"
html2 = urlopen(url1).read().decode("utf-8")
url2 = re.search(r"^[a-z0-1/_]+\.m3u8$", html2, flags=re.MULTILINE)[0]
# url3 = "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_16x9/gear1/prog_index.m3u8"
url3 = url1[:url1.rindex("/")+1] + url2
print("ffmpeg:" + url3 + "#video=copy")'
example_expr: |
expr:
let html1 = fetch("https://developer.apple.com/streaming/examples/basic-stream-osx-ios5.html").text;
let url1 = match(html1, "https.+?m3u8")[0];
let html2 = fetch(url1).text;
let url2 = match(html2, "^[a-z0-1/_]+\\.m3u8$", "m")[0];
let url3 = url1[:lastIndexOf(url1, "/")+1] + url2;
"ffmpeg:" + url3 + "#video=copy"
```
## Comparsion
| expr | python | js |
|------------------------------|----------------------------|--------------------------------|
| let x = 1; | x = 1 | let x = 1 |
| {a: 1, b: 2} | {"a": 1, "b": 2} | {a: 1, b: 2} |
| let r = fetch(url, {method}) | r = request(method, url) | r = await fetch(url, {method}) |
| r.ok | r.ok | r.ok |
| r.status | r.status_code | r.status |
| r.text | r.text | await r.text() |
| r.json() | r.json() | await r.json() |
| r.headers | r.headers | r.headers |
| let m = match(text, "abc") | m = re.search("abc", text) | let m = text.match(/abc/) |

28
internal/expr/expr.go Normal file
View File

@@ -0,0 +1,28 @@
package expr
import (
"errors"
"github.com/AlexxIT/go2rtc/internal/app"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/expr"
)
func Init() {
log := app.GetLogger("expr")
streams.RedirectFunc("expr", func(url string) (string, error) {
v, err := expr.Eval(url[5:], nil)
if err != nil {
return "", err
}
log.Debug().Msgf("[expr] url=%s", url)
if url = v.(string); url == "" {
return "", errors.New("expr: result is empty")
}
return url, nil
})
}

View File

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

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

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

View File

@@ -0,0 +1,99 @@
//go:build freebsd || netbsd || openbsd || dragonfly
package device
import (
"net/url"
"os"
"os/exec"
"regexp"
"strings"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/pkg/core"
)
func queryToInput(query url.Values) string {
if video := query.Get("video"); video != "" {
// https://ffmpeg.org/ffmpeg-devices.html#video4linux2_002c-v4l2
input := "-f v4l2"
for key, value := range query {
switch key {
case "resolution":
input += " -video_size " + value[0]
case "video_size", "pixel_format", "input_format", "framerate", "use_libv4l2":
input += " -" + key + " " + value[0]
}
}
return input + " -i " + indexToItem(videos, video)
}
if audio := query.Get("audio"); audio != "" {
input := "-f oss"
for key, value := range query {
switch key {
case "channels", "sample_rate":
input += " -" + key + " " + value[0]
}
}
return input + " -i " + indexToItem(audios, audio)
}
return ""
}
func initDevices() {
files, err := os.ReadDir("/dev")
if err != nil {
return
}
for _, file := range files {
if !strings.HasPrefix(file.Name(), core.KindVideo) {
continue
}
name := "/dev/" + file.Name()
cmd := exec.Command(
Bin, "-hide_banner", "-f", "v4l2", "-list_formats", "all", "-i", name,
)
b, _ := cmd.CombinedOutput()
// [video4linux2,v4l2 @ 0x860b92280] Raw : yuyv422 : YUYV 4:2:2 : 640x480 160x120 176x144 320x176 320x240 352x288 432x240 544x288 640x360 752x416 800x448 800x600 864x480 960x544 960x720 1024x576 1184x656 1280x720 1280x960
// [video4linux2,v4l2 @ 0x860b92280] Compressed: mjpeg : Motion-JPEG : 640x480 160x120 176x144 320x176 320x240 352x288 432x240 544x288 640x360 752x416 800x448 800x600 864x480 960x544 960x720 1024x576 1184x656 1280x720 1280x960
re := regexp.MustCompile("(Raw *|Compressed): +(.+?) : +(.+?) : (.+)")
m := re.FindAllStringSubmatch(string(b), -1)
for _, i := range m {
size, _, _ := strings.Cut(i[4], " ")
stream := &api.Source{
Name: i[3],
Info: i[4],
URL: "ffmpeg:device?video=" + name + "&input_format=" + i[2] + "&video_size=" + size,
}
if i[1] != "Compressed" {
stream.URL += "#video=h264#hardware"
}
videos = append(videos, name)
streams = append(streams, stream)
}
}
err = exec.Command(Bin, "-f", "oss", "-i", "/dev/dsp", "-t", "1", "-f", "null", "-").Run()
if err == nil {
stream := &api.Source{
Name: "OSS default",
Info: " ",
URL: "ffmpeg:device?audio=default&channels=1&sample_rate=16000&#audio=opus",
}
audios = append(audios, "default")
streams = append(streams, stream)
}
}

View File

@@ -1,26 +1,46 @@
//go:build darwin || ios
package device
import (
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/pkg/core"
"net/url"
"os/exec"
"regexp"
"strings"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/pkg/core"
)
// https://trac.ffmpeg.org/wiki/Capture/Webcam
const deviceInputPrefix = "-f avfoundation"
func queryToInput(query url.Values) string {
video := query.Get("video")
audio := query.Get("audio")
func deviceInputSuffix(video, audio string) string {
switch {
case video != "" && audio != "":
return `"` + video + `:` + audio + `"`
case video != "":
return `"` + video + `"`
case audio != "":
return `":` + audio + `"`
if video == "" && audio == "" {
return ""
}
return ""
// https://ffmpeg.org/ffmpeg-devices.html#avfoundation
input := "-f avfoundation"
if video != "" {
video = indexToItem(videos, video)
for key, value := range query {
switch key {
case "resolution":
input += " -video_size " + value[0]
case "pixel_format", "framerate", "video_size", "capture_cursor", "capture_mouse_clicks", "capture_raw_data":
input += " -" + key + " " + value[0]
}
}
}
if audio != "" {
audio = indexToItem(audios, audio)
}
return input + ` -i "` + video + `:` + audio + `"`
}
func initDevices() {
@@ -61,7 +81,7 @@ func initDevices() {
audios = append(audios, name)
}
streams = append(streams, api.Stream{
streams = append(streams, &api.Source{
Name: name, URL: "ffmpeg:device?" + kind + "=" + name,
})
}

View File

@@ -1,21 +1,49 @@
//go:build unix && !darwin && !freebsd && !netbsd && !openbsd && !dragonfly
package device
import (
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/pkg/core"
"net/url"
"os"
"os/exec"
"regexp"
"strings"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/pkg/core"
)
// https://trac.ffmpeg.org/wiki/Capture/Webcam
const deviceInputPrefix = "-f v4l2"
func queryToInput(query url.Values) string {
if video := query.Get("video"); video != "" {
// https://ffmpeg.org/ffmpeg-devices.html#video4linux2_002c-v4l2
input := "-f v4l2"
func deviceInputSuffix(video, audio string) string {
if video != "" {
return video
for key, value := range query {
switch key {
case "resolution":
input += " -video_size " + value[0]
case "video_size", "pixel_format", "input_format", "framerate", "use_libv4l2":
input += " -" + key + " " + value[0]
}
}
return input + " -i " + indexToItem(videos, video)
}
if audio := query.Get("audio"); audio != "" {
// https://trac.ffmpeg.org/wiki/Capture/ALSA
input := "-f alsa"
for key, value := range query {
switch key {
case "channels", "sample_rate":
input += " -" + key + " " + value[0]
}
}
return input + " -i " + indexToItem(audios, audio)
}
return ""
}
@@ -44,8 +72,9 @@ func initDevices() {
m := re.FindAllStringSubmatch(string(b), -1)
for _, i := range m {
size, _, _ := strings.Cut(i[4], " ")
stream := api.Stream{
Name: i[3] + " | " + i[4],
stream := &api.Source{
Name: i[3],
Info: i[4],
URL: "ffmpeg:device?video=" + name + "&input_format=" + i[2] + "&video_size=" + size,
}
@@ -57,4 +86,16 @@ func initDevices() {
streams = append(streams, stream)
}
}
err = exec.Command(Bin, "-f", "alsa", "-i", "default", "-t", "1", "-f", "null", "-").Run()
if err == nil {
stream := &api.Source{
Name: "ALSA default",
Info: " ",
URL: "ffmpeg:device?audio=default&channels=1&sample_rate=16000&#audio=opus",
}
audios = append(audios, "default")
streams = append(streams, stream)
}
}

View File

@@ -1,25 +1,64 @@
//go:build windows
package device
import (
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/pkg/core"
"net/url"
"os/exec"
"regexp"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/pkg/core"
)
// https://trac.ffmpeg.org/wiki/DirectShow
const deviceInputPrefix = "-f dshow"
func queryToInput(query url.Values) string {
video := query.Get("video")
audio := query.Get("audio")
func deviceInputSuffix(video, audio string) string {
switch {
case video != "" && audio != "":
return `video="` + video + `":audio=` + audio + `"`
case video != "":
return `video="` + video + `"`
case audio != "":
return `audio="` + audio + `"`
if video == "" && audio == "" {
return ""
}
return ""
// https://ffmpeg.org/ffmpeg-devices.html#dshow
input := "-f dshow"
if video != "" {
video = indexToItem(videos, video)
for key, value := range query {
switch key {
case "resolution":
input += " -video_size " + value[0]
case "video_size", "framerate", "pixel_format":
input += " -" + key + " " + value[0]
}
}
}
if audio != "" {
audio = indexToItem(audios, audio)
for key, value := range query {
switch key {
case "sample_rate", "sample_size", "channels", "audio_buffer_size":
input += " -" + key + " " + value[0]
}
}
}
if video != "" {
input += ` -i "video=` + video
if audio != "" {
input += `:audio=` + audio
}
input += `"`
} else {
input += ` -i "audio=` + audio + `"`
}
return input
}
func initDevices() {
@@ -33,7 +72,7 @@ func initDevices() {
name := m[1]
kind := m[2]
stream := api.Stream{
stream := &api.Source{
Name: name, URL: "ffmpeg:device?" + kind + "=" + name,
}
@@ -43,6 +82,7 @@ func initDevices() {
stream.URL += "#video=h264#hardware"
case core.KindAudio:
audios = append(audios, name)
stream.URL += "&channels=1&sample_rate=16000&audio_buffer_size=10"
}
streams = append(streams, stream)

View File

@@ -1,12 +1,12 @@
package device
import (
"github.com/AlexxIT/go2rtc/internal/api"
"net/http"
"net/url"
"strconv"
"strings"
"sync"
"github.com/AlexxIT/go2rtc/internal/api"
)
func Init(bin string) {
@@ -15,56 +15,32 @@ func Init(bin string) {
api.HandleFunc("api/ffmpeg/devices", apiDevices)
}
func GetInput(src string) (string, error) {
func GetInput(src string) string {
query, err := url.ParseQuery(src)
if err != nil {
return ""
}
runonce.Do(initDevices)
input := deviceInputPrefix
var video, audio string
if i := strings.IndexByte(src, '?'); i > 0 {
query, err := url.ParseQuery(src[i+1:])
if err != nil {
return "", err
}
for key, value := range query {
switch key {
case "video":
video = value[0]
case "audio":
audio = value[0]
case "resolution":
input += " -video_size " + value[0]
default: // "input_format", "framerate", "video_size"
input += " -" + key + " " + value[0]
}
}
}
if video != "" {
if i, err := strconv.Atoi(video); err == nil && i < len(videos) {
video = videos[i]
}
}
if audio != "" {
if i, err := strconv.Atoi(audio); err == nil && i < len(audios) {
audio = audios[i]
}
}
input += " -i " + deviceInputSuffix(video, audio)
return input, nil
return queryToInput(query)
}
var Bin string
var videos, audios []string
var streams []api.Stream
var streams []*api.Source
var runonce sync.Once
func apiDevices(w http.ResponseWriter, r *http.Request) {
runonce.Do(initDevices)
api.ResponseStreams(w, streams)
api.ResponseSources(w, streams)
}
func indexToItem(items []string, index string) string {
if i, err := strconv.Atoi(index); err == nil && i < len(items) {
return items[i]
}
return index
}

View File

@@ -1,39 +1,58 @@
package ffmpeg
import (
"errors"
"net/url"
"strings"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/app"
"github.com/AlexxIT/go2rtc/internal/ffmpeg/device"
"github.com/AlexxIT/go2rtc/internal/ffmpeg/hardware"
"github.com/AlexxIT/go2rtc/internal/ffmpeg/virtual"
"github.com/AlexxIT/go2rtc/internal/rtsp"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/ffmpeg"
"net/url"
"strings"
"github.com/rs/zerolog"
)
func Init() {
var cfg struct {
Mod map[string]string `yaml:"ffmpeg"`
Log struct {
Level string `yaml:"ffmpeg"`
} `yaml:"log"`
}
cfg.Mod = defaults // will be overriden from yaml
cfg.Log.Level = "error"
app.LoadConfig(&cfg)
if app.GetLogger("exec").GetLevel() >= 0 {
defaults["global"] += " -v error"
}
log = app.GetLogger("ffmpeg")
streams.HandleFunc("ffmpeg", func(url string) (core.Producer, error) {
args := parseArgs(url[7:]) // remove `ffmpeg:`
if args == nil {
return nil, errors.New("can't generate ffmpeg command")
// zerolog levels: trace debug info warn error fatal panic disabled
// FFmpeg levels: trace debug verbose info warning error fatal panic quiet
if cfg.Log.Level == "warn" {
cfg.Log.Level = "warning"
}
defaults["global"] += " -v " + cfg.Log.Level
streams.RedirectFunc("ffmpeg", func(url string) (string, error) {
if _, err := Version(); err != nil {
return "", err
}
return streams.GetProducer("exec:" + args.String())
args := parseArgs(url[7:])
if core.Contains(args.Codecs, "auto") {
return "", nil // force call streams.HandleFunc("ffmpeg")
}
return "exec:" + args.String(), nil
})
streams.HandleFunc("ffmpeg", NewProducer)
api.HandleFunc("api/ffmpeg", apiFFmpeg)
device.Init(defaults["bin"])
hardware.Init(defaults["bin"])
}
@@ -45,72 +64,108 @@ var defaults = map[string]string{
// inputs
"file": "-re -i {input}",
"http": "-fflags nobuffer -flags low_delay -i {input}",
"rtsp": "-fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_transport tcp -i {input}",
"rtsp": "-fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i {input}",
"rtsp/udp": "-fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -i {input}",
// output
"output": "-user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}",
"output/mjpeg": "-f mjpeg -",
"output/raw": "-f yuv4mpegpipe -",
"output/aac": "-f adts -",
"output/wav": "-f wav -",
// `-preset superfast` - we can't use ultrafast because it doesn't support `-profile main -level 4.1`
// `-tune zerolatency` - for minimal latency
// `-profile high -level 4.1` - most used streaming profile
"h264": "-c:v libx264 -g 50 -profile:v high -level:v 4.1 -preset:v superfast -tune:v zerolatency -pix_fmt:v yuvj420p",
"h265": "-c:v libx265 -g 50 -profile:v main -level:v 5.1 -preset:v superfast -tune:v zerolatency",
// `-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",
"mjpeg": "-c:v mjpeg",
//"mjpeg": "-c:v mjpeg -force_duplicated_matrix:v 1 -huffman:v 0 -pix_fmt:v yuvj420p",
"raw": "-c:v rawvideo",
"raw/gray8": "-c:v rawvideo -pix_fmt:v gray8",
"raw/yuv420p": "-c:v rawvideo -pix_fmt:v yuv420p",
"raw/yuv422p": "-c:v rawvideo -pix_fmt:v yuv422p",
"raw/yuv444p": "-c:v rawvideo -pix_fmt:v yuv444p",
// https://ffmpeg.org/ffmpeg-codecs.html#libopus-1
"opus": "-c:a libopus -ar:a 48000 -ac:a 2 -application:a voip -compression_level:a 0",
// https://github.com/pion/webrtc/issues/1514
// https://ffmpeg.org/ffmpeg-resampler.html
// `-async 1` or `-min_comp 0` - force resampling for static timestamp inc, important for WebRTC audio quality
"opus": "-c:a libopus -application:a lowdelay -min_comp 0",
"opus/16000": "-c:a libopus -application:a lowdelay -min_comp 0 -ar:a 16000 -ac:a 1",
"pcmu": "-c:a pcm_mulaw -ar:a 8000 -ac:a 1",
"pcmu/8000": "-c:a pcm_mulaw -ar:a 8000 -ac:a 1",
"pcmu/16000": "-c:a pcm_mulaw -ar:a 16000 -ac:a 1",
"pcmu/48000": "-c:a pcm_mulaw -ar:a 48000 -ac:a 1",
"pcma": "-c:a pcm_alaw -ar:a 8000 -ac:a 1",
"pcma/8000": "-c:a pcm_alaw -ar:a 8000 -ac:a 1",
"pcma/16000": "-c:a pcm_alaw -ar:a 16000 -ac:a 1",
"pcma/48000": "-c:a pcm_alaw -ar:a 48000 -ac:a 1",
"aac": "-c:a aac", // keep sample rate and channels
"aac/16000": "-c:a aac -ar:a 16000 -ac:a 1",
"mp3": "-c:a libmp3lame -q:a 8",
"pcm": "-c:a pcm_s16be -ar:a 8000 -ac:a 1",
"pcm/8000": "-c:a pcm_s16be -ar:a 8000 -ac:a 1",
"pcm/16000": "-c:a pcm_s16be -ar:a 16000 -ac:a 1",
"pcm/48000": "-c:a pcm_s16be -ar:a 48000 -ac:a 1",
"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
// better not to set `-async_depth:v 1` like for QSV, because framedrops
// `-bf 0` - disable B-frames is very important
"h264/vaapi": "-c:v h264_vaapi -g 50 -bf 0 -profile:v high -level:v 4.1 -sei:v 0",
"h265/vaapi": "-c:v hevc_vaapi -g 50 -bf 0 -profile:v high -level:v 5.1 -sei:v 0",
"h265/vaapi": "-c:v hevc_vaapi -g 50 -bf 0 -profile:v main -level:v 5.1 -sei:v 0",
"mjpeg/vaapi": "-c:v mjpeg_vaapi",
// hardware Raspberry
"h264/v4l2m2m": "-c:v h264_v4l2m2m -g 50 -bf 0",
"h265/v4l2m2m": "-c:v hevc_v4l2m2m -g 50 -bf 0",
// hardware 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 -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
"h264/cuda": "-c:v h264_nvenc -g 50 -profile:v high -level:v auto -preset:v p2 -tune:v ll",
"h265/cuda": "-c:v hevc_nvenc -g 50 -profile:v high -level:v auto",
"h264/cuda": "-c:v h264_nvenc -g 50 -bf 0 -profile:v high -level:v auto -preset:v p2 -tune:v ll",
"h265/cuda": "-c:v hevc_nvenc -g 50 -bf 0 -profile:v main -level:v auto",
// hardware Intel on Windows
"h264/dxva2": "-c:v h264_qsv -g 50 -bf 0 -profile:v high -level:v 4.1 -async_depth:v 1",
"h265/dxva2": "-c:v hevc_qsv -g 50 -bf 0 -profile:v high -level:v 5.1 -async_depth:v 1",
"mjpeg/dxva2": "-c:v mjpeg_qsv -profile:v high -level:v 5.1",
"h265/dxva2": "-c:v hevc_qsv -g 50 -bf 0 -profile:v main -level:v 5.1 -async_depth:v 1",
"mjpeg/dxva2": "-c:v mjpeg_qsv",
// hardware macOS
"h264/videotoolbox": "-c:v h264_videotoolbox -g 50 -bf 0 -profile:v high -level:v 4.1",
"h265/videotoolbox": "-c:v hevc_videotoolbox -g 50 -bf 0 -profile:v high -level:v 5.1",
"h265/videotoolbox": "-c:v hevc_videotoolbox -g 50 -bf 0 -profile:v main -level:v 5.1",
}
var log zerolog.Logger
// configTemplate - return template from config (defaults) if exist or return raw template
func configTemplate(template string) string {
if s := defaults[template]; s != "" {
return s
}
return template
}
// inputTemplate - select input template from YAML config by template name
// if query has input param - select another tempalte by this name
// if query has input param - select another template by this name
// if there is no another template - use input param as template
func inputTemplate(name, s string, query url.Values) string {
var template string
if input := query.Get("input"); input != "" {
if template = defaults[input]; template == "" {
template = input
}
template = configTemplate(input)
} else {
template = defaults[name]
}
@@ -120,14 +175,16 @@ func inputTemplate(name, s string, query url.Values) string {
func parseArgs(s string) *ffmpeg.Args {
// init FFmpeg arguments
args := &ffmpeg.Args{
Bin: defaults["bin"],
Global: defaults["global"],
Output: defaults["output"],
Bin: defaults["bin"],
Global: defaults["global"],
Output: defaults["output"],
Version: verAV,
}
var source = s
var query url.Values
if i := strings.IndexByte(s, '#'); i > 0 {
query = parseQuery(s[i+1:])
if i := strings.IndexByte(s, '#'); i >= 0 {
query = streams.ParseQuery(s[i+1:])
args.Video = len(query["video"])
args.Audio = len(query["audio"])
s = s[:i]
@@ -167,12 +224,19 @@ func parseArgs(s string) *ffmpeg.Args {
default:
s += "?video&audio"
}
s += "&source=ffmpeg:" + url.QueryEscape(source)
for _, v := range query["query"] {
s += "&" + v
}
args.Input = inputTemplate("rtsp", s, query)
} else if strings.HasPrefix(s, "device?") {
var err error
args.Input, err = device.GetInput(s)
if err != nil {
return nil
} else if i = strings.Index(s, "?"); i > 0 {
switch s[:i] {
case "device":
args.Input = device.GetInput(s[i+1:])
case "virtual":
args.Input = virtual.GetInput(s[i+1:])
case "tts":
args.Input = virtual.GetInputTTS(s[i+1:])
}
} else {
args.Input = inputTemplate("file", s, query)
@@ -191,6 +255,8 @@ func parseArgs(s string) *ffmpeg.Args {
if query != nil {
// 1. Process raw params for FFmpeg
for _, raw := range query["raw"] {
// support templates https://github.com/AlexxIT/go2rtc/issues/487
raw = configTemplate(raw)
args.AddCodec(raw)
}
@@ -226,6 +292,18 @@ func parseArgs(s string) *ffmpeg.Args {
}
}
for _, drawtext := range query["drawtext"] {
// support templates https://github.com/AlexxIT/go2rtc/issues/487
drawtext = configTemplate(drawtext)
// support default timestamp format
if !strings.Contains(drawtext, "text=") {
drawtext += `:text='%{localtime\:%Y-%m-%d %X}'`
}
args.AddFilter("drawtext=" + drawtext)
}
// 3. Process video codecs
if args.Video > 0 {
for _, video := range query["video"] {
@@ -239,8 +317,12 @@ func parseArgs(s string) *ffmpeg.Args {
args.AddCodec("-c:v copy")
}
}
} else {
args.AddCodec("-vn")
}
if query["bitrate"] != nil {
// https://trac.ffmpeg.org/wiki/Limiting%20the%20output%20bitrate
b := query["bitrate"][0]
args.AddCodec("-b:v " + b + " -maxrate " + b + " -bufsize " + b)
}
// 4. Process audio codecs
@@ -256,8 +338,6 @@ func parseArgs(s string) *ffmpeg.Args {
args.AddCodec("-c:a copy")
}
}
} else {
args.AddCodec("-an")
}
if query["hardware"] != nil {
@@ -265,29 +345,37 @@ func parseArgs(s string) *ffmpeg.Args {
}
}
if args.Codecs == nil {
switch {
case args.Video == 0 && args.Audio == 0:
args.AddCodec("-c copy")
case args.Video == 0:
args.AddCodec("-vn")
case args.Audio == 0:
args.AddCodec("-an")
}
// transcoding to only mjpeg
if (args.Video == 1 && args.Audio == 0 && query.Get("video") == "mjpeg") ||
// no transcoding from mjpeg input
(args.Video == 0 && args.Audio == 0 && strings.Contains(args.Input, " mjpeg ")) {
args.Output = defaults["output/mjpeg"]
// change otput from RTSP to some other pipe format
switch {
case args.Video == 0 && args.Audio == 0:
// no transcoding from mjpeg input (ffmpeg device with support output as raw MJPEG)
if strings.Contains(args.Input, " mjpeg ") {
args.Output = defaults["output/mjpeg"]
}
case args.Video == 1 && args.Audio == 0:
switch core.Before(query.Get("video"), "/") {
case "mjpeg":
args.Output = defaults["output/mjpeg"]
case "raw":
args.Output = defaults["output/raw"]
}
case args.Video == 0 && args.Audio == 1:
switch core.Before(query.Get("audio"), "/") {
case "aac":
args.Output = defaults["output/aac"]
case "pcma", "pcmu", "pcml":
args.Output = defaults["output/wav"]
}
}
return args
}
func parseQuery(s string) map[string][]string {
query := map[string][]string{}
for _, key := range strings.Split(s, "#") {
var value string
i := strings.IndexByte(key, '=')
if i > 0 {
key, value = key[:i], key[i+1:]
}
query[key] = append(query[key], value)
}
return query
}

View File

@@ -1,23 +1,391 @@
package ffmpeg
import (
"github.com/stretchr/testify/assert"
"testing"
"github.com/AlexxIT/go2rtc/pkg/ffmpeg"
"github.com/stretchr/testify/require"
)
func TestParseArgs(t *testing.T) {
args := parseArgs("rtsp://example.com#video=h264#rotate=180")
assert.Equal(t, "ffmpeg -hide_banner -allowed_media_types video -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_transport tcp -i rtsp://example.com -c:v libx264 -g 50 -profile:v high -level:v 4.1 -preset:v superfast -tune:v zerolatency -pix_fmt:v yuvj420p -an -vf transpose=1,transpose=1 -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}", args.String())
args = parseArgs("rtsp://example.com#video=h264#rotate=180#hardware=vaapi")
assert.Equal(t, "ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -allowed_media_types video -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_transport tcp -i rtsp://example.com -c:v h264_vaapi -g 50 -bf 0 -profile:v high -level:v 4.1 -sei:v 0 -an -vf format=vaapi|nv12,hwupload,transpose_vaapi=4 -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}", args.String())
args = parseArgs("/media/bbb.mp4#video=mjpeg")
assert.Equal(t, "ffmpeg -hide_banner -re -i /media/bbb.mp4 -c:v mjpeg -an -f mjpeg -", args.String())
args = parseArgs("/media/bbb.mp4#video=mjpeg#hardware=vaapi")
assert.Equal(t, "ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -re -i /media/bbb.mp4 -c:v mjpeg_vaapi -an -vf format=vaapi|nv12,hwupload -f mjpeg -", args.String())
args = parseArgs("device?video=0&input_format=mjpeg&video_size=1920x1080")
assert.Equal(t, `ffmpeg -hide_banner -f dshow -input_format mjpeg -video_size 1920x1080 -i video="0" -c copy -f mjpeg -`, args.String())
func TestParseArgsFile(t *testing.T) {
tests := []struct {
name string
source string
expect string
}{
{
name: "[FILE] all tracks will be copied without transcoding codecs",
source: "/media/bbb.mp4",
expect: `ffmpeg -hide_banner -re -i /media/bbb.mp4 -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
},
{
name: "[FILE] video will be transcoded to H264, audio will be skipped",
source: "/media/bbb.mp4#video=h264",
expect: `ffmpeg -hide_banner -re -i /media/bbb.mp4 -c:v libx264 -g 50 -profile:v high -level:v 4.1 -preset:v superfast -tune:v zerolatency -pix_fmt:v yuv420p -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
},
{
name: "[FILE] video will be copied, audio will be transcoded to pcmu",
source: "/media/bbb.mp4#video=copy#audio=pcmu",
expect: `ffmpeg -hide_banner -re -i /media/bbb.mp4 -c:v copy -c:a pcm_mulaw -ar:a 8000 -ac:a 1 -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
},
{
name: "[FILE] video will be transcoded to H265 and rotate 270º, audio will be skipped",
source: "/media/bbb.mp4#video=h265#rotate=-90",
expect: `ffmpeg -hide_banner -re -i /media/bbb.mp4 -c:v libx265 -g 50 -profile:v main -level:v 5.1 -preset:v superfast -tune:v zerolatency -pix_fmt:v yuv420p -an -vf "transpose=2" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
},
{
name: "[FILE] video will be output for MJPEG to pipe, audio will be skipped",
source: "/media/bbb.mp4#video=mjpeg",
expect: `ffmpeg -hide_banner -re -i /media/bbb.mp4 -c:v mjpeg -an -f mjpeg -`,
},
{
name: "https://github.com/AlexxIT/go2rtc/issues/509",
source: "ffmpeg:test.mp4#raw=-ss 00:00:20",
expect: `ffmpeg -hide_banner -re -i ffmpeg:test.mp4 -ss 00:00:20 -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
args := parseArgs(test.source)
require.Equal(t, test.expect, args.String())
})
}
}
func TestParseArgsDevice(t *testing.T) {
tests := []struct {
name string
source string
expect string
}{
{
name: "[DEVICE] video will be output for MJPEG to pipe, with size 1920x1080",
source: "device?video=0&video_size=1920x1080",
expect: `ffmpeg -hide_banner -f dshow -video_size 1920x1080 -i "video=0" -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
},
{
name: "[DEVICE] video will be transcoded to H265 with framerate 20, audio will be skipped",
source: "device?video=0&framerate=20#video=h265",
expect: `ffmpeg -hide_banner -f dshow -framerate 20 -i "video=0" -c:v libx265 -g 50 -profile:v main -level:v 5.1 -preset:v superfast -tune:v zerolatency -pix_fmt:v yuv420p -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
},
{
name: "[DEVICE] video/audio",
source: "device?video=FaceTime HD Camera&audio=Microphone (High Definition Audio Device)",
expect: `ffmpeg -hide_banner -f dshow -i "video=FaceTime HD Camera:audio=Microphone (High Definition Audio Device)" -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
args := parseArgs(test.source)
require.Equal(t, test.expect, args.String())
})
}
}
func TestParseArgsIpCam(t *testing.T) {
tests := []struct {
name string
source string
expect string
}{
{
name: "[HTTP] video will be copied",
source: "http://example.com",
expect: `ffmpeg -hide_banner -fflags nobuffer -flags low_delay -i http://example.com -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
},
{
name: "[HTTP-MJPEG] video will be transcoded to H264",
source: "http://example.com#video=h264",
expect: `ffmpeg -hide_banner -fflags nobuffer -flags low_delay -i http://example.com -c:v libx264 -g 50 -profile:v high -level:v 4.1 -preset:v superfast -tune:v zerolatency -pix_fmt:v yuv420p -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
},
{
name: "[HLS] video will be copied, audio will be skipped",
source: "https://example.com#video=copy",
expect: `ffmpeg -hide_banner -fflags nobuffer -flags low_delay -i https://example.com -c:v copy -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
},
{
name: "[RTSP] video will be copied without transcoding codecs",
source: "rtsp://example.com",
expect: `ffmpeg -hide_banner -allowed_media_types video+audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
},
{
name: "[RTSP] video with resize to 1280x720, should be transcoded, so select H265",
source: "rtsp://example.com#video=h265#width=1280#height=720",
expect: `ffmpeg -hide_banner -allowed_media_types video -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:v libx265 -g 50 -profile:v main -level:v 5.1 -preset:v superfast -tune:v zerolatency -pix_fmt:v yuv420p -an -vf "scale=1280:720" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
},
{
name: "[RTSP] video will be copied, changing RTSP transport from TCP to UDP+TCP",
source: "rtsp://example.com#input=rtsp/udp",
expect: `ffmpeg -hide_banner -allowed_media_types video+audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -i rtsp://example.com -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
},
{
name: "[RTMP] video will be copied, changing RTSP transport from TCP to UDP+TCP",
source: "rtmp://example.com#input=rtsp/udp",
expect: `ffmpeg -hide_banner -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -i rtmp://example.com -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
args := parseArgs(test.source)
require.Equal(t, test.expect, args.String())
})
}
}
func TestParseArgsAudio(t *testing.T) {
tests := []struct {
name string
source string
expect string
}{
{
name: "[AUDIO] audio will be transcoded to AAC, video will be skipped",
source: "rtsp://example.com#audio=aac",
expect: `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:a aac -vn -f adts -`,
},
{
name: "[AUDIO] audio will be transcoded to AAC/16000, video will be skipped",
source: "rtsp://example.com#audio=aac/16000",
expect: `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:a aac -ar:a 16000 -ac:a 1 -vn -f adts -`,
},
{
name: "[AUDIO] audio will be transcoded to OPUS, video will be skipped",
source: "rtsp://example.com#audio=opus",
expect: `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:a libopus -application:a lowdelay -min_comp 0 -vn -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
},
{
name: "[AUDIO] audio will be transcoded to PCMU, video will be skipped",
source: "rtsp://example.com#audio=pcmu",
expect: `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:a pcm_mulaw -ar:a 8000 -ac:a 1 -vn -f wav -`,
},
{
name: "[AUDIO] audio will be transcoded to PCMU/16000, video will be skipped",
source: "rtsp://example.com#audio=pcmu/16000",
expect: `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:a pcm_mulaw -ar:a 16000 -ac:a 1 -vn -f wav -`,
},
{
name: "[AUDIO] audio will be transcoded to PCMU/48000, video will be skipped",
source: "rtsp://example.com#audio=pcmu/48000",
expect: `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:a pcm_mulaw -ar:a 48000 -ac:a 1 -vn -f wav -`,
},
{
name: "[AUDIO] audio will be transcoded to PCMA, video will be skipped",
source: "rtsp://example.com#audio=pcma",
expect: `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:a pcm_alaw -ar:a 8000 -ac:a 1 -vn -f wav -`,
},
{
name: "[AUDIO] audio will be transcoded to PCMA/16000, video will be skipped",
source: "rtsp://example.com#audio=pcma/16000",
expect: `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:a pcm_alaw -ar:a 16000 -ac:a 1 -vn -f wav -`,
},
{
name: "[AUDIO] audio will be transcoded to PCMA/48000, video will be skipped",
source: "rtsp://example.com#audio=pcma/48000",
expect: `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:a pcm_alaw -ar:a 48000 -ac:a 1 -vn -f wav -`,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
args := parseArgs(test.source)
require.Equal(t, test.expect, args.String())
})
}
}
func TestParseArgsHwVaapi(t *testing.T) {
tests := []struct {
name string
source string
expect string
}{
{
name: "[HTTP-MJPEG] video will be transcoded to H264",
source: "http:///example.com#video=h264#hardware=vaapi",
expect: `ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -hwaccel_flags allow_profile_mismatch -fflags nobuffer -flags low_delay -i http:///example.com -c:v h264_vaapi -g 50 -bf 0 -profile:v high -level:v 4.1 -sei:v 0 -an -vf "format=vaapi|nv12,hwupload,scale_vaapi=out_color_matrix=bt709:out_range=tv:format=nv12" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
},
{
name: "[RTSP] video with rotation, should be transcoded, so select H264",
source: "rtsp://example.com#video=h264#rotate=180#hardware=vaapi",
expect: `ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -hwaccel_flags allow_profile_mismatch -allowed_media_types video -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:v h264_vaapi -g 50 -bf 0 -profile:v high -level:v 4.1 -sei:v 0 -an -vf "format=vaapi|nv12,hwupload,transpose_vaapi=4,scale_vaapi=out_color_matrix=bt709:out_range=tv:format=nv12" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
},
{
name: "[RTSP] video with resize to 1280x720, should be transcoded, so select H265",
source: "rtsp://example.com#video=h265#width=1280#height=720#hardware=vaapi",
expect: `ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -hwaccel_flags allow_profile_mismatch -allowed_media_types video -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:v hevc_vaapi -g 50 -bf 0 -profile:v main -level:v 5.1 -sei:v 0 -an -vf "format=vaapi|nv12,hwupload,scale_vaapi=1280:720" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
},
{
name: "[FILE] video will be output for MJPEG to pipe, audio will be skipped",
source: "/media/bbb.mp4#video=mjpeg#hardware=vaapi",
expect: `ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -hwaccel_flags allow_profile_mismatch -re -i /media/bbb.mp4 -c:v mjpeg_vaapi -an -vf "format=vaapi|nv12,hwupload" -f mjpeg -`,
},
{
name: "[DEVICE] MJPEG video with size 1920x1080 will be transcoded to H265",
source: "device?video=0&video_size=1920x1080#video=h265#hardware=vaapi",
expect: `ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -hwaccel_flags allow_profile_mismatch -f dshow -video_size 1920x1080 -i "video=0" -c:v hevc_vaapi -g 50 -bf 0 -profile:v main -level:v 5.1 -sei:v 0 -an -vf "format=vaapi|nv12,hwupload" -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 _TestParseArgsHwV4l2m2m(t *testing.T) {
// [HTTP-MJPEG] video will be transcoded to H264
args := parseArgs("http:///example.com#video=h264#hardware=v4l2m2m")
require.Equal(t, `ffmpeg -hide_banner -fflags nobuffer -flags low_delay -i http:///example.com -c:v h264_v4l2m2m -g 50 -bf 0 -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
// [RTSP] video with rotation, should be transcoded, so select H264
args = parseArgs("rtsp://example.com#video=h264#rotate=180#hardware=v4l2m2m")
require.Equal(t, `ffmpeg -hide_banner -allowed_media_types video -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:v h264_v4l2m2m -g 50 -bf 0 -an -vf "transpose=1,transpose=1" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
// [RTSP] video with resize to 1280x720, should be transcoded, so select H265
args = parseArgs("rtsp://example.com#video=h265#width=1280#height=720#hardware=v4l2m2m")
require.Equal(t, `ffmpeg -hide_banner -allowed_media_types video -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:v hevc_v4l2m2m -g 50 -bf 0 -an -vf "scale=1280:720" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
// [DEVICE] MJPEG video with size 1920x1080 will be transcoded to H265
args = parseArgs("device?video=0&video_size=1920x1080#video=h265#hardware=v4l2m2m")
require.Equal(t, `ffmpeg -hide_banner -f dshow -video_size 1920x1080 -i video="0" -c:v hevc_v4l2m2m -g 50 -bf 0 -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
}
func TestParseArgsHwRKMPP(t *testing.T) {
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) {
// [HTTP-MJPEG] video will be transcoded to H264
args := parseArgs("http:///example.com#video=h264#hardware=cuda")
require.Equal(t, `ffmpeg -hide_banner -hwaccel cuda -hwaccel_output_format cuda -fflags nobuffer -flags low_delay -i http:///example.com -c:v h264_nvenc -g 50 -bf 0 -profile:v high -level:v auto -preset:v p2 -tune:v ll -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
// [RTSP] video with rotation, should be transcoded, so select H264
args = parseArgs("rtsp://example.com#video=h264#rotate=180#hardware=cuda")
require.Equal(t, `ffmpeg -hide_banner -hwaccel cuda -hwaccel_output_format nv12 -allowed_media_types video -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:v h264_nvenc -g 50 -bf 0 -profile:v high -level:v auto -preset:v p2 -tune:v ll -an -vf "transpose=1,transpose=1,hwupload" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
// [RTSP] video with resize to 1280x720, should be transcoded, so select H265
args = parseArgs("rtsp://example.com#video=h265#width=1280#height=720#hardware=cuda")
require.Equal(t, `ffmpeg -hide_banner -hwaccel cuda -hwaccel_output_format cuda -allowed_media_types video -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:v hevc_nvenc -g 50 -bf 0 -profile:v high -level:v auto -an -vf "scale_cuda=1280:720" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
// [DEVICE] MJPEG video with size 1920x1080 will be transcoded to H265
args = parseArgs("device?video=0&video_size=1920x1080#video=h265#hardware=cuda")
require.Equal(t, `ffmpeg -hide_banner -hwaccel cuda -hwaccel_output_format cuda -f dshow -video_size 1920x1080 -i video="0" -c:v hevc_nvenc -g 50 -bf 0 -profile:v high -level:v auto -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
}
func _TestParseArgsHwDxva2(t *testing.T) {
// [HTTP-MJPEG] video will be transcoded to H264
args := parseArgs("http:///example.com#video=h264#hardware=dxva2")
require.Equal(t, `ffmpeg -hide_banner -hwaccel dxva2 -hwaccel_output_format dxva2_vld -fflags nobuffer -flags low_delay -i http:///example.com -c:v h264_qsv -g 50 -bf 0 -profile:v high -level:v 4.1 -async_depth:v 1 -an -vf "hwmap=derive_device=qsv,format=qsv" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
// [RTSP] video with rotation, should be transcoded, so select H264
args = parseArgs("rtsp://example.com#video=h264#rotate=180#hardware=dxva2")
require.Equal(t, `ffmpeg -hide_banner -hwaccel dxva2 -hwaccel_output_format dxva2_vld -allowed_media_types video -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:v h264_qsv -g 50 -bf 0 -profile:v high -level:v 4.1 -async_depth:v 1 -an -vf "hwmap=derive_device=qsv,format=qsv,transpose=1,transpose=1" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
// [RTSP] video with resize to 1280x720, should be transcoded, so select H265
args = parseArgs("rtsp://example.com#video=h265#width=1280#height=720#hardware=dxva2")
require.Equal(t, `ffmpeg -hide_banner -hwaccel dxva2 -hwaccel_output_format dxva2_vld -allowed_media_types video -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:v hevc_qsv -g 50 -bf 0 -profile:v high -level:v 5.1 -async_depth:v 1 -an -vf "hwmap=derive_device=qsv,format=qsv,scale_qsv=1280:720" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
// [FILE] video will be output for MJPEG to pipe, audio will be skipped
args = parseArgs("/media/bbb.mp4#video=mjpeg#hardware=dxva2")
require.Equal(t, `ffmpeg -hide_banner -hwaccel dxva2 -hwaccel_output_format dxva2_vld -re -i /media/bbb.mp4 -c:v mjpeg_qsv -profile:v high -level:v 5.1 -an -vf "hwmap=derive_device=qsv,format=qsv" -f mjpeg -`, args.String())
// [DEVICE] MJPEG video with size 1920x1080 will be transcoded to H265
args = parseArgs("device?video=0&video_size=1920x1080#video=h265#hardware=dxva2")
require.Equal(t, `ffmpeg -hide_banner -hwaccel dxva2 -hwaccel_output_format dxva2_vld -f dshow -video_size 1920x1080 -i video="0" -c:v hevc_qsv -g 50 -bf 0 -profile:v high -level:v 5.1 -async_depth:v 1 -an -vf "hwmap=derive_device=qsv,format=qsv" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
}
func _TestParseArgsHwVideotoolbox(t *testing.T) {
// [HTTP-MJPEG] video will be transcoded to H264
args := parseArgs("http:///example.com#video=h264#hardware=videotoolbox")
require.Equal(t, `ffmpeg -hide_banner -hwaccel videotoolbox -hwaccel_output_format videotoolbox_vld -fflags nobuffer -flags low_delay -i http:///example.com -c:v h264_videotoolbox -g 50 -bf 0 -profile:v high -level:v 4.1 -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
// [RTSP] video with rotation, should be transcoded, so select H264
args = parseArgs("rtsp://example.com#video=h264#rotate=180#hardware=videotoolbox")
require.Equal(t, `ffmpeg -hide_banner -hwaccel videotoolbox -hwaccel_output_format videotoolbox_vld -allowed_media_types video -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:v h264_videotoolbox -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())
// [RTSP] video with resize to 1280x720, should be transcoded, so select H265
args = parseArgs("rtsp://example.com#video=h265#width=1280#height=720#hardware=videotoolbox")
require.Equal(t, `ffmpeg -hide_banner -hwaccel videotoolbox -hwaccel_output_format videotoolbox_vld -allowed_media_types video -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:v hevc_videotoolbox -g 50 -bf 0 -profile:v high -level:v 5.1 -an -vf "scale=1280:720" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
// [DEVICE] MJPEG video with size 1920x1080 will be transcoded to H265
args = parseArgs("device?video=0&video_size=1920x1080#video=h265#hardware=videotoolbox")
require.Equal(t, `ffmpeg -hide_banner -hwaccel videotoolbox -hwaccel_output_format videotoolbox_vld -f dshow -video_size 1920x1080 -i video="0" -c:v hevc_videotoolbox -g 50 -bf 0 -profile:v high -level:v 5.1 -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
}
func TestDeckLink(t *testing.T) {
args := parseArgs(`DeckLink SDI (2)#video=h264#hardware=vaapi#input=-format_code Hp29 -f decklink -i "{input}"`)
require.Equal(t, `ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -hwaccel_flags allow_profile_mismatch -format_code Hp29 -f decklink -i "DeckLink SDI (2)" -c:v h264_vaapi -g 50 -bf 0 -profile:v high -level:v 4.1 -sei:v 0 -an -vf "format=vaapi|nv12,hwupload,scale_vaapi=out_color_matrix=bt709:out_range=tv:format=nv12" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
}
func TestDrawText(t *testing.T) {
tests := []struct {
name string
source string
expect string
}{
{
source: "http:///example.com#video=h264#drawtext=fontsize=12",
expect: `ffmpeg -hide_banner -fflags nobuffer -flags low_delay -i http:///example.com -c:v libx264 -g 50 -profile:v high -level:v 4.1 -preset:v superfast -tune:v zerolatency -pix_fmt:v yuv420p -an -vf "drawtext=fontsize=12:text='%{localtime\:%Y-%m-%d %X}'" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
},
{
source: "http:///example.com#video=h264#width=640#drawtext=fontsize=12",
expect: `ffmpeg -hide_banner -fflags nobuffer -flags low_delay -i http:///example.com -c:v libx264 -g 50 -profile:v high -level:v 4.1 -preset:v superfast -tune:v zerolatency -pix_fmt:v yuv420p -an -vf "scale=640:-1,drawtext=fontsize=12:text='%{localtime\:%Y-%m-%d %X}'" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
},
{
source: "http:///example.com#video=h264#width=640#drawtext=fontsize=12#hardware=vaapi",
expect: `ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format nv12 -hwaccel_flags allow_profile_mismatch -fflags nobuffer -flags low_delay -i http:///example.com -c:v h264_vaapi -g 50 -bf 0 -profile:v high -level:v 4.1 -sei:v 0 -an -vf "scale=640:-1,drawtext=fontsize=12:text='%{localtime\:%Y-%m-%d %X}',hwupload" -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 TestVersion(t *testing.T) {
verAV = ffmpeg.Version61
tests := []struct {
name string
source string
expect string
}{
{
source: "/media/bbb.mp4",
expect: `ffmpeg -hide_banner -readrate_initial_burst 0.001 -re -i /media/bbb.mp4 -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
args := parseArgs(test.source)
require.Equal(t, test.expect, args.String())
})
}
}

View File

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

View File

@@ -1,13 +1,12 @@
package hardware
import (
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/pkg/ffmpeg"
"net/http"
"os/exec"
"strings"
"github.com/rs/zerolog/log"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/pkg/ffmpeg"
)
const (
@@ -17,11 +16,12 @@ const (
EngineCUDA = "cuda" // NVidia on Windows and Linux
EngineDXVA2 = "dxva2" // Intel on Windows
EngineVideoToolbox = "videotoolbox" // macOS
EngineRKMPP = "rkmpp" // Rockchip
)
func Init(bin string) {
api.HandleFunc("api/ffmpeg/hardware", func(w http.ResponseWriter, r *http.Request) {
api.ResponseStreams(w, ProbeAll(bin))
api.ResponseSources(w, ProbeAll(bin))
})
}
@@ -55,33 +55,55 @@ func MakeHardware(args *ffmpeg.Args, engine string, defaults map[string]string)
switch engine {
case EngineVAAPI:
args.Input = "-hwaccel vaapi -hwaccel_output_format vaapi " + args.Input
args.Codecs[i] = defaults[name+"/"+engine]
for i, filter := range args.Filters {
if strings.HasPrefix(filter, "scale=") {
args.Filters[i] = "scale_vaapi=" + filter[6:]
if !args.HasFilters("drawtext=") {
args.Input = "-hwaccel vaapi -hwaccel_output_format vaapi -hwaccel_flags allow_profile_mismatch " + args.Input
if name == "h264" {
fixPixelFormat(args)
}
if strings.HasPrefix(filter, "transpose=") {
if filter == "transpose=1,transpose=1" { // 180 degrees half-turn
args.Filters[i] = "transpose_vaapi=4" // reversal
} else {
args.Filters[i] = "transpose_vaapi=" + filter[10:]
for i, filter := range args.Filters {
if strings.HasPrefix(filter, "scale=") {
args.Filters[i] = "scale_vaapi=" + filter[6:]
}
if strings.HasPrefix(filter, "transpose=") {
if filter == "transpose=1,transpose=1" { // 180 degrees half-turn
args.Filters[i] = "transpose_vaapi=4" // reversal
} else {
args.Filters[i] = "transpose_vaapi=" + filter[10:]
}
}
}
// fix if input doesn't support hwaccel, do nothing when support
// insert as first filter before hardware scale and transpose
args.InsertFilter("format=vaapi|nv12,hwupload")
} else {
// enable software pixel for drawtext, scale and transpose
args.Input = "-hwaccel vaapi -hwaccel_output_format nv12 -hwaccel_flags allow_profile_mismatch " + args.Input
args.AddFilter("hwupload")
}
// fix if input doesn't support hwaccel, do nothing when support
args.InsertFilter("format=vaapi|nv12,hwupload")
case EngineCUDA:
args.Input = "-hwaccel cuda -hwaccel_output_format cuda -extra_hw_frames 2 " + args.Input
args.Codecs[i] = defaults[name+"/"+engine]
for i, filter := range args.Filters {
if strings.HasPrefix(filter, "scale=") {
args.Filters[i] = "scale_cuda=" + filter[6:]
// CUDA doesn't support hardware transpose
// https://github.com/AlexxIT/go2rtc/issues/389
if !args.HasFilters("drawtext=", "transpose=") {
args.Input = "-hwaccel cuda -hwaccel_output_format cuda " + args.Input
for i, filter := range args.Filters {
if strings.HasPrefix(filter, "scale=") {
args.Filters[i] = "scale_cuda=" + filter[6:]
}
}
} else {
args.Input = "-hwaccel cuda -hwaccel_output_format nv12 " + args.Input
args.AddFilter("hwupload")
}
case EngineDXVA2:
@@ -102,6 +124,37 @@ func MakeHardware(args *ffmpeg.Args, engine string, defaults map[string]string)
case EngineV4L2M2M:
args.Codecs[i] = defaults[name+"/"+engine]
case EngineRKMPP:
args.Codecs[i] = defaults[name+"/"+engine]
if !args.HasFilters("drawtext=") {
args.Input = "-hwaccel rkmpp -hwaccel_output_format drm_prime -afbc rga " + args.Input
for i, filter := range args.Filters {
if strings.HasPrefix(filter, "scale=") {
args.Filters[i] = "scale_rkrga=" + filter[6:] + ":force_original_aspect_ratio=0"
}
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:]
}
}
}
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")
}
}
}
}
@@ -110,7 +163,6 @@ var cache = map[string]string{}
func run(bin string, args string) bool {
err := exec.Command(bin, strings.Split(args, " ")...).Run()
log.Printf("%v %v", args, err)
return err == nil
}
@@ -135,3 +187,24 @@ func cut(s string, sep byte, pos int) string {
}
return s
}
// fixPixelFormat:
// - good h264 pixel: yuv420p(tv, bt709) == yuv420p (mpeg/limited/tv)
// - bad h264 pixel: yuvj420p(pc, bt709) == yuvj420p (jpeg/full/pc)
// - bad jpeg pixel: yuvj422p(pc, bt470bg)
func fixPixelFormat(args *ffmpeg.Args) {
// in my tests this filters has same CPU/GPU load:
// - "hwupload"
// - "hwupload,scale_vaapi=out_color_matrix=bt709:out_range=tv"
// - "hwupload,scale_vaapi=out_color_matrix=bt709:out_range=tv:format=nv12"
const fixPixFmt = "out_color_matrix=bt709:out_range=tv:format=nv12"
for i, filter := range args.Filters {
if strings.HasPrefix(filter, "scale=") {
args.Filters[i] = filter + ":" + fixPixFmt
return
}
}
args.Filters = append(args.Filters, "scale="+fixPixFmt)
}

View File

@@ -0,0 +1,62 @@
//go:build freebsd || netbsd || openbsd || dragonfly
package hardware
import (
"runtime"
"github.com/AlexxIT/go2rtc/internal/api"
)
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 -"
)
func ProbeAll(bin string) []*api.Source {
return []*api.Source{
{
Name: runToString(bin, ProbeV4L2M2MH264),
URL: "ffmpeg:...#video=h264#hardware=" + EngineV4L2M2M,
},
{
Name: runToString(bin, ProbeV4L2M2MH265),
URL: "ffmpeg:...#video=h265#hardware=" + EngineV4L2M2M,
},
{
Name: runToString(bin, ProbeRKMPPH264),
URL: "ffmpeg:...#video=h264#hardware=" + EngineRKMPP,
},
{
Name: runToString(bin, ProbeRKMPPH265),
URL: "ffmpeg:...#video=h265#hardware=" + EngineRKMPP,
},
}
}
func ProbeHardware(bin, name string) string {
if runtime.GOARCH == "arm64" || runtime.GOARCH == "arm" {
switch name {
case "h264":
if run(bin, ProbeV4L2M2MH264) {
return EngineV4L2M2M
}
if run(bin, ProbeRKMPPH264) {
return EngineRKMPP
}
case "h265":
if run(bin, ProbeV4L2M2MH265) {
return EngineV4L2M2M
}
if run(bin, ProbeRKMPPH265) {
return EngineRKMPP
}
}
return EngineSoftware
}
return EngineSoftware
}

View File

@@ -1,14 +1,16 @@
//go:build darwin || ios
package hardware
import (
"github.com/AlexxIT/go2rtc/internal/api"
)
const ProbeVideoToolboxH264 = "-f lavfi -i testsrc2 -t 1 -c h264_videotoolbox -f null -"
const ProbeVideoToolboxH265 = "-f lavfi -i testsrc2 -t 1 -c hevc_videotoolbox -f null -"
const ProbeVideoToolboxH264 = "-f lavfi -i testsrc2=size=svga -t 1 -c h264_videotoolbox -f null -"
const ProbeVideoToolboxH265 = "-f lavfi -i testsrc2=size=svga -t 1 -c hevc_videotoolbox -f null -"
func ProbeAll(bin string) []api.Stream {
return []api.Stream{
func ProbeAll(bin string) []*api.Source {
return []*api.Source{
{
Name: runToString(bin, ProbeVideoToolboxH264),
URL: "ffmpeg:...#video=h264#hardware=" + EngineVideoToolbox,

View File

@@ -1,21 +1,29 @@
//go:build unix && !darwin && !freebsd && !netbsd && !openbsd && !dragonfly
package hardware
import (
"github.com/AlexxIT/go2rtc/internal/api"
"runtime"
"github.com/AlexxIT/go2rtc/internal/api"
)
const ProbeV4L2M2MH264 = "-f lavfi -i testsrc2 -t 1 -c h264_v4l2m2m -f null -"
const ProbeV4L2M2MH265 = "-f lavfi -i testsrc2 -t 1 -c hevc_v4l2m2m -f null -"
const ProbeVAAPIH264 = "-init_hw_device vaapi -f lavfi -i testsrc2 -t 1 -vf format=nv12,hwupload -c h264_vaapi -f null -"
const ProbeVAAPIH265 = "-init_hw_device vaapi -f lavfi -i testsrc2 -t 1 -vf format=nv12,hwupload -c hevc_vaapi -f null -"
const ProbeVAAPIJPEG = "-init_hw_device vaapi -f lavfi -i testsrc2 -t 1 -vf format=nv12,hwupload -c mjpeg_vaapi -f null -"
const ProbeCUDAH264 = "-init_hw_device cuda -f lavfi -i testsrc2 -t 1 -c h264_nvenc -f null -"
const ProbeCUDAH265 = "-init_hw_device cuda -f lavfi -i testsrc2 -t 1 -c hevc_nvenc -f null -"
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 -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 -"
ProbeCUDAH264 = "-init_hw_device cuda -f lavfi -i testsrc2 -t 1 -c h264_nvenc -f null -"
ProbeCUDAH265 = "-init_hw_device cuda -f lavfi -i testsrc2 -t 1 -c hevc_nvenc -f null -"
)
func ProbeAll(bin string) []api.Stream {
func ProbeAll(bin string) []*api.Source {
if runtime.GOARCH == "arm64" || runtime.GOARCH == "arm" {
return []api.Stream{
return []*api.Source{
{
Name: runToString(bin, ProbeV4L2M2MH264),
URL: "ffmpeg:...#video=h264#hardware=" + EngineV4L2M2M,
@@ -24,10 +32,22 @@ func ProbeAll(bin string) []api.Stream {
Name: runToString(bin, ProbeV4L2M2MH265),
URL: "ffmpeg:...#video=h265#hardware=" + EngineV4L2M2M,
},
{
Name: runToString(bin, ProbeRKMPPH264),
URL: "ffmpeg:...#video=h264#hardware=" + EngineRKMPP,
},
{
Name: runToString(bin, ProbeRKMPPH265),
URL: "ffmpeg:...#video=h265#hardware=" + EngineRKMPP,
},
{
Name: runToString(bin, ProbeRKMPPJPEG),
URL: "ffmpeg:...#video=mjpeg#hardware=" + EngineRKMPP,
},
}
}
return []api.Stream{
return []*api.Source{
{
Name: runToString(bin, ProbeVAAPIH264),
URL: "ffmpeg:...#video=h264#hardware=" + EngineVAAPI,
@@ -58,10 +78,20 @@ func ProbeHardware(bin, name string) string {
if run(bin, ProbeV4L2M2MH264) {
return EngineV4L2M2M
}
if run(bin, ProbeRKMPPH264) {
return EngineRKMPP
}
case "h265":
if run(bin, ProbeV4L2M2MH265) {
return EngineV4L2M2M
}
if run(bin, ProbeRKMPPH265) {
return EngineRKMPP
}
case "mjpeg":
if run(bin, ProbeRKMPPJPEG) {
return EngineRKMPP
}
}
return EngineSoftware

View File

@@ -1,3 +1,5 @@
//go:build windows
package hardware
import "github.com/AlexxIT/go2rtc/internal/api"
@@ -8,8 +10,8 @@ const ProbeDXVA2JPEG = "-init_hw_device dxva2 -f lavfi -i testsrc2 -t 1 -c mjpeg
const ProbeCUDAH264 = "-init_hw_device cuda -f lavfi -i testsrc2 -t 1 -c h264_nvenc -f null -"
const ProbeCUDAH265 = "-init_hw_device cuda -f lavfi -i testsrc2 -t 1 -c hevc_nvenc -f null -"
func ProbeAll(bin string) []api.Stream {
return []api.Stream{
func ProbeAll(bin string) []*api.Source {
return []*api.Source{
{
Name: runToString(bin, ProbeDXVA2H264),
URL: "ffmpeg:...#video=h264#hardware=" + EngineDXVA2,

View File

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

83
internal/ffmpeg/jpeg.go Normal file
View File

@@ -0,0 +1,83 @@
package ffmpeg
import (
"bytes"
"fmt"
"net/url"
"os/exec"
"github.com/AlexxIT/go2rtc/internal/ffmpeg/hardware"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/ffmpeg"
"github.com/AlexxIT/go2rtc/pkg/shell"
)
func JPEGWithQuery(b []byte, query url.Values) ([]byte, error) {
args := parseQuery(query)
return transcode(b, args.String())
}
func JPEGWithScale(b []byte, width, height int) ([]byte, error) {
args := defaultArgs()
args.AddFilter(fmt.Sprintf("scale=%d:%d", width, height))
return transcode(b, args.String())
}
func transcode(b []byte, args string) ([]byte, error) {
cmdArgs := shell.QuoteSplit(args)
cmd := exec.Command(cmdArgs[0], cmdArgs[1:]...)
cmd.Stdin = bytes.NewBuffer(b)
return cmd.Output()
}
func defaultArgs() *ffmpeg.Args {
return &ffmpeg.Args{
Bin: defaults["bin"],
Global: defaults["global"],
Input: "-i -",
Codecs: []string{defaults["mjpeg"]},
Output: defaults["output/mjpeg"],
}
}
func parseQuery(query url.Values) *ffmpeg.Args {
args := defaultArgs()
var width = -1
var height = -1
var r, hw string
for k, v := range query {
switch k {
case "width", "w":
width = core.Atoi(v[0])
case "height", "h":
height = core.Atoi(v[0])
case "rotate":
r = v[0]
case "hardware", "hw":
hw = v[0]
}
}
if width > 0 || height > 0 {
args.AddFilter(fmt.Sprintf("scale=%d:%d", width, height))
}
if r != "" {
switch r {
case "90":
args.AddFilter("transpose=1") // 90 degrees clockwise
case "180":
args.AddFilter("transpose=1,transpose=1")
case "-90", "270":
args.AddFilter("transpose=2") // 90 degrees counterclockwise
}
}
if hw != "" {
hardware.MakeHardware(args, hw, defaults)
}
return args
}

View File

@@ -0,0 +1,23 @@
package ffmpeg
import (
"net/url"
"testing"
"github.com/stretchr/testify/require"
)
func TestParseQuery(t *testing.T) {
args := parseQuery(nil)
require.Equal(t, `ffmpeg -hide_banner -i - -c:v mjpeg -f mjpeg -`, args.String())
query, err := url.ParseQuery("h=480")
require.Nil(t, err)
args = parseQuery(query)
require.Equal(t, `ffmpeg -hide_banner -i - -c:v mjpeg -vf "scale=-1:480" -f mjpeg -`, args.String())
query, err = url.ParseQuery("hw=vaapi")
require.Nil(t, err)
args = parseQuery(query)
require.Equal(t, `ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -hwaccel_flags allow_profile_mismatch -i - -c:v mjpeg_vaapi -vf "format=vaapi|nv12,hwupload" -f mjpeg -`, args.String())
}

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

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

View File

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

View File

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

View File

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

View File

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

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

@@ -0,0 +1,25 @@
# GoPro
Supported models: HERO9, HERO10, HERO11, HERO12.
Supported OS: Linux, Mac, Windows, [HassOS](https://www.home-assistant.io/installation/)
The other camera models have different APIs. I will try to add them in the next versions.
## Config
- USB-connected cameras create a new network interface in the system
- Linux users do not need to install anything
- Windows users should install the [network driver](https://community.gopro.com/s/article/GoPro-Webcam)
- if the camera is detected but the stream does not start - you need to disable firewall
1. Discover camera address: WebUI > Add > GoPro
2. Add camera to config
```yaml
streams:
hero12: gopro://172.20.100.51
```
## Useful links
- https://gopro.github.io/OpenGoPro/

28
internal/gopro/gopro.go Normal file
View File

@@ -0,0 +1,28 @@
package gopro
import (
"net/http"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/gopro"
)
func Init() {
streams.HandleFunc("gopro", func(source string) (core.Producer, error) {
return gopro.Dial(source)
})
api.HandleFunc("api/gopro", apiGoPro)
}
func apiGoPro(w http.ResponseWriter, r *http.Request) {
var items []*api.Source
for _, host := range gopro.Discovery() {
items = append(items, &api.Source{Name: host, URL: "gopro://" + host})
}
api.ResponseSources(w, items)
}

View File

@@ -3,87 +3,75 @@ package hass
import (
"encoding/base64"
"encoding/json"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/internal/webrtc"
"net"
"net/http"
"strings"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/internal/webrtc"
)
func initAPI() {
ok := func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"status":1,"payload":{}}`))
}
func apiOK(w http.ResponseWriter, r *http.Request) {
api.Response(w, `{"status":1,"payload":{}}`, api.MimeJSON)
}
// support https://www.home-assistant.io/integrations/rtsp_to_webrtc/
api.HandleFunc("/static", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
api.HandleFunc("/streams", ok)
// api from RTSPtoWeb
api.HandleFunc("/stream/", func(w http.ResponseWriter, r *http.Request) {
switch {
// /stream/{id}/add
case strings.HasSuffix(r.RequestURI, "/add"):
var v addJSON
if err := json.NewDecoder(r.Body).Decode(&v); err != nil {
return
}
// we can get three types of links:
// 1. link to go2rtc stream: rtsp://...:8554/{stream_name}
// 2. static link to Hass camera
// 3. dynamic link to Hass camera
stream := streams.Get(v.Name)
if stream == nil {
stream = streams.NewTemplate(v.Name, v.Channels.First.Url)
}
stream.SetSource(v.Channels.First.Url)
ok(w, r)
// /stream/{id}/channel/0/webrtc
default:
i := strings.IndexByte(r.RequestURI[8:], '/')
if i <= 0 {
log.Warn().Msgf("wrong request: %s", r.RequestURI)
return
}
name := r.RequestURI[8 : 8+i]
stream := streams.Get(name)
if stream == nil {
w.WriteHeader(http.StatusNotFound)
return
}
if err := r.ParseForm(); err != nil {
log.Error().Err(err).Msg("[api.hass] parse form")
return
}
s := r.FormValue("data")
offer, err := base64.StdEncoding.DecodeString(s)
if err != nil {
log.Error().Err(err).Msg("[api.hass] sdp64 decode")
return
}
s, err = webrtc.ExchangeSDP(stream, string(offer), "WebRTC/Hass sync", r.UserAgent())
if err != nil {
log.Error().Err(err).Msg("[api.hass] exchange SDP")
return
}
s = base64.StdEncoding.EncodeToString([]byte(s))
_, _ = w.Write([]byte(s))
func apiStream(w http.ResponseWriter, r *http.Request) {
switch {
// /stream/{id}/add
case strings.HasSuffix(r.RequestURI, "/add"):
var v addJSON
if err := json.NewDecoder(r.Body).Decode(&v); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
})
// we can get three types of links:
// 1. link to go2rtc stream: rtsp://...:8554/{stream_name}
// 2. static link to Hass camera
// 3. dynamic link to Hass camera
if streams.Patch(v.Name, v.Channels.First.Url) != nil {
apiOK(w, r)
} else {
http.Error(w, "", http.StatusBadRequest)
}
// /stream/{id}/channel/0/webrtc
default:
i := strings.IndexByte(r.RequestURI[8:], '/')
if i <= 0 {
http.Error(w, "", http.StatusBadRequest)
return
}
name := r.RequestURI[8 : 8+i]
stream := streams.Get(name)
if stream == nil {
http.Error(w, api.StreamNotFound, http.StatusNotFound)
return
}
if err := r.ParseForm(); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
s := r.FormValue("data")
offer, err := base64.StdEncoding.DecodeString(s)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
s, err = webrtc.ExchangeSDP(stream, string(offer), "hass/webrtc", r.UserAgent())
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
s = base64.StdEncoding.EncodeToString([]byte(s))
_, _ = w.Write([]byte(s))
}
}
func HassioAddr() string {

View File

@@ -4,21 +4,24 @@ import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"os"
"path"
"sync"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/app"
"github.com/AlexxIT/go2rtc/internal/roborock"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/hass"
"github.com/rs/zerolog"
"net/http"
"os"
"path"
)
func Init() {
var conf struct {
API struct {
Listen string `json:"listen"`
Listen string `yaml:"listen"`
} `yaml:"api"`
Mod struct {
Config string `yaml:"config"`
@@ -29,10 +32,28 @@ func Init() {
log = app.GetLogger("hass")
initAPI()
// support API for https://www.home-assistant.io/integrations/rtsp_to_webrtc/
api.HandleFunc("/static", apiOK)
api.HandleFunc("/streams", apiOK)
api.HandleFunc("/stream/", apiStream)
streams.RedirectFunc("hass", func(url string) (string, error) {
if location := entities[url[5:]]; location != "" {
return location, nil
}
return "", nil
})
streams.HandleFunc("hass", func(source string) (core.Producer, error) {
// support hass://supervisor?entity_id=camera.driveway_doorbell
return hass.NewClient(source)
})
// load static entries from Hass config
if err := importConfig(conf.Mod.Config); err != nil {
log.Trace().Msgf("[hass] can't import config: %s", err)
entries := importEntries(conf.Mod.Config)
if entries == nil {
api.HandleFunc("api/hass", func(w http.ResponseWriter, _ *http.Request) {
http.Error(w, "no hass config", http.StatusNotFound)
})
@@ -40,18 +61,22 @@ func Init() {
}
api.HandleFunc("api/hass", func(w http.ResponseWriter, _ *http.Request) {
var items []api.Stream
for name, url := range entries {
items = append(items, api.Stream{Name: name, URL: url})
}
api.ResponseStreams(w, items)
})
once.Do(func() {
// load WebRTC entities from Hass API, works only for add-on version
if token := hass.SupervisorToken(); token != "" {
if err := importWebRTC(token); err != nil {
log.Warn().Err(err).Caller().Send()
}
}
})
streams.HandleFunc("hass", func(url string) (core.Producer, error) {
if hurl := entries[url[5:]]; hurl != "" {
return streams.GetProducer(hurl)
var items []*api.Source
for name, url := range entities {
items = append(items, &api.Source{
Name: name, URL: "hass:" + name, Location: url,
})
}
return nil, fmt.Errorf("can't get url: %s", url)
api.ResponseSources(w, items)
})
// for Addon listen on hassio interface, so WebUI feature will work
@@ -68,12 +93,12 @@ func Init() {
}
}
func importEntries(config string) map[string]string {
func importConfig(config string) error {
// support load cameras from Hass config file
filename := path.Join(config, ".storage/core.config_entries")
b, err := os.ReadFile(filename)
if err != nil {
return nil
return err
}
var storage struct {
@@ -88,11 +113,9 @@ func importEntries(config string) map[string]string {
}
if err = json.Unmarshal(b, &storage); err != nil {
return nil
return err
}
urls := map[string]string{}
for _, entrie := range storage.Data.Entries {
switch entrie.Domain {
case "generic":
@@ -102,7 +125,7 @@ func importEntries(config string) map[string]string {
if err = json.Unmarshal(entrie.Options, &options); err != nil {
continue
}
urls[entrie.Title] = options.StreamSource
entities[entrie.Title] = options.StreamSource
case "homekit_controller":
if !bytes.Contains(entrie.Data, []byte("iOSPairingId")) {
@@ -121,7 +144,7 @@ func importEntries(config string) map[string]string {
if err = json.Unmarshal(entrie.Data, &data); err != nil {
continue
}
urls[entrie.Title] = fmt.Sprintf(
entities[entrie.Title] = fmt.Sprintf(
"homekit://%s:%d?client_id=%s&client_private=%s%s&device_id=%s&device_public=%s",
data.DeviceHost, data.DevicePort,
data.ClientID, data.ClientPrivate, data.ClientPublic,
@@ -143,22 +166,48 @@ func importEntries(config string) map[string]string {
}
if data.Username != "" && data.Password != "" {
urls[entrie.Title] = fmt.Sprintf(
entities[entrie.Title] = fmt.Sprintf(
"onvif://%s:%s@%s:%d", data.Username, data.Password, data.Host, data.Port,
)
} else {
urls[entrie.Title] = fmt.Sprintf("onvif://%s:%d", data.Host, data.Port)
entities[entrie.Title] = fmt.Sprintf("onvif://%s:%d", data.Host, data.Port)
}
default:
continue
}
log.Info().Str("url", "hass:"+entrie.Title).Msg("[hass] load stream")
log.Debug().Str("url", "hass:"+entrie.Title).Msg("[hass] load config")
//streams.Get("hass:" + entrie.Title)
}
return urls
return nil
}
func importWebRTC(token string) error {
hassAPI, err := hass.NewAPI("ws://supervisor/core/websocket", token)
if err != nil {
return err
}
webrtcEntities, err := hassAPI.GetWebRTCEntities()
if err != nil {
return err
}
if len(webrtcEntities) == 0 {
log.Debug().Msg("[hass] webrtc cameras not found")
}
for name, entityID := range webrtcEntities {
entities[name] = "hass://supervisor?entity_id=" + entityID
log.Debug().Msgf("[hass] load webrtc name=%s entity_id=%s", name, entityID)
}
return nil
}
var entities = map[string]string{}
var log zerolog.Logger
var once sync.Once

3
internal/hls/README.md Normal file
View File

@@ -0,0 +1,3 @@
## Useful links
- https://walterebert.com/playground/video/hls/

View File

@@ -1,21 +1,23 @@
package hls
import (
"fmt"
"net/http"
"sync"
"time"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/api/ws"
"github.com/AlexxIT/go2rtc/internal/app"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/mp4"
"github.com/AlexxIT/go2rtc/pkg/mpegts"
"github.com/AlexxIT/go2rtc/pkg/tcp"
"github.com/rs/zerolog/log"
"net/http"
"strings"
"sync"
"time"
"github.com/rs/zerolog"
)
func Init() {
log = app.GetLogger("hls")
api.HandleFunc("api/stream.m3u8", handlerStream)
api.HandleFunc("api/hls/playlist.m3u8", handlerPlaylist)
@@ -25,31 +27,16 @@ func Init() {
// HLS (fMP4)
api.HandleFunc("api/hls/init.mp4", handlerInit)
api.HandleFunc("api/hls/segment.m4s", handlerSegmentMP4)
ws.HandleFunc("hls", handlerWSHLS)
}
type Consumer interface {
core.Consumer
Listen(f core.EventFunc)
Init() ([]byte, error)
MimeCodecs() string
Start()
}
type Session struct {
cons Consumer
playlist string
init []byte
segment []byte
seq int
alive *time.Timer
mu sync.Mutex
}
var log zerolog.Logger
const keepalive = 5 * time.Second
var sessions = map[string]*Session{}
// once I saw 404 on MP4 segment, so better to use mutex
var sessions = map[string]*Session{}
var sessionsMu sync.RWMutex
func handlerStream(w http.ResponseWriter, r *http.Request) {
@@ -63,88 +50,49 @@ func handlerStream(w http.ResponseWriter, r *http.Request) {
}
src := r.URL.Query().Get("src")
stream := streams.GetOrNew(src)
stream := streams.Get(src)
if stream == nil {
http.Error(w, api.StreamNotFound, http.StatusNotFound)
return
}
var cons Consumer
var cons core.Consumer
// use fMP4 with codecs filter and TS without
medias := mp4.ParseQuery(r.URL.Query())
if medias != nil {
cons = &mp4.Consumer{
RemoteAddr: tcp.RemoteAddr(r),
UserAgent: r.UserAgent(),
Medias: medias,
}
c := mp4.NewConsumer(medias)
c.FormatName = "hls/fmp4"
c.WithRequest(r)
cons = c
} else {
cons = &mpegts.Consumer{
RemoteAddr: tcp.RemoteAddr(r),
UserAgent: r.UserAgent(),
}
c := mpegts.NewConsumer()
c.FormatName = "hls/mpegts"
c.WithRequest(r)
cons = c
}
session := &Session{cons: cons}
cons.Listen(func(msg any) {
if data, ok := msg.([]byte); ok {
session.mu.Lock()
session.segment = append(session.segment, data...)
session.mu.Unlock()
}
})
if err := stream.AddConsumer(cons); err != nil {
log.Error().Err(err).Caller().Send()
return
}
session := NewSession(cons)
session.alive = time.AfterFunc(keepalive, func() {
sessionsMu.Lock()
delete(sessions, session.id)
sessionsMu.Unlock()
stream.RemoveConsumer(cons)
})
session.init, _ = cons.Init()
cons.Start()
sid := core.RandString(8, 62)
// two segments important for Chromecast
if medias != nil {
session.playlist = `#EXTM3U
#EXT-X-VERSION:6
#EXT-X-TARGETDURATION:1
#EXT-X-MEDIA-SEQUENCE:%d
#EXT-X-MAP:URI="init.mp4?id=` + sid + `"
#EXTINF:0.500,
segment.m4s?id=` + sid + `&n=%d
#EXTINF:0.500,
segment.m4s?id=` + sid + `&n=%d`
} else {
session.playlist = `#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:1
#EXT-X-MEDIA-SEQUENCE:%d
#EXTINF:0.500,
segment.ts?id=` + sid + `&n=%d
#EXTINF:0.500,
segment.ts?id=` + sid + `&n=%d`
}
sessionsMu.Lock()
sessions[sid] = session
sessions[session.id] = session
sessionsMu.Unlock()
// Apple Safari can play FLAC codec, but fail it it in m3u8 playlist
codecs := strings.Replace(cons.MimeCodecs(), mp4.MimeFlac, mp4.MimeAAC, 1)
go session.Run()
// bandwidth important for Safari, codecs useful for smooth playback
data := []byte(`#EXTM3U
#EXT-X-STREAM-INF:BANDWIDTH=1000000,CODECS="` + codecs + `"
hls/playlist.m3u8?id=` + sid)
if _, err := w.Write(data); err != nil {
if _, err := w.Write(session.Main()); err != nil {
log.Error().Err(err).Caller().Send()
}
}
@@ -167,9 +115,7 @@ func handlerPlaylist(w http.ResponseWriter, r *http.Request) {
return
}
s := fmt.Sprintf(session.playlist, session.seq, session.seq, session.seq+1)
if _, err := w.Write([]byte(s)); err != nil {
if _, err := w.Write(session.Playlist()); err != nil {
log.Error().Err(err).Caller().Send()
}
}
@@ -194,22 +140,13 @@ func handlerSegmentTS(w http.ResponseWriter, r *http.Request) {
session.alive.Reset(keepalive)
var i byte
for len(session.segment) == 0 {
if i++; i > 10 {
http.NotFound(w, r)
return
}
time.Sleep(time.Millisecond * 100)
data := session.Segment()
if data == nil {
log.Warn().Msgf("[hls] can't get segment %s", r.URL.RawQuery)
http.NotFound(w, r)
return
}
session.mu.Lock()
data := session.segment
// important to start new segment with init
session.segment = session.init
session.seq++
session.mu.Unlock()
if _, err := w.Write(data); err != nil {
log.Error().Err(err).Caller().Send()
}
@@ -233,7 +170,14 @@ func handlerInit(w http.ResponseWriter, r *http.Request) {
return
}
if _, err := w.Write(session.init); err != nil {
data := session.Init()
if data == nil {
log.Warn().Msgf("[hls] can't get init %s", r.URL.RawQuery)
http.NotFound(w, r)
return
}
if _, err := w.Write(data); err != nil {
log.Error().Err(err).Caller().Send()
}
}
@@ -243,11 +187,13 @@ func handlerSegmentMP4(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Content-Type", "video/iso.segment")
if r.Method == "OPTIONS" {
w.Header().Set("Access-Control-Allow-Methods", "GET, OPTIONS")
w.Header().Set("Access-Control-Allow-Methods", "GET")
return
}
sid := r.URL.Query().Get("id")
query := r.URL.Query()
sid := query.Get("id")
sessionsMu.RLock()
session := sessions[sid]
sessionsMu.RUnlock()
@@ -258,21 +204,13 @@ func handlerSegmentMP4(w http.ResponseWriter, r *http.Request) {
session.alive.Reset(keepalive)
var i byte
for len(session.segment) == 0 {
if i++; i > 10 {
http.NotFound(w, r)
return
}
time.Sleep(time.Millisecond * 100)
data := session.Segment()
if data == nil {
log.Warn().Msgf("[hls] can't get segment %s", r.URL.RawQuery)
http.NotFound(w, r)
return
}
session.mu.Lock()
data := session.segment
session.segment = nil
session.seq++
session.mu.Unlock()
if _, err := w.Write(data); err != nil {
log.Error().Err(err).Caller().Send()
}

127
internal/hls/session.go Normal file
View File

@@ -0,0 +1,127 @@
package hls
import (
"fmt"
"io"
"strings"
"sync"
"time"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/mp4"
)
type Session struct {
cons core.Consumer
id string
template string
init []byte
buffer []byte
seq int
alive *time.Timer
mu sync.Mutex
}
func NewSession(cons core.Consumer) *Session {
s := &Session{
id: core.RandString(8, 62),
cons: cons,
}
// two segments important for Chromecast
if _, ok := cons.(*mp4.Consumer); ok {
s.template = `#EXTM3U
#EXT-X-VERSION:6
#EXT-X-TARGETDURATION:1
#EXT-X-MEDIA-SEQUENCE:%d
#EXT-X-MAP:URI="init.mp4?id=` + s.id + `"
#EXTINF:0.500,
segment.m4s?id=` + s.id + `&n=%d
#EXTINF:0.500,
segment.m4s?id=` + s.id + `&n=%d`
} else {
s.template = `#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:1
#EXT-X-MEDIA-SEQUENCE:%d
#EXTINF:0.500,
segment.ts?id=` + s.id + `&n=%d
#EXTINF:0.500,
segment.ts?id=` + s.id + `&n=%d`
}
return s
}
func (s *Session) Write(p []byte) (n int, err error) {
s.mu.Lock()
if s.init == nil {
s.init = p
} else {
s.buffer = append(s.buffer, p...)
}
s.mu.Unlock()
return len(p), nil
}
func (s *Session) Run() {
_, _ = s.cons.(io.WriterTo).WriteTo(s)
}
func (s *Session) Main() []byte {
type withCodecs interface {
Codecs() []*core.Codec
}
codecs := mp4.MimeCodecs(s.cons.(withCodecs).Codecs())
codecs = strings.Replace(codecs, mp4.MimeFlac, "fLaC", 1)
// bandwidth important for Safari, codecs useful for smooth playback
return []byte(`#EXTM3U
#EXT-X-STREAM-INF:BANDWIDTH=192000,CODECS="` + codecs + `"
hls/playlist.m3u8?id=` + s.id)
}
func (s *Session) Playlist() []byte {
return []byte(fmt.Sprintf(s.template, s.seq, s.seq, s.seq+1))
}
func (s *Session) Init() (init []byte) {
for i := 0; i < 60 && init == nil; i++ {
if i > 0 {
time.Sleep(50 * time.Millisecond)
}
s.mu.Lock()
// return init only when have some buffer
if len(s.buffer) > 0 {
init = s.init
}
s.mu.Unlock()
}
return
}
func (s *Session) Segment() (segment []byte) {
for i := 0; i < 60 && segment == nil; i++ {
if i > 0 {
time.Sleep(50 * time.Millisecond)
}
s.mu.Lock()
if len(s.buffer) > 0 {
segment = s.buffer
if _, ok := s.cons.(*mp4.Consumer); ok {
s.buffer = nil
} else {
// for TS important to start new segment with init
s.buffer = s.init
}
s.seq++
}
s.mu.Unlock()
}
return
}

52
internal/hls/ws.go Normal file
View File

@@ -0,0 +1,52 @@
package hls
import (
"errors"
"time"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/api/ws"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/mp4"
)
func handlerWSHLS(tr *ws.Transport, msg *ws.Message) error {
stream := streams.GetOrPatch(tr.Request.URL.Query())
if stream == nil {
return errors.New(api.StreamNotFound)
}
codecs := msg.String()
medias := mp4.ParseCodecs(codecs, true)
cons := mp4.NewConsumer(medias)
cons.FormatName = "hls/fmp4"
cons.WithRequest(tr.Request)
log.Trace().Msgf("[hls] new ws consumer codecs=%s", codecs)
if err := stream.AddConsumer(cons); err != nil {
log.Error().Err(err).Caller().Send()
return err
}
session := NewSession(cons)
session.alive = time.AfterFunc(keepalive, func() {
sessionsMu.Lock()
delete(sessions, session.id)
sessionsMu.Unlock()
stream.RemoveConsumer(cons)
})
sessionsMu.Lock()
sessions[session.id] = session
sessionsMu.Unlock()
go session.Run()
main := session.Main()
tr.Write(&ws.Message{Type: "hls", Value: string(main)})
return nil
}

View File

@@ -1,140 +1,139 @@
package homekit
import (
"encoding/json"
"errors"
"fmt"
"github.com/AlexxIT/go2rtc/internal/app/store"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/hap"
"github.com/AlexxIT/go2rtc/pkg/hap/mdns"
"net/http"
"net/url"
"strings"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/app"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/hap"
"github.com/AlexxIT/go2rtc/pkg/mdns"
)
func apiHandler(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case "GET":
items := make([]any, 0)
sources, err := discovery()
if err != nil {
api.Error(w, err)
return
}
for name, src := range store.GetDict("streams") {
if src := src.(string); strings.HasPrefix(src, "homekit") {
u, err := url.Parse(src)
if err != nil {
continue
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
}
device := Device{
Name: name,
Addr: u.Host,
Paired: true,
}
items = append(items, device)
}
}
for info := range mdns.GetAll() {
if !strings.HasSuffix(info.Name, mdns.Suffix) {
continue
for _, source := range sources {
if source.Location == "" {
source.Location = " "
}
name := info.Name[:len(info.Name)-len(mdns.Suffix)]
device := Device{
Name: strings.ReplaceAll(name, "\\", ""),
Addr: fmt.Sprintf("%s:%d", info.AddrV4, info.Port),
}
for _, field := range info.InfoFields {
switch field[:2] {
case "id":
device.ID = field[3:]
case "md":
device.Model = field[3:]
case "sf":
device.Paired = field[3] == '0'
}
}
items = append(items, device)
}
_ = json.NewEncoder(w).Encode(items)
api.ResponseSources(w, sources)
case "POST":
// TODO: post params...
if err := r.ParseMultipartForm(1024); err != nil {
api.Error(w, err)
return
}
id := r.URL.Query().Get("id")
pin := r.URL.Query().Get("pin")
name := r.URL.Query().Get("name")
if err := hkPair(id, pin, name); err != nil {
log.Error().Err(err).Caller().Send()
_, err = w.Write([]byte(err.Error()))
if err := apiPair(r.Form.Get("id"), r.Form.Get("url")); err != nil {
api.Error(w, err)
}
case "DELETE":
src := r.URL.Query().Get("src")
if err := hkDelete(src); err != nil {
log.Error().Err(err).Caller().Send()
_, err = w.Write([]byte(err.Error()))
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)
}
}
}
func hkPair(deviceID, pin, name string) (err error) {
var conn *hap.Conn
func discovery() ([]*api.Source, error) {
var sources []*api.Source
if conn, err = hap.Pair(deviceID, pin); err != nil {
return
}
// 1. Get streams from Discovery
err := mdns.Discovery(mdns.ServiceHAP, func(entry *mdns.ServiceEntry) bool {
log.Trace().Msgf("[homekit] mdns=%s", entry)
streams.New(name, conn.URL())
dict := store.GetDict("streams")
dict[name] = conn.URL()
return store.Set("streams", dict)
}
func hkDelete(name string) (err error) {
dict := store.GetDict("streams")
for key, rawURL := range dict {
if key != name {
continue
}
var conn *hap.Conn
if conn, err = hap.NewConn(rawURL.(string)); err != nil {
return
}
if err = conn.Dial(); err != nil {
return
}
go func() {
if err = conn.Handle(); err != nil {
log.Warn().Err(err).Caller().Send()
category := entry.Info[hap.TXTCategory]
if entry.Complete() && (category == hap.CategoryCamera || category == hap.CategoryDoorbell) {
source := &api.Source{
Name: entry.Name,
Info: entry.Info[hap.TXTModel],
URL: fmt.Sprintf(
"homekit://%s:%d?device_id=%s&feature=%s&status=%s",
entry.IP, entry.Port, entry.Info[hap.TXTDeviceID],
entry.Info[hap.TXTFeatureFlags], entry.Info[hap.TXTStatusFlags],
),
}
}()
if err = conn.ListPairings(); err != nil {
return
sources = append(sources, source)
}
return false
})
if err = conn.DeletePairing(conn.ClientID); err != nil {
log.Error().Err(err).Caller().Send()
}
delete(dict, name)
return store.Set("streams", dict)
if err != nil {
return nil, err
}
return nil
return sources, nil
}
type Device struct {
ID string `json:"id"`
Name string `json:"name"`
Addr string `json:"addr"`
Model string `json:"model"`
Paired bool `json:"paired"`
//Type string `json:"type"`
func apiPair(id, url string) error {
conn, err := hap.Pair(url)
if err != nil {
return err
}
streams.New(id, conn.URL())
return app.PatchConfig([]string{"streams", id}, conn.URL())
}
func apiUnpair(id string) error {
stream := streams.Get(id)
if stream == nil {
return errors.New(api.StreamNotFound)
}
rawURL := findHomeKitURL(stream.Sources())
if rawURL == "" {
return errors.New("not homekit source")
}
if err := hap.Unpair(rawURL); err != nil {
return err
}
streams.Delete(id)
return app.PatchConfig([]string{"streams", id}, nil)
}
func findHomeKitURLs() map[string]*url.URL {
urls := map[string]*url.URL{}
for name, sources := range streams.GetAllSources() {
if rawURL := findHomeKitURL(sources); rawURL != "" {
if u, err := url.Parse(rawURL); err == nil {
urls[name] = u
}
}
}
return urls
}

View File

@@ -1,32 +1,232 @@
package homekit
import (
"errors"
"io"
"net"
"net/http"
"strings"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/app"
"github.com/AlexxIT/go2rtc/internal/srtp"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/hap"
"github.com/AlexxIT/go2rtc/pkg/hap/camera"
"github.com/AlexxIT/go2rtc/pkg/homekit"
"github.com/AlexxIT/go2rtc/pkg/mdns"
"github.com/rs/zerolog"
)
func Init() {
var cfg struct {
Mod map[string]struct {
Pin string `yaml:"pin"`
Name string `yaml:"name"`
DeviceID string `yaml:"device_id"`
DevicePrivate string `yaml:"device_private"`
Pairings []string `yaml:"pairings"`
} `yaml:"homekit"`
}
app.LoadConfig(&cfg)
log = app.GetLogger("homekit")
streams.HandleFunc("homekit", streamHandler)
api.HandleFunc("api/homekit", apiHandler)
if cfg.Mod == nil {
return
}
servers = map[string]*server{}
var entries []*mdns.ServiceEntry
for id, conf := range cfg.Mod {
stream := streams.Get(id)
if stream == nil {
log.Warn().Msgf("[homekit] missing stream: %s", id)
continue
}
if conf.Pin == "" {
conf.Pin = "19550224" // default PIN
}
pin, err := hap.SanitizePin(conf.Pin)
if err != nil {
log.Error().Err(err).Caller().Send()
continue
}
deviceID := calcDeviceID(conf.DeviceID, id) // random MAC-address
name := calcName(conf.Name, deviceID)
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)
}
srv.mdns = &mdns.ServiceEntry{
Name: name,
Port: uint16(api.Port),
Info: map[string]string{
hap.TXTConfigNumber: "1",
hap.TXTFeatureFlags: "0",
hap.TXTDeviceID: deviceID,
hap.TXTModel: app.UserAgent,
hap.TXTProtoVersion: "1.1",
hap.TXTStateNumber: "1",
hap.TXTStatusFlags: hap.StatusNotPaired,
hap.TXTCategory: hap.CategoryCamera,
hap.TXTSetupHash: srv.hap.SetupHash(),
},
}
entries = append(entries, srv.mdns)
srv.UpdateStatus()
host := srv.mdns.Host(mdns.ServiceHAP)
servers[host] = srv
}
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()
}
}()
}
var log zerolog.Logger
var servers map[string]*server
func streamHandler(url string) (core.Producer, error) {
conn, err := homekit.NewClient(url, srtp.Server)
if err != nil {
return nil, err
func streamHandler(rawURL string) (core.Producer, error) {
if srtp.Server == nil {
return nil, errors.New("homekit: can't work without SRTP server")
}
if err = conn.Dial(); err != nil {
return nil, err
rawURL, rawQuery, _ := strings.Cut(rawURL, "#")
client, err := homekit.Dial(rawURL, srtp.Server)
if client != nil && rawQuery != "" {
query := streams.ParseQuery(rawQuery)
client.Bitrate = parseBitrate(query.Get("bitrate"))
}
return conn, nil
return client, err
}
func resolve(host string) *server {
if len(servers) == 1 {
for _, srv := range servers {
return srv
}
}
if srv, ok := servers[host]; ok {
return srv
}
return nil
}
func hapHandler(w http.ResponseWriter, r *http.Request) {
conn, rw, err := w.(http.Hijacker).Hijack()
if err != nil {
return
}
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()
}
}
func findHomeKitURL(sources []string) string {
if len(sources) == 0 {
return ""
}
url := sources[0]
if strings.HasPrefix(url, "homekit") {
return url
}
if strings.HasPrefix(url, "hass") {
location, _ := streams.Location(url)
if strings.HasPrefix(location, "homekit") {
return url
}
}
return ""
}
func parseBitrate(s string) int {
n := len(s)
if n == 0 {
return 0
}
var k int
switch n--; s[n] {
case 'K':
k = 1024
s = s[:n]
case 'M':
k = 1024 * 1024
s = s[:n]
default:
k = 1
}
return k * core.Atoi(s)
}

265
internal/homekit/server.go Normal file
View File

@@ -0,0 +1,265 @@
package homekit
import (
"crypto/ed25519"
"crypto/sha512"
"encoding/hex"
"fmt"
"net"
"net/url"
"strings"
"github.com/AlexxIT/go2rtc/internal/app"
"github.com/AlexxIT/go2rtc/internal/ffmpeg"
srtp2 "github.com/AlexxIT/go2rtc/internal/srtp"
"github.com/AlexxIT/go2rtc/internal/streams"
"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/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
streams map[string]*homekit.Consumer
consumer *homekit.Consumer
}
func (s *server) UpdateStatus() {
// true status is important, or device may be offline in Apple Home
if len(s.pairings) == 0 {
s.mdns.Info[hap.TXTStatusFlags] = hap.StatusNotPaired
} else {
s.mdns.Info[hap.TXTStatusFlags] = hap.StatusPaired
}
}
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)
char := s.accessory.GetCharacterByID(iid)
if char == nil {
log.Warn().Msgf("[homekit] get unknown characteristic: %d", iid)
return nil
}
switch char.Type {
case camera.TypeSetupEndpoints:
if s.consumer == nil {
return nil
}
answer := s.consumer.GetAnswer()
v, err := tlv8.MarshalBase64(answer)
if err != nil {
return nil
}
return v
}
return char.Value
}
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)
char := s.accessory.GetCharacterByID(iid)
if char == nil {
log.Warn().Msgf("[homekit] set unknown characteristic: %d", iid)
return
}
switch char.Type {
case camera.TypeSetupEndpoints:
var offer camera.SetupEndpoints
if err := tlv8.UnmarshalBase64(value, &offer); err != nil {
return
}
s.consumer = homekit.NewConsumer(conn, srtp2.Server)
s.consumer.SetOffer(&offer)
case camera.TypeSelectedStreamConfiguration:
var conf camera.SelectedStreamConfig
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)
switch conf.Control.Command {
case camera.SessionCommandEnd:
if consumer := s.streams[conf.Control.SessionID]; consumer != nil {
_ = consumer.Stop()
}
case camera.SessionCommandStart:
if s.consumer == nil {
return
}
if !s.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
stream := streams.Get(s.stream)
if err := stream.AddConsumer(s.consumer); err != nil {
return
}
go func() {
_, _ = s.consumer.WriteTo(nil)
stream.RemoveConsumer(s.consumer)
delete(s.streams, conf.Control.SessionID)
}()
}
}
}
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)
stream := streams.Get(s.stream)
cons := magic.NewKeyframe()
if err := stream.AddConsumer(cons); err != nil {
return nil
}
once := &core.OnceBuffer{} // init and first frame
_, _ = cons.WriteTo(once)
b := once.Buffer()
stream.RemoveConsumer(cons)
switch cons.CodecName() {
case core.CodecH264, core.CodecH265:
var err error
if b, err = ffmpeg.JPEGWithScale(b, width, height); err != nil {
return nil
}
}
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
}
b := sha512.Sum512([]byte(seed))
return fmt.Sprintf("go2rtc-%02X%02X", b[0], b[2])
}
func calcDeviceID(deviceID, seed string) string {
if deviceID != "" {
if len(deviceID) >= 17 {
// 1. Returd device_id as is (ex. AA:BB:CC:DD:EE:FF)
return deviceID
}
// 2. Use device_id as seed if not zero
seed = deviceID
}
b := sha512.Sum512([]byte(seed))
return fmt.Sprintf("%02X:%02X:%02X:%02X:%02X:%02X", b[32], b[34], b[36], b[38], b[40], b[42])
}
func calcDevicePrivate(private, seed string) []byte {
if private != "" {
// 1. Decode private from HEX string
if b, _ := hex.DecodeString(private); len(b) == ed25519.PrivateKeySize {
// 2. Return if OK
return b
}
// 3. Use private as seed if not zero
seed = private
}
b := sha512.Sum512([]byte(seed))
return ed25519.NewKeyFromSeed(b[:ed25519.SeedSize])
}

View File

@@ -2,17 +2,20 @@ package http
import (
"errors"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/magic"
"github.com/AlexxIT/go2rtc/pkg/mjpeg"
"github.com/AlexxIT/go2rtc/pkg/rtmp"
"github.com/AlexxIT/go2rtc/pkg/tcp"
"net"
"net/http"
"net/url"
"strings"
"time"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/hls"
"github.com/AlexxIT/go2rtc/pkg/image"
"github.com/AlexxIT/go2rtc/pkg/magic"
"github.com/AlexxIT/go2rtc/pkg/mpjpeg"
"github.com/AlexxIT/go2rtc/pkg/pcm"
"github.com/AlexxIT/go2rtc/pkg/tcp"
)
func Init() {
@@ -21,15 +24,43 @@ func Init() {
streams.HandleFunc("httpx", handleHTTP)
streams.HandleFunc("tcp", handleTCP)
api.HandleFunc("api/stream", apiStream)
}
func handleHTTP(url string) (core.Producer, error) {
func handleHTTP(rawURL string) (core.Producer, error) {
rawURL, rawQuery, _ := strings.Cut(rawURL, "#")
// first we get the Content-Type to define supported producer
req, err := http.NewRequest("GET", url, nil)
req, err := http.NewRequest("GET", rawURL, nil)
if err != nil {
return nil, err
}
if rawQuery != "" {
query := streams.ParseQuery(rawQuery)
for _, header := range query["header"] {
key, value, _ := strings.Cut(header, ":")
req.Header.Add(key, strings.TrimSpace(value))
}
}
prod, err := do(req)
if err != nil {
return nil, err
}
if info, ok := prod.(core.Info); ok {
info.SetProtocol("http")
info.SetRemoteAddr(req.URL.Host) // TODO: rewrite to net.Conn
info.SetURL(rawURL)
}
return prod, nil
}
func do(req *http.Request) (core.Producer, error) {
res, err := tcp.Do(req)
if err != nil {
return nil, err
@@ -39,37 +70,30 @@ func handleHTTP(url string) (core.Producer, error) {
return nil, errors.New(res.Status)
}
// 1. Guess format from content type
ct := res.Header.Get("Content-Type")
if i := strings.IndexByte(ct, ';'); i > 0 {
ct = ct[:i]
}
switch ct {
case "image/jpeg", "multipart/x-mixed-replace":
return mjpeg.NewClient(res), nil
case "video/x-flv":
var conn *rtmp.Client
if conn, err = rtmp.Accept(res); err != nil {
return nil, err
}
if err = conn.Describe(); err != nil {
return nil, err
}
return conn, nil
default: // "video/mpeg":
var ext string
if i := strings.LastIndexByte(req.URL.Path, '.'); i > 0 {
ext = req.URL.Path[i+1:]
}
client := magic.NewClient(res.Body)
if err = client.Probe(); err != nil {
return nil, err
switch {
case ct == "application/vnd.apple.mpegurl" || ext == "m3u8":
return hls.OpenURL(req.URL, res.Body)
case ct == "image/jpeg":
return image.Open(res)
case ct == "multipart/x-mixed-replace":
return mpjpeg.Open(res.Body)
//https://www.iana.org/assignments/media-types/audio/basic
case ct == "audio/basic":
return pcm.Open(res.Body)
}
client.Desc = "HTTP active producer"
client.URL = url
return client, nil
return magic.Open(res.Body)
}
func handleTCP(rawURL string) (core.Producer, error) {
@@ -78,18 +102,33 @@ func handleTCP(rawURL string) (core.Producer, error) {
return nil, err
}
conn, err := net.DialTimeout("tcp", u.Host, time.Second*3)
conn, err := net.DialTimeout("tcp", u.Host, core.ConnDialTimeout)
if err != nil {
return nil, err
}
client := magic.NewClient(conn)
if err = client.Probe(); err != nil {
return nil, err
return magic.Open(conn)
}
func apiStream(w http.ResponseWriter, r *http.Request) {
dst := r.URL.Query().Get("dst")
stream := streams.Get(dst)
if stream == nil {
http.Error(w, api.StreamNotFound, http.StatusNotFound)
return
}
client.Desc = "TCP active producer"
client.URL = rawURL
client, err := magic.Open(r.Body)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
return client, nil
stream.AddProducer(client)
defer stream.RemoveProducer(client)
if err = client.Start(); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}

View File

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

View File

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

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

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

View File

@@ -2,56 +2,57 @@ package mjpeg
import (
"errors"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/ffmpeg"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/magic"
"github.com/AlexxIT/go2rtc/pkg/mjpeg"
"github.com/AlexxIT/go2rtc/pkg/tcp"
"github.com/rs/zerolog/log"
"io"
"net/http"
"strconv"
"strings"
"time"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/api/ws"
"github.com/AlexxIT/go2rtc/internal/app"
"github.com/AlexxIT/go2rtc/internal/ffmpeg"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/ascii"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/magic"
"github.com/AlexxIT/go2rtc/pkg/mjpeg"
"github.com/AlexxIT/go2rtc/pkg/mpjpeg"
"github.com/AlexxIT/go2rtc/pkg/y4m"
"github.com/rs/zerolog"
)
func Init() {
api.HandleFunc("api/frame.jpeg", handlerKeyframe)
api.HandleFunc("api/stream.mjpeg", handlerStream)
api.HandleFunc("api/stream.ascii", handlerStream)
api.HandleFunc("api/stream.y4m", apiStreamY4M)
api.HandleWS("mjpeg", handlerWS)
ws.HandleFunc("mjpeg", handlerWS)
log = app.GetLogger("mjpeg")
}
var log zerolog.Logger
func handlerKeyframe(w http.ResponseWriter, r *http.Request) {
src := r.URL.Query().Get("src")
stream := streams.GetOrNew(src)
stream := streams.GetOrPatch(r.URL.Query())
if stream == nil {
http.Error(w, api.StreamNotFound, http.StatusNotFound)
return
}
exit := make(chan []byte)
cons := &magic.Keyframe{
RemoteAddr: tcp.RemoteAddr(r),
UserAgent: r.UserAgent(),
}
cons.Listen(func(msg any) {
if b, ok := msg.([]byte); ok {
select {
case exit <- b:
default:
}
}
})
cons := magic.NewKeyframe()
cons.WithRequest(r)
if err := stream.AddConsumer(cons); err != nil {
log.Error().Err(err).Caller().Send()
return
}
data := <-exit
once := &core.OnceBuffer{} // init and first frame
_, _ = cons.WriteTo(once)
b := once.Buffer()
stream.RemoveConsumer(cons)
@@ -59,27 +60,27 @@ func handlerKeyframe(w http.ResponseWriter, r *http.Request) {
case core.CodecH264, core.CodecH265:
ts := time.Now()
var err error
if data, err = ffmpeg.TranscodeToJPEG(data); err != nil {
if b, err = ffmpeg.JPEGWithQuery(b, r.URL.Query()); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
log.Debug().Msgf("[mjpeg] transcoding time=%s", time.Since(ts))
case core.CodecJPEG:
b = mjpeg.FixJPEG(b)
}
h := w.Header()
h.Set("Content-Type", "image/jpeg")
h.Set("Content-Length", strconv.Itoa(len(data)))
h.Set("Content-Length", strconv.Itoa(len(b)))
h.Set("Cache-Control", "no-cache")
h.Set("Connection", "close")
h.Set("Pragma", "no-cache")
if _, err := w.Write(data); err != nil {
if _, err := w.Write(b); err != nil {
log.Error().Err(err).Caller().Send()
}
}
const header = "--frame\r\nContent-Type: image/jpeg\r\nContent-Length: "
func handlerStream(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
outputMjpeg(w, r)
@@ -90,32 +91,14 @@ func handlerStream(w http.ResponseWriter, r *http.Request) {
func outputMjpeg(w http.ResponseWriter, r *http.Request) {
src := r.URL.Query().Get("src")
stream := streams.GetOrNew(src)
stream := streams.Get(src)
if stream == nil {
http.Error(w, api.StreamNotFound, http.StatusNotFound)
return
}
flusher := w.(http.Flusher)
cons := &mjpeg.Consumer{
RemoteAddr: tcp.RemoteAddr(r),
UserAgent: r.UserAgent(),
}
cons.Listen(func(msg any) {
switch msg := msg.(type) {
case []byte:
data := []byte(header + strconv.Itoa(len(msg)))
data = append(data, '\r', '\n', '\r', '\n')
data = append(data, msg...)
data = append(data, '\r', '\n')
// Chrome bug: mjpeg image always shows the second to last image
// https://bugs.chromium.org/p/chromium/issues/detail?id=527446
_, _ = w.Write(data)
flusher.Flush()
}
})
cons := mjpeg.NewConsumer()
cons.WithRequest(r)
if err := stream.AddConsumer(cons); err != nil {
log.Error().Err(err).Msg("[api.mjpeg] add consumer")
@@ -123,16 +106,22 @@ func outputMjpeg(w http.ResponseWriter, r *http.Request) {
}
h := w.Header()
h.Set("Content-Type", "multipart/x-mixed-replace; boundary=frame")
h.Set("Cache-Control", "no-cache")
h.Set("Connection", "close")
h.Set("Pragma", "no-cache")
<-r.Context().Done()
if strings.HasSuffix(r.URL.Path, "mjpeg") {
wr := mjpeg.NewWriter(w)
_, _ = cons.WriteTo(wr)
} else {
cons.FormatName = "ascii"
query := r.URL.Query()
wr := ascii.NewWriter(w, query.Get("color"), query.Get("back"), query.Get("text"))
_, _ = cons.WriteTo(wr)
}
stream.RemoveConsumer(cons)
//log.Trace().Msg("[api.mjpeg] close")
}
func inputMjpeg(w http.ResponseWriter, r *http.Request) {
@@ -143,42 +132,35 @@ func inputMjpeg(w http.ResponseWriter, r *http.Request) {
return
}
res := &http.Response{Body: r.Body, Header: r.Header, Request: r}
res.Header.Set("Content-Type", "multipart/mixed;boundary=")
prod, _ := mpjpeg.Open(r.Body)
prod.WithRequest(r)
client := mjpeg.NewClient(res)
stream.AddProducer(client)
stream.AddProducer(prod)
if err := client.Start(); err != nil && err != io.EOF {
if err := prod.Start(); err != nil && err != io.EOF {
log.Warn().Err(err).Caller().Send()
}
stream.RemoveProducer(client)
stream.RemoveProducer(prod)
}
func handlerWS(tr *api.Transport, _ *api.Message) error {
src := tr.Request.URL.Query().Get("src")
stream := streams.GetOrNew(src)
func handlerWS(tr *ws.Transport, _ *ws.Message) error {
stream := streams.GetOrPatch(tr.Request.URL.Query())
if stream == nil {
return errors.New(api.StreamNotFound)
}
cons := &mjpeg.Consumer{
RemoteAddr: tcp.RemoteAddr(tr.Request),
UserAgent: tr.Request.UserAgent(),
}
cons.Listen(func(msg any) {
if data, ok := msg.([]byte); ok {
tr.Write(data)
}
})
cons := mjpeg.NewConsumer()
cons.WithRequest(tr.Request)
if err := stream.AddConsumer(cons); err != nil {
log.Error().Err(err).Caller().Send()
log.Debug().Err(err).Msg("[mjpeg] add consumer")
return err
}
tr.Write(&api.Message{Type: "mjpeg"})
tr.Write(&ws.Message{Type: "mjpeg"})
go cons.WriteTo(tr.Writer())
tr.OnClose(func() {
stream.RemoveConsumer(cons)
@@ -186,3 +168,24 @@ func handlerWS(tr *api.Transport, _ *api.Message) error {
return nil
}
func apiStreamY4M(w http.ResponseWriter, r *http.Request) {
src := r.URL.Query().Get("src")
stream := streams.Get(src)
if stream == nil {
http.Error(w, api.StreamNotFound, http.StatusNotFound)
return
}
cons := y4m.NewConsumer()
cons.WithRequest(r)
if err := stream.AddConsumer(cons); err != nil {
log.Error().Err(err).Caller().Send()
return
}
_, _ = cons.WriteTo(w)
stream.RemoveConsumer(cons)
}

View File

@@ -1,24 +1,26 @@
package mp4
import (
"context"
"net/http"
"strconv"
"strings"
"time"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/api/ws"
"github.com/AlexxIT/go2rtc/internal/app"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/mp4"
"github.com/AlexxIT/go2rtc/pkg/tcp"
"github.com/rs/zerolog"
)
func Init() {
log = app.GetLogger("mp4")
api.HandleWS("mse", handlerWSMSE)
api.HandleWS("mp4", handlerWSMP4)
ws.HandleFunc("mse", handlerWSMSE)
ws.HandleFunc("mp4", handlerWSMP4)
api.HandleFunc("api/frame.mp4", handlerKeyframe)
api.HandleFunc("api/stream.mp4", handlerMP4)
@@ -37,25 +39,15 @@ func handlerKeyframe(w http.ResponseWriter, r *http.Request) {
}
}
src := r.URL.Query().Get("src")
stream := streams.GetOrNew(src)
query := r.URL.Query()
src := query.Get("src")
stream := streams.Get(src)
if stream == nil {
http.Error(w, api.StreamNotFound, http.StatusNotFound)
return
}
exit := make(chan []byte, 1)
cons := &mp4.Segment{OnlyKeyframe: true}
cons.Listen(func(msg any) {
if data, ok := msg.([]byte); ok && exit != nil {
select {
case exit <- data:
default:
}
exit = nil
}
})
cons := mp4.NewKeyframe(nil)
if err := stream.AddConsumer(cons); err != nil {
log.Error().Err(err).Caller().Send()
@@ -63,15 +55,21 @@ func handlerKeyframe(w http.ResponseWriter, r *http.Request) {
return
}
data := <-exit
once := &core.OnceBuffer{} // init and first frame
_, _ = cons.WriteTo(once)
stream.RemoveConsumer(cons)
// Apple Safari won't show frame without length
w.Header().Set("Content-Length", strconv.Itoa(len(data)))
w.Header().Set("Content-Type", cons.MimeType)
header := w.Header()
header.Set("Content-Length", strconv.Itoa(once.Len()))
header.Set("Content-Type", mp4.ContentType(cons.Codecs()))
if _, err := w.Write(data); err != nil {
if filename := query.Get("filename"); filename != "" {
header.Set("Content-Disposition", `attachment; filename="`+filename+`"`)
}
if _, err := once.WriteTo(w); err != nil {
log.Error().Err(err).Caller().Send()
}
}
@@ -93,35 +91,17 @@ func handlerMP4(w http.ResponseWriter, r *http.Request) {
return
}
src := query.Get("src")
stream := streams.GetOrNew(src)
stream := streams.GetOrPatch(query)
if stream == nil {
http.Error(w, api.StreamNotFound, http.StatusNotFound)
return
}
exit := make(chan error, 1) // Add buffer to prevent blocking
cons := &mp4.Consumer{
RemoteAddr: tcp.RemoteAddr(r),
UserAgent: r.UserAgent(),
Medias: mp4.ParseQuery(r.URL.Query()),
}
cons.Listen(func(msg any) {
if exit == nil {
return
}
if data, ok := msg.([]byte); ok {
if _, err := w.Write(data); err != nil {
select {
case exit <- err:
default:
}
exit = nil
}
}
})
medias := mp4.ParseQuery(r.URL.Query())
cons := mp4.NewConsumer(medias)
cons.FormatName = "mp4"
cons.Protocol = "http"
cons.WithRequest(r)
if err := stream.AddConsumer(cons); err != nil {
log.Error().Err(err).Caller().Send()
@@ -129,46 +109,38 @@ func handlerMP4(w http.ResponseWriter, r *http.Request) {
return
}
defer stream.RemoveConsumer(cons)
w.Header().Set("Content-Type", cons.MimeType())
data, err := cons.Init()
if err != nil {
log.Error().Err(err).Caller().Send()
http.Error(w, err.Error(), http.StatusInternalServerError)
return
if rotate := query.Get("rotate"); rotate != "" {
cons.Rotate = core.Atoi(rotate)
}
if _, err = w.Write(data); err != nil {
log.Error().Err(err).Caller().Send()
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
cons.Start()
var duration *time.Timer
if s := query.Get("duration"); s != "" {
if i, _ := strconv.Atoi(s); i > 0 {
duration = time.AfterFunc(time.Second*time.Duration(i), func() {
if exit != nil {
select {
case exit <- nil:
default:
}
exit = nil
}
})
if scale := query.Get("scale"); scale != "" {
if sx, sy, ok := strings.Cut(scale, ":"); ok {
cons.ScaleX = core.Atoi(sx)
cons.ScaleY = core.Atoi(sy)
}
}
err = <-exit
exit = nil
header := w.Header()
header.Set("Content-Type", mp4.ContentType(cons.Codecs()))
log.Trace().Err(err).Caller().Send()
if duration != nil {
duration.Stop()
if filename := query.Get("filename"); filename != "" {
header.Set("Content-Disposition", `attachment; filename="`+filename+`"`)
}
ctx := r.Context() // handle when the client drops the connection
if i := core.Atoi(query.Get("duration")); i > 0 {
timeout := time.Second * time.Duration(i)
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, timeout)
defer cancel()
}
go func() {
<-ctx.Done()
_ = cons.Stop()
stream.RemoveConsumer(cons)
}()
_, _ = cons.WriteTo(w)
}

View File

@@ -2,91 +2,69 @@ package mp4
import (
"errors"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/api/ws"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/mp4"
"github.com/AlexxIT/go2rtc/pkg/tcp"
"strings"
)
func handlerWSMSE(tr *api.Transport, msg *api.Message) error {
src := tr.Request.URL.Query().Get("src")
stream := streams.GetOrNew(src)
func handlerWSMSE(tr *ws.Transport, msg *ws.Message) error {
stream := streams.GetOrPatch(tr.Request.URL.Query())
if stream == nil {
return errors.New(api.StreamNotFound)
}
cons := &mp4.Consumer{
RemoteAddr: tcp.RemoteAddr(tr.Request),
UserAgent: tr.Request.UserAgent(),
}
var medias []*core.Media
if codecs := msg.String(); codecs != "" {
log.Trace().Str("codecs", codecs).Msgf("[mp4] new WS/MSE consumer")
cons.Medias = parseMedias(codecs, true)
medias = mp4.ParseCodecs(codecs, true)
}
cons.Listen(func(msg any) {
if data, ok := msg.([]byte); ok {
tr.Write(data)
}
})
cons := mp4.NewConsumer(medias)
cons.FormatName = "mse/fmp4"
cons.WithRequest(tr.Request)
if err := stream.AddConsumer(cons); err != nil {
log.Debug().Err(err).Msg("[mp4] add consumer")
return err
}
tr.Write(&ws.Message{Type: "mse", Value: mp4.ContentType(cons.Codecs())})
go cons.WriteTo(tr.Writer())
tr.OnClose(func() {
stream.RemoveConsumer(cons)
})
tr.Write(&api.Message{Type: "mse", Value: cons.MimeType()})
data, err := cons.Init()
if err != nil {
log.Warn().Err(err).Caller().Send()
return err
}
tr.Write(data)
cons.Start()
return nil
}
func handlerWSMP4(tr *api.Transport, msg *api.Message) error {
src := tr.Request.URL.Query().Get("src")
stream := streams.GetOrNew(src)
func handlerWSMP4(tr *ws.Transport, msg *ws.Message) error {
stream := streams.GetOrPatch(tr.Request.URL.Query())
if stream == nil {
return errors.New(api.StreamNotFound)
}
cons := &mp4.Segment{
RemoteAddr: tcp.RemoteAddr(tr.Request),
UserAgent: tr.Request.UserAgent(),
OnlyKeyframe: true,
}
var medias []*core.Media
if codecs := msg.String(); codecs != "" {
log.Trace().Str("codecs", codecs).Msgf("[mp4] new WS/MP4 consumer")
cons.Medias = parseMedias(codecs, false)
medias = mp4.ParseCodecs(codecs, false)
}
cons.Listen(func(msg any) {
if data, ok := msg.([]byte); ok {
tr.Write(data)
}
})
cons := mp4.NewKeyframe(medias)
cons.WithRequest(tr.Request)
if err := stream.AddConsumer(cons); err != nil {
log.Error().Err(err).Caller().Send()
return err
}
tr.Write(&api.Message{Type: "mp4", Value: cons.MimeType})
tr.Write(&ws.Message{Type: "mse", Value: mp4.ContentType(cons.Codecs())})
go cons.WriteTo(tr.Writer())
tr.OnClose(func() {
stream.RemoveConsumer(cons)
@@ -94,51 +72,3 @@ func handlerWSMP4(tr *api.Transport, msg *api.Message) error {
return nil
}
func parseMedias(codecs string, parseAudio bool) (medias []*core.Media) {
var videos []*core.Codec
var audios []*core.Codec
for _, name := range strings.Split(codecs, ",") {
switch name {
case mp4.MimeH264:
codec := &core.Codec{Name: core.CodecH264}
videos = append(videos, codec)
case mp4.MimeH265:
codec := &core.Codec{Name: core.CodecH265}
videos = append(videos, codec)
case mp4.MimeAAC:
codec := &core.Codec{Name: core.CodecAAC}
audios = append(audios, codec)
case mp4.MimeFlac:
audios = append(audios,
&core.Codec{Name: core.CodecPCMA},
&core.Codec{Name: core.CodecPCMU},
&core.Codec{Name: core.CodecPCM},
)
case mp4.MimeOpus:
codec := &core.Codec{Name: core.CodecOpus}
audios = append(audios, codec)
}
}
if videos != nil {
media := &core.Media{
Kind: core.KindVideo,
Direction: core.DirectionSendonly,
Codecs: videos,
}
medias = append(medias, media)
}
if audios != nil && parseAudio {
media := &core.Media{
Kind: core.KindAudio,
Direction: core.DirectionSendonly,
Codecs: audios,
}
medias = append(medias, media)
}
return
}

32
internal/mpegts/aac.go Normal file
View File

@@ -0,0 +1,32 @@
package mpegts
import (
"net/http"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/aac"
)
func apiStreamAAC(w http.ResponseWriter, r *http.Request) {
src := r.URL.Query().Get("src")
stream := streams.Get(src)
if stream == nil {
http.Error(w, api.StreamNotFound, http.StatusNotFound)
return
}
cons := aac.NewConsumer()
cons.WithRequest(r)
if err := stream.AddConsumer(cons); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Add("Content-Type", "audio/aac")
_, _ = cons.WriteTo(w)
stream.RemoveConsumer(cons)
}

View File

@@ -1,22 +1,50 @@
package mpegts
import (
"net/http"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/mpegts"
"net/http"
)
func Init() {
api.HandleFunc("api/stream.ts", apiHandle)
api.HandleFunc("api/stream.aac", apiStreamAAC)
}
func apiHandle(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
http.Error(w, "", http.StatusMethodNotAllowed)
outputMpegTS(w, r)
} else {
inputMpegTS(w, r)
}
}
func outputMpegTS(w http.ResponseWriter, r *http.Request) {
src := r.URL.Query().Get("src")
stream := streams.Get(src)
if stream == nil {
http.Error(w, api.StreamNotFound, http.StatusNotFound)
return
}
cons := mpegts.NewConsumer()
cons.WithRequest(r)
if err := stream.AddConsumer(cons); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Add("Content-Type", "video/mp2t")
_, _ = cons.WriteTo(w)
stream.RemoveConsumer(cons)
}
func inputMpegTS(w http.ResponseWriter, r *http.Request) {
dst := r.URL.Query().Get("dst")
stream := streams.Get(dst)
if stream == nil {
@@ -24,20 +52,17 @@ func apiHandle(w http.ResponseWriter, r *http.Request) {
return
}
res := &http.Response{Body: r.Body, Request: r}
client := mpegts.NewClient(res)
if err := client.Handle(); err != nil {
client, err := mpegts.Open(r.Body)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
stream.AddProducer(client)
defer stream.RemoveProducer(client)
if err := client.Handle(); err != nil {
if err = client.Start(); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
stream.RemoveProducer(client)
}

52
internal/nest/init.go Normal file
View File

@@ -0,0 +1,52 @@
package nest
import (
"net/http"
"strings"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/nest"
)
func Init() {
streams.HandleFunc("nest", func(source string) (core.Producer, error) {
return nest.Dial(source)
})
api.HandleFunc("api/nest", apiNest)
}
func apiNest(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query()
cliendID := query.Get("client_id")
cliendSecret := query.Get("client_secret")
refreshToken := query.Get("refresh_token")
projectID := query.Get("project_id")
nestAPI, err := nest.NewAPI(cliendID, cliendSecret, refreshToken)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
devices, err := nestAPI.GetDevices(projectID)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
var items []*api.Source
for _, device := range devices {
query.Set("device_id", device.DeviceID)
query.Set("protocols", strings.Join(device.Protocols, ","))
items = append(items, &api.Source{
Name: device.Name, URL: "nest:?" + query.Encode(),
})
}
api.ResponseSources(w, items)
}

View File

@@ -2,12 +2,13 @@ package ngrok
import (
"fmt"
"net"
"strings"
"github.com/AlexxIT/go2rtc/internal/app"
"github.com/AlexxIT/go2rtc/internal/webrtc"
"github.com/AlexxIT/go2rtc/pkg/ngrok"
"github.com/rs/zerolog"
"net"
"strings"
)
func Init() {
@@ -39,7 +40,7 @@ func Init() {
}
// Addr: "//localhost:8555", URL: "tcp://1.tcp.eu.ngrok.io:12345"
if msg.Addr == "//localhost:"+webrtc.Port && strings.HasPrefix(msg.URL, "tcp://") {
if strings.HasPrefix(msg.Addr, "//localhost:") && strings.HasPrefix(msg.URL, "tcp://") {
// don't know if really necessary use IP
address, err := ConvertHostToIP(msg.URL[6:])
if err != nil {
@@ -49,7 +50,7 @@ func Init() {
log.Info().Str("addr", address).Msg("[ngrok] add external candidate for WebRTC")
webrtc.AddCandidate(address)
webrtc.AddCandidate("tcp", address)
}
}
})

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

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

View File

@@ -1,13 +1,6 @@
package onvif
import (
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/app"
"github.com/AlexxIT/go2rtc/internal/rtsp"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/onvif"
"github.com/rs/zerolog"
"io"
"net"
"net/http"
@@ -15,6 +8,14 @@ import (
"os"
"strconv"
"time"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/app"
"github.com/AlexxIT/go2rtc/internal/rtsp"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/onvif"
"github.com/rs/zerolog"
)
func Init() {
@@ -54,49 +55,73 @@ func onvifDeviceService(w http.ResponseWriter, r *http.Request) {
return
}
action := onvif.GetRequestAction(b)
if action == "" {
operation := onvif.GetRequestAction(b)
if operation == "" {
http.Error(w, "malformed request body", http.StatusBadRequest)
return
}
log.Trace().Msgf("[onvif] %s", action)
log.Trace().Msgf("[onvif] server request %s %s:\n%s", r.Method, r.RequestURI, b)
var res string
switch operation {
case onvif.DeviceGetNetworkInterfaces, // important for Hass
onvif.DeviceGetSystemDateAndTime, // important for Hass
onvif.DeviceGetDiscoveryMode,
onvif.DeviceGetDNS,
onvif.DeviceGetHostname,
onvif.DeviceGetNetworkDefaultGateway,
onvif.DeviceGetNetworkProtocols,
onvif.DeviceGetNTP,
onvif.DeviceGetScopes,
onvif.MediaGetVideoEncoderConfigurations,
onvif.MediaGetAudioEncoderConfigurations,
onvif.MediaGetAudioSources,
onvif.MediaGetAudioSourceConfigurations:
b = onvif.StaticResponse(operation)
switch action {
case onvif.ActionGetCapabilities:
case onvif.DeviceGetCapabilities:
// important for Hass: Media section
res = onvif.GetCapabilitiesResponse(r.Host)
b = onvif.GetCapabilitiesResponse(r.Host)
case onvif.ActionGetSystemDateAndTime:
// important for Hass
res = onvif.GetSystemDateAndTimeResponse()
case onvif.DeviceGetServices:
b = onvif.GetServicesResponse(r.Host)
case onvif.ActionGetNetworkInterfaces:
// important for Hass: none
res = onvif.GetNetworkInterfacesResponse()
case onvif.ActionGetDeviceInformation:
case onvif.DeviceGetDeviceInformation:
// important for Hass: SerialNumber (unique server ID)
res = onvif.GetDeviceInformationResponse("", "go2rtc", app.Version, r.Host)
b = onvif.GetDeviceInformationResponse("", "go2rtc", app.Version, r.Host)
case onvif.ActionGetServiceCapabilities:
case onvif.ServiceGetServiceCapabilities:
// important for Hass
res = onvif.GetServiceCapabilitiesResponse()
// TODO: check path links to media
b = onvif.GetMediaServiceCapabilitiesResponse()
case onvif.ActionSystemReboot:
res = onvif.SystemRebootResponse()
case onvif.DeviceSystemReboot:
b = onvif.StaticResponse(operation)
time.AfterFunc(time.Second, func() {
os.Exit(0)
})
case onvif.ActionGetProfiles:
// important for Hass: H264 codec, width, height
res = onvif.GetProfilesResponse(streams.GetAll())
case onvif.MediaGetVideoSources:
b = onvif.GetVideoSourcesResponse(streams.GetAllNames())
case onvif.ActionGetStreamUri:
case onvif.MediaGetProfiles:
// important for Hass: H264 codec, width, height
b = onvif.GetProfilesResponse(streams.GetAllNames())
case onvif.MediaGetProfile:
token := onvif.FindTagValue(b, "ProfileToken")
b = onvif.GetProfileResponse(token)
case onvif.MediaGetVideoSourceConfigurations:
// important for Happytime Onvif Client
b = onvif.GetVideoSourceConfigurationsResponse(streams.GetAllNames())
case onvif.MediaGetVideoSourceConfiguration:
token := onvif.FindTagValue(b, "ConfigurationToken")
b = onvif.GetVideoSourceConfigurationResponse(token)
case onvif.MediaGetStreamUri:
host, _, err := net.SplitHostPort(r.Host)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
@@ -104,16 +129,23 @@ func onvifDeviceService(w http.ResponseWriter, r *http.Request) {
}
uri := "rtsp://" + host + ":" + rtsp.Port + "/" + onvif.FindTagValue(b, "ProfileToken")
res = onvif.GetStreamUriResponse(uri)
b = onvif.GetStreamUriResponse(uri)
case onvif.MediaGetSnapshotUri:
uri := "http://" + r.Host + "/api/frame.jpeg?src=" + onvif.FindTagValue(b, "ProfileToken")
b = onvif.GetSnapshotUriResponse(uri)
default:
http.Error(w, "unsupported action", http.StatusBadRequest)
http.Error(w, "unsupported operation", http.StatusBadRequest)
log.Warn().Msgf("[onvif] unsupported operation: %s", operation)
log.Debug().Msgf("[onvif] unsupported request:\n%s", b)
return
}
log.Trace().Msgf("[onvif] server response:\n%s", b)
w.Header().Set("Content-Type", "application/soap+xml; charset=utf-8")
if _, err = w.Write([]byte(res)); err != nil {
if _, err = w.Write(b); err != nil {
log.Error().Err(err).Caller().Send()
}
}
@@ -121,7 +153,7 @@ func onvifDeviceService(w http.ResponseWriter, r *http.Request) {
func apiOnvif(w http.ResponseWriter, r *http.Request) {
src := r.URL.Query().Get("src")
var items []api.Stream
var items []*api.Source
if src == "" {
urls, err := onvif.DiscoveryStreamingURLs()
@@ -149,7 +181,7 @@ func apiOnvif(w http.ResponseWriter, r *http.Request) {
u.Path = ""
}
items = append(items, api.Stream{Name: u.Host, URL: u.String()})
items = append(items, &api.Source{Name: u.Host, URL: u.String()})
}
} else {
client, err := onvif.NewClient(src)
@@ -159,7 +191,7 @@ func apiOnvif(w http.ResponseWriter, r *http.Request) {
}
if l := log.Trace(); l.Enabled() {
b, _ := client.GetProfiles()
b, _ := client.MediaRequest(onvif.MediaGetProfiles)
l.Msgf("[onvif] src=%s profiles:\n%s", src, b)
}
@@ -176,19 +208,19 @@ func apiOnvif(w http.ResponseWriter, r *http.Request) {
}
for i, token := range tokens {
items = append(items, api.Stream{
items = append(items, &api.Source{
Name: name + " stream" + strconv.Itoa(i),
URL: src + "?subtype=" + token,
})
}
if len(tokens) > 0 && client.HasSnapshots() {
items = append(items, api.Stream{
items = append(items, &api.Source{
Name: name + " snapshot",
URL: src + "?subtype=" + tokens[0] + "&snapshot",
})
}
}
api.ResponseStreams(w, items)
api.ResponseSources(w, items)
}

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

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

View File

@@ -2,30 +2,22 @@ package roborock
import (
"fmt"
"net/http"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/roborock"
"net/http"
)
func Init() {
streams.HandleFunc("roborock", handle)
streams.HandleFunc("roborock", func(source string) (core.Producer, error) {
return roborock.Dial(source)
})
api.HandleFunc("api/roborock", apiHandle)
}
func handle(url string) (core.Producer, error) {
conn := roborock.NewClient(url)
if err := conn.Dial(); err != nil {
return nil, err
}
if err := conn.Connect(); err != nil {
return nil, err
}
return conn, nil
}
var Auth struct {
UserData *roborock.UserInfo `json:"user_data"`
BaseURL string `json:"base_url"`
@@ -84,7 +76,7 @@ func apiHandle(w http.ResponseWriter, r *http.Request) {
return
}
var items []api.Stream
var items []*api.Source
for _, device := range devices {
source := fmt.Sprintf(
@@ -93,8 +85,8 @@ func apiHandle(w http.ResponseWriter, r *http.Request) {
Auth.UserData.IoT.User, Auth.UserData.IoT.Pass, Auth.UserData.IoT.Domain,
device.DID, device.Key,
)
items = append(items, api.Stream{Name: device.Name, URL: source})
items = append(items, &api.Source{Name: device.Name, URL: source})
}
api.ResponseStreams(w, items)
api.ResponseSources(w, items)
}

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