Compare commits

..

457 Commits

Author SHA1 Message Date
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
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
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
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
b389d0eb9c fix(app): handle daemon process correctly on Unix systems 2024-06-06 18:54:40 +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
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
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
264 changed files with 11703 additions and 3561 deletions

View File

@@ -15,126 +15,151 @@ jobs:
env: { CGO_ENABLED: 0 }
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Setup Go
uses: actions/setup-go@v4
with: { go-version: '1.21' }
uses: actions/setup-go@v5
with: { go-version: '1.22' }
- name: Build go2rtc_win64
env: { GOOS: windows, GOARCH: amd64 }
run: go build -ldflags "-s -w" -trimpath
- name: Upload go2rtc_win64
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with: { name: go2rtc_win64, path: go2rtc.exe }
- name: Build go2rtc_win32
env: { GOOS: windows, GOARCH: 386 }
env: { GOOS: windows, GOARCH: 386, GOTOOLCHAIN: go1.20.14 }
run: go build -ldflags "-s -w" -trimpath
- name: Upload go2rtc_win32
uses: actions/upload-artifact@v3
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@v3
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@v3
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@v3
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@v3
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@v3
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@v3
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@v3
uses: actions/upload-artifact@v4
with: { name: go2rtc_linux_mipsel, path: go2rtc }
- name: Build go2rtc_mac_amd64
env: { GOOS: darwin, GOARCH: amd64 }
env: { GOOS: darwin, GOARCH: amd64, GOTOOLCHAIN: go1.20.14 }
run: go build -ldflags "-s -w" -trimpath
- name: Upload go2rtc_mac_amd64
uses: actions/upload-artifact@v3
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@v3
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@v3
with: { name: go2rtc_freebsd_amd64, path: go2rtc }
- name: Build go2rtc_freebsd_arm64
env: { GOOS: freebsd, GOARCH: arm64 }
run: go build -ldflags "-s -w" -trimpath
- name: Upload go2rtc_freebsd_arm64
uses: actions/upload-artifact@v3
with: { name: go2rtc_freebsd_arm64, path: go2rtc }
docker-master:
name: Build docker master
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Docker meta
id: meta
uses: docker/metadata-action@v4
uses: docker/metadata-action@v5
with:
images: ${{ github.repository }}
images: |
${{ github.repository }}
ghcr.io/${{ github.repository }}
tags: |
type=ref,event=branch
type=semver,pattern={{version}},enable=false
type=match,pattern=v(.*),group=1
- 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: Login to DockerHub
if: github.event_name != 'pull_request'
uses: docker/login-action@v2
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@v4
uses: docker/build-push-action@v5
with:
context: .
platforms: |
linux/amd64
linux/386
linux/arm/v6
linux/arm/v7
linux/arm64/v8
push: ${{ github.event_name != 'pull_request' }}
@@ -148,36 +173,46 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Docker meta
id: meta-hw
uses: docker/metadata-action@v4
uses: docker/metadata-action@v5
with:
images: ${{ github.repository }}
images: |
${{ github.repository }}
ghcr.io/${{ github.repository }}
flavor: |
suffix=-hardware
latest=false
suffix=-hardware,onlatest=true
latest=auto
tags: |
type=ref,event=branch
type=semver,pattern={{version}},enable=false
type=match,pattern=v(.*),group=1
- name: Set up QEMU
uses: docker/setup-qemu-action@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: Login to DockerHub
if: github.event_name != 'pull_request'
uses: docker/login-action@v2
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@v4
uses: docker/build-push-action@v5
with:
context: .
file: hardware.Dockerfile

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

@@ -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.21'
go-version: '1.22'
- name: Build Go binary
run: go build -ldflags "-s -w" -trimpath -o ./go2rtc
@@ -70,13 +70,13 @@ 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: .
platforms: linux/${{ matrix.platform }}
@@ -89,7 +89,7 @@ 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

6
.gitignore vendored
View File

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

View File

@@ -2,14 +2,19 @@
# 0. Prepare images
ARG PYTHON_VERSION="3.11"
ARG GO_VERSION="1.21"
ARG NGROK_VERSION="3"
FROM python:${PYTHON_VERSION}-alpine AS base
FROM ngrok/ngrok:${NGROK_VERSION}-alpine AS ngrok
ARG GO_VERSION="1.22"
# 1. Build go2rtc binary
# 1. Download ngrok binary (for support arm/v6)
FROM alpine AS ngrok
ARG TARGETARCH
ARG TARGETOS
ADD https://bin.equinox.io/c/bNyj1mQVY4c/ngrok-v3-stable-${TARGETOS}-${TARGETARCH}.tgz /
RUN tar -xzf /ngrok-v3-stable-${TARGETOS}-${TARGETARCH}.tgz -C /bin
# 2. Build go2rtc binary
FROM --platform=$BUILDPLATFORM golang:${GO_VERSION}-alpine AS build
ARG TARGETPLATFORM
ARG TARGETOS
@@ -20,6 +25,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,15 +35,8 @@ 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
FROM python:${PYTHON_VERSION}-alpine AS base
# Install ffmpeg, tini (for signal handling),
# and other common tools for the echo source.
@@ -54,7 +54,8 @@ 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/
COPY --from=ngrok /bin/ngrok /usr/local/bin/
ENTRYPOINT ["/sbin/tini", "--"]
VOLUME /config

267
README.md
View File

@@ -1,9 +1,12 @@
# go2rtc
<h1 align="center">
[![](https://img.shields.io/github/stars/AlexxIT/go2rtc?style=flat-square&logo=github)](https://github.com/AlexxIT/go2rtc/stargazers)
[![](https://img.shields.io/docker/pulls/alexxit/go2rtc?style=flat-square&logo=docker&logoColor=white&label=pulls)](https://hub.docker.com/r/alexxit/go2rtc)
[![](https://img.shields.io/github/downloads/AlexxIT/go2rtc/total?color=blue&style=flat-square&logo=github)](https://github.com/AlexxIT/go2rtc/releases)
[![](https://goreportcard.com/badge/github.com/AlexxIT/go2rtc)](https://goreportcard.com/report/github.com/AlexxIT/go2rtc)
![go2rtc](assets/logo.gif)
<br>
[![stars](https://img.shields.io/github/stars/AlexxIT/go2rtc?style=flat-square&logo=github)](https://github.com/AlexxIT/go2rtc/stargazers)
[![docker pulls](https://img.shields.io/docker/pulls/alexxit/go2rtc?style=flat-square&logo=docker&logoColor=white&label=pulls)](https://hub.docker.com/r/alexxit/go2rtc)
[![releases](https://img.shields.io/github/downloads/AlexxIT/go2rtc/total?color=blue&style=flat-square&logo=github)](https://github.com/AlexxIT/go2rtc/releases)
[![goreport](https://goreportcard.com/badge/github.com/AlexxIT/go2rtc)](https://goreportcard.com/report/github.com/AlexxIT/go2rtc)
</h1>
Ultimate camera streaming application with support RTSP, WebRTC, HomeKit, FFmpeg, RTMP, etc.
@@ -14,6 +17,7 @@ Ultimate camera streaming application with support RTSP, WebRTC, HomeKit, FFmpeg
- streaming from [RTSP](#source-rtsp), [RTMP](#source-rtmp), [DVRIP](#source-dvrip), [HTTP](#source-http) (FLV/MJPEG/JPEG/TS), [USB Cameras](#source-ffmpeg-device) and [other sources](#module-streams)
- streaming from any sources, supported by [FFmpeg](#source-ffmpeg)
- streaming to [RTSP](#module-rtsp), [WebRTC](#module-webrtc), [MSE/MP4](#module-mp4), [HomeKit](#module-homekit) [HLS](#module-hls) or [MJPEG](#module-mjpeg)
- [publish](#publish-stream) any source to popular streaming services (YouTube, Telegram, etc.)
- first project in the World with support streaming from [HomeKit Cameras](#source-homekit)
- support H265 for WebRTC in browser (Safari only, [read more](https://github.com/AlexxIT/Blog/issues/5))
- on the fly transcoding for unsupported codecs via [FFmpeg](#source-ffmpeg)
@@ -22,7 +26,7 @@ Ultimate camera streaming application with support RTSP, WebRTC, HomeKit, FFmpeg
- mixing tracks from different sources to single stream
- auto match client supported codecs
- [2-way audio](#two-way-audio) for some cameras
- streaming from private networks via [Ngrok](#module-ngrok)
- streaming from private networks via [ngrok](#module-ngrok)
- can be [integrated to](#module-api) any smart home platform or be used as [standalone app](#go2rtc-binary)
**Inspired by:**
@@ -33,6 +37,7 @@ Ultimate camera streaming application with support RTSP, WebRTC, HomeKit, FFmpeg
- [GStreamer](https://gstreamer.freedesktop.org/) framework pipeline idea
- [MediaSoup](https://mediasoup.org/) framework routing idea
- HomeKit Accessory Protocol from [@brutella](https://github.com/brutella/hap)
- creator of the project's logo [@v_novoseltsev](https://www.instagram.com/v_novoseltsev)
---
@@ -53,11 +58,13 @@ Ultimate camera streaming application with support RTSP, WebRTC, HomeKit, FFmpeg
* [Source: FFmpeg Device](#source-ffmpeg-device)
* [Source: Exec](#source-exec)
* [Source: Echo](#source-echo)
* [Source: Expr](#source-expr)
* [Source: HomeKit](#source-homekit)
* [Source: Bubble](#source-bubble)
* [Source: DVRIP](#source-dvrip)
* [Source: Tapo](#source-tapo)
* [Source: Kasa](#source-kasa)
* [Source: GoPro](#source-gopro)
* [Source: Ivideon](#source-ivideon)
* [Source: Hass](#source-hass)
* [Source: ISAPI](#source-isapi)
@@ -67,12 +74,14 @@ Ultimate camera streaming application with support RTSP, WebRTC, HomeKit, FFmpeg
* [Source: WebTorrent](#source-webtorrent)
* [Incoming sources](#incoming-sources)
* [Stream to camera](#stream-to-camera)
* [Publish stream](#publish-stream)
* [Module: API](#module-api)
* [Module: RTSP](#module-rtsp)
* [Module: RTMP](#module-rtmp)
* [Module: WebRTC](#module-webrtc)
* [Module: HomeKit](#module-homekit)
* [Module: WebTorrent](#module-webtorrent)
* [Module: Ngrok](#module-ngrok)
* [Module: ngrok](#module-ngrok)
* [Module: Hass](#module-hass)
* [Module: MP4](#module-mp4)
* [Module: HLS](#module-hls)
@@ -106,8 +115,8 @@ Ultimate camera streaming application with support RTSP, WebRTC, HomeKit, FFmpeg
Download binary for your OS from [latest release](https://github.com/AlexxIT/go2rtc/releases/):
- `go2rtc_win64.zip` - Windows 64-bit
- `go2rtc_win32.zip` - Windows 32-bit
- `go2rtc_win64.zip` - Windows 10+ 64-bit
- `go2rtc_win32.zip` - Windows 7+ 32-bit
- `go2rtc_win_arm64.zip` - Windows ARM 64-bit
- `go2rtc_linux_amd64` - Linux 64-bit
- `go2rtc_linux_i386` - Linux 32-bit
@@ -115,14 +124,14 @@ Download binary for your OS from [latest release](https://github.com/AlexxIT/go2
- `go2rtc_linux_arm` - Linux ARM 32-bit (ex. Raspberry 32-bit OS)
- `go2rtc_linux_armv6` - Linux ARMv6 (for old Raspberry 1 and Zero)
- `go2rtc_linux_mipsel` - Linux MIPS (ex. [Xiaomi Gateway 3](https://github.com/AlexxIT/XiaomiGateway3), [Wyze cameras](https://github.com/gtxaspec/wz_mini_hacks))
- `go2rtc_mac_amd64.zip` - Mac Intel 64-bit
- `go2rtc_mac_arm64.zip` - Mac ARM 64-bit
- `go2rtc_mac_amd64.zip` - macOS 10.13+ Intel 64-bit
- `go2rtc_mac_arm64.zip` - macOS ARM 64-bit
Don't forget to fix the rights `chmod +x go2rtc_xxx_xxx` on Linux and Mac.
### go2rtc: Docker
Container [alexxit/go2rtc](https://hub.docker.com/r/alexxit/go2rtc) with support `amd64`, `386`, `arm64`, `arm`. This container is the same as [Home Assistant Add-on](#go2rtc-home-assistant-add-on), but can be used separately from Home Assistant. Container has preinstalled [FFmpeg](#source-ffmpeg), [Ngrok](#module-ngrok) and [Python](#source-echo).
The Docker container [`alexxit/go2rtc`](https://hub.docker.com/r/alexxit/go2rtc) supports multiple architectures including `amd64`, `386`, `arm64`, and `arm`. This container offers the same functionality as the [Home Assistant Add-on](#go2rtc-home-assistant-add-on) but is designed to operate independently of Home Assistant. It comes preinstalled with [FFmpeg](#source-ffmpeg), [ngrok](#module-ngrok), and [Python](#source-echo).
### go2rtc: Home Assistant Add-on
@@ -141,13 +150,13 @@ Container [alexxit/go2rtc](https://hub.docker.com/r/alexxit/go2rtc) with support
Latest, but maybe unstable version:
- Binary: GitHub > [Actions](https://github.com/AlexxIT/go2rtc/actions) > [Build and Push](https://github.com/AlexxIT/go2rtc/actions/workflows/build.yml) > latest run > Artifacts section (you should be logged in to GitHub)
- Binary: [latest nightly release](https://nightly.link/AlexxIT/go2rtc/workflows/build/master)
- Docker: `alexxit/go2rtc:master` or `alexxit/go2rtc:master-hardware` versions
- Hass Add-on: `go2rtc master` or `go2rtc master hardware` versions
## Configuration
- by default go2rtc will search `go2rtc.yaml` in the current work dirrectory
- by default go2rtc will search `go2rtc.yaml` in the current work directory
- `api` server will start on default **1984 port** (TCP)
- `rtsp` server will start on default **8554 port** (TCP)
- `webrtc` will use port **8555** (TCP/UDP) for connections
@@ -165,7 +174,7 @@ Available modules:
- [hls](#module-hls) - HLS TS or fMP4 stream Server
- [mjpeg](#module-mjpeg) - MJPEG Server
- [ffmpeg](#source-ffmpeg) - FFmpeg integration
- [ngrok](#module-ngrok) - Ngrok integration (external access for private network)
- [ngrok](#module-ngrok) - ngrok integration (external access for private network)
- [hass](#module-hass) - Home Assistant integration
- [log](#module-log) - logs config
@@ -183,11 +192,13 @@ Available source types:
- [ffmpeg:device](#source-ffmpeg-device) - local USB Camera or Webcam
- [exec](#source-exec) - get media from external app output
- [echo](#source-echo) - get stream link from bash or python
- [expr](#source-expr) - get stream link via built-in expression language
- [homekit](#source-homekit) - streaming from HomeKit Camera
- [bubble](#source-bubble) - streaming from ESeeCloud/dvr163 NVR
- [dvrip](#source-dvrip) - streaming from DVR-IP NVR
- [tapo](#source-tapo) - TP-Link Tapo cameras with [two way audio](#two-way-audio) support
- [kasa](#source-tapo) - TP-Link Kasa cameras
- [gopro](#source-gopro) - GoPro cameras
- [ivideon](#source-ivideon) - public cameras from [Ivideon](https://tv.ivideon.com/) service
- [hass](#source-hass) - Home Assistant integration
- [isapi](#source-isapi) - two way audio for Hikvision (ISAPI) cameras
@@ -202,9 +213,11 @@ Read more about [incoming sources](#incoming-sources)
Supported for sources:
- [RTSP cameras](#source-rtsp) with [ONVIF Profile T](https://www.onvif.org/specs/stream/ONVIF-Streaming-Spec.pdf) (back channel connection)
- [DVRIP](#source-dvrip) cameras
- [TP-Link Tapo](#source-tapo) cameras
- [Hikvision ISAPI](#source-isapi) cameras
- [Roborock vacuums](#source-roborock) models with cameras
- [Exec](#source-exec) audio on server
- [Any Browser](#incoming-browser) as IP-camera
Two way audio can be used in browser with [WebRTC](#module-webrtc) technology. The browser will give access to the microphone only for HTTPS sites ([read more](https://stackoverflow.com/questions/52759992/how-to-access-camera-and-microphone-in-chrome-without-https)).
@@ -222,7 +235,7 @@ streams:
amcrest_doorbell:
- rtsp://username:password@192.168.1.123:554/cam/realmonitor?channel=1&subtype=0#backchannel=0
unifi_camera: rtspx://192.168.1.123:7441/fD6ouM72bWoFijxK
glichy_camera: ffmpeg:rstp://username:password@192.168.1.123/live/ch00_1
glichy_camera: ffmpeg:rtsp://username:password@192.168.1.123/live/ch00_1
```
**Recommendations**
@@ -257,7 +270,7 @@ streams:
#### Source: RTMP
You can get stream from RTMP server, for example [Frigate](https://docs.frigate.video/configuration/rtmp).
You can get stream from RTMP server, for example [Nginx with nginx-rtmp-module](https://github.com/arut/nginx-rtmp-module).
```yaml
streams:
@@ -297,6 +310,8 @@ streams:
#### Source: ONVIF
*[New in v1.5.0](https://github.com/AlexxIT/go2rtc/releases/tag/v1.5.0)*
The source is not very useful if you already know RTSP and snapshot links for your camera. But it can be useful if you don't.
**WebUI > Add** webpage support ONVIF autodiscovery. Your server must be on the same subnet as the camera. If you use docker, you must use "network host".
@@ -389,24 +404,35 @@ streams:
#### Source: Exec
Exec source can run any external application and expect data from it. Two transports are supported - **pipe** and **RTSP**.
Exec source can run any external application and expect data from it. Two transports are supported - **pipe** (*from [v1.5.0](https://github.com/AlexxIT/go2rtc/releases/tag/v1.5.0)*) and **RTSP**.
If you want to use **RTSP** transport - the command must contain the `{output}` argument in any place. On launch, it will be replaced by the local address of the RTSP server.
**pipe** reads data from app stdout in different formats: **MJPEG**, **H.264/H.265 bitstream**, **MPEG-TS**.
**pipe** reads data from app stdout in different formats: **MJPEG**, **H.264/H.265 bitstream**, **MPEG-TS**. Also pipe can write data to app stdin in two formats: **PCMA** and **PCM/48000**.
The source can be used with:
- [FFmpeg](https://ffmpeg.org/) - go2rtc ffmpeg source just a shortcut to exec source
- [FFplay](https://ffmpeg.org/ffplay.html) - play audio on your server
- [GStreamer](https://gstreamer.freedesktop.org/)
- [Raspberry Pi Cameras](https://www.raspberrypi.com/documentation/computers/camera_software.html)
- any your own software
Pipe commands support parameters (format: `exec:{command}#{param1}#{param2}`):
- `killsignal` - signal which will be send to stop the process (numeric form)
- `killtimeout` - time in seconds for forced termination with sigkill
- `backchannel` - enable backchannel for two-way audio
```yaml
streams:
stream: exec:ffmpeg -re -i /media/BigBuckBunny.mp4 -c copy -rtsp_transport tcp -f rtsp {output}
picam_h264: exec:libcamera-vid -t 0 --inline -o -
picam_mjpeg: exec:libcamera-vid -t 0 --codec mjpeg -o -
pi5cam_h264: exec:libcamera-vid -t 0 --libav-format h264 -o -
canon: exec:gphoto2 --capture-movie --stdout#killsignal=2#killtimeout=5
play_pcma: exec:ffplay -fflags nobuffer -f alaw -ar 8000 -i -#backchannel=1
play_pcm48k: exec:ffplay -fflags nobuffer -f s16be -ar 48000 -i -#backchannel=1
```
#### Source: Echo
@@ -422,6 +448,12 @@ streams:
apple_hls: echo:python3 hls.py https://developer.apple.com/streaming/examples/basic-stream-osx-ios5.html
```
#### Source: Expr
*[New in v1.8.2](https://github.com/AlexxIT/go2rtc/releases/tag/v1.8.2)*
Like `echo` source, but uses the built-in [expr](https://github.com/antonmedv/expr) expression language ([read more](https://github.com/AlexxIT/go2rtc/blob/master/internal/expr/README.md)).
#### Source: HomeKit
**Important:**
@@ -457,6 +489,8 @@ RTSP link with "normal" audio for any player: `rtsp://192.168.1.123:8554/aqara_g
#### Source: Bubble
*[New in v1.6.1](https://github.com/AlexxIT/go2rtc/releases/tag/v1.6.1)*
Other names: [ESeeCloud](http://www.eseecloud.com/), [dvr163](http://help.dvr163.com/).
- you can skip `username`, `password`, `port`, `ch` and `stream` if they are default
@@ -469,6 +503,8 @@ streams:
#### Source: DVRIP
*[New in v1.2.0](https://github.com/AlexxIT/go2rtc/releases/tag/v1.2.0)*
Other names: DVR-IP, NetSurveillance, Sofia protocol (NETsurveillance ActiveX plugin XMeye SDK).
- you can skip `username`, `password`, `port`, `channel` and `subtype` if they are default
@@ -478,34 +514,61 @@ Other names: DVR-IP, NetSurveillance, Sofia protocol (NETsurveillance ActiveX pl
```yaml
streams:
camera1: dvrip://username:password@192.168.1.123:34567?channel=0&subtype=0
only_stream: dvrip://username:password@192.168.1.123:34567?channel=0&subtype=0
only_tts: dvrip://username:password@192.168.1.123:34567?backchannel=1
two_way_audio:
- dvrip://username:password@192.168.1.123:34567?channel=0&subtype=0
- dvrip://username:password@192.168.1.123:34567?backchannel=1
```
#### Source: Tapo
*[New in v1.2.0](https://github.com/AlexxIT/go2rtc/releases/tag/v1.2.0)*
[TP-Link Tapo](https://www.tapo.com/) proprietary camera protocol with **two way audio** support.
- stream quality is the same as [RTSP protocol](https://www.tapo.com/en/faq/34/)
- use the **cloud password**, this is not the RTSP password! you do not need to add a login!
- you can also use UPPERCASE MD5 hash from your cloud password with `admin` username
- some new camera firmwares requires SHA256 instead of MD5
```yaml
streams:
# cloud password without username
camera1: tapo://cloud-password@192.168.1.123
# admin username and UPPERCASE MD5 cloud-password hash
camera2: tapo://admin:MD5-PASSWORD-HASH@192.168.1.123
camera2: tapo://admin:UPPERCASE-MD5@192.168.1.123
# admin username and UPPERCASE SHA256 cloud-password hash
camera3: tapo://admin:UPPERCASE-SHA256@192.168.1.123
```
```bash
echo -n "cloud password" | md5 | awk '{print toupper($0)}'
echo -n "cloud password" | shasum -a 256 | awk '{print toupper($0)}'
```
#### Source: Kasa
*[New in v1.7.0](https://github.com/AlexxIT/go2rtc/releases/tag/v1.7.0)*
[TP-Link Kasa](https://www.kasasmart.com/) non-standard protocol [more info](https://medium.com/@hu3vjeen/reverse-engineering-tp-link-kc100-bac4641bf1cd).
- `username` - urlsafe email, `alex@gmail.com` -> `alex%40gmail.com`
- `password` - base64password, `secret1` -> `c2VjcmV0MQ==`
```yaml
streams:
kasa: kasa://user:pass@192.168.1.123:19443/https/stream/mixed
kc401: kasa://username:password@192.168.1.123:19443/https/stream/mixed
```
Tested: KD110, KC200, KC401, KC420WS, EC71.
#### Source: GoPro
*[New in v1.8.3](https://github.com/AlexxIT/go2rtc/releases/tag/v1.8.3)*
Support streaming from [GoPro](https://gopro.com/) cameras, connected via USB or Wi-Fi to Linux, Mac, Windows. [Read more](https://github.com/AlexxIT/go2rtc/tree/master/internal/gopro).
#### Source: Ivideon
Support public cameras from service [Ivideon](https://tv.ivideon.com/).
@@ -533,11 +596,11 @@ streams:
aqara_g3: hass:Camera-Hub-G3-AB12
```
**WebRTC Cameras**
**WebRTC Cameras** (*from [v1.6.0](https://github.com/AlexxIT/go2rtc/releases/tag/v1.6.0)*)
Any cameras in WebRTC format are supported. But at the moment Home Assistant only supports some [Nest](https://www.home-assistant.io/integrations/nest/) cameras in this fomat.
The Nest API only allows you to get a link to a stream for 5 minutes. So every 5 minutes the stream will be reconnected.
**Important.** The Nest API only allows you to get a link to a stream for 5 minutes. Do not use this with Frigate! If the stream expires, Frigate will consume all available ram on your machine within seconds. It's recommended to use [Nest source](#source-nest) - it supports extending the stream.
```yaml
streams:
@@ -553,6 +616,8 @@ By default, the Home Assistant API does not allow you to get dynamic RTSP link t
#### Source: ISAPI
*[New in v1.3.0](https://github.com/AlexxIT/go2rtc/releases/tag/v1.3.0)*
This source type support only backchannel audio for Hikvision ISAPI protocol. So it should be used as second source in addition to the RTSP protocol.
```yaml
@@ -564,7 +629,9 @@ streams:
#### Source: Nest
Currently only WebRTC cameras are supported. Stream reconnects every 5 minutes.
*[New in v1.6.0](https://github.com/AlexxIT/go2rtc/releases/tag/v1.6.0)*
Currently only WebRTC cameras are supported.
For simplicity, it is recommended to connect the Nest/WebRTC camera to the [Home Assistant](#source-hass). But if you can somehow get the below parameters - Nest/WebRTC source will work without Hass.
@@ -575,6 +642,8 @@ streams:
#### Source: Roborock
*[New in v1.3.0](https://github.com/AlexxIT/go2rtc/releases/tag/v1.3.0)*
This source type support Roborock vacuums with cameras. Known working models:
- Roborock S6 MaxV - only video (the vacuum has no microphone)
@@ -586,25 +655,27 @@ If you have graphic pin for your vacuum - add it as numeric pin (lines: 123, 456
#### Source: WebRTC
*[New in v1.3.0](https://github.com/AlexxIT/go2rtc/releases/tag/v1.3.0)*
This source type support four connection formats.
**whep**
[WebRTC/WHEP](https://www.ietf.org/id/draft-murillo-whep-01.html) - is an unapproved standard for WebRTC video/audio viewers. But it may already be supported in some third-party software. It is supported in go2rtc.
[WebRTC/WHEP](https://datatracker.ietf.org/doc/draft-murillo-whep/) - is replaced by [WebRTC/WISH](https://datatracker.ietf.org/doc/charter-ietf-wish/02/) standard for WebRTC video/audio viewers. But it may already be supported in some third-party software. It is supported in go2rtc.
**go2rtc**
This format is only supported in go2rtc. Unlike WHEP it supports asynchronous WebRTC connection and two way audio.
**openipc**
**openipc** (*from [v1.7.0](https://github.com/AlexxIT/go2rtc/releases/tag/v1.7.0)*)
Support connection to [OpenIPC](https://openipc.org/) cameras.
**wyze**
**wyze** (*from [v1.6.1](https://github.com/AlexxIT/go2rtc/releases/tag/v1.6.1)*)
Supports connection to [Wyze](https://www.wyze.com/) cameras, using WebRTC protocol. You can use [docker-wyze-bridge](https://github.com/mrlt8/docker-wyze-bridge) project to get connection credentials.
**kinesis**
**kinesis** (*from [v1.6.1](https://github.com/AlexxIT/go2rtc/releases/tag/v1.6.1)*)
Supports [Amazon Kinesis Video Streams](https://aws.amazon.com/kinesis/video-streams/), using WebRTC protocol. You need to specify signalling WebSocket URL with all credentials in query params, `client_id` and `ice_servers` list in [JSON format](https://developer.mozilla.org/en-US/docs/Web/API/RTCIceServer).
@@ -621,6 +692,8 @@ streams:
#### Source: WebTorrent
*[New in v1.3.0](https://github.com/AlexxIT/go2rtc/releases/tag/v1.3.0)*
This source can get a stream from another go2rtc via [WebTorrent](#module-webtorrent) protocol.
```yaml
@@ -632,7 +705,7 @@ streams:
By default, go2rtc establishes a connection to the source when any client requests it. Go2rtc drops the connection to the source when it has no clients left.
- Go2rtc also can accepts incoming sources in [RTSP](#source-rtsp), [HTTP](#source-http) and **WebRTC/WHIP** formats
- Go2rtc also can accepts incoming sources in [RTSP](#module-rtsp), [RTMP](#module-rtmp), [HTTP](#source-http) and **WebRTC/WHIP** formats
- Go2rtc won't stop such a source if it has no clients
- You can push data only to existing stream (create stream with empty source in config)
- You can push multiple incoming sources to same stream
@@ -659,6 +732,8 @@ By default, go2rtc establishes a connection to the source when any client reques
#### Incoming: Browser
*[New in v1.3.0](https://github.com/AlexxIT/go2rtc/releases/tag/v1.3.0)*
You can turn the browser of any PC or mobile into an IP-camera with support video and two way audio. Or even broadcast your PC screen:
1. Create empty stream in the `go2rtc.yaml`
@@ -669,12 +744,16 @@ You can turn the browser of any PC or mobile into an IP-camera with support vide
#### Incoming: WebRTC/WHIP
*[New in v1.3.0](https://github.com/AlexxIT/go2rtc/releases/tag/v1.3.0)*
You can use **OBS Studio** or any other broadcast software with [WHIP](https://www.ietf.org/archive/id/draft-ietf-wish-whip-01.html) protocol support. This standard has not yet been approved. But you can download OBS Studio [dev version](https://github.com/obsproject/obs-studio/actions/runs/3969201209):
- Settings > Stream > Service: WHIP > http://192.168.1.123:1984/api/webrtc?dst=camera1
#### Stream to camera
*[New in v1.3.0](https://github.com/AlexxIT/go2rtc/releases/tag/v1.3.0)*
go2rtc support play audio files (ex. music or [TTS](https://www.home-assistant.io/integrations/#text-to-speech)) and live streams (ex. radio) on cameras with [two way audio](#two-way-audio) support (RTSP/ONVIF cameras, TP-Link Tapo, Hikvision ISAPI, Roborock vacuums, any Browser).
API example:
@@ -693,10 +772,50 @@ POST http://localhost:1984/api/streams?dst=camera1&src=ffmpeg:http://example.com
- you can stop active playback by calling the API with the empty `src` parameter
- you will see one active producer and one active consumer in go2rtc WebUI info page during streaming
### Publish stream
*[New in v1.8.0](https://github.com/AlexxIT/go2rtc/releases/tag/v1.8.0)*
You can publish any stream to streaming services (YouTube, Telegram, etc.) via RTMP/RTMPS. Important:
- Supported codecs: H264 for video and AAC for audio
- AAC audio is required for YouTube, videos without audio will not work
- You don't need to enable [RTMP module](#module-rtmp) listening for this task
You can use API:
```
POST http://localhost:1984/api/streams?src=camera1&dst=rtmps://...
```
Or config file:
```yaml
publish:
# publish stream "video_audio_transcode" to Telegram
video_audio_transcode:
- rtmps://xxx-x.rtmp.t.me/s/xxxxxxxxxx:xxxxxxxxxxxxxxxxxxxxxx
# publish stream "audio_transcode" to Telegram and YouTube
audio_transcode:
- rtmps://xxx-x.rtmp.t.me/s/xxxxxxxxxx:xxxxxxxxxxxxxxxxxxxxxx
- rtmp://xxx.rtmp.youtube.com/live2/xxxx-xxxx-xxxx-xxxx-xxxx
streams:
video_audio_transcode:
- ffmpeg:rtsp://user:pass@192.168.1.123/stream1#video=h264#hardware#audio=aac
audio_transcode:
- ffmpeg:rtsp://user:pass@192.168.1.123/stream1#video=copy#audio=aac
```
- **Telegram Desktop App** > Any public or private channel or group (where you admin) > Live stream > Start with... > Start streaming.
- **YouTube** > Create > Go live > Stream latency: Ultra low-latency > Copy: Stream URL + Stream key.
### Module: API
The HTTP API is the main part for interacting with the application. Default address: `http://localhost:1984/`.
**Important!** go2rtc passes requests from localhost and from unix socket without HTTP authorisation, even if you have it configured! It is your responsibility to set up secure external access to API. If not properly configured, an attacker can gain access to your cameras and even your server.
[API description](https://github.com/AlexxIT/go2rtc/tree/master/api).
**Module config**
@@ -705,6 +824,7 @@ The HTTP API is the main part for interacting with the application. Default addr
- you can enable HTTP API only on localhost with `listen: "127.0.0.1:1984"` setting
- you can change API `base_path` and host go2rtc on your main app webserver suburl
- all files from `static_dir` hosted on root path: `/`
- you can use raw TLS cert/key content or path to files
```yaml
api:
@@ -723,6 +843,7 @@ api:
-----BEGIN PRIVATE KEY-----
...
-----END PRIVATE KEY-----
unix_listen: "/tmp/go2rtc.sock" # default "", unix socket listener for API
```
**PS:**
@@ -753,6 +874,19 @@ By default go2rtc provide RTSP-stream with only one first video and only one fir
Read more about [codecs filters](#codecs-filters).
### Module: RTMP
*[New in v1.8.0](https://github.com/AlexxIT/go2rtc/releases/tag/v1.8.0)*
You can get any stream as RTMP-stream: `rtmp://192.168.1.123/{stream_name}`. Only H264/AAC codecs supported right now.
[Incoming stream](#incoming-sources) in RTMP-format tested only with [OBS Studio](https://obsproject.com/) and Dahua camera. Different FFmpeg versions has differnt problems with this format.
```yaml
rtmp:
listen: ":1935" # by default - disabled!
```
### Module: WebRTC
In most cases [WebRTC](https://en.wikipedia.org/wiki/WebRTC) uses direct peer-to-peer connection from your browser to go2rtc and sends media data via UDP.
@@ -796,7 +930,7 @@ webrtc:
**Private IP**
- setup integration with [Ngrok service](#module-ngrok)
- setup integration with [ngrok service](#module-ngrok)
```yaml
ngrok:
@@ -822,6 +956,8 @@ webrtc:
### Module: HomeKit
*[New in v1.7.0](https://github.com/AlexxIT/go2rtc/releases/tag/v1.7.0)*
HomeKit module can work in two modes:
- export any H264 camera to Apple HomeKit
@@ -874,6 +1010,8 @@ homekit:
### Module: WebTorrent
*[New in v1.3.0](https://github.com/AlexxIT/go2rtc/releases/tag/v1.3.0)*
This module support:
- Share any local stream via [WebTorrent](https://webtorrent.io/) technology
@@ -898,29 +1036,29 @@ Link example: https://alexxit.github.io/go2rtc/#share=02SNtgjKXY&pwd=wznEQqznxW&
TODO: article how it works...
### Module: Ngrok
### Module: ngrok
With Ngrok integration you can get external access to your streams in situation when you have Internet with private IP-address.
With ngrok integration you can get external access to your streams in situations when you have Internet with private IP-address.
- Ngrok preistalled for **Docker** and **Hass Add-on** users
- ngrok is pre-installed for **Docker** and **Hass Add-on** users
- you may need external access for two different things:
- WebRTC stream, so you need tunnel WebRTC TCP port (ex. 8555)
- go2rtc web interface, so you need tunnel API HTTP port (ex. 1984)
- Ngrok support authorization for your web interface
- Ngrok automatically adds HTTPS to your web interface
- ngrok support authorization for your web interface
- ngrok automatically adds HTTPS to your web interface
Ngrok free subscription limitations:
The ngrok free subscription has the following limitations:
- you will always get random external address (not a problem for webrtc stream)
- you can forward multiple ports but use only one Ngrok app
- You can reserve a free domain for serving the web interface, but the TCP address you get will always be random and change with each restart of the ngrok agent (not a problem for webrtc stream)
- You can forward multiple ports from a single agent, but you can only run one ngrok agent on the free plan
go2rtc will automatically get your external TCP address (if you enable it in ngrok config) and use it with WebRTC connection (if you enable it in webrtc config).
You need manually download [Ngrok agent app](https://ngrok.com/download) for your OS and register in [Ngrok service](https://ngrok.com/).
You need to manually download the [ngrok agent app](https://ngrok.com/download) for your OS and register with the [ngrok service](https://ngrok.com/signup).
**Tunnel for only WebRTC Stream**
You need to add your [Ngrok token](https://dashboard.ngrok.com/get-started/your-authtoken) and WebRTC TCP port to YAML:
You need to add your [ngrok authtoken](https://dashboard.ngrok.com/get-started/your-authtoken) and WebRTC TCP port to YAML:
```yaml
ngrok:
@@ -936,7 +1074,7 @@ ngrok:
command: ngrok start --all --config ngrok.yaml
```
Ngrok config example:
ngrok config example:
```yaml
version: "2"
@@ -952,6 +1090,8 @@ tunnels:
proto: tcp
```
See the [ngrok agent documentation](https://ngrok.com/docs/agent/config/) for more details on the ngrok configuration file.
### Module: Hass
The best and easiest way to use go2rtc inside the Home Assistant is to install the custom integration [WebRTC Camera](#go2rtc-home-assistant-integration) and custom lovelace card.
@@ -1015,6 +1155,8 @@ Read more about [codecs filters](#codecs-filters).
### Module: HLS
*[New in v1.1.0](https://github.com/AlexxIT/go2rtc/releases/tag/v1.1.0)*
[HLS](https://en.wikipedia.org/wiki/HTTP_Live_Streaming) is the worst technology for real-time streaming. It can only be useful on devices that do not support more modern technology, like [WebRTC](#module-webrtc), [MSE/MP4](#module-mp4).
The go2rtc implementation differs from the standards and may not work with all players.
@@ -1054,6 +1196,10 @@ API examples:
- You can use `rotate` param with `90`, `180`, `270` or `-90` values
- You can use `hardware`/`hw` param [read more](https://github.com/AlexxIT/go2rtc/wiki/Hardware-acceleration)
**PS.** This module also supports streaming to the server console (terminal) in the **animated ASCII art** format ([read more](https://github.com/AlexxIT/go2rtc/blob/master/internal/mjpeg/README.md)):
[![](https://img.youtube.com/vi/sHj_3h_sX7M/mqdefault.jpg)](https://www.youtube.com/watch?v=sHj_3h_sX7M)
### Module: Log
You can set different log levels for different modules.
@@ -1091,7 +1237,7 @@ webrtc:
- external access to WebRTC TCP port is not a problem, because it used only for transmit encrypted media data
- anyway you need to open this port to your local network and to the Internet in order for WebRTC to work
If you need Web interface protection without Home Assistant Add-on - you need to use reverse proxy, like [Nginx](https://nginx.org/), [Caddy](https://caddyserver.com/), [Ngrok](https://ngrok.com/), etc.
If you need Web interface protection without Home Assistant Add-on - you need to use reverse proxy, like [Nginx](https://nginx.org/), [Caddy](https://caddyserver.com/), [ngrok](https://ngrok.com/), etc.
PS. Additionally WebRTC will try to use the 8555 UDP port for transmit encrypted media. It works without problems on the local network. And sometimes also works for external access, even if you haven't opened this port on your router ([read more](https://en.wikipedia.org/wiki/UDP_hole_punching)). But for stable external WebRTC access, you need to open the 8555 port on your router for both TCP and UDP.
@@ -1122,17 +1268,14 @@ Some examples:
`AVC/H.264` video can be played almost anywhere. But `HEVC/H.265` has a lot of limitations in supporting with different devices and browsers. It's all about patents and money, you can't do anything about it.
| Device | WebRTC | MSE | HTTP | HLS |
|---------------------|-------------------------------|-------------------------------|------------------------------------|------------------------|
| *latency* | best | medium | bad | bad |
| Desktop Chrome 107+ | H264, OPUS, PCMU, PCMA | H264, H265*, AAC, FLAC*, OPUS | H264, H265*, AAC, FLAC*, OPUS, MP3 | no |
| Desktop Edge | H264, OPUS, PCMU, PCMA | H264, H265*, AAC, FLAC*, OPUS | H264, H265*, AAC, FLAC*, OPUS, MP3 | no |
| Android Chrome 107+ | H264, OPUS, PCMU, PCMA | H264, H265*, AAC, FLAC*, OPUS | H264, H265*, AAC, FLAC*, OPUS, MP3 | no |
| Desktop Firefox | H264, OPUS, PCMU, PCMA | H264, AAC, FLAC*, OPUS | H264, AAC, FLAC*, OPUS | no |
| Desktop Safari 14+ | H264, H265*, OPUS, PCMU, PCMA | H264, H265, AAC, FLAC* | **no!** | H264, H265, AAC, FLAC* |
| iPad Safari 14+ | H264, H265*, OPUS, PCMU, PCMA | H264, H265, AAC, FLAC* | **no!** | H264, H265, AAC, FLAC* |
| iPhone Safari 14+ | H264, H265*, OPUS, PCMU, PCMA | **no!** | **no!** | H264, H265, AAC, FLAC* |
| macOS [Hass App][1] | no | no | no | H264, H265, AAC, FLAC* |
| Device | WebRTC | MSE | HTTP* | HLS |
|--------------------------------------------------------------------------|-----------------------------------------|-----------------------------------------|----------------------------------------------|-----------------------------|
| *latency* | best | medium | bad | bad |
| - Desktop Chrome 107+ <br/> - Desktop Edge <br/> - Android Chrome 107+ | H264 <br/> PCMU, PCMA <br/> OPUS | H264, H265* <br/> AAC, FLAC* <br/> OPUS | H264, H265* <br/> AAC, FLAC* <br/> OPUS, MP3 | no |
| Desktop Firefox | H264 <br/> PCMU, PCMA <br/> OPUS | H264 <br/> AAC, FLAC* <br/> OPUS | H264 <br/> AAC, FLAC* <br/> OPUS | no |
| - Desktop Safari 14+ <br/> - iPad Safari 14+ <br/> - iPhone Safari 17.1+ | H264, H265* <br/> PCMU, PCMA <br/> OPUS | H264, H265 <br/> AAC, FLAC* | **no!** | H264, H265 <br/> AAC, FLAC* |
| iPhone Safari 14+ | H264, H265* <br/> PCMU, PCMA <br/> OPUS | **no!** | **no!** | H264, H265 <br/> AAC, FLAC* |
| macOS [Hass App][1] | no | no | no | H264, H265 <br/> AAC, FLAC* |
[1]: https://apps.apple.com/app/home-assistant/id1099568401
@@ -1159,8 +1302,8 @@ Some examples:
- H264 = H.264 = AVC (Advanced Video Coding)
- H265 = H.265 = HEVC (High Efficiency Video Coding)
- PCMU = G.711 PCM (A-law) = PCM A-law (`alaw`)
- PCMA = G.711 PCM (µ-law) = PCM mu-law (`mulaw`)
- PCMA = G.711 PCM (A-law) = PCM A-law (`alaw`)
- PCMU = G.711 PCM (µ-law) = PCM mu-law (`mulaw`)
- PCM = L16 = PCM signed 16-bit big-endian (`s16be`)
- AAC = MPEG4-GENERIC
- MP3 = MPEG-1 Audio Layer III or MPEG-2 Audio Layer III
@@ -1227,14 +1370,22 @@ streams:
- [Frigate 12+](https://frigate.video/) - open source NVR built around real-time AI object detection
- [Frigate Lovelace Card](https://github.com/dermotduffy/frigate-hass-card) - custom card for Home Assistant
- [ring-mqtt](https://github.com/tsightler/ring-mqtt) - Ring devices to MQTT Bridge
- [OpenIPC](https://github.com/OpenIPC/firmware/tree/master/general/package/go2rtc) - Alternative IP Camera firmware from an open community
- [wz_mini_hacks](https://github.com/gtxaspec/wz_mini_hacks) - Custom firmware for Wyze cameras
- [EufyP2PStream](https://github.com/oischinger/eufyp2pstream) - A small project that provides a Video/Audio Stream from Eufy cameras that don't directly support RTSP
- [ioBroker.euSec](https://github.com/bropat/ioBroker.eusec) - [ioBroker](https://www.iobroker.net/) adapter for control Eufy security devices
- [wz_mini_hacks](https://github.com/gtxaspec/wz_mini_hacks) - Custom firmware for Wyze cameras
- [MMM-go2rtc](https://github.com/Anonym-tsk/MMM-go2rtc) - MagicMirror² Module
- [ring-mqtt](https://github.com/tsightler/ring-mqtt) - Ring devices to MQTT Bridge
**Distributions**
- [Alpine Linux](https://pkgs.alpinelinux.org/packages?name=go2rtc)
- [Arch User Repository](https://linux-packages.com/aur/package/go2rtc)
- [Gentoo](https://github.com/inode64/inode64-overlay/tree/main/media-video/go2rtc)
- [NixOS](https://search.nixos.org/packages?query=go2rtc)
- [Proxmox Helper Scripts](https://tteck.github.io/Proxmox/)
- [QNAP](https://www.myqnap.org/product/go2rtc/)
- [Synology NAS](https://synocommunity.com/package/go2rtc)
- [Unraid](https://unraid.net/community/apps?q=go2rtc)
## Cameras experience

View File

@@ -1,4 +1,4 @@
openapi: 3.0.0
openapi: 3.1.0
info:
title: go2rtc
@@ -111,9 +111,18 @@ paths:
required: false
schema: { type: integer }
example: 100
responses: { }
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:
@@ -130,14 +139,18 @@ paths:
requestBody:
content:
"*/*": { example: "streams:..." }
responses: { }
responses:
default:
description: Default response
patch:
summary: Merge changes to main config file
tags: [ Config ]
requestBody:
content:
"*/*": { example: "streams:..." }
responses: { }
responses:
default:
description: Default response
@@ -166,7 +179,9 @@ paths:
required: false
schema: { type: string }
example: camera1
responses: { }
responses:
default:
description: Default response
patch:
summary: Update stream source
tags: [ Streams list ]
@@ -183,7 +198,9 @@ paths:
required: true
schema: { type: string }
example: camera1
responses: { }
responses:
default:
description: Default response
delete:
summary: Delete stream
tags: [ Streams list ]
@@ -194,7 +211,9 @@ paths:
required: true
schema: { type: string }
example: camera1
responses: { }
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)"
@@ -212,7 +231,9 @@ paths:
required: true
schema: { type: string }
example: camera1
responses: { }
responses:
default:
description: Default response
@@ -347,7 +368,9 @@ paths:
tags: [ Produce stream ]
parameters:
- $ref: "#/components/parameters/stream_dst_path"
responses: { }
responses:
default:
description: Default response
/api/stream.flv?dst={dst}:
post:
summary: Post stream in FLV format
@@ -355,7 +378,9 @@ paths:
tags: [ Produce stream ]
parameters:
- $ref: "#/components/parameters/stream_dst_path"
responses: { }
responses:
default:
description: Default response
/api/stream.ts?dst={dst}:
post:
summary: Post stream in MPEG-TS format
@@ -363,7 +388,9 @@ paths:
tags: [ Produce stream ]
parameters:
- $ref: "#/components/parameters/stream_dst_path"
responses: { }
responses:
default:
description: Default response
/api/stream.mjpeg?dst={dst}:
post:
summary: Post stream in MJPEG format
@@ -371,7 +398,9 @@ paths:
tags: [ Produce stream ]
parameters:
- $ref: "#/components/parameters/stream_dst_path"
responses: { }
responses:
default:
description: Default response
@@ -380,49 +409,65 @@ paths:
summary: DVRIP cameras discovery
description: "[Source: DVRIP](https://github.com/AlexxIT/go2rtc#source-dvrip)"
tags: [ Discovery ]
responses: { }
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: { }
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: { }
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: { }
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: { }
responses:
default:
description: Default response
/api/nest:
get:
summary: Nest cameras discovery
tags: [ Discovery ]
responses: { }
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: { }
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: { }
responses:
default:
description: Default response
@@ -431,7 +476,9 @@ paths:
summary: ONVIF server implementation
description: Simple realisation of the ONVIF protocol. Accepts any suburl requests
tags: [ ONVIF ]
responses: { }
responses:
default:
description: Default response
@@ -440,7 +487,9 @@ paths:
summary: RTSPtoWebRTC server implementation
description: Simple API for support [RTSPtoWebRTC](https://www.home-assistant.io/integrations/rtsp_to_webrtc/) integration
tags: [ RTSPtoWebRTC ]
responses: { }
responses:
default:
description: Default response
@@ -465,7 +514,9 @@ paths:
tags: [ WebTorrent ]
parameters:
- $ref: "#/components/parameters/stream_src_path"
responses: { }
responses:
default:
description: Default response
/api/webtorrent:
get:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 202 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

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

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

59
go.mod
View File

@@ -1,44 +1,49 @@
module github.com/AlexxIT/go2rtc
go 1.21
go 1.20
require (
github.com/gorilla/websocket v1.5.0
github.com/miekg/dns v1.1.55
github.com/pion/ice/v2 v2.3.11
github.com/pion/interceptor v0.1.19
github.com/pion/rtcp v1.2.10
github.com/pion/rtp v1.8.1
github.com/pion/sdp/v3 v3.0.6
github.com/pion/srtp/v2 v2.0.17
github.com/asticode/go-astits v1.13.0
github.com/expr-lang/expr v1.16.9
github.com/gorilla/websocket v1.5.3
github.com/mattn/go-isatty v0.0.20
github.com/miekg/dns v1.1.62
github.com/pion/ice/v2 v2.3.36
github.com/pion/interceptor v0.1.37
github.com/pion/rtcp v1.2.14
github.com/pion/rtp v1.8.9
github.com/pion/sdp/v3 v3.0.9
github.com/pion/srtp/v2 v2.0.20
github.com/pion/stun v0.6.1
github.com/pion/webrtc/v3 v3.2.19
github.com/rs/zerolog v1.30.0
github.com/sigurn/crc16 v0.0.0-20211026045750-20ab5afb07e3
github.com/pion/webrtc/v3 v3.3.4
github.com/rs/zerolog v1.33.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.4
github.com/stretchr/testify v1.9.0
github.com/tadglines/go-pkgs v0.0.0-20210623144937-b983b20f54f9
golang.org/x/crypto v0.13.0
golang.org/x/crypto v0.28.0
gopkg.in/yaml.v3 v3.0.1
)
require (
github.com/asticode/go-astikit v0.45.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/google/uuid v1.3.1 // indirect
github.com/kr/pretty v0.2.1 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/pion/datachannel v1.5.5 // indirect
github.com/pion/dtls/v2 v2.2.7 // indirect
github.com/pion/datachannel v1.5.9 // indirect
github.com/pion/dtls/v2 v2.2.12 // indirect
github.com/pion/logging v0.2.2 // indirect
github.com/pion/mdns v0.0.9 // indirect
github.com/pion/mdns v0.0.12 // indirect
github.com/pion/randutil v0.1.0 // indirect
github.com/pion/sctp v1.8.9 // indirect
github.com/pion/transport/v2 v2.2.4 // indirect
github.com/pion/turn/v2 v2.1.3 // indirect
github.com/pion/sctp v1.8.33 // indirect
github.com/pion/transport/v2 v2.2.10 // indirect
github.com/pion/turn/v2 v2.1.6 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
golang.org/x/mod v0.12.0 // indirect
golang.org/x/net v0.15.0 // indirect
golang.org/x/sys v0.12.0 // indirect
golang.org/x/tools v0.13.0 // indirect
github.com/wlynxg/anet v0.0.5 // indirect
golang.org/x/mod v0.18.0 // indirect
golang.org/x/net v0.27.0 // indirect
golang.org/x/sync v0.7.0 // indirect
golang.org/x/sys v0.26.0 // indirect
golang.org/x/tools v0.22.0 // indirect
)

224
go.sum
View File

@@ -1,237 +1,175 @@
github.com/asticode/go-astikit v0.30.0/go.mod h1:h4ly7idim1tNhaVkdVBeXQZEE3L0xblP7fCWbgwipF0=
github.com/asticode/go-astikit v0.45.0 h1:08to/jrbod9tchF2bJ9moW+RTDK7DBUxLdIeSE7v7Sw=
github.com/asticode/go-astikit v0.45.0/go.mod h1:h4ly7idim1tNhaVkdVBeXQZEE3L0xblP7fCWbgwipF0=
github.com/asticode/go-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-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
github.com/expr-lang/expr v1.16.9 h1:WUAzmR0JNI9JCiF0/ewwHB1gmcGw5wW7nWt8gc6PpCI=
github.com/expr-lang/expr v1.16.9/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4=
github.com/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.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4=
github.com/google/uuid v1.3.1/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/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 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/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/miekg/dns v1.1.55 h1:GoQ4hpsj0nFLYe+bWiCToyrBEJXkQfOOIvFGFy0lEgo=
github.com/miekg/dns v1.1.55/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.7 h1:cSUBsETxepsCSFSxC3mc/aDo14qQLMSL+O6IjG28yV8=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ=
github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ=
github.com/pion/datachannel v1.5.9 h1:LpIWAOYPyDrXtU+BW7X0Yt/vGtYxtXQ8ql7dFfYUVZA=
github.com/pion/datachannel v1.5.9/go.mod h1:kDUuk4CU4Uxp82NH4LQZbISULkX/HtzKa4P7ldf9izE=
github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s=
github.com/pion/ice/v2 v2.3.11 h1:rZjVmUwyT55cmN8ySMpL7rsS8KYsJERsrxJLLxpKhdw=
github.com/pion/ice/v2 v2.3.11/go.mod h1:hPcLC3kxMa+JGRzMHqQzjoSj3xtE9F+eoncmXLlCL4E=
github.com/pion/interceptor v0.1.18/go.mod h1:tpvvF4cPM6NGxFA1DUMbhabzQBxdWMATDGEUYOR9x6I=
github.com/pion/interceptor v0.1.19 h1:tq0TGBzuZQqipyBhaC1mVUCfCh8XjDKUuibq9rIl5t4=
github.com/pion/interceptor v0.1.19/go.mod h1:VANhFxdJezB8mwToMMmrmyHyP9gym6xLqIUch31xryg=
github.com/pion/dtls/v2 v2.2.12 h1:KP7H5/c1EiVAAKUmXyCzPiQe5+bCJrpOeKg/L05dunk=
github.com/pion/dtls/v2 v2.2.12/go.mod h1:d9SYc9fch0CqK90mRk1dC7AkzzpwJj6u2GU3u+9pqFE=
github.com/pion/ice/v2 v2.3.36 h1:SopeXiVbbcooUg2EIR8sq4b13RQ8gzrkkldOVg+bBsc=
github.com/pion/ice/v2 v2.3.36/go.mod h1:mBF7lnigdqgtB+YHkaY/Y6s6tsyRyo4u4rPGRuOjUBQ=
github.com/pion/interceptor v0.1.37 h1:aRA8Zpab/wE7/c0O3fh1PqY0AJI3fCSEM5lRWJVorwI=
github.com/pion/interceptor v0.1.37/go.mod h1:JzxbJ4umVTlZAf+/utHzNesY8tmRkM2lVmkS82TTj8Y=
github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
github.com/pion/mdns v0.0.8/go.mod h1:hYE72WX8WDveIhg7fmXgMKivD3Puklk0Ymzog0lSyaI=
github.com/pion/mdns v0.0.9 h1:7Ue5KZsqq8EuqStnpPWV33vYYEH0+skdDN5L7EiEsI4=
github.com/pion/mdns v0.0.9/go.mod h1:2JA5exfxwzXiCihmxpTKgFUpiQws2MnipoPK09vecIc=
github.com/pion/mdns v0.0.12 h1:CiMYlY+O0azojWDmxdNr7ADGrnZ+V6Ilfner+6mSVK8=
github.com/pion/mdns v0.0.12/go.mod h1:VExJjv8to/6Wqm1FXK+Ii/Z9tsVk/F5sD/N70cnYFbk=
github.com/pion/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.8.1 h1:26OxTc6lKg/qLSGir5agLyj0QKaOv8OP5wps2SFnVNQ=
github.com/pion/rtp v1.8.1/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU=
github.com/pion/sctp v1.8.5/go.mod h1:SUFFfDpViyKejTAdwD1d/HQsCu+V/40cCs2nZIvC3s0=
github.com/pion/sctp v1.8.8/go.mod h1:igF9nZBrjh5AtmKc7U30jXltsFHicFCXSmWA2GWRaWs=
github.com/pion/sctp v1.8.9 h1:TP5ZVxV5J7rz7uZmbyvnUvsn7EJ2x/5q9uhsTtXbI3g=
github.com/pion/sctp v1.8.9/go.mod h1:cMLT45jqw3+jiJCrtHVwfQLnfR0MGZ4rgOJwUOIqLkI=
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.17 h1:ECuOk+7uIpY6HUlTb0nXhfvu4REG2hjtC4ronYFCZE4=
github.com/pion/srtp/v2 v2.0.17/go.mod h1:y5WSHcJY4YfNB/5r7ca5YjHeIr1H3LM1rKArGGs8jMc=
github.com/pion/rtcp v1.2.12/go.mod h1:sn6qjxvnwyAkkPzPULIbVqSKI5Dv54Rv7VG0kNxh9L4=
github.com/pion/rtcp v1.2.14 h1:KCkGV3vJ+4DAJmvP0vaQShsb0xkRfWkO540Gy102KyE=
github.com/pion/rtcp v1.2.14/go.mod h1:sn6qjxvnwyAkkPzPULIbVqSKI5Dv54Rv7VG0kNxh9L4=
github.com/pion/rtp v1.8.3/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU=
github.com/pion/rtp v1.8.9 h1:E2HX740TZKaqdcPmf4pw6ZZuG8u5RlMMt+l3dxeu6Wk=
github.com/pion/rtp v1.8.9/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU=
github.com/pion/sctp v1.8.33 h1:dSE4wX6uTJBcNm8+YlMg7lw1wqyKHggsP5uKbdj+NZw=
github.com/pion/sctp v1.8.33/go.mod h1:beTnqSzewI53KWoG3nqB282oDMGrhNxBdb+JZnkCwRM=
github.com/pion/sdp/v3 v3.0.9 h1:pX++dCHoHUwq43kuwf3PyJfHlwIj4hXA7Vrifiq0IJY=
github.com/pion/sdp/v3 v3.0.9/go.mod h1:B5xmvENq5IXJimIO4zfp6LAe1fD9N+kFv+V/1lOdz8M=
github.com/pion/srtp/v2 v2.0.20 h1:HNNny4s+OUmG280ETrCdgFndp4ufx3/uy85EawYEhTk=
github.com/pion/srtp/v2 v2.0.20/go.mod h1:0KJQjA99A6/a0DOVTu1PhDSw0CXF2jTkqOoMg3ODqdA=
github.com/pion/stun v0.6.1 h1:8lp6YejULeHBF8NmV8e2787BogQhduZugh5PdhDyyN4=
github.com/pion/stun v0.6.1/go.mod h1:/hO7APkX4hZKu/D0f2lHzNyvdkTGtIy3NDmLR7kSz/8=
github.com/pion/transport 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.2.1/go.mod h1:cXXWavvCnFF6McHTft3DWS9iic2Mftcz1Aq29pGcU5g=
github.com/pion/transport/v2 v2.2.2/go.mod h1:OJg3ojoBJopjEeECq2yJdXH9YVrUJ1uQ++NjXLOUorc=
github.com/pion/transport/v2 v2.2.3/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0=
github.com/pion/transport/v2 v2.2.4 h1:41JJK6DZQYSeVLxILA2+F4ZkKb4Xd/tFJZRFZQ9QAlo=
github.com/pion/transport/v2 v2.2.4/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0=
github.com/pion/transport/v3 v3.0.1 h1:gDTlPJwROfSfz6QfSi0ZmeCSkFcnWWiiR9ES0ouANiM=
github.com/pion/transport/v2 v2.2.10 h1:ucLBLE8nuxiHfvkFKnkDQRYWYfp8ejf4YBOPfaQpw6Q=
github.com/pion/transport/v2 v2.2.10/go.mod h1:sq1kSLWs+cHW9E+2fJP95QudkzbK7wscs8yYgQToO5E=
github.com/pion/transport/v3 v3.0.1/go.mod h1:UY7kiITrlMv7/IKgd5eTUcaahZx5oUN3l9SzK5f5xE0=
github.com/pion/turn/v2 v2.1.3 h1:pYxTVWG2gpC97opdRc5IGsQ1lJ9O/IlNhkzj7MMrGAA=
github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0=
github.com/pion/turn/v2 v2.1.3/go.mod h1:huEpByKKHix2/b9kmTAM3YoX6MKP+/D//0ClgUYR2fY=
github.com/pion/webrtc/v3 v3.2.19 h1:XNu5e62mkzafw1qYuKtQ+Dviw4JpbzC/SLx3zZt49JY=
github.com/pion/webrtc/v3 v3.2.19/go.mod h1:vVURQTBOG5BpWKOJz3nlr23NfTDeyKVmubRNqzQp+Tg=
github.com/pion/turn/v2 v2.1.6 h1:Xr2niVsiPTB0FPtt+yAWKFUkU1eotQbGgpTIld4x1Gc=
github.com/pion/turn/v2 v2.1.6/go.mod h1:huEpByKKHix2/b9kmTAM3YoX6MKP+/D//0ClgUYR2fY=
github.com/pion/webrtc/v3 v3.3.4 h1:v2heQVnXTSqNRXcaFQVOhIOYkLMxOu1iJG8uy1djvkk=
github.com/pion/webrtc/v3 v3.3.4/go.mod h1:liNa+E1iwyzyXqNUwvoMRNQ10x8h8FOeJKL8RkIbamE=
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/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.30.0 h1:SymVODrcRsaRaSInD9yQtKbtWqwsfoPcRff/oRXLj4c=
github.com/rs/zerolog v1.30.0/go.mod h1:/tk+P47gFdPXq4QYjvCmT5/Gsug2nagsFWBWhAiSi1w=
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/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8=
github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
github.com/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.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tadglines/go-pkgs v0.0.0-20210623144937-b983b20f54f9 h1:aeN+ghOV0b2VCmKKO3gqnDQ8mLbpABZgRR2FVYx4ouI=
github.com/tadglines/go-pkgs v0.0.0-20210623144937-b983b20f54f9/go.mod h1:roo6cZ/uqpwKMuvPG0YmzI5+AmUiMWfjCBZpGXqbTxE=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/wlynxg/anet v0.0.3/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU=
github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-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.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I=
golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc=
golang.org/x/mod v0.12.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/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0=
golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-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-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
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.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ=
golang.org/x/net v0.13.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
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-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-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-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-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=
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.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.9.0/go.mod h1:M6DEAAIenWoTxdKrOltXcmDY3rSplQUkrvaDU5FcQyo=
golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o=
golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.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.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA=
golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c=
golang.org/x/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=
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.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,9 +1,10 @@
# syntax=docker/dockerfile:labs
# 0. Prepare images
# only debian 12 (bookworm) has latest ffmpeg
ARG DEBIAN_VERSION="bookworm-slim"
ARG GO_VERSION="1.21-bookworm"
# only debian 13 (trixie) has latest ffmpeg
# https://packages.debian.org/trixie/ffmpeg
ARG DEBIAN_VERSION="trixie-slim"
ARG GO_VERSION="1.22-bookworm"
ARG NGROK_VERSION="3"
FROM debian:${DEBIAN_VERSION} AS base
@@ -44,13 +45,16 @@ RUN rm -f /etc/apt/apt.conf.d/docker-clean \
# Install ffmpeg, bash (for run.sh), tini (for signal handling),
# and other common tools for the echo source.
# non-free for Intel QSV support (not used by go2rtc, just for tests)
# 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 && \
echo 'deb http://deb.debian.org/debian trixie non-free' > /etc/apt/sources.list.d/debian-non-free.list && \
apt-get -y update && apt-get -y install tini ffmpeg \
python3 curl jq \
intel-media-va-driver-non-free \
libasound2-plugins
mesa-va-drivers \
libasound2-plugins && \
apt-get clean && rm -rf /var/lib/apt/lists/*
COPY --link --from=rootfs / /

View File

@@ -10,6 +10,8 @@ import (
"strconv"
"strings"
"sync"
"syscall"
"time"
"github.com/AlexxIT/go2rtc/internal/app"
"github.com/rs/zerolog"
@@ -18,25 +20,26 @@ import (
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"`
TLSListen string `yaml:"tls_listen"`
TLSCert string `yaml:"tls_cert"`
TLSKey string `yaml:"tls_key"`
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"`
}
// default config
cfg.Mod.Listen = "0.0.0.0:1984"
cfg.Mod.Listen = ":1984"
// load config from YAML
app.LoadConfig(&cfg)
if cfg.Mod.Listen == "" {
if cfg.Mod.Listen == "" && cfg.Mod.UnixListen == "" && cfg.Mod.TLSListen == "" {
return
}
@@ -48,16 +51,8 @@ func Init() {
HandleFunc("api", apiHandler)
HandleFunc("api/config", configHandler)
HandleFunc("api/exit", exitHandler)
// ensure we can listen without errors
var err error
ln, 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
@@ -73,52 +68,78 @@ func Init() {
Handler = middlewareLog(Handler) // 1st
}
go func() {
s := http.Server{}
s.Handler = Handler
if err = s.Serve(ln); err != nil {
log.Fatal().Err(err).Msg("[api] serve")
}
}()
if cfg.Mod.Listen != "" {
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 != "" {
cert, err := tls.X509KeyPair([]byte(cfg.Mod.TLSCert), []byte(cfg.Mod.TLSKey))
if err != nil {
log.Error().Err(err).Caller().Send()
return
}
tlsListener, err := net.Listen("tcp", cfg.Mod.TLSListen)
if err != nil {
log.Fatal().Err(err).Caller().Send()
return
}
log.Info().Str("addr", cfg.Mod.TLSListen).Msg("[api] tls listen")
tlsServer := &http.Server{
Handler: Handler,
TLSConfig: &tls.Config{
Certificates: []tls.Certificate{cert},
},
}
go func() {
if err := tlsServer.ServeTLS(tlsListener, "", ""); err != nil {
log.Fatal().Err(err).Msg("[api] tls serve")
}
}()
go tlsListen("tcp", cfg.Mod.TLSListen, cfg.Mod.TLSCert, cfg.Mod.TLSKey)
}
}
func Port() int {
if ln == nil {
return 0
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")
if network == "tcp" {
Port = ln.Addr().(*net.TCPAddr).Port
}
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")
}
return ln.Addr().(*net.TCPAddr).Port
}
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"
@@ -178,7 +199,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"`)
@@ -195,12 +216,11 @@ 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)
})
}
var ln net.Listener
var mu sync.Mutex
func apiHandler(w http.ResponseWriter, r *http.Request) {
@@ -218,10 +238,48 @@ func exitHandler(w http.ResponseWriter, r *http.Request) {
}
s := r.URL.Query().Get("code")
code, _ := strconv.Atoi(s)
code, err := strconv.Atoi(s)
// 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
}
os.Exit(code)
}
func restartHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
http.Error(w, "", http.StatusBadRequest)
return
}
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"`

View File

@@ -1,6 +1,7 @@
package ws
import (
"encoding/json"
"io"
"net/http"
"net/url"
@@ -11,7 +12,7 @@ import (
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/app"
"github.com/gorilla/websocket"
"github.com/rs/zerolog/log"
"github.com/rs/zerolog"
)
func Init() {
@@ -23,31 +24,29 @@ func Init() {
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"`
Raw []byte `json:"-"`
}
func (m *Message) String() string {
if s, ok := m.Value.(string); ok {
return s
}
return ""
func (m *Message) String() (value string) {
_ = json.Unmarshal(m.Raw, &value)
return
}
func (m *Message) GetString(key string) string {
if v, ok := m.Value.(map[string]any); ok {
if s, ok := v[key].(string); ok {
return s
}
}
return ""
func (m *Message) Unmarshal(v any) error {
return json.Unmarshal(m.Raw, v)
}
type WSHandler func(tr *Transport, msg *Message) error
@@ -79,7 +78,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
@@ -114,8 +113,11 @@ func apiWS(w http.ResponseWriter, r *http.Request) {
})
for {
msg := new(Message)
if err = ws.ReadJSON(msg); err != nil {
var raw struct {
Type string `json:"type"`
Value json.RawMessage `json:"value"`
}
if err = ws.ReadJSON(&raw); err != nil {
if !websocket.IsCloseError(err, websocket.CloseNoStatusReceived) {
log.Trace().Err(err).Caller().Send()
}
@@ -123,7 +125,9 @@ func apiWS(w http.ResponseWriter, r *http.Request) {
break
}
log.Trace().Str("type", msg.Type).Msg("[api.ws] msg")
msg := &Message{Type: raw.Type, Raw: raw.Value}
log.Trace().Str("type", msg.Type).Msg("[api] ws msg")
if handler := wsHandlers[msg.Type]; handler != nil {
go func() {

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

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

View File

@@ -1,161 +1,101 @@
package app
import (
"errors"
"flag"
"fmt"
"io"
"os"
"path/filepath"
"os/exec"
"runtime"
"strings"
"time"
"github.com/AlexxIT/go2rtc/pkg/shell"
"github.com/AlexxIT/go2rtc/pkg/yaml"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"runtime/debug"
)
var Version = "1.7.1"
var UserAgent = "go2rtc/" + Version
var (
Version string
UserAgent string
ConfigPath string
Info = make(map[string]any)
)
var ConfigPath string
var Info = map[string]any{
"version": Version,
}
const usage = `Usage of go2rtc:
-c, --config Path to config file or config string as YAML or JSON, support multiple
-d, --daemon Run in background
-v, --version Print version and exit
`
func Init() {
var confs Config
var config flagConfig
var daemon bool
var version bool
flag.Var(&confs, "config", "go2rtc config (path to file or raw text), support multiple")
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)
migrateStore()
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
}
func PatchConfig(key string, value any, path ...string) error {
if ConfigPath == "" {
return errors.New("config file disabled")
}
// empty config is OK
b, _ := os.ReadFile(ConfigPath)
b, err := yaml.Patch(b, key, value, path...)
if err != nil {
return err
}
return os.WriteFile(ConfigPath, b, 0644)
}
// internal
type Config []string
func (c *Config) String() string {
return strings.Join(*c, " ")
}
func (c *Config) Set(value string) error {
*c = append(*c, value)
return nil
}
var configs [][]byte
// modules log levels
var modules map[string]string

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

@@ -0,0 +1,109 @@
package app
import (
"errors"
"os"
"path/filepath"
"strings"
"github.com/AlexxIT/go2rtc/pkg/shell"
"github.com/AlexxIT/go2rtc/pkg/yaml"
)
func LoadConfig(v any) {
for _, data := range configs {
if err := yaml.Unmarshal(data, v); err != nil {
Logger.Warn().Err(err).Send()
}
}
}
func PatchConfig(key string, value any, path ...string) error {
if ConfigPath == "" {
return errors.New("config file disabled")
}
// empty config is OK
b, _ := os.ReadFile(ConfigPath)
b, err := yaml.Patch(b, key, value, path...)
if err != nil {
return err
}
return os.WriteFile(ConfigPath, b, 0644)
}
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)
}

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

@@ -0,0 +1,168 @@
package app
import (
"io"
"os"
"github.com/mattn/go-isatty"
"github.com/rs/zerolog"
)
var MemoryLog = newBuffer(16)
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 modules["output"] {
case "stderr":
writer = os.Stderr
case "stdout":
writer = os.Stdout
}
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 chunkSize = 1 << 16
type circularBuffer struct {
chunks [][]byte
r, w int
}
func newBuffer(chunks int) *circularBuffer {
b := &circularBuffer{chunks: make([][]byte, 0, chunks)}
// 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)
// check if chunk has size
if len(b.chunks[b.w])+n > chunkSize {
// increase write chunk index
if b.w++; b.w == cap(b.chunks) {
b.w = 0
}
// check overflow
if b.r == b.w {
// increase read chunk index
if b.r++; b.r == cap(b.chunks) {
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...)
return
}
func (b *circularBuffer) WriteTo(w io.Writer) (n int64, err error) {
for i := b.r; ; {
var nn int
if nn, err = w.Write(b.chunks[i]); err != nil {
return
}
n += int64(nn)
if i == b.w {
break
}
if i++; i == cap(b.chunks) {
i = 0
}
}
return
}
func (b *circularBuffer) Reset() {
b.chunks[0] = b.chunks[0][:0]
b.r = 0
b.w = 0
}

View File

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

View File

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

View File

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

View File

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

39
internal/exec/closer.go Normal file
View File

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

View File

@@ -1,12 +1,16 @@
package exec
import (
"bufio"
"crypto/md5"
"encoding/hex"
"errors"
"fmt"
"io"
"net/url"
"os"
"os/exec"
"strings"
"sync"
"time"
@@ -17,6 +21,7 @@ import (
"github.com/AlexxIT/go2rtc/pkg/magic"
pkg "github.com/AlexxIT/go2rtc/pkg/rtsp"
"github.com/AlexxIT/go2rtc/pkg/shell"
"github.com/AlexxIT/go2rtc/pkg/stdin"
"github.com/rs/zerolog"
)
@@ -44,57 +49,92 @@ func Init() {
log = app.GetLogger("exec")
}
func execHandle(url string) (core.Producer, error) {
func execHandle(rawURL string) (core.Producer, 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")
}
sum := md5.Sum([]byte(url))
path = "/" + hex.EncodeToString(sum[:])
args[i] = "rtsp://127.0.0.1:" + rtsp.Port + path
break
// 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(rawURL))
path = "/" + hex.EncodeToString(sum[:])
rawURL = rawURL[:i] + "rtsp://127.0.0.1:" + rtsp.Port + path + rawURL[i+8:]
}
args := shell.QuoteSplit(rawURL[5:]) // remove `exec:`
cmd := exec.Command(args[0], args[1:]...)
if log.Debug().Enabled() {
cmd.Stderr = os.Stderr
cmd.Stderr = &logWriter{
buf: make([]byte, 512),
debug: log.Debug().Enabled(),
}
if query.Get("backchannel") == "1" {
return stdin.NewClient(cmd)
}
cl := &closer{cmd: cmd, query: query}
if path == "" {
return handlePipe(url, cmd)
return handlePipe(rawURL, cmd, cl)
}
return handleRTSP(url, path, cmd)
return handleRTSP(rawURL, cmd, cl, path)
}
func handlePipe(url string, cmd *exec.Cmd) (core.Producer, error) {
r, err := PipeCloser(cmd)
func handlePipe(source string, cmd *exec.Cmd, cl io.Closer) (core.Producer, error) {
stdout, err := cmd.StdoutPipe()
if err != nil {
return nil, err
}
rc := struct {
io.Reader
io.Closer
}{
// add buffer for pipe reader to reduce syscall
bufio.NewReaderSize(stdout, core.BufferSize),
cl,
}
log.Debug().Strs("args", cmd.Args).Msg("[exec] run pipe")
ts := time.Now()
if err = cmd.Start(); err != nil {
return nil, err
}
return magic.Open(r)
prod, err := magic.Open(rc)
if err != nil {
_ = rc.Close()
return nil, fmt.Errorf("exec/pipe: %w\n%s", err, cmd.Stderr)
}
if info, ok := prod.(core.Info); ok {
info.SetProtocol("pipe")
setRemoteInfo(info, source, cmd.Args)
}
log.Debug().Stringer("launch", time.Since(ts)).Msg("[exec] run pipe")
return prod, nil
}
func handleRTSP(url, path string, cmd *exec.Cmd) (core.Producer, error) {
func handleRTSP(source string, cmd *exec.Cmd, cl io.Closer, 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() {
@@ -103,42 +143,97 @@ 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)
done := make(chan error, 1)
go func() {
err := cmd.Wait()
// unblocking write to channel
select {
case chErr <- err:
default:
log.Trace().Str("url", url).Msg("[exec] close")
}
done <- cmd.Wait()
}()
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 <-time.After(time.Minute):
log.Error().Str("source", source).Msg("[exec] timeout")
_ = cl.Close()
return nil, errors.New("exec: timeout")
case <-done:
// limit message size
return nil, fmt.Errorf("exec/rtsp\n%s", cmd.Stderr)
case prod := <-waiter:
log.Debug().Stringer("launch", time.Since(ts)).Msg("[exec] run rtsp")
setRemoteInfo(prod, source, cmd.Args)
prod.OnClose = cl.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,30 +0,0 @@
package exec
import (
"bufio"
"io"
"os/exec"
"github.com/AlexxIT/go2rtc/pkg/core"
)
// 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
}
// add buffer for pipe reader to reduce syscall
return pipeCloser{bufio.NewReaderSize(stdout, core.BufferSize), stdout, cmd}, nil
}
type pipeCloser struct {
io.Reader
io.Closer
cmd *exec.Cmd
}
func (p pipeCloser) Close() error {
return core.Any(p.Closer.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.Run(url[5:])
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,3 +1,5 @@
//go:build darwin || ios
package device
import (

View File

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

View File

@@ -1,3 +1,5 @@
//go:build windows
package device
import (
@@ -45,30 +47,20 @@ func queryToInput(query url.Values) string {
}
if video != "" {
input += ` -i video="` + video + `"`
input += ` -i "video=` + video
if audio != "" {
input += `:audio="` + audio + `"`
input += `:audio=` + audio
}
input += `"`
} else {
input += ` -i audio="` + audio + `"`
input += ` -i "audio=` + audio + `"`
}
return input
}
func deviceInputSuffix(video, audio string) string {
switch {
case video != "" && audio != "":
return `video="` + video + `":audio=` + audio + `"`
case video != "":
return `video="` + video + `"`
case audio != "":
return `audio="` + audio + `"`
}
return ""
}
func initDevices() {
cmd := exec.Command(
Bin, "-hide_banner", "-list_devices", "true", "-f", "dshow", "-i", "",

View File

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

View File

@@ -4,32 +4,55 @@ import (
"net/url"
"strings"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/app"
"github.com/AlexxIT/go2rtc/internal/ffmpeg/device"
"github.com/AlexxIT/go2rtc/internal/ffmpeg/hardware"
"github.com/AlexxIT/go2rtc/internal/ffmpeg/virtual"
"github.com/AlexxIT/go2rtc/internal/rtsp"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/ffmpeg"
"github.com/rs/zerolog"
)
func Init() {
var cfg struct {
Mod map[string]string `yaml:"ffmpeg"`
Log struct {
Level string `yaml:"ffmpeg"`
} `yaml:"log"`
}
cfg.Mod = defaults // will be overriden from yaml
cfg.Log.Level = "error"
app.LoadConfig(&cfg)
if app.GetLogger("exec").GetLevel() >= 0 {
defaults["global"] += " -v error"
log = app.GetLogger("ffmpeg")
// zerolog levels: trace debug info warn error fatal panic disabled
// FFmpeg levels: trace debug verbose info warning error fatal panic quiet
if cfg.Log.Level == "warn" {
cfg.Log.Level = "warning"
}
defaults["global"] += " -v " + cfg.Log.Level
streams.RedirectFunc("ffmpeg", func(url string) (string, error) {
if _, err := Version(); err != nil {
return "", err
}
args := parseArgs(url[7:])
if core.Contains(args.Codecs, "auto") {
return "", nil // force call streams.HandleFunc("ffmpeg")
}
return "exec:" + args.String(), nil
})
streams.HandleFunc("ffmpeg", NewProducer)
api.HandleFunc("api/ffmpeg", apiFFmpeg)
device.Init(defaults["bin"])
hardware.Init(defaults["bin"])
}
@@ -48,61 +71,84 @@ var defaults = map[string]string{
// output
"output": "-user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}",
"output/mjpeg": "-f mjpeg -",
"output/raw": "-f yuv4mpegpipe -",
"output/aac": "-f adts -",
"output/wav": "-f wav -",
// `-preset superfast` - we can't use ultrafast because it doesn't support `-profile main -level 4.1`
// `-tune zerolatency` - for minimal latency
// `-profile high -level 4.1` - most used streaming profile
"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
// https://github.com/pion/webrtc/issues/1514
// https://ffmpeg.org/ffmpeg-resampler.html
// `-async 1` or `-min_comp 0` - force frame_size=960, important for WebRTC audio quality
"opus": "-c:a libopus -application:a lowdelay -frame_duration 20 -min_comp 0",
// `-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/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_encoder -g 50 -bf 0 -profile:v high -level:v 4.1",
"h265/rkmpp": "-c:v hevc_rkmpp_encoder -g 50 -bf 0 -level:v 5.1",
// hardware NVidia on Linux and Windows
// preset=p2 - faster, tune=ll - low latency
"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 high -level:v auto",
"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 != "" {
@@ -127,13 +173,14 @@ 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 query url.Values
if i := strings.IndexByte(s, '#'); i > 0 {
if i := strings.IndexByte(s, '#'); i >= 0 {
query = streams.ParseQuery(s[i+1:])
args.Video = len(query["video"])
args.Audio = len(query["audio"])
@@ -175,11 +222,14 @@ func parseArgs(s string) *ffmpeg.Args {
s += "?video&audio"
}
args.Input = inputTemplate("rtsp", s, query)
} else if strings.HasPrefix(s, "device?") {
var err error
args.Input, err = device.GetInput(s)
if err != nil {
return nil
} 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)
@@ -262,6 +312,12 @@ func parseArgs(s string) *ffmpeg.Args {
}
}
if query["bitrate"] != nil {
// https://trac.ffmpeg.org/wiki/Limiting%20the%20output%20bitrate
b := query["bitrate"][0]
args.AddCodec("-b:v " + b + " -maxrate " + b + " -bufsize " + b)
}
// 4. Process audio codecs
if args.Audio > 0 {
for _, audio := range query["audio"] {
@@ -291,11 +347,27 @@ func parseArgs(s string) *ffmpeg.Args {
args.AddCodec("-an")
}
// transcoding to only mjpeg
if (args.Video == 1 && args.Audio == 0 && query.Get("video") == "mjpeg") ||
// no transcoding from mjpeg input
(args.Video == 0 && args.Audio == 0 && strings.Contains(args.Input, " mjpeg ")) {
args.Output = defaults["output/mjpeg"]
// change otput from RTSP to some other pipe format
switch {
case args.Video == 0 && args.Audio == 0:
// no transcoding from mjpeg input (ffmpeg device with support output as raw MJPEG)
if strings.Contains(args.Input, " mjpeg ") {
args.Output = defaults["output/mjpeg"]
}
case args.Video == 1 && args.Audio == 0:
switch core.Before(query.Get("video"), "/") {
case "mjpeg":
args.Output = defaults["output/mjpeg"]
case "raw":
args.Output = defaults["output/raw"]
}
case args.Video == 0 && args.Audio == 1:
switch core.Before(query.Get("audio"), "/") {
case "aac":
args.Output = defaults["output/aac"]
case "pcma", "pcmu", "pcml":
args.Output = defaults["output/wav"]
}
}
return args

View File

@@ -3,43 +3,67 @@ package ffmpeg
import (
"testing"
"github.com/AlexxIT/go2rtc/pkg/ffmpeg"
"github.com/stretchr/testify/require"
)
func TestParseArgsFile(t *testing.T) {
// [FILE] all tracks will be copied without transcoding codecs
args := parseArgs("/media/bbb.mp4")
require.Equal(t, `ffmpeg -hide_banner -re -i /media/bbb.mp4 -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
// [FILE] video will be transcoded to H264, audio will be skipped
args = parseArgs("/media/bbb.mp4#video=h264")
require.Equal(t, `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 yuvj420p -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
// [FILE] video will be copied, audio will be transcoded to pcmu
args = parseArgs("/media/bbb.mp4#video=copy#audio=pcmu")
require.Equal(t, `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}`, args.String())
// [FILE] video will be transcoded to H265 and rotate 270º, audio will be skipped
args = parseArgs("/media/bbb.mp4#video=h265#rotate=-90")
require.Equal(t, `ffmpeg -hide_banner -re -i /media/bbb.mp4 -c:v libx265 -g 50 -profile:v main -level:v 5.1 -preset:v superfast -tune:v zerolatency -an -vf "transpose=2" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
// [FILE] video will be output for MJPEG to pipe, audio will be skipped
args = parseArgs("/media/bbb.mp4#video=mjpeg")
require.Equal(t, `ffmpeg -hide_banner -re -i /media/bbb.mp4 -c:v mjpeg -an -f mjpeg -`, args.String())
// https://github.com/AlexxIT/go2rtc/issues/509
args = parseArgs("ffmpeg:test.mp4#raw=-ss 00:00:20")
require.Equal(t, `ffmpeg -hide_banner -re -i ffmpeg:test.mp4 -ss 00:00:20 -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
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 -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) {
// [DEVICE] video will be output for MJPEG to pipe, with size 1920x1080
args := parseArgs("device?video=0&video_size=1920x1080")
require.Equal(t, `ffmpeg -hide_banner -f dshow -video_size 1920x1080 -i video="0" -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
require.Equal(t, `ffmpeg -hide_banner -f dshow -video_size 1920x1080 -i "video=0" -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
// [DEVICE] video will be transcoded to H265 with framerate 20, audio will be skipped
args = parseArgs("device?video=0&video_size=1280x720&framerate=20#video=h265#audio=pcma")
require.Equal(t, `ffmpeg -hide_banner -f dshow -video_size 1280x720 -framerate 20 -i video="0" -c:v libx265 -g 50 -profile:v main -level:v 5.1 -preset:v superfast -tune:v zerolatency -c:a pcm_alaw -ar:a 8000 -ac:a 1 -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
//args = parseArgs("device?video=0&video_size=1280x720&framerate=20#video=h265#audio=pcma")
args = parseArgs("device?video=0&framerate=20#video=h265")
require.Equal(t, `ffmpeg -hide_banner -f dshow -framerate 20 -i "video=0" -c:v libx265 -g 50 -profile:v main -level:v 5.1 -preset:v superfast -tune:v zerolatency -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
args = parseArgs("device?video=FaceTime HD Camera&audio=Microphone (High Definition Audio Device)")
require.Equal(t, `ffmpeg -hide_banner -f dshow -i "video=FaceTime HD Camera:audio=Microphone (High Definition Audio Device)" -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
}
func TestParseArgsIpCam(t *testing.T) {
@@ -49,7 +73,7 @@ func TestParseArgsIpCam(t *testing.T) {
// [HTTP-MJPEG] video will be transcoded to H264
args = parseArgs("http://example.com#video=h264")
require.Equal(t, `ffmpeg -hide_banner -fflags nobuffer -flags low_delay -i http://example.com -c:v libx264 -g 50 -profile:v high -level:v 4.1 -preset:v superfast -tune:v zerolatency -pix_fmt:v yuvj420p -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
require.Equal(t, `ffmpeg -hide_banner -fflags nobuffer -flags low_delay -i http://example.com -c:v libx264 -g 50 -profile:v high -level:v 4.1 -preset:v superfast -tune:v zerolatency -pix_fmt:v yuv420p -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
// [HLS] video will be copied, audio will be skipped
args = parseArgs("https://example.com#video=copy")
@@ -83,7 +107,7 @@ func TestParseArgsAudio(t *testing.T) {
// [AUDIO] audio will be transcoded to OPUS, video will be skipped
args = parseArgs("rtsp:///example.com#audio=opus")
require.Equal(t, `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp:///example.com -c:a libopus -ar:a 48000 -ac:a 2 -application:a voip -min_comp 0 -vn -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
require.Equal(t, `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp:///example.com -c:a libopus -application:a lowdelay -min_comp 0 -vn -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
// [AUDIO] audio will be transcoded to PCMU, video will be skipped
args = parseArgs("rtsp:///example.com#audio=pcmu")
@@ -111,28 +135,46 @@ func TestParseArgsAudio(t *testing.T) {
}
func TestParseArgsHwVaapi(t *testing.T) {
// [HTTP-MJPEG] video will be transcoded to H264
args := parseArgs("http:///example.com#video=h264#hardware=vaapi")
require.Equal(t, `ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -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" -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=vaapi")
require.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_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" -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=vaapi")
require.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_flags prefer_tcp -i rtsp://example.com -c:v hevc_vaapi -g 50 -bf 0 -profile:v high -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}`, args.String())
// [FILE] video will be output for MJPEG to pipe, audio will be skipped
args = parseArgs("/media/bbb.mp4#video=mjpeg#hardware=vaapi")
require.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())
// [DEVICE] MJPEG video with size 1920x1080 will be transcoded to H265
args = parseArgs("device?video=0&video_size=1920x1080#video=h265#hardware=vaapi")
require.Equal(t, `ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -f dshow -video_size 1920x1080 -i video="0" -c:v hevc_vaapi -g 50 -bf 0 -profile:v high -level:v 5.1 -sei:v 0 -an -vf "format=vaapi|nv12,hwupload" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
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) {
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())
@@ -150,7 +192,19 @@ func TestParseArgsHwV4l2m2m(t *testing.T) {
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 TestParseArgsHwCuda(t *testing.T) {
func TestParseArgsHwRKMPP(t *testing.T) {
// [HTTP-MJPEG] video will be transcoded to H264
args := parseArgs("http://example.com#video=h264#hardware=rkmpp")
require.Equal(t, `ffmpeg -hide_banner -fflags nobuffer -flags low_delay -i http://example.com -c:v h264_rkmpp_encoder -g 50 -bf 0 -profile:v high -level:v 4.1 -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
args = parseArgs("http://example.com#video=h264#rotate=180#hardware=rkmpp")
require.Equal(t, `ffmpeg -hide_banner -fflags nobuffer -flags low_delay -i http://example.com -c:v h264_rkmpp_encoder -g 50 -bf 0 -profile:v high -level:v 4.1 -an -vf "transpose=1,transpose=1" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
args = parseArgs("http://example.com#video=h264#height=320#hardware=rkmpp")
require.Equal(t, `ffmpeg -hide_banner -fflags nobuffer -flags low_delay -i http://example.com -c:v h264_rkmpp_encoder -g 50 -bf 0 -profile:v high -level:v 4.1 -height 320 -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
}
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())
@@ -168,7 +222,7 @@ func TestParseArgsHwCuda(t *testing.T) {
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) {
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())
@@ -190,7 +244,7 @@ func TestParseArgsHwDxva2(t *testing.T) {
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) {
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())
@@ -207,3 +261,55 @@ func TestParseArgsHwVideotoolbox(t *testing.T) {
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

@@ -7,8 +7,6 @@ import (
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/pkg/ffmpeg"
"github.com/rs/zerolog/log"
)
const (
@@ -18,6 +16,7 @@ const (
EngineCUDA = "cuda" // NVidia on Windows and Linux
EngineDXVA2 = "dxva2" // Intel on Windows
EngineVideoToolbox = "videotoolbox" // macOS
EngineRKMPP = "rkmpp" // Rockchip
)
func Init(bin string) {
@@ -61,6 +60,10 @@ func MakeHardware(args *ffmpeg.Args, engine string, defaults map[string]string)
if !args.HasFilters("drawtext=") {
args.Input = "-hwaccel vaapi -hwaccel_output_format vaapi -hwaccel_flags allow_profile_mismatch " + args.Input
if name == "h264" {
fixPixelFormat(args)
}
for i, filter := range args.Filters {
if strings.HasPrefix(filter, "scale=") {
args.Filters[i] = "scale_vaapi=" + filter[6:]
@@ -121,6 +124,24 @@ 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]
for j, filter := range args.Filters {
if strings.HasPrefix(filter, "scale=") {
args.Filters = append(args.Filters[:j], args.Filters[j+1:]...)
width, height, _ := strings.Cut(filter[6:], ":")
if width != "-1" {
args.Codecs[i] += " -width " + width
}
if height != "-1" {
args.Codecs[i] += " -height " + height
}
break
}
}
}
}
}
@@ -129,7 +150,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
}
@@ -154,3 +174,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,3 +1,5 @@
//go:build darwin || ios
package hardware
import (

View File

@@ -1,3 +1,5 @@
//go:build unix && !darwin && !freebsd && !netbsd && !openbsd && !dragonfly
package hardware
import (
@@ -6,13 +8,17 @@ import (
"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_encoder -f null -"
ProbeRKMPPH265 = "-f lavfi -i testsrc2 -t 1 -c hevc_rkmpp_encoder -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.Source {
if runtime.GOARCH == "arm64" || runtime.GOARCH == "arm" {
@@ -25,6 +31,14 @@ func ProbeAll(bin string) []*api.Source {
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,
},
}
}
@@ -59,10 +73,16 @@ 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
}
}
return EngineSoftware

View File

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

View File

@@ -19,5 +19,5 @@ func TestParseQuery(t *testing.T) {
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 -i - -c:v mjpeg_vaapi -vf "format=vaapi|nv12,hwupload" -f mjpeg -`, args.String())
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())
}

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

@@ -0,0 +1,118 @@
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.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.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)
}

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

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

View File

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

View File

@@ -12,7 +12,6 @@ import (
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/mp4"
"github.com/AlexxIT/go2rtc/pkg/mpegts"
"github.com/AlexxIT/go2rtc/pkg/tcp"
"github.com/rs/zerolog"
)
@@ -63,15 +62,13 @@ func handlerStream(w http.ResponseWriter, r *http.Request) {
medias := mp4.ParseQuery(r.URL.Query())
if medias != nil {
c := mp4.NewConsumer(medias)
c.Type = "HLS/fMP4 consumer"
c.RemoteAddr = tcp.RemoteAddr(r)
c.UserAgent = r.UserAgent()
c.FormatName = "hls/fmp4"
c.WithRequest(r)
cons = c
} else {
c := mpegts.NewConsumer()
c.Type = "HLS/TS consumer"
c.RemoteAddr = tcp.RemoteAddr(r)
c.UserAgent = r.UserAgent()
c.FormatName = "hls/mpegts"
c.WithRequest(r)
cons = c
}

View File

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

View File

@@ -1,6 +1,7 @@
package homekit
import (
"errors"
"io"
"net"
"net/http"
@@ -21,12 +22,11 @@ import (
func Init() {
var cfg struct {
Mod map[string]struct {
Pin string `json:"pin"`
Name string `json:"name"`
DeviceID string `json:"device_id"`
DevicePrivate string `json:"device_private"`
Pairings []string `json:"pairings"`
//Listen string `json:"listen"`
Pin string `yaml:"pin"`
Name string `yaml:"name"`
DeviceID string `yaml:"device_id"`
DevicePrivate string `yaml:"device_private"`
Pairings []string `yaml:"pairings"`
} `yaml:"homekit"`
}
app.LoadConfig(&cfg)
@@ -97,7 +97,7 @@ func Init() {
srv.mdns = &mdns.ServiceEntry{
Name: name,
Port: uint16(api.Port()),
Port: uint16(api.Port),
Info: map[string]string{
hap.TXTConfigNumber: "1",
hap.TXTFeatureFlags: "0",
@@ -121,7 +121,7 @@ func Init() {
api.HandleFunc(hap.PathPairSetup, hapPairSetup)
api.HandleFunc(hap.PathPairVerify, hapPairVerify)
log.Trace().Msgf("[homekit] mnds: %s", entries)
log.Trace().Msgf("[homekit] mdns: %s", entries)
go func() {
if err := mdns.Serve(mdns.ServiceHAP, entries); err != nil {
@@ -133,8 +133,19 @@ func Init() {
var log zerolog.Logger
var servers map[string]*server
func streamHandler(url string) (core.Producer, error) {
return homekit.Dial(url, srtp.Server)
func streamHandler(rawURL string) (core.Producer, error) {
if srtp.Server == nil {
return nil, errors.New("homekit: can't work without SRTP server")
}
rawURL, rawQuery, _ := strings.Cut(rawURL, "#")
client, err := homekit.Dial(rawURL, srtp.Server)
if client != nil && rawQuery != "" {
query := streams.ParseQuery(rawQuery)
client.Bitrate = parseBitrate(query.Get("bitrate"))
}
return client, err
}
func hapPairSetup(w http.ResponseWriter, r *http.Request) {
@@ -195,3 +206,24 @@ func findHomeKitURL(stream *streams.Stream) string {
return ""
}
func parseBitrate(s string) int {
n := len(s)
if n == 0 {
return 0
}
var k int
switch n--; s[n] {
case 'K':
k = 1024
s = s[:n]
case 'M':
k = 1024 * 1024
s = s[:n]
default:
k = 1
}
return k * core.Atoi(s)
}

View File

@@ -198,9 +198,11 @@ func (s *server) AddPair(conn net.Conn, id string, public []byte, permissions by
"client_public": []string{hex.EncodeToString(public)},
"permissions": []string{string('0' + permissions)},
}
s.pairings = append(s.pairings, query.Encode())
s.UpdateStatus()
s.PatchConfig()
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) {

View File

@@ -7,12 +7,13 @@ import (
"net/url"
"strings"
"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/mjpeg"
"github.com/AlexxIT/go2rtc/pkg/multipart"
"github.com/AlexxIT/go2rtc/pkg/mpjpeg"
"github.com/AlexxIT/go2rtc/pkg/tcp"
)
@@ -22,6 +23,8 @@ func Init() {
streams.HandleFunc("httpx", handleHTTP)
streams.HandleFunc("tcp", handleTCP)
api.HandleFunc("api/stream", apiStream)
}
func handleHTTP(rawURL string) (core.Producer, error) {
@@ -42,6 +45,21 @@ func handleHTTP(rawURL string) (core.Producer, error) {
}
}
prod, err := do(req)
if err != nil {
return nil, err
}
if info, ok := prod.(core.Info); ok {
info.SetProtocol("http")
info.SetRemoteAddr(req.URL.Host) // TODO: rewrite to net.Conn
info.SetURL(rawURL)
}
return prod, nil
}
func do(req *http.Request) (core.Producer, error) {
res, err := tcp.Do(req)
if err != nil {
return nil, err
@@ -63,14 +81,12 @@ func handleHTTP(rawURL string) (core.Producer, error) {
}
switch {
case ct == "image/jpeg":
return mjpeg.NewClient(res), nil
case ct == "multipart/x-mixed-replace":
return multipart.Open(res.Body)
case ct == "application/vnd.apple.mpegurl" || ext == "m3u8":
return hls.OpenURL(req.URL, res.Body)
case ct == "image/jpeg":
return image.Open(res)
case ct == "multipart/x-mixed-replace":
return mpjpeg.Open(res.Body)
}
return magic.Open(res.Body)
@@ -89,3 +105,26 @@ func handleTCP(rawURL string) (core.Producer, error) {
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, err := magic.Open(r.Body)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
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

@@ -4,16 +4,10 @@ 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", func(source string) (core.Producer, error) {
return ivideon.Dial(source)
})
}

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

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -6,8 +6,6 @@ import (
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/mpegts"
"github.com/AlexxIT/go2rtc/pkg/tcp"
"github.com/rs/zerolog/log"
)
func Init() {
@@ -32,11 +30,9 @@ func outputMpegTS(w http.ResponseWriter, r *http.Request) {
}
cons := mpegts.NewConsumer()
cons.RemoteAddr = tcp.RemoteAddr(r)
cons.UserAgent = r.UserAgent()
cons.WithRequest(r)
if err := stream.AddConsumer(cons); err != nil {
log.Error().Err(err).Caller().Send()
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
@@ -56,19 +52,17 @@ func inputMpegTS(w http.ResponseWriter, r *http.Request) {
return
}
res := &http.Response{Body: r.Body, Request: r}
client, err := mpegts.Open(res.Body)
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.Start(); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
stream.RemoveProducer(client)
}

View File

@@ -10,19 +10,13 @@ import (
)
func Init() {
streams.HandleFunc("nest", streamNest)
streams.HandleFunc("nest", func(source string) (core.Producer, error) {
return nest.Dial(source)
})
api.HandleFunc("api/nest", apiNest)
}
func streamNest(url string) (core.Producer, error) {
client, err := nest.NewClient(url)
if err != nil {
return nil, err
}
return client, nil
}
func apiNest(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query()
cliendID := query.Get("client_id")

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

View File

@@ -11,22 +11,13 @@ import (
)
func Init() {
streams.HandleFunc("roborock", handle)
streams.HandleFunc("roborock", func(source string) (core.Producer, error) {
return roborock.Dial(source)
})
api.HandleFunc("api/roborock", apiHandle)
}
func handle(url string) (core.Producer, error) {
conn := roborock.NewClient(url)
if err := conn.Dial(); err != nil {
return nil, err
}
if err := conn.Connect(); err != nil {
return nil, err
}
return conn, nil
}
var Auth struct {
UserData *roborock.UserInfo `json:"user_data"`
BaseURL string `json:"base_url"`

View File

@@ -1,39 +1,181 @@
package rtmp
import (
"errors"
"io"
"net"
"net/http"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/app"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/flv"
"github.com/AlexxIT/go2rtc/pkg/rtmp"
"github.com/rs/zerolog/log"
"github.com/rs/zerolog"
)
func Init() {
var conf struct {
Mod struct {
Listen string `yaml:"listen" json:"listen"`
} `yaml:"rtmp"`
}
app.LoadConfig(&conf)
log = app.GetLogger("rtmp")
streams.HandleFunc("rtmp", streamsHandle)
streams.HandleFunc("rtmps", streamsHandle)
streams.HandleFunc("rtmpx", streamsHandle)
api.HandleFunc("api/stream.flv", apiHandle)
streams.HandleConsumerFunc("rtmp", streamsConsumerHandle)
streams.HandleConsumerFunc("rtmps", streamsConsumerHandle)
streams.HandleConsumerFunc("rtmpx", streamsConsumerHandle)
address := conf.Mod.Listen
if address == "" {
return
}
ln, err := net.Listen("tcp", address)
if err != nil {
log.Error().Err(err).Caller().Send()
return
}
log.Info().Str("addr", address).Msg("[rtmp] listen")
go func() {
for {
conn, err := ln.Accept()
if err != nil {
return
}
go func() {
if err = tcpHandle(conn); err != nil {
log.Error().Err(err).Caller().Send()
}
}()
}
}()
}
func streamsHandle(url string) (core.Producer, error) {
client, err := rtmp.Dial(url)
func tcpHandle(netConn net.Conn) error {
rtmpConn, err := rtmp.NewServer(netConn)
if err != nil {
return nil, err
return err
}
return client, nil
if err = rtmpConn.ReadCommands(); err != nil {
return err
}
switch rtmpConn.Intent {
case rtmp.CommandPlay:
stream := streams.Get(rtmpConn.App)
if stream == nil {
return errors.New("stream not found: " + rtmpConn.App)
}
cons := flv.NewConsumer()
if err = stream.AddConsumer(cons); err != nil {
return err
}
defer stream.RemoveConsumer(cons)
if err = rtmpConn.WriteStart(); err != nil {
return err
}
_, _ = cons.WriteTo(rtmpConn)
return nil
case rtmp.CommandPublish:
stream := streams.Get(rtmpConn.App)
if stream == nil {
return errors.New("stream not found: " + rtmpConn.App)
}
if err = rtmpConn.WriteStart(); err != nil {
return err
}
prod, err := rtmpConn.Producer()
if err != nil {
return err
}
stream.AddProducer(prod)
defer stream.RemoveProducer(prod)
_ = prod.Start()
return nil
}
return errors.New("rtmp: unknown command: " + rtmpConn.Intent)
}
var log zerolog.Logger
func streamsHandle(url string) (core.Producer, error) {
return rtmp.DialPlay(url)
}
func streamsConsumerHandle(url string) (core.Consumer, func(), error) {
cons := flv.NewConsumer()
run := func() {
wr, err := rtmp.DialPublish(url, cons)
if err != nil {
return
}
_, err = cons.WriteTo(wr)
}
return cons, run, nil
}
func apiHandle(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
http.Error(w, "", http.StatusMethodNotAllowed)
outputFLV(w, r)
} else {
inputFLV(w, r)
}
}
func outputFLV(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 := flv.NewConsumer()
cons.WithRequest(r)
if err := stream.AddConsumer(cons); err != nil {
log.Error().Err(err).Caller().Send()
return
}
h := w.Header()
h.Set("Content-Type", "video/x-flv")
_, _ = cons.WriteTo(w)
stream.RemoveConsumer(cons)
}
func inputFLV(w http.ResponseWriter, r *http.Request) {
dst := r.URL.Query().Get("dst")
stream := streams.Get(dst)
if stream == nil {

View File

@@ -21,12 +21,12 @@ func Init() {
Username string `yaml:"username" json:"-"`
Password string `yaml:"password" json:"-"`
DefaultQuery string `yaml:"default_query" json:"default_query"`
PacketSize uint16 `yaml:"pkt_size"`
PacketSize uint16 `yaml:"pkt_size" json:"pkt_size,omitempty"`
} `yaml:"rtsp"`
}
// default config
conf.Mod.Listen = "0.0.0.0:8554"
conf.Mod.Listen = ":8554"
conf.Mod.DefaultQuery = "video&audio"
app.LoadConfig(&conf)
@@ -210,6 +210,11 @@ func tcpHandler(conn *rtsp.Conn) {
return
}
query := conn.URL.Query()
if s := query.Get("timeout"); s != "" {
conn.Timeout = core.Atoi(s)
}
log.Debug().Str("stream", name).Msg("[rtsp] new producer")
stream.AddProducer(conn)
@@ -239,7 +244,7 @@ func tcpHandler(conn *rtsp.Conn) {
if closer != nil {
if err := conn.Handle(); err != nil {
log.Debug().Msgf("[rtsp] handle=%s", err)
log.Debug().Err(err).Msg("[rtsp] handle")
}
closer()

View File

@@ -13,7 +13,7 @@ func Init() {
}
// default config
cfg.Mod.Listen = "0.0.0.0:8443"
cfg.Mod.Listen = ":8443"
// load config from YAML
app.LoadConfig(&cfg)

View File

@@ -0,0 +1,8 @@
## Testing notes
```yaml
streams:
test1-basic: ffmpeg:virtual?video#video=h264
test2-reconnect: ffmpeg:virtual?video&duration=10#video=h264
test3-execkill: exec:./examples/rtsp_client/rtsp_client/rtsp_client {output}
```

View File

@@ -3,18 +3,17 @@ package streams
import (
"errors"
"strings"
"sync/atomic"
"github.com/AlexxIT/go2rtc/pkg/core"
)
func (s *Stream) AddConsumer(cons core.Consumer) (err error) {
// support for multiple simultaneous requests from different consumers
consN := atomic.AddInt32(&s.requests, 1) - 1
// support for multiple simultaneous pending from different consumers
consN := s.pending.Add(1) - 1
var prodErrors []error
var prodErrors = make([]error, len(s.producers))
var prodMedias []*core.Media
var prods []*Producer // matched producers for consumer
var prodStarts []*Producer
// Step 1. Get consumer medias
consMedias := cons.GetMedias()
@@ -23,15 +22,20 @@ func (s *Stream) AddConsumer(cons core.Consumer) (err error) {
producers:
for prodN, prod := range s.producers {
if prodErrors[prodN] != nil {
log.Trace().Msgf("[streams] skip cons=%d prod=%d", consN, prodN)
continue
}
if err = prod.Dial(); err != nil {
log.Trace().Err(err).Msgf("[streams] skip prod=%s", prod.url)
prodErrors = append(prodErrors, err)
log.Trace().Err(err).Msgf("[streams] dial cons=%d prod=%d", consN, prodN)
prodErrors[prodN] = err
continue
}
// Step 2. Get producer medias (not tracks yet)
for _, prodMedia := range prod.GetMedias() {
log.Trace().Msgf("[streams] check prod=%d media=%s", prodN, prodMedia)
log.Trace().Msgf("[streams] check cons=%d prod=%d media=%s", consN, prodN, prodMedia)
prodMedias = append(prodMedias, prodMedia)
// Step 3. Match consumer/producer codecs list
@@ -44,11 +48,12 @@ func (s *Stream) AddConsumer(cons core.Consumer) (err error) {
switch prodMedia.Direction {
case core.DirectionRecvonly:
log.Trace().Msgf("[streams] match prod=%d => cons=%d", prodN, consN)
log.Trace().Msgf("[streams] match cons=%d <= prod=%d", consN, prodN)
// Step 4. Get recvonly track from producer
if track, err = prod.GetTrack(prodMedia, prodCodec); err != nil {
log.Info().Err(err).Msg("[streams] can't get track")
prodErrors[prodN] = err
continue
}
// Step 5. Add track to consumer
@@ -68,11 +73,12 @@ func (s *Stream) AddConsumer(cons core.Consumer) (err error) {
// Step 5. Add track to producer
if err = prod.AddTrack(prodMedia, prodCodec, track); err != nil {
log.Info().Err(err).Msg("[streams] can't add track")
prodErrors[prodN] = err
continue
}
}
prods = append(prods, prod)
prodStarts = append(prodStarts, prod)
if !consMedia.MatchAll() {
break producers
@@ -82,11 +88,11 @@ func (s *Stream) AddConsumer(cons core.Consumer) (err error) {
}
// stop producers if they don't have readers
if atomic.AddInt32(&s.requests, -1) == 0 {
if s.pending.Add(-1) == 0 {
s.stopProducers()
}
if len(prods) == 0 {
if len(prodStarts) == 0 {
return formatError(consMedias, prodMedias, prodErrors)
}
@@ -95,7 +101,7 @@ func (s *Stream) AddConsumer(cons core.Consumer) (err error) {
s.mu.Unlock()
// there may be duplicates, but that's not a problem
for _, prod := range prods {
for _, prod := range prodStarts {
prod.start()
}
@@ -103,6 +109,20 @@ func (s *Stream) AddConsumer(cons core.Consumer) (err error) {
}
func formatError(consMedias, prodMedias []*core.Media, prodErrors []error) error {
// 1. Return errors if any not nil
var text string
for _, err := range prodErrors {
if err != nil {
text = appendString(text, err.Error())
}
}
if len(text) != 0 {
return errors.New("streams: " + text)
}
// 2. Return "codecs not matched"
if prodMedias != nil {
var prod, cons string
@@ -125,16 +145,7 @@ func formatError(consMedias, prodMedias []*core.Media, prodErrors []error) error
return errors.New("streams: codecs not matched: " + prod + " => " + cons)
}
if prodErrors != nil {
var text string
for _, err := range prodErrors {
text = appendString(text, err.Error())
}
return errors.New("streams: " + text)
}
// 3. Return unknown error
return errors.New("streams: unknown error")
}

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

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

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

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

View File

@@ -7,7 +7,7 @@ import (
"github.com/AlexxIT/go2rtc/pkg/core"
)
type Handler func(url string) (core.Producer, error)
type Handler func(source string) (core.Producer, error)
var handlers = map[string]Handler{}
@@ -73,3 +73,25 @@ func Location(url string) (string, error) {
return "", nil
}
// TODO: rework
type ConsumerHandler func(url string) (core.Consumer, func(), error)
var consumerHandlers = map[string]ConsumerHandler{}
func HandleConsumerFunc(scheme string, handler ConsumerHandler) {
consumerHandlers[scheme] = handler
}
func GetConsumer(url string) (core.Consumer, func(), error) {
if i := strings.IndexByte(url, ':'); i > 0 {
scheme := url[:i]
if handler, ok := consumerHandlers[scheme]; ok {
return handler(url)
}
}
return nil, nil, errors.New("streams: unsupported scheme: " + url)
}

View File

@@ -6,6 +6,9 @@ import (
)
func ParseQuery(s string) url.Values {
if len(s) == 0 {
return nil
}
params := url.Values{}
for _, key := range strings.Split(s, "#") {
var value string

View File

@@ -2,6 +2,8 @@ package streams
import (
"errors"
"time"
"github.com/AlexxIT/go2rtc/pkg/core"
)
@@ -80,18 +82,20 @@ func (s *Stream) Play(source string) error {
s.AddInternalProducer(src)
s.AddInternalConsumer(cons)
go func() {
_ = src.Start()
_ = dst.Stop()
s.RemoveProducer(src)
}()
go func() {
_ = dst.Start()
_ = src.Stop()
s.RemoveInternalConsumer(cons)
}()
go func() {
_ = src.Start()
// little timeout before stop dst, so the buffer can be transferred
time.Sleep(time.Second)
_ = dst.Stop()
s.RemoveProducer(src)
}()
return nil
}

View File

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

View File

@@ -0,0 +1,38 @@
package streams
import "time"
func (s *Stream) Publish(url string) error {
cons, run, err := GetConsumer(url)
if err != nil {
return err
}
if err = s.AddConsumer(cons); err != nil {
return err
}
go func() {
run()
s.RemoveConsumer(cons)
// TODO: more smart retry
time.Sleep(5 * time.Second)
_ = s.Publish(url)
}()
return nil
}
func Publish(stream *Stream, destination any) {
switch v := destination.(type) {
case string:
if err := stream.Publish(v); err != nil {
log.Error().Err(err).Caller().Send()
}
case []any:
for _, v := range v {
Publish(stream, v)
}
}
}

View File

@@ -3,6 +3,7 @@ package streams
import (
"encoding/json"
"sync"
"sync/atomic"
"github.com/AlexxIT/go2rtc/pkg/core"
)
@@ -11,7 +12,7 @@ type Stream struct {
producers []*Producer
consumers []core.Consumer
mu sync.Mutex
requests int32
pending atomic.Int32
}
func NewStream(source any) *Stream {
@@ -20,10 +21,21 @@ func NewStream(source any) *Stream {
return &Stream{
producers: []*Producer{NewProducer(source)},
}
case []string:
s := new(Stream)
for _, str := range source {
s.producers = append(s.producers, NewProducer(str))
}
return s
case []any:
s := new(Stream)
for _, source := range source {
s.producers = append(s.producers, NewProducer(source.(string)))
for _, src := range source {
str, ok := src.(string)
if !ok {
log.Error().Msgf("[stream] NewStream: Expected string, got %v", src)
continue
}
s.producers = append(s.producers, NewProducer(str))
}
return s
case map[string]any:
@@ -82,6 +94,11 @@ func (s *Stream) RemoveProducer(prod core.Producer) {
}
func (s *Stream) stopProducers() {
if s.pending.Load() > 0 {
log.Trace().Msg("[streams] skip stop pending producer")
return
}
s.mu.Lock()
producers:
for _, producer := range s.producers {
@@ -101,19 +118,12 @@ producers:
}
func (s *Stream) MarshalJSON() ([]byte, error) {
if !s.mu.TryLock() {
log.Warn().Msgf("[streams] json locked")
return json.Marshal(nil)
}
var info struct {
var info = struct {
Producers []*Producer `json:"producers"`
Consumers []core.Consumer `json:"consumers"`
}{
Producers: s.producers,
Consumers: s.consumers,
}
info.Producers = s.producers
info.Consumers = s.consumers
s.mu.Unlock()
return json.Marshal(info)
}

View File

@@ -10,7 +10,7 @@ import (
func TestRecursion(t *testing.T) {
// create stream with some source
stream1 := New("from_yaml", "does not matter")
stream1 := New("from_yaml", "does_not_matter")
require.Len(t, streams, 1)
// ask another unnamed stream that links go2rtc

View File

@@ -1,10 +1,11 @@
package streams
import (
"net/http"
"errors"
"net/url"
"regexp"
"sync"
"time"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/app"
@@ -13,18 +14,32 @@ import (
func Init() {
var cfg struct {
Mod map[string]any `yaml:"streams"`
Streams map[string]any `yaml:"streams"`
Publish map[string]any `yaml:"publish"`
}
app.LoadConfig(&cfg)
log = app.GetLogger("streams")
for name, item := range cfg.Mod {
for name, item := range cfg.Streams {
streams[name] = NewStream(item)
}
api.HandleFunc("api/streams", streamsHandler)
api.HandleFunc("api/streams", apiStreams)
api.HandleFunc("api/streams.dot", apiStreamsDOT)
if cfg.Publish == nil {
return
}
time.AfterFunc(time.Second, func() {
for name, dst := range cfg.Publish {
if stream := Get(name); stream != nil {
Publish(stream, dst)
}
}
})
}
func Get(name string) *Stream {
@@ -33,14 +48,26 @@ func Get(name string) *Stream {
var sanitize = regexp.MustCompile(`\s`)
func New(name string, source string) *Stream {
// not allow creating dynamic streams with spaces in the source
// Validate - not allow creating dynamic streams with spaces in the source
func Validate(source string) error {
if sanitize.MatchString(source) {
return nil
return errors.New("streams: invalid dynamic source")
}
return nil
}
func New(name string, sources ...string) *Stream {
for _, source := range sources {
if Validate(source) != nil {
return nil
}
}
stream := NewStream(source)
stream := NewStream(sources)
streamsMu.Lock()
streams[name] = stream
streamsMu.Unlock()
return stream
}
@@ -121,73 +148,6 @@ func Delete(id string) {
delete(streams, id)
}
func streamsHandler(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query()
src := query.Get("src")
// without source - return all streams list
if src == "" && r.Method != "POST" {
api.ResponseJSON(w, streams)
return
}
// Not sure about all this API. Should be rewrited...
switch r.Method {
case "GET":
api.ResponsePrettyJSON(w, streams[src])
case "PUT":
name := query.Get("name")
if name == "" {
name = src
}
if New(name, src) == nil {
http.Error(w, "", http.StatusBadRequest)
return
}
if err := app.PatchConfig(name, src, "streams"); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
}
case "PATCH":
name := query.Get("name")
if name == "" {
http.Error(w, "", http.StatusBadRequest)
return
}
// support {input} templates: https://github.com/AlexxIT/go2rtc#module-hass
if Patch(name, src) == nil {
http.Error(w, "", http.StatusBadRequest)
}
case "POST":
// with dst - redirect source to dst
if dst := query.Get("dst"); dst != "" {
if stream := Get(dst); stream != nil {
if err := stream.Play(src); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
} else {
api.ResponseJSON(w, stream)
}
} else {
http.Error(w, "", http.StatusNotFound)
}
} else {
http.Error(w, "", http.StatusBadRequest)
}
case "DELETE":
delete(streams, src)
if err := app.PatchConfig(src, nil, "streams"); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
}
}
}
var log zerolog.Logger
var streams = map[string]*Stream{}
var streamsMu sync.Mutex

View File

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

View File

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

View File

@@ -1,59 +1,102 @@
package webrtc
import (
"github.com/AlexxIT/go2rtc/internal/api/ws"
"github.com/AlexxIT/go2rtc/pkg/webrtc"
"github.com/pion/sdp/v3"
"strconv"
"net"
"strings"
"github.com/AlexxIT/go2rtc/internal/api/ws"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/webrtc"
pion "github.com/pion/webrtc/v3"
)
type Address struct {
Host string
Port int
host string
Port string
Network string
Priority uint32
}
var addresses []Address
func AddCandidate(address string) {
var port int
// try to get port from address string
if i := strings.LastIndexByte(address, ':'); i > 0 {
if v, _ := strconv.Atoi(address[i+1:]); v != 0 {
address = address[:i]
port = v
func (a *Address) Host() string {
if a.host == "stun" {
ip, err := webrtc.GetCachedPublicIP()
if err != nil {
return ""
}
return ip.String()
}
return a.host
}
func (a *Address) Marshal() string {
if host := a.Host(); host != "" {
return webrtc.CandidateICE(a.Network, host, a.Port, a.Priority)
}
return ""
}
var addresses []*Address
var filters webrtc.Filters
func AddCandidate(network, address string) {
if network == "" {
AddCandidate("tcp", address)
AddCandidate("udp", address)
return
}
// use default WebRTC port
if port == 0 {
port, _ = strconv.Atoi(Port)
host, port, err := net.SplitHostPort(address)
if err != nil {
return
}
addresses = append(addresses, Address{Host: address, Port: port})
// start from 1, so manual candidates will be lower than built-in
// and every next candidate will have a lower priority
candidateIndex := 1 + len(addresses)
priority := webrtc.CandidateHostPriority(network, candidateIndex)
addresses = append(addresses, &Address{host, port, network, priority})
}
func GetCandidates() (candidates []string) {
for _, address := range addresses {
// using stun server for receive public IP-address
if address.Host == "stun" {
ip, err := webrtc.GetCachedPublicIP()
if err != nil {
continue
}
// this is a copy, original host unchanged
address.Host = ip.String()
if candidate := address.Marshal(); candidate != "" {
candidates = append(candidates, candidate)
}
}
return
}
candidates = append(
candidates,
webrtc.CandidateManualHostUDP(address.Host, address.Port),
webrtc.CandidateManualHostTCPPassive(address.Host, address.Port),
)
// FilterCandidate return true if candidate passed the check
func FilterCandidate(candidate *pion.ICECandidate) bool {
if candidate == nil {
return false
}
return
// host candidate should be in the hosts list
if candidate.Typ == pion.ICECandidateTypeHost && filters.Candidates != nil {
if !core.Contains(filters.Candidates, candidate.Address) {
return false
}
}
if filters.Networks != nil {
networkType := NetworkType(candidate.Protocol.String(), candidate.Address)
if !core.Contains(filters.Networks, networkType) {
return false
}
}
return true
}
// NetworkType convert tcp/udp network to tcp4/tcp6/udp4/udp6
func NetworkType(network, host string) string {
if strings.IndexByte(host, ':') >= 0 {
return network + "6"
} else {
return network + "4"
}
}
func asyncCandidates(tr *ws.Transport, cons *webrtc.Conn) {
@@ -78,30 +121,6 @@ func asyncCandidates(tr *ws.Transport, cons *webrtc.Conn) {
}
}
func syncCanditates(answer string) (string, error) {
if len(addresses) == 0 {
return answer, nil
}
sd := &sdp.SessionDescription{}
if err := sd.Unmarshal([]byte(answer)); err != nil {
return "", err
}
md := sd.MediaDescriptions[0]
for _, candidate := range GetCandidates() {
md.WithPropertyAttribute(candidate)
}
data, err := sd.Marshal()
if err != nil {
return "", err
}
return string(data), nil
}
func candidateHandler(tr *ws.Transport, msg *ws.Message) error {
// process incoming candidate in sync function
tr.WithContext(func(ctx map[any]any) {

View File

@@ -7,6 +7,7 @@ import (
"net/http"
"net/url"
"strings"
"sync"
"time"
"github.com/AlexxIT/go2rtc/internal/api/ws"
@@ -40,7 +41,7 @@ func streamsHandler(rawURL string) (core.Producer, error) {
// https://aws.amazon.com/kinesis/video-streams/
// https://docs.aws.amazon.com/kinesisvideostreams-webrtc-dg/latest/devguide/what-is-kvswebrtc.html
// https://github.com/orgs/awslabs/repositories?q=kinesis+webrtc
return kinesisClient(rawURL, query, "WebRTC/Kinesis")
return kinesisClient(rawURL, query, "webrtc/kinesis")
} else if format == "openipc" {
return openIPCClient(rawURL, query)
} else {
@@ -48,7 +49,9 @@ func streamsHandler(rawURL string) (core.Producer, error) {
}
case "http", "https":
if format == "wyze" {
if format == "milestone" {
return milestoneClient(rawURL, query)
} else if format == "wyze" {
// https://github.com/mrlt8/docker-wyze-bridge
return wyzeClient(rawURL)
} else {
@@ -74,22 +77,31 @@ func go2rtcClient(url string) (core.Producer, error) {
// 2. Create PeerConnection
pc, err := PeerConnection(true)
if err != nil {
log.Error().Err(err).Caller().Send()
return nil, err
}
defer func() {
if err != nil {
_ = pc.Close()
}
}()
// waiter will wait PC error or WS error or nil (connection OK)
var connState core.Waiter
var connMu sync.Mutex
prod := webrtc.NewConn(pc)
prod.Desc = "WebRTC/WebSocket async"
prod.Mode = core.ModeActiveProducer
prod.Protocol = "ws"
prod.URL = url
prod.Listen(func(msg any) {
switch msg := msg.(type) {
case *pion.ICECandidate:
s := msg.ToJSON().Candidate
log.Trace().Str("candidate", s).Msg("[webrtc] local")
log.Trace().Str("candidate", s).Msg("[webrtc] local ")
connMu.Lock()
_ = conn.WriteJSON(&ws.Message{Type: "webrtc/candidate", Value: s})
connMu.Unlock()
case pion.PeerConnectionState:
switch msg {
@@ -116,9 +128,9 @@ func go2rtcClient(url string) (core.Producer, error) {
// 4. Send offer
msg := &ws.Message{Type: "webrtc/offer", Value: offer}
if err = conn.WriteJSON(msg); err != nil {
return nil, err
}
connMu.Lock()
_ = conn.WriteJSON(msg)
connMu.Unlock()
// 5. Get answer
if err = conn.ReadJSON(msg); err != nil {
@@ -126,7 +138,8 @@ func go2rtcClient(url string) (core.Producer, error) {
}
if msg.Type != "webrtc/answer" {
return nil, errors.New("wrong answer: " + msg.Type)
err = errors.New("wrong answer: " + msg.String())
return nil, err
}
answer := msg.String()
@@ -174,8 +187,9 @@ func whepClient(url string) (core.Producer, error) {
}
prod := webrtc.NewConn(pc)
prod.Desc = "WebRTC/WHEP sync"
prod.Mode = core.ModeActiveProducer
prod.Protocol = "http"
prod.URL = url
medias := []*core.Media{
{Kind: core.KindVideo, Direction: core.DirectionRecvonly},
@@ -189,10 +203,10 @@ func whepClient(url string) (core.Producer, error) {
}
req, err := http.NewRequest("POST", url, strings.NewReader(offer))
req.Header.Set("Content-Type", MimeSDP)
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", MimeSDP)
client := http.Client{Timeout: time.Second * 5000}
defer client.CloseIdleConnections()

View File

@@ -34,7 +34,7 @@ func (k kinesisResponse) String() string {
return fmt.Sprintf("type=%s, payload=%s", k.Type, k.Payload)
}
func kinesisClient(rawURL string, query url.Values, desc string) (core.Producer, error) {
func kinesisClient(rawURL string, query url.Values, format string) (core.Producer, error) {
// 1. Connect to signalign server
conn, _, err := websocket.DefaultDialer.Dial(rawURL, nil)
if err != nil {
@@ -55,7 +55,7 @@ func kinesisClient(rawURL string, query url.Values, desc string) (core.Producer,
defer conn.Close()
// 3. Create Peer Connection
api, err := webrtc.NewAPI("")
api, err := webrtc.NewAPI()
if err != nil {
return nil, err
}
@@ -79,8 +79,10 @@ func kinesisClient(rawURL string, query url.Values, desc string) (core.Producer,
}
prod := webrtc.NewConn(pc)
prod.Desc = desc
prod.FormatName = format
prod.Mode = core.ModeActiveProducer
prod.Protocol = "ws"
prod.URL = rawURL
prod.Listen(func(msg any) {
switch msg := msg.(type) {
case *pion.ICECandidate:
@@ -216,5 +218,5 @@ func wyzeClient(rawURL string) (core.Producer, error) {
"ice_servers": []string{string(kvs.Servers)},
}
return kinesisClient(kvs.URL, query, "WebRTC/Wyze")
return kinesisClient(kvs.URL, query, "webrtc/wyze")
}

View File

@@ -0,0 +1,220 @@
package webrtc
import (
"bytes"
"encoding/json"
"errors"
"net/http"
"net/url"
"strconv"
"strings"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/tcp"
"github.com/AlexxIT/go2rtc/pkg/webrtc"
pion "github.com/pion/webrtc/v3"
)
// This package handles the Milestone WebRTC session lifecycle, including authentication,
// session creation, and session update with an SDP answer. It is designed to be used with
// a specific URL format that encodes session parameters. For example:
// webrtc:https://milestone-host/api#format=milestone#username=User#password=TestPassword#cameraId=a539f254-af05-4d67-a1bb-cd9b3c74d122
//
// https://github.com/milestonesys/mipsdk-samples-protocol/tree/main/WebRTC_JavaScript
type milestoneAPI struct {
url string
query url.Values
token string
sessionID string
}
func (m *milestoneAPI) GetToken() error {
data := url.Values{
"client_id": {"GrantValidatorClient"},
"grant_type": {"password"},
"username": m.query["username"],
"password": m.query["password"],
}
req, err := http.NewRequest("POST", m.url+"/IDP/connect/token", strings.NewReader(data.Encode()))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
// support httpx protocol
res, err := tcp.Do(req)
if err != nil {
return err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return errors.New("milesone: authentication failed: " + res.Status)
}
var payload map[string]interface{}
if err = json.NewDecoder(res.Body).Decode(&payload); err != nil {
return err
}
token, ok := payload["access_token"].(string)
if !ok {
return errors.New("milesone: token not found in the response")
}
m.token = token
return nil
}
func parseFloat(s string) float64 {
if s == "" {
return 0
}
f, _ := strconv.ParseFloat(s, 64)
return f
}
func (m *milestoneAPI) GetOffer() (string, error) {
request := struct {
CameraId string `json:"cameraId"`
StreamId string `json:"streamId,omitempty"`
PlaybackTimeNode struct {
PlaybackTime string `json:"playbackTime,omitempty"`
SkipGaps bool `json:"skipGaps,omitempty"`
Speed float64 `json:"speed,omitempty"`
} `json:"playbackTimeNode,omitempty"`
//ICEServers []string `json:"iceServers,omitempty"`
//Resolution string `json:"resolution,omitempty"`
}{
CameraId: m.query.Get("cameraId"),
StreamId: m.query.Get("streamId"),
}
request.PlaybackTimeNode.PlaybackTime = m.query.Get("playbackTime")
request.PlaybackTimeNode.SkipGaps = m.query.Has("skipGaps")
request.PlaybackTimeNode.Speed = parseFloat(m.query.Get("speed"))
data, err := json.Marshal(request)
if err != nil {
return "", err
}
req, err := http.NewRequest("POST", m.url+"/REST/v1/WebRTC/Session", bytes.NewBuffer(data))
if err != nil {
return "", err
}
req.Header.Set("Authorization", "Bearer "+m.token)
req.Header.Set("Content-Type", "application/json")
res, err := tcp.Do(req)
if err != nil {
return "", err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return "", errors.New("milesone: create session: " + res.Status)
}
var response struct {
SessionId string `json:"sessionId"`
OfferSDP string `json:"offerSDP"`
}
if err = json.NewDecoder(res.Body).Decode(&response); err != nil {
return "", err
}
var offer pion.SessionDescription
if err = json.Unmarshal([]byte(response.OfferSDP), &offer); err != nil {
return "", err
}
m.sessionID = response.SessionId
return offer.SDP, nil
}
func (m *milestoneAPI) SetAnswer(sdp string) error {
answer := pion.SessionDescription{
Type: pion.SDPTypeAnswer,
SDP: sdp,
}
data, err := json.Marshal(answer)
if err != nil {
return err
}
request := struct {
AnswerSDP string `json:"answerSDP"`
}{
AnswerSDP: string(data),
}
if data, err = json.Marshal(request); err != nil {
return err
}
req, err := http.NewRequest("PATCH", m.url+"/REST/v1/WebRTC/Session/"+m.sessionID, bytes.NewBuffer(data))
if err != nil {
return err
}
req.Header.Set("Authorization", "Bearer "+m.token)
req.Header.Set("Content-Type", "application/json")
res, err := tcp.Do(req)
if err != nil {
return err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return errors.New("milesone: patch session: " + res.Status)
}
return nil
}
func milestoneClient(rawURL string, query url.Values) (core.Producer, error) {
mc := &milestoneAPI{url: rawURL, query: query}
if err := mc.GetToken(); err != nil {
return nil, err
}
api, err := webrtc.NewAPI()
if err != nil {
return nil, err
}
conf := pion.Configuration{}
pc, err := api.NewPeerConnection(conf)
if err != nil {
return nil, err
}
prod := webrtc.NewConn(pc)
prod.FormatName = "webrtc/milestone"
prod.Mode = core.ModeActiveProducer
prod.Protocol = "http"
prod.URL = rawURL
offer, err := mc.GetOffer()
if err != nil {
return nil, err
}
if err = prod.SetOffer(offer); err != nil {
return nil, err
}
answer, err := prod.GetAnswer()
if err != nil {
return nil, err
}
if err = mc.SetAnswer(answer); err != nil {
return nil, err
}
return prod, nil
}

View File

@@ -33,7 +33,7 @@ func openIPCClient(rawURL string, query url.Values) (core.Producer, error) {
defer conn.Close()
// 3. Create Peer Connection
api, err := webrtc.NewAPI("")
api, err := webrtc.NewAPI()
if err != nil {
return nil, err
}
@@ -53,8 +53,10 @@ func openIPCClient(rawURL string, query url.Values) (core.Producer, error) {
var connState core.Waiter
prod := webrtc.NewConn(pc)
prod.Desc = "WebRTC/OpenIPC"
prod.FormatName = "webrtc/openipc"
prod.Mode = core.ModeActiveProducer
prod.Protocol = "ws"
prod.URL = rawURL
prod.Listen(func(msg any) {
switch msg := msg.(type) {
case *pion.ICECandidate:

View File

@@ -49,6 +49,9 @@ func syncHandler(w http.ResponseWriter, r *http.Request) {
http.Error(w, "", http.StatusBadRequest)
}
case "OPTIONS":
w.WriteHeader(http.StatusNoContent)
default:
http.Error(w, "", http.StatusMethodNotAllowed)
}
@@ -62,6 +65,7 @@ func outputWebRTC(w http.ResponseWriter, r *http.Request) {
url := r.URL.Query().Get("src")
stream := streams.Get(url)
if stream == nil {
http.Error(w, api.StreamNotFound, http.StatusNotFound)
return
}
@@ -97,11 +101,11 @@ func outputWebRTC(w http.ResponseWriter, r *http.Request) {
switch mediaType {
case "application/json":
desc = "WebRTC/JSON sync"
desc = "webrtc/json"
case MimeSDP:
desc = "WebRTC/WHEP sync"
desc = "webrtc/whep"
default:
desc = "WebRTC/HTTP sync"
desc = "webrtc/post"
}
answer, err := ExchangeSDP(stream, offer, desc, r.UserAgent())
@@ -165,8 +169,8 @@ func inputWebRTC(w http.ResponseWriter, r *http.Request) {
// create new webrtc instance
prod := webrtc.NewConn(pc)
prod.Desc = "WebRTC/WHIP sync"
prod.Mode = core.ModePassiveProducer
prod.Protocol = "http"
prod.UserAgent = r.UserAgent()
if err = prod.SetOffer(string(offer)); err != nil {
@@ -175,10 +179,7 @@ func inputWebRTC(w http.ResponseWriter, r *http.Request) {
return
}
answer, err := prod.GetCompleteAnswer()
if err == nil {
answer, err = syncCanditates(answer)
}
answer, err := prod.GetCompleteAnswer(GetCandidates(), FilterCandidate)
if err != nil {
log.Warn().Err(err).Caller().Send()
http.Error(w, err.Error(), http.StatusInternalServerError)
@@ -195,9 +196,7 @@ func inputWebRTC(w http.ResponseWriter, r *http.Request) {
case pion.PeerConnectionState:
if msg == pion.PeerConnectionStateClosed {
stream.RemoveProducer(prod)
if _, ok := sessions[id]; ok {
delete(sessions, id)
}
delete(sessions, id)
}
}
})

View File

@@ -2,7 +2,7 @@ package webrtc
import (
"errors"
"net"
"strings"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/api/ws"
@@ -20,10 +20,11 @@ func Init() {
Listen string `yaml:"listen"`
Candidates []string `yaml:"candidates"`
IceServers []pion.ICEServer `yaml:"ice_servers"`
Filters webrtc.Filters `yaml:"filters"`
} `yaml:"webrtc"`
}
cfg.Mod.Listen = "0.0.0.0:8555/tcp"
cfg.Mod.Listen = ":8555/tcp"
cfg.Mod.IceServers = []pion.ICEServer{
{URLs: []string{"stun:stun.l.google.com:19302"}},
}
@@ -32,23 +33,28 @@ func Init() {
log = app.GetLogger("webrtc")
address := cfg.Mod.Listen
filters = cfg.Mod.Filters
address, network, _ := strings.Cut(cfg.Mod.Listen, "/")
for _, candidate := range cfg.Mod.Candidates {
AddCandidate(network, candidate)
}
var err error
// create pionAPI with custom codecs list and custom network settings
serverAPI, err := webrtc.NewAPI(address)
serverAPI, err = webrtc.NewServerAPI(network, address, &filters)
if err != nil {
log.Error().Err(err).Caller().Send()
return
}
// use same API for WebRTC server and client if no address
clientAPI := serverAPI
clientAPI = serverAPI
if address != "" {
log.Info().Str("addr", address).Msg("[webrtc] listen")
_, Port, _ = net.SplitHostPort(address)
clientAPI, _ = webrtc.NewAPI("")
log.Info().Str("addr", cfg.Mod.Listen).Msg("[webrtc] listen")
clientAPI, _ = webrtc.NewAPI()
}
pionConf := pion.Configuration{
@@ -65,10 +71,6 @@ func Init() {
}
}
for _, candidate := range cfg.Mod.Candidates {
AddCandidate(candidate)
}
// async WebRTC server (two API versions)
ws.HandleFunc("webrtc", asyncHandler)
ws.HandleFunc("webrtc/offer", asyncHandler)
@@ -81,12 +83,13 @@ func Init() {
streams.HandleFunc("webrtc", streamsHandler)
}
var Port string
var serverAPI, clientAPI *pion.API
var log zerolog.Logger
var PeerConnection func(active bool) (*pion.PeerConnection, error)
func asyncHandler(tr *ws.Transport, msg *ws.Message) error {
func asyncHandler(tr *ws.Transport, msg *ws.Message) (err error) {
var stream *streams.Stream
var mode core.Mode
@@ -105,8 +108,30 @@ func asyncHandler(tr *ws.Transport, msg *ws.Message) error {
return errors.New(api.StreamNotFound)
}
var offer struct {
Type string `json:"type"`
SDP string `json:"sdp"`
ICEServers []pion.ICEServer `json:"ice_servers"`
}
// V2 - json/object exchange, V1 - raw SDP exchange
apiV2 := msg.Type == "webrtc"
if apiV2 {
if err = msg.Unmarshal(&offer); err != nil {
return err
}
} else {
offer.SDP = msg.String()
}
// create new PeerConnection instance
pc, err := PeerConnection(false)
var pc *pion.PeerConnection
if offer.ICEServers == nil {
pc, err = PeerConnection(false)
} else {
pc, err = serverAPI.NewPeerConnection(pion.Configuration{ICEServers: offer.ICEServers})
}
if err != nil {
log.Error().Err(err).Caller().Send()
return err
@@ -118,8 +143,8 @@ func asyncHandler(tr *ws.Transport, msg *ws.Message) error {
defer sendAnswer.Done(nil)
conn := webrtc.NewConn(pc)
conn.Desc = "WebRTC/WebSocket async"
conn.Mode = mode
conn.Protocol = "ws"
conn.UserAgent = tr.Request.UserAgent()
conn.Listen(func(msg any) {
switch msg := msg.(type) {
@@ -135,28 +160,21 @@ func asyncHandler(tr *ws.Transport, msg *ws.Message) error {
}
case *pion.ICECandidate:
if !FilterCandidate(msg) {
return
}
_ = sendAnswer.Wait()
s := msg.ToJSON().Candidate
log.Trace().Str("candidate", s).Msg("[webrtc] local")
log.Trace().Str("candidate", s).Msg("[webrtc] local ")
tr.Write(&ws.Message{Type: "webrtc/candidate", Value: s})
}
})
// V2 - json/object exchange, V1 - raw SDP exchange
apiV2 := msg.Type == "webrtc"
log.Trace().Msgf("[webrtc] offer:\n%s", offer.SDP)
// 1. SetOffer, so we can get remote client codecs
var offer string
if apiV2 {
offer = msg.GetString("sdp")
} else {
offer = msg.String()
}
log.Trace().Msgf("[webrtc] offer:\n%s", offer)
if err = conn.SetOffer(offer); err != nil {
if err = conn.SetOffer(offer.SDP); err != nil {
log.Warn().Err(err).Caller().Send()
return err
}
@@ -205,8 +223,9 @@ func ExchangeSDP(stream *streams.Stream, offer, desc, userAgent string) (answer
// create new webrtc instance
conn := webrtc.NewConn(pc)
conn.Desc = desc
conn.FormatName = desc
conn.UserAgent = userAgent
conn.Protocol = "http"
conn.Listen(func(msg any) {
switch msg := msg.(type) {
case pion.PeerConnectionState:
@@ -244,10 +263,7 @@ func ExchangeSDP(stream *streams.Stream, offer, desc, userAgent string) (answer
stream.AddProducer(conn)
}
answer, err = conn.GetCompleteAnswer()
if err == nil {
answer, err = syncCanditates(answer)
}
answer, err = conn.GetCompleteAnswer(GetCandidates(), FilterCandidate)
log.Trace().Msgf("[webrtc] answer\n%s", answer)
if err != nil {

View File

@@ -47,7 +47,7 @@ func Init() {
if stream == nil {
return "", errors.New(api.StreamNotFound)
}
return webrtc.ExchangeSDP(stream, offer, "WebRTC/WebTorrent sync", "")
return webrtc.ExchangeSDP(stream, offer, "webtorrent", "")
},
}

View File

@@ -9,7 +9,9 @@ import (
"github.com/AlexxIT/go2rtc/internal/dvrip"
"github.com/AlexxIT/go2rtc/internal/echo"
"github.com/AlexxIT/go2rtc/internal/exec"
"github.com/AlexxIT/go2rtc/internal/expr"
"github.com/AlexxIT/go2rtc/internal/ffmpeg"
"github.com/AlexxIT/go2rtc/internal/gopro"
"github.com/AlexxIT/go2rtc/internal/hass"
"github.com/AlexxIT/go2rtc/internal/hls"
"github.com/AlexxIT/go2rtc/internal/homekit"
@@ -34,6 +36,8 @@ import (
)
func main() {
app.Version = "1.9.5"
// 1. Core modules: app, api/ws, streams
app.Init() // init config and logs
@@ -76,10 +80,12 @@ func main() {
homekit.Init() // homekit source
nest.Init() // nest source
bubble.Init() // bubble source
expr.Init() // expr source
gopro.Init() // gopro source
// 6. Helper modules
ngrok.Init() // Ngrok module
ngrok.Init() // ngrok module
srtp.Init() // SRTP server
debug.Init() // debug API

View File

@@ -1,3 +1,85 @@
# Notes
go2rtc tries to name formats, protocols and codecs the same way they are named in FFmpeg.
Some formats and protocols go2rtc supports exclusively. They have no equivalent in FFmpeg.
## Producers (input)
- The initiator of the connection can be go2rtc - **Source protocols**
- The initiator of the connection can be an external program - **Ingress protocols**
- Codecs can be incoming - **Recevers codecs**
- Codecs can be outgoing (two way audio) - **Senders codecs**
| Format | Source protocols | Ingress protocols | Recevers codecs | Senders codecs | Example |
|--------------|------------------|-------------------|------------------------------|--------------------|---------------|
| adts | http,tcp,pipe | http | aac | | `http:` |
| bubble | http | | h264,hevc,pcm_alaw | | `bubble:` |
| dvrip | tcp | | h264,hevc,pcm_alaw,pcm_mulaw | pcm_alaw | `dvrip:` |
| flv | http,tcp,pipe | http | h264,aac | | `http:` |
| gopro | http+udp | | TODO | | `gopro:` |
| hass/webrtc | ws+udp,tcp | | TODO | | `hass:` |
| hls/mpegts | http | | h264,h265,aac,opus | | `http:` |
| homekit | homekit+udp | | h264,eld* | | `homekit:` |
| isapi | http | | | pcm_alaw,pcm_mulaw | `isapi:` |
| ivideon | ws | | h264 | | `ivideon:` |
| kasa | http | | h264,pcm_mulaw | | `kasa:` |
| h264 | http,tcp,pipe | http | h264 | | `http:` |
| hevc | http,tcp,pipe | http | hevc | | `http:` |
| mjpeg | http,tcp,pipe | http | mjpeg | | `http:` |
| mpjpeg | http,tcp,pipe | http | mjpeg | | `http:` |
| mpegts | http,tcp,pipe | http | h264,hevc,aac,opus | | `http:` |
| nest/webrtc | http+udp | | TODO | | `nest:` |
| roborock | mqtt+udp | | h264,opus | opus | `roborock:` |
| rtmp | rtmp | rtmp | h264,aac | | `rtmp:` |
| rtsp | rtsp+tcp,ws | rtsp+tcp | h264,hevc,aac,pcm*,opus | pcm*,opus | `rtsp:` |
| stdin | pipe | | | pcm_alaw,pcm_mulaw | `stdin:` |
| tapo | http | | h264,pcma | pcm_alaw | `tapo:` |
| wav | http,tcp,pipe | http | pcm_alaw,pcm_mulaw | | `http:` |
| webrtc* | TODO | TODO | h264,pcm_alaw,pcm_mulaw,opus | pcm_alaw,pcm_mulaw | `webrtc:` |
| webtorrent | TODO | TODO | TODO | TODO | `webtorrent:` |
| yuv4mpegpipe | http,tcp,pipe | http | rawvideo | | `http:` |
- **eld** - rare variant of aac codec
- **pcm** - pcm_alaw pcm_mulaw pcm_s16be pcm_s16le
- **webrtc** - webrtc/kinesis, webrtc/openipc, webrtc/milestone, webrtc/wyze, webrtc/whep
## Consumers (output)
| Format | Protocol | Send codecs | Recv codecs | Example |
|--------------|-------------|------------------------------|-------------------------|---------------------------------------|
| adts | http | aac | | `GET /api/stream.adts` |
| ascii | http | mjpeg | | `GET /api/stream.ascii` |
| flv | http | h264,aac | | `GET /api/stream.flv` |
| hls/mpegts | http | h264,hevc,aac | | `GET /api/stream.m3u8` |
| hls/fmp4 | http | h264,hevc,aac,pcm*,opus | | `GET /api/stream.m3u8?mp4` |
| homekit | homekit+udp | h264,opus | | Apple HomeKit app |
| mjpeg | ws | mjpeg | | `{"type":"mjpeg"}` -> `/api/ws` |
| mpjpeg | http | mjpeg | | `GET /api/stream.mjpeg` |
| mp4 | http | h264,hevc,aac,pcm*,opus | | `GET /api/stream.mp4` |
| mse/fmp4 | ws | h264,hevc,aac,pcm*,opus | | `{"type":"mse"}` -> `/api/ws` |
| mpegts | http | h264,hevc,aac | | `GET /api/stream.ts` |
| rtmp | rtmp | h264,aac | | `rtmp://localhost:1935/{stream_name}` |
| rtsp | rtsp+tcp | h264,hevc,aac,pcm*,opus | | `rtsp://localhost:8554/{stream_name}` |
| webrtc | TODO | h264,pcm_alaw,pcm_mulaw,opus | pcm_alaw,pcm_mulaw,opus | `{"type":"webrtc"}` -> `/api/ws` |
| yuv4mpegpipe | http | rawvideo | | `GET /api/stream.y4m` |
- **pcm** - pcm_alaw pcm_mulaw pcm_s16be pcm_s16le
## Snapshots
| Format | Protocol | Send codecs | Example |
|--------|----------|-------------|-----------------------|
| jpeg | http | mjpeg | `GET /api/frame.jpeg` |
| mp4 | http | h264,hevc | `GET /api/frame.mp4` |
## Developers
File naming:
- `pkg/{format}/producer.go` - producer for this format (also if support backchannel)
- `pkg/{format}/consumer.go` - consumer for this format
- `pkg/{format}/backchanel.go` - producer with only backchannel func
## Useful links
- https://www.wowza.com/blog/streaming-protocols

View File

@@ -69,6 +69,8 @@ func DecodeConfig(b []byte) (objType, sampleFreqIdx, channels byte, sampleRate u
sampleFreqIdx = rd.ReadBits8(4)
if sampleFreqIdx == 0b1111 {
sampleRate = rd.ReadBits(24)
} else {
sampleRate = sampleRates[sampleFreqIdx]
}
channels = rd.ReadBits8(4)

View File

@@ -41,3 +41,12 @@ func TestADTS(t *testing.T) {
require.Equal(t, src[:len(dst)], dst)
}
func TestEncodeConfig(t *testing.T) {
conf := EncodeConfig(TypeAACLC, 48000, 1, false)
require.Equal(t, "1188", hex.EncodeToString(conf))
conf = EncodeConfig(TypeAACLC, 16000, 1, false)
require.Equal(t, "1408", hex.EncodeToString(conf))
conf = EncodeConfig(TypeAACLC, 8000, 1, false)
require.Equal(t, "1588", hex.EncodeToString(conf))
}

View File

@@ -9,8 +9,7 @@ import (
)
func IsADTS(b []byte) bool {
_ = b[1]
return len(b) > 7 && b[0] == 0xFF && b[1]&0xF0 == 0xF0
return len(b) > 7 && b[0] == 0xFF && b[1]&0xF6 == 0xF0
}
func ADTSToCodec(b []byte) *core.Codec {

View File

@@ -8,15 +8,12 @@ import (
)
type Consumer struct {
core.SuperConsumer
core.Connection
wr *core.WriteBuffer
}
func NewConsumer() *Consumer {
cons := &Consumer{
wr: core.NewWriteBuffer(nil),
}
cons.Medias = []*core.Media{
medias := []*core.Media{
{
Kind: core.KindAudio,
Direction: core.DirectionSendonly,
@@ -25,7 +22,16 @@ func NewConsumer() *Consumer {
},
},
}
return cons
wr := core.NewWriteBuffer(nil)
return &Consumer{
Connection: core.Connection{
ID: core.NewID(),
FormatName: "adts",
Medias: medias,
Transport: wr,
},
wr: wr,
}
}
func (c *Consumer) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiver) error {
@@ -51,8 +57,3 @@ func (c *Consumer) AddTrack(media *core.Media, codec *core.Codec, track *core.Re
func (c *Consumer) WriteTo(wr io.Writer) (int64, error) {
return c.wr.WriteTo(wr)
}
func (c *Consumer) Stop() error {
_ = c.SuperConsumer.Close()
return c.wr.Close()
}

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