Compare commits

...

191 Commits

Author SHA1 Message Date
Cedric Verstraeten
ca367611d7 Update docker-nightly.yml 2024-01-07 08:15:24 +01:00
Cedric Verstraeten
eb8f073856 Merge branch 'master' into develop 2024-01-03 22:03:00 +01:00
Cedric Verstraeten
3ae43eba16 hot fix: close client on verifying connection (will keep client open) 2024-01-03 22:02:44 +01:00
Cedric Verstraeten
9719a08eaa Merge branch 'master' into develop 2024-01-03 21:54:30 +01:00
Cedric Verstraeten
1e165cbeb8 hotfix: try to create pullpoint subscription if first time failed 2024-01-03 18:44:53 +01:00
Cedric Verstraeten
8be8cafd00 force release mode in GIN 2024-01-03 18:26:10 +01:00
Cedric Verstraeten
e74d2aadb5 Merge branch 'develop' 2024-01-03 18:16:23 +01:00
Cedric Verstraeten
9c97422f43 properly handle cameras without PTZ function 2024-01-03 18:12:02 +01:00
Cedric Verstraeten
deb0a3ff1f hotfix: position or zoom can be nil 2024-01-03 13:37:38 +01:00
Cedric Verstraeten
95ed1f0e97 move error to debug 2024-01-03 12:36:08 +01:00
Cedric Verstraeten
6a111dadd6 typo in readme (wrong formatting link) 2024-01-03 12:24:35 +01:00
Cedric Verstraeten
95b3623c04 change startup command (new flag method) 2024-01-03 12:19:18 +01:00
Cedric Verstraeten
326d62a640 snap was moved to dedicated repository to better control release: https://github.com/kerberos-io/snap-agent
the repository https://github.com/kerberos-io/snap-agent is linked to the snap build system and will generate new releases
2024-01-03 12:17:47 +01:00
Cedric Verstraeten
9d990650f3 hotfix: onvif endpoint changed 2024-01-03 10:19:04 +01:00
Cedric Verstraeten
4bc891b640 hotfix: move from warning to debug 2024-01-03 10:12:18 +01:00
Cedric Verstraeten
1f133afb89 Merge branch 'develop' 2024-01-03 09:57:51 +01:00
Cedric Verstraeten
8da34a6a1a hotfix: restart agent when nog rtsp url was defined 2024-01-03 09:56:56 +01:00
Cédric Verstraeten
57c49a8325 Update snapcraft.yaml 2024-01-02 22:16:41 +01:00
Cedric Verstraeten
f739d52505 Update docker-nightly.yml 2024-01-01 23:46:12 +01:00
Cedric Verstraeten
793022eb0f no longer support go '1.17', '1.18', '1.19', 2024-01-01 23:41:45 +01:00
Cedric Verstraeten
6b1fd739f4 add as safe directory 2024-01-01 23:38:50 +01:00
Cedric Verstraeten
4efa7048dc add runner user - setup as a workaround 2024-01-01 23:33:08 +01:00
Cedric Verstraeten
4931700d06 try checkout v4, you never know.. 2024-01-01 23:29:50 +01:00
Cedric Verstraeten
4bd49dbee1 run go build as specific user 2024-01-01 23:25:32 +01:00
Cedric Verstraeten
c278a66f0e make go versions as string, removes the 0 (weird issue though) 2024-01-01 23:18:55 +01:00
Cedric Verstraeten
d64e6b631c extending versions + base image 2024-01-01 23:16:50 +01:00
Cedric Verstraeten
fa91e84977 Merge branch 'port-to-gortsplib' into develop 2024-01-01 23:11:24 +01:00
Cedric Verstraeten
8c231d3b63 Merge branch 'master' into develop 2024-01-01 23:10:36 +01:00
Cedric Verstraeten
775c1b7051 show correct error message for failing onvif 2024-01-01 19:36:14 +01:00
Cedric Verstraeten
fb23815210 add support for H265 in UI 2024-01-01 19:31:58 +01:00
Cedric Verstraeten
5261c1cbfc debug condition 2023-12-31 15:46:25 +01:00
Cedric Verstraeten
f2aa3d9176 onvif is enabled, currently expects ptz, which is not the case 2023-12-30 22:07:45 +01:00
Cedric Verstraeten
113b02d665 Update Cloud.go 2023-12-30 09:18:46 +01:00
Cedric Verstraeten
957d2fd095 Update Cloud.go 2023-12-29 14:59:34 +01:00
Cedric Verstraeten
78e7fb595a make sure to set onvifEventsList = []byte("[]") 2023-12-29 11:37:32 +01:00
Cedric Verstraeten
b5415284e2 rename + add conceptual hidden function (not yet added) 2023-12-29 08:10:01 +01:00
Cedric Verstraeten
e94a9a1000 update base image 2023-12-28 16:33:39 +01:00
Cedric Verstraeten
60bb9a521c Update README.md 2023-12-28 11:32:46 +01:00
Cedric Verstraeten
3ac34a366f Update README.md 2023-12-28 11:29:33 +01:00
Cedric Verstraeten
77449a29e7 add h264 and h265 discussion 2023-12-28 11:24:36 +01:00
Cedric Verstraeten
242ff48ab6 add more description error with onvif invalid credentials + send capabilitites as part of onvif/login or verify 2023-12-28 10:55:11 +01:00
Cedric Verstraeten
b71dbddc1a add support for snapshots (raw + base64) #130
also tweaked the logging as bit more
2023-12-28 10:24:15 +01:00
Cedric Verstraeten
6407f3da3d recover from failled pullpoint subscription 2023-12-28 08:22:37 +01:00
Cedric Verstraeten
776571c7b3 improve logging 2023-12-27 14:30:12 +01:00
Cedric Verstraeten
2df35a1999 add remote trigger relay output (mqtt endpoint) + rename a few methods 2023-12-27 10:39:12 +01:00
Cedric Verstraeten
b1ab6bf522 improve logging + updated readme 2023-12-27 10:25:03 +01:00
Cedric Verstraeten
e7fd0bd8a3 add logging output variable (json or text) + improve logging 2023-12-27 10:06:55 +01:00
Cedric Verstraeten
4f5597c441 remove unnecessary prints 2023-12-25 23:10:04 +01:00
Cedric Verstraeten
400457af9f upgrade onvif to 14 2023-12-25 21:37:35 +01:00
Cedric Verstraeten
c48e3a5683 Update go.mod 2023-12-25 21:01:52 +01:00
Cedric Verstraeten
67064879e4 input/output methods 2023-12-25 20:55:51 +01:00
Cedric Verstraeten
698b9c6b54 cleanup comments + add ouputs 2023-12-15 15:07:25 +01:00
Cedric Verstraeten
0e8a89c4c3 add onvif inputs function 2023-12-12 23:34:04 +01:00
Cedric Verstraeten
b0bcf73b52 add condition uri implementation, wrapped condition class so it's easier to extend 2023-12-12 17:30:41 +01:00
Cedric Verstraeten
15a51e7987 align logging 2023-12-12 09:52:35 +01:00
Cedric Verstraeten
b5f5567bcf cleanup names of files (still need more cleanup)+ rework discover method + separated conditions in separate package 2023-12-12 09:15:54 +01:00
Cedric Verstraeten
9151b38e7f document more swagger endpoints + cleanup source 2023-12-11 21:02:01 +01:00
Cedric Verstraeten
898b3a52c2 update loggin + add new swagger endpoints 2023-12-11 20:32:03 +01:00
Cedric Verstraeten
be6eb6165c get keyframe and decode on requesting config (required for factory) 2023-12-10 23:13:42 +01:00
Cedric Verstraeten
e95f545bf4 upgrade deps + fix nil error 2023-12-09 23:02:18 +01:00
Cedric Verstraeten
fd01fc640e get rid of snapshots + was blocking stream and corrupted recordings 2023-12-07 21:33:32 +01:00
Cedric Verstraeten
8cfcfe4643 upgrade onvif 2023-12-07 19:33:18 +01:00
Cedric Verstraeten
60d7b4b356 if we have no backchannel we'll skip the setup 2023-12-06 19:03:36 +01:00
Cedric Verstraeten
9b796c049d mem leak for http close (still one) + not closing some channels properly 2023-12-06 18:53:55 +01:00
Cedric Verstraeten
c8c9f6dff1 implement better logging, making logging levels configurable (WIP) 2023-12-05 23:05:59 +01:00
Cedric Verstraeten
8293d29ee8 make recording write directly to file + fix memory leaks with http on ONVIF API 2023-12-05 22:07:29 +01:00
Cedric Verstraeten
34a0d8f5c4 force TCP + ignore motion detection if no region is set 2023-12-05 08:30:00 +01:00
Cedric Verstraeten
0a195a0dfb Update Dockerfile 2023-12-04 14:47:53 +01:00
Cedric Verstraeten
c82ead31f2 decode using H265 2023-12-04 14:02:41 +01:00
Cedric Verstraeten
3ab4b5b54b OOPS: missing encryption at some points 2023-12-03 20:12:23 +01:00
Cedric Verstraeten
5765f7c4f6 additional checks for closed decoder + properly close recording when closed 2023-12-03 20:10:05 +01:00
Cedric Verstraeten
d1dd30577b get rid of VPS, fails to write in h265 (also upgrade dependencies) 2023-12-03 19:18:01 +01:00
Cedric Verstraeten
1145008c62 reference implementation for transcoding from MULAW to AAC 2023-12-03 09:53:20 +01:00
Cedric Verstraeten
3f1e01e665 dont panic on fail bachchannel 2023-12-03 08:14:56 +01:00
Cedric Verstraeten
ced9355b78 Run Backchannel on a seperate Gortsplib instance 2023-12-02 22:28:26 +01:00
Cedric Verstraeten
6e7ade036e add logging + fix private key pass through + fixed crash on websocket livestreaming 2023-12-02 21:30:07 +01:00
Cedric Verstraeten
976fbb65aa Update Kerberos.go 2023-12-02 15:41:36 +01:00
Cedric Verstraeten
ba7f870d4b wait a bit to close the motion channel, also close audio channel 2023-12-02 15:18:49 +01:00
Cedric Verstraeten
cb3dce5ffd closing 2023-12-02 13:07:52 +01:00
Cedric Verstraeten
b317a6a9db fix closing of rtspclient + integrate h265 support
now we can record in H265 and stream in H264 using webrtc or websocket
2023-12-02 12:34:28 +01:00
Cedric Verstraeten
e42f430bb8 add MPEG4 (AAC support), put ready for H265 2023-12-02 00:43:31 +01:00
Cedric Verstraeten
bd984ea1c7 works now, but needed to change size of paylod 2023-12-01 23:17:32 +01:00
Cedric Verstraeten
6798569b7f first try for the backchannel using gortsplib
getting error short buffer
2023-12-01 22:57:33 +01:00
Cedric Verstraeten
df3183ec1c add backchannel support 2023-12-01 22:18:06 +01:00
Cedric Verstraeten
25c35ba91b fix hull 2023-12-01 21:27:58 +01:00
Cedric Verstraeten
68b9c5f679 fix videostream for subclient 2023-12-01 20:24:35 +01:00
Cedric Verstraeten
9757bc9b18 Calculate width and height + add FPS 2023-12-01 19:47:31 +01:00
Cedric Verstraeten
1e4affbf5c dont write trailer do +1 prerecording reader 2023-12-01 15:05:39 +01:00
Cedric Verstraeten
22f4a7e08a fix closing of stream 2023-12-01 11:05:58 +01:00
Cedric Verstraeten
044e167dd2 add lock + motion detection 2023-12-01 08:34:09 +01:00
Cedric Verstraeten
bffd377461 add substream 2023-11-30 21:33:14 +01:00
Cedric Verstraeten
677c9e334b add decoder, fix livestream 2023-11-30 21:01:57 +01:00
Cedric Verstraeten
df38784a8d fixes 2023-11-30 17:34:03 +01:00
Cedric Verstraeten
dae2c1b5c4 fix keyframing 2023-11-30 17:17:10 +01:00
Cedric Verstraeten
fd6449b377 remove dtsextractor is blocks the stream 2023-11-30 14:50:09 +01:00
Cedric Verstraeten
cd09ed3321 fix 2023-11-30 14:33:12 +01:00
Cedric Verstraeten
e7dc9aa64d swap to joy4 2023-11-30 14:10:07 +01:00
Cedric Verstraeten
fec2587b6d Update Gortsplib.go 2023-11-30 13:49:46 +01:00
Cedric Verstraeten
7c285d36a1 isolate rtsp clients to be able to pass them through 2023-11-30 13:45:34 +01:00
Cedric Verstraeten
ed46cbe35a cleanup enable more features 2023-11-30 00:47:30 +01:00
Cedric Verstraeten
0a8f097c76 cleanup and fix for recording (wrong DTS value) + fix for recording using "old" joy library 2023-11-29 19:33:03 +01:00
Cedric Verstraeten
bce5d443d5 try new muxer 2023-11-29 17:18:51 +01:00
Cedric Verstraeten
19bf456bda adding fragmented mp4 (not working) trying to fix black screen on quicktime player mp4 2023-11-29 16:28:09 +01:00
Cedric Verstraeten
1359858e42 updates and cleanup 2023-11-29 15:01:36 +01:00
Cedric Verstraeten
55b1abe243 Add mp4 muxer, still some work to do 2023-11-29 10:21:58 +01:00
Cedric Verstraeten
c6428d8c5a Fix for WebRTC using new library had to encode nalu 2023-11-27 17:05:55 +01:00
Cedric Verstraeten
e241a03fc4 comment out unused code! 2023-11-26 17:30:05 +01:00
Cedric Verstraeten
ac2b99a3dd inherit from golibrtsp rtp.packet + fix the decoding for livestream + motion 2023-11-26 16:58:55 +01:00
Cedric Verstraeten
341a6a7fae refactoring the rtspclient to be able to swap out easily 2023-11-26 00:07:53 +01:00
Cedric Verstraeten
e74facfb7f fix: blocking state candidates 2023-11-23 22:21:56 +01:00
Cedric Verstraeten
54bc1989f9 fix: update locking webrtc 2023-11-23 21:17:39 +01:00
Cedric Verstraeten
94b71a0868 fix: enabling backchannel on the mainstream 2023-11-20 09:57:55 +01:00
Cedric Verstraeten
c071057eec hotfix: do fallback without backchannel if camera didnt support it, some cameras such as Dahua will fail on the header. 2023-11-20 09:35:41 +01:00
Cedric Verstraeten
e8a355d992 upgrade joy4: add setreaddeadline for RTSP connection 2023-11-19 21:40:08 +01:00
Cedric Verstraeten
ca84664071 hotfix: add locks to make sure candidates are not send to a closed candidate channel 2023-11-18 20:38:29 +01:00
Cedric Verstraeten
dd7fcb31b1 Add ONVIF backchannel functionality with G711 encoding 2023-11-17 16:28:03 +01:00
Cédric Verstraeten
324fffde6b Merge pull request #125 from Izzotop/feat/add-russian-language-support
Add Russian language
2023-11-14 21:22:33 +01:00
Izzotop
cd8347d20f Add Russian language 2023-11-09 16:03:40 +03:00
Cedric Verstraeten
efcbf52b06 Merge branch 'master' of https://github.com/kerberos-io/agent 2023-11-06 17:07:50 +01:00
Cedric Verstraeten
c33469a7b3 add --fix-missing to fix random broken builds (armv6 image) 2023-11-06 17:07:35 +01:00
Cédric Verstraeten
3717535f0b Merge pull request #121 from Chaitanya110703/patch-1
doc(README): remove typo
2023-11-06 16:54:28 +01:00
Cedric Verstraeten
8eb2de5e28 When Kerberos Vault is configured without Kerberos Hub, cameras do not show up in Kerberos Vault #123 2023-11-06 14:37:54 +01:00
Cedric Verstraeten
96f6bcb1dd test file in wrong directory 2023-11-03 13:18:00 +01:00
Cedric Verstraeten
860077a3eb turn off linting for: jsx-a11y/control-has-associated-label 2023-11-02 08:28:26 +01:00
Cedric Verstraeten
8be9343314 upgrade joy issues with audio codec (wrong FFMPEG version) 2023-11-02 08:15:32 +01:00
Cedric Verstraeten
dac04fbb57 upgrade joy for https://github.com/kerberos-io/agent/issues/105 2023-11-01 22:03:48 +01:00
Cedric Verstraeten
b9acf4c150 hot fix: factory needs to override encryption settings 2023-10-24 22:50:55 +02:00
Chaitanya110703
6608018f86 doc(README): remove typo 2023-10-24 21:25:45 +05:30
Cedric Verstraeten
552f5dbea6 hotfix: check if encryption is set for old agents 2023-10-24 17:52:09 +02:00
Cedric Verstraeten
2844a5a419 webrtc: disable relay allow other 2023-10-24 16:44:42 +02:00
Cedric Verstraeten
c4b9610f58 hotfix: mqtt webrtc - wrong session key 2023-10-24 16:22:15 +02:00
Cedric Verstraeten
6a44498730 hot fix: readd locks 2023-10-24 13:39:17 +02:00
Cedric Verstraeten
a2cebaf90b hot fix: wait for token in webrtc 2023-10-24 13:14:14 +02:00
Cedric Verstraeten
3f58f26dfd decrypt recordings through the UI automatically using the existing AES key, you can still use the decrypt action or openssl afterwards 2023-10-23 14:38:29 +02:00
Cedric Verstraeten
a8d5f56f1e hotfix - build error encryption key value 2023-10-23 11:07:54 +02:00
Cédric Verstraeten
1eb62d80c7 add encryption + end-to-end encryption to feature list 2023-10-23 10:59:13 +02:00
Cedric Verstraeten
e474a62dbc Add hindi #119 + allow recordings encryption + decryption tooling. 2023-10-23 10:56:36 +02:00
Cédric Verstraeten
f29b952001 Merge pull request #119 from fadkeabhi/feat#47-add-hindi-language-support
Added transaltions for hindi language
2023-10-22 22:15:07 +02:00
Cedric Verstraeten
38247ac9f6 Add italian to language selector #115 2023-10-22 20:00:10 +02:00
Cédric Verstraeten
580f17028a Merge pull request #115 from LeoSpyke/master
i18n: adds Italian locale
2023-10-22 19:56:55 +02:00
Cedric Verstraeten
48d933a561 backwards compatible when no encryption key was added in previous config 2023-10-20 14:35:09 +02:00
Cedric Verstraeten
0c70ab6158 Refactor MQTT endpoints + Introduce End-to-End encryption using RSA and AES keys + finetune PTZ 2023-10-20 13:31:02 +02:00
ABHISHEK FADAKE
839185dac8 Added transaltions for hindi language 2023-10-03 19:24:47 +05:30
LeoSpyke
ba6cdef9d5 i18n(it): translate persistence and bugfix 2023-09-15 08:17:12 +00:00
LeoSpyke
bedb3c0d7f Merge branch 'kerberos-io:master' into master 2023-09-14 12:47:46 +02:00
Leonardo Papini
2539255940 i18n: Italian translations 2023-09-14 12:47:28 +02:00
Cedric Verstraeten
24136f8b15 we didn't reset the main configuration, causing some config vars still to be set 2023-09-14 10:47:18 +02:00
Cedric Verstraeten
910bb3c079 merging timetable was giving issues 2023-09-14 10:13:50 +02:00
Cedric Verstraeten
47f4c19617 Update Config.go 2023-09-13 08:14:25 +02:00
Cedric Verstraeten
280a81809a Update Config.go 2023-09-12 22:38:26 +02:00
Cedric Verstraeten
59358acb30 add logging + empty friendly name 2023-09-12 15:17:56 +02:00
Cedric Verstraeten
ebd655ac73 Allow remote configuration through MQTT + restructure config method 2023-09-12 10:50:36 +02:00
Cedric Verstraeten
6325e37aae empty presets caused hub connection failing 2023-09-07 08:16:46 +02:00
Cedric Verstraeten
ecabc47847 integrate ondevice configurated presets 2023-08-30 14:12:07 +02:00
Cedric Verstraeten
31cc3d8939 Rely on continuous move will fix the PTZFunctions later 2023-08-29 14:53:48 +02:00
Cedric Verstraeten
c71cb71d08 We should reenable debugging, modifying to Info for now 2023-08-29 14:43:14 +02:00
Cedric Verstraeten
65a739ea75 logging PTZ functions 2023-08-29 14:30:25 +02:00
Cedric Verstraeten
410a62e9ef Some cameras do not support AbsoluteMovement, therefore we'll simulate it with ContinuousMove and a polling mechanism 2023-08-28 09:30:08 +02:00
Cedric Verstraeten
aa76dd1ec8 enable PTZ preset + introduce new MQTT messaging between Hub and Agent (introduction e2e encryption) 2023-08-25 09:05:53 +02:00
Cedric Verstraeten
384448d123 panic when no mongodb + remove files when no longer available + do not cleanup recordings by default, however cleanup when recordings have been uploaded 2023-07-31 08:49:34 +02:00
Cedric Verstraeten
414f74758c remove curly brackets 2023-07-26 19:22:19 +02:00
Cedric Verstraeten
25403ccdab dont restart if previously was not set! https://github.com/kerberos-io/agent/issues/110 2023-07-12 17:48:43 +02:00
Cedric Verstraeten
4c03132b83 Fail agent when no mongodb can be reached in Kerberos Factory deployment 2023-07-12 09:58:31 +02:00
Cedric Verstraeten
470f8f1cb6 some deployments might miss the variable, such as Kerberos Factory, we'll default these values to "true" 2023-07-11 21:57:07 +02:00
Cedric Verstraeten
5308376a67 add terraform deployment example 2023-07-04 21:04:16 +02:00
Cedric Verstraeten
2b112d29cf further detail snap deployment 2023-07-01 11:52:13 +02:00
Cedric Verstraeten
20d2517e74 add snapcraft 2023-07-01 07:42:12 +02:00
Cedric Verstraeten
12902e2482 disable snapcraft for the time being 2023-06-29 23:15:34 +02:00
Cedric Verstraeten
baca44beef Update docker.yml 2023-06-29 21:07:12 +02:00
Cedric Verstraeten
d7580744e2 Update docker.yml 2023-06-29 20:52:58 +02:00
Cedric Verstraeten
04f4bc9bf2 Update docker.yml 2023-06-29 20:47:56 +02:00
Cedric Verstraeten
d879174f4c add user to lxd group for snapcraft build 2023-06-29 20:40:28 +02:00
Cedric Verstraeten
5a1a62a723 Update docker.yml 2023-06-29 20:31:18 +02:00
Cedric Verstraeten
c519b01092 add snapcraft and try to snap the build 2023-06-29 20:23:11 +02:00
Cedric Verstraeten
c2ff7ff785 add missing custom config directory references 2023-06-29 20:10:16 +02:00
Cedric Verstraeten
44ec8c0534 try upgrading the dockerfile 2023-06-29 14:06:41 +02:00
Cedric Verstraeten
21c0e01137 add additional environment variables to tweak the internal agent "disable motion, disable liveview" 2023-06-29 12:28:44 +02:00
Cedric Verstraeten
f7ced6056d update to port 80 + allow frontend to take into account a custom config directory 2023-06-28 20:24:41 +02:00
Cedric Verstraeten
00917e3f88 add flag arguments instead of absolute arguments (we now support names)
added option to define the config location, can be different than the relative location of the agent binary
2023-06-28 19:28:07 +02:00
Cedric Verstraeten
bcfed04a07 add AGENT_TLS_INSECURE to enable Insecure TLS mode 2023-06-28 17:09:29 +02:00
Cedric Verstraeten
bf97bd72f1 add osusergo 2023-06-28 08:41:51 +02:00
Cedric Verstraeten
4b8b6bf66a fix balena links 2023-06-26 08:22:12 +02:00
Cedric Verstraeten
4b6c25bb85 documentation for balena apps 2023-06-25 23:25:42 +02:00
Cedric Verstraeten
729b38999e add deploy with balena 2023-06-25 22:14:23 +02:00
Cedric Verstraeten
4cbf0323f1 Reference separate balena repositories 2023-06-25 20:21:44 +02:00
Cedric Verstraeten
1f5cb8ca88 merge directories 2023-06-25 20:05:03 +02:00
Cedric Verstraeten
8be0a04502 add balena deployment (app + block) 2023-06-25 20:03:37 +02:00
Cedric Verstraeten
bdc0039a24 fix: might be empty if not set, so will never fire motion alert 2023-06-24 20:22:51 +02:00
Cedric Verstraeten
756b893ecd reference latest tags 2023-06-24 12:58:16 +02:00
Cedric Verstraeten
36323b076f Fix for Kerberos Vault persistence check 2023-06-23 21:13:13 +02:00
Cedric Verstraeten
d2dd3dfa62 add outputconfiguration + change endpoint 2023-06-21 15:55:51 +02:00
97 changed files with 8806 additions and 2934 deletions

View File

@@ -5,7 +5,7 @@ version: 2
jobs:
machinery:
docker:
- image: kerberos/base:91ab4d4
- image: kerberos/base:0a50dc9
working_directory: /go/src/github.com/{{ORG_NAME}}/{{REPO_NAME}}
steps:
- checkout

View File

@@ -1,2 +1,2 @@
FROM kerberos/devcontainer:b2bc659
FROM kerberos/devcontainer:0a50dc9
LABEL AUTHOR=Kerberos.io

View File

@@ -18,7 +18,7 @@ jobs:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Checkout
uses: actions/checkout@v3
run: git clone https://github.com/kerberos-io/agent && cd agent
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
@@ -26,9 +26,9 @@ jobs:
- name: Available platforms
run: echo ${{ steps.buildx.outputs.platforms }}
- name: Run Buildx
run: docker buildx build --platform linux/${{matrix.architecture}} -t kerberos/agent-nightly:arch-$(echo ${{matrix.architecture}} | tr / -)-$(echo $GITHUB_SHA | cut -c1-7) --push .
run: cd agent && docker buildx build --platform linux/${{matrix.architecture}} -t kerberos/agent-nightly:arch-$(echo ${{matrix.architecture}} | tr / -)-$(echo $GITHUB_SHA | cut -c1-7) --push .
- name: Create new and append to manifest
run: docker buildx imagetools create -t kerberos/agent-nightly:$(echo $GITHUB_SHA | cut -c1-7) kerberos/agent-nightly:arch-$(echo ${{matrix.architecture}} | tr / -)-$(echo $GITHUB_SHA | cut -c1-7)
run: cd agent && docker buildx imagetools create -t kerberos/agent-nightly:$(echo $GITHUB_SHA | cut -c1-7) kerberos/agent-nightly:arch-$(echo ${{matrix.architecture}} | tr / -)-$(echo $GITHUB_SHA | cut -c1-7)
build-other:
runs-on: ubuntu-latest
strategy:
@@ -41,7 +41,7 @@ jobs:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Checkout
uses: actions/checkout@v3
run: git clone https://github.com/kerberos-io/agent && cd agent
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
@@ -49,6 +49,6 @@ jobs:
- name: Available platforms
run: echo ${{ steps.buildx.outputs.platforms }}
- name: Run Buildx
run: docker buildx build --platform linux/${{matrix.architecture}} -t kerberos/agent-nightly:arch-$(echo ${{matrix.architecture}} | tr / -)-$(echo $GITHUB_SHA | cut -c1-7) --push .
run: cd agent && docker buildx build --platform linux/${{matrix.architecture}} -t kerberos/agent-nightly:arch-$(echo ${{matrix.architecture}} | tr / -)-$(echo $GITHUB_SHA | cut -c1-7) --push .
- name: Create new and append to manifest
run: docker buildx imagetools create --append -t kerberos/agent-nightly:$(echo $GITHUB_SHA | cut -c1-7) kerberos/agent-nightly:arch-$(echo ${{matrix.architecture}} | tr / -)-$(echo $GITHUB_SHA | cut -c1-7)
run: cd agent && docker buildx imagetools create --append -t kerberos/agent-nightly:$(echo $GITHUB_SHA | cut -c1-7) kerberos/agent-nightly:arch-$(echo ${{matrix.architecture}} | tr / -)-$(echo $GITHUB_SHA | cut -c1-7)

View File

@@ -43,6 +43,7 @@ jobs:
run: docker buildx build --platform linux/$(echo ${{matrix.architecture}} | tr - /) -t $REPO-arch:arch-$(echo ${{matrix.architecture}} | tr / -)-${{steps.short-sha.outputs.sha}} --output type=tar,dest=output-${{matrix.architecture}}.tar .
- name: Strip binary
run: mkdir -p output/ && tar -xf output-${{matrix.architecture}}.tar -C output && rm output-${{matrix.architecture}}.tar && cd output/ && tar -cf ../agent-${{matrix.architecture}}.tar -C home/agent . && rm -rf output
# We'll make a GitHub release and push the build (tar) as an artifact
- uses: rickstaa/action-create-tag@v1
with:
tag: ${{ steps.short-sha.outputs.sha }}
@@ -54,6 +55,17 @@ jobs:
name: ${{ steps.short-sha.outputs.sha }}
tag: ${{ steps.short-sha.outputs.sha }}
artifacts: "agent-${{matrix.architecture}}.tar"
# Taken from GoReleaser's own release workflow.
# The available Snapcraft Action has some bugs described in the issue below.
# The mkdirs are a hack for https://github.com/goreleaser/goreleaser/issues/1715.
#- name: Setup Snapcraft
# run: |
# sudo apt-get update
# sudo apt-get -yq --no-install-suggests --no-install-recommends install snapcraft
# mkdir -p $HOME/.cache/snapcraft/download
# mkdir -p $HOME/.cache/snapcraft/stage-packages
#- name: Use Snapcraft
# run: tar -xf agent-${{matrix.architecture}}.tar && snapcraft
build-other:
runs-on: ubuntu-latest
permissions:

View File

@@ -7,17 +7,17 @@ on:
branches: [ develop, master ]
jobs:
build:
name: Build
runs-on: ubuntu-latest
container:
image: kerberos/base:70d69dc
image: kerberos/base:0a50dc9
strategy:
matrix:
go-version: [1.17, 1.18, 1.19]
#No longer supported Go versions.
#go-version: ['1.17', '1.18', '1.19']
go-version: ['1.20', '1.21']
steps:
- name: Set up Go ${{ matrix.go-version }}
@@ -25,7 +25,9 @@ jobs:
with:
go-version: ${{ matrix.go-version }}
- name: Check out code into the Go module directory
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Set up git ownershi
run: git config --system --add safe.directory /__w/agent/agent
- name: Get dependencies
run: cd machinery && go mod download
- name: Build

View File

@@ -1,5 +1,5 @@
FROM kerberos/base:dc12d68 AS build-machinery
FROM kerberos/base:0a50dc9 AS build-machinery
LABEL AUTHOR=Kerberos.io
ENV GOROOT=/usr/local/go
@@ -10,7 +10,7 @@ ENV GOSUMDB=off
##########################################
# Installing some additional dependencies.
RUN apt-get update && apt-get install -y --no-install-recommends \
RUN apt-get upgrade -y && apt-get update && apt-get install -y --fix-missing --no-install-recommends \
git build-essential cmake pkg-config unzip libgtk2.0-dev \
curl ca-certificates libcurl4-openssl-dev libssl-dev libjpeg62-turbo-dev && \
rm -rf /var/lib/apt/lists/*
@@ -20,6 +20,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
RUN mkdir -p /go/src/github.com/kerberos-io/agent
COPY machinery /go/src/github.com/kerberos-io/agent/machinery
RUN rm -rf /go/src/github.com/kerberos-io/agent/machinery/.env
##################################################################
# Get the latest commit hash, so we know which version we're running
@@ -32,7 +33,7 @@ RUN cat /go/src/github.com/kerberos-io/agent/machinery/version
RUN cd /go/src/github.com/kerberos-io/agent/machinery && \
go mod download && \
go build -tags timetzdata,netgo --ldflags '-s -w -extldflags "-static -latomic"' main.go && \
go build -tags timetzdata,netgo,osusergo --ldflags '-s -w -extldflags "-static -latomic"' main.go && \
mkdir -p /agent && \
mv main /agent && \
mv version /agent && \
@@ -147,4 +148,4 @@ HEALTHCHECK CMD curl --fail http://localhost:80 || exit 1
# Leeeeettttt'ssss goooooo!!!
# Run the shizzle from the right working directory.
WORKDIR /home/agent
CMD ["./main", "run", "opensource", "80"]
CMD ["./main", "-action", "run", "-port", "80"]

132
README.md
View File

@@ -18,6 +18,7 @@
[![donate](https://brianmacdonald.github.io/Ethonate/svg/eth-donate-blue.svg)](https://brianmacdonald.github.io/Ethonate/address#0xf4a759C9436E2280Ea9cdd23d3144D95538fF4bE)
<a target="_blank" href="https://twitter.com/kerberosio?ref_src=twsrc%5Etfw"><img src="https://img.shields.io/twitter/url.svg?label=Follow%20%40kerberosio&style=social&url=https%3A%2F%2Ftwitter.com%2Fkerberosio" alt="Twitter Widget"></a>
[![Discord Shield](https://discordapp.com/api/guilds/1039619181731135499/widget.png?style=shield)](https://discord.gg/Bj77Vqfp2G)
[![kerberosio](https://snapcraft.io/kerberosio/badge.svg)](https://snapcraft.io/kerberosio)
[**Docker Hub**](https://hub.docker.com/r/kerberos/agent) | [**Documentation**](https://doc.kerberos.io) | [**Website**](https://kerberos.io) | [**View Demo**](https://demo.kerberos.io)
@@ -27,8 +28,8 @@ Kerberos Agent is an isolated and scalable video (surveillance) management agent
## :thinking: Prerequisites
- An IP camera which supports a RTSP H264 encoded stream,
- (or) a USB camera, Raspberry Pi camera or other camera, that [you can tranform to a valid RTSP H264 stream](https://github.com/kerberos-io/camera-to-rtsp).
- An IP camera which supports a RTSP H264 or H265 encoded stream,
- (or) a USB camera, Raspberry Pi camera or other camera, that [you can transform to a valid RTSP H264 or H265 stream](https://github.com/kerberos-io/camera-to-rtsp).
- Any hardware (ARMv6, ARMv7, ARM64, AMD) that can run a binary or container, for example: a Raspberry Pi, NVidia Jetson, Intel NUC, a VM, Bare metal machine or a full blown Kubernetes cluster.
## :video_camera: Is my camera working?
@@ -41,30 +42,36 @@ There are a myriad of cameras out there (USB, IP and other cameras), and it migh
1. [Quickstart - Docker](#quickstart---docker)
2. [Quickstart - Balena](#quickstart---balena)
3. [Quickstart - Snap](#quickstart---snap)
### Introduction
3. [A world of Kerberos Agents](#a-world-of-kerberos-agents)
1. [A world of Kerberos Agents](#a-world-of-kerberos-agents)
### Running and automation
4. [How to run and deploy a Kerberos Agent](#how-to-run-and-deploy-a-kerberos-agent)
5. [Access the Kerberos Agent](#access-the-kerberos-agent)
6. [Configure and persist with volume mounts](#configure-and-persist-with-volume-mounts)
7. [Configure with environment variables](#configure-with-environment-variables)
1. [How to run and deploy a Kerberos Agent](#how-to-run-and-deploy-a-kerberos-agent)
2. [Access the Kerberos Agent](#access-the-kerberos-agent)
3. [Configure and persist with volume mounts](#configure-and-persist-with-volume-mounts)
4. [Configure with environment variables](#configure-with-environment-variables)
### Insights
1. [Encryption](#encryption)
2. [H264 vs H265](#h264-vs-h265)
### Contributing
8. [Contribute with Codespaces](#contribute-with-codespaces)
9. [Develop and build](#develop-and-build)
10. [Building from source](#building-from-source)
11. [Building for Docker](#building-for-docker)
1. [Contribute with Codespaces](#contribute-with-codespaces)
2. [Develop and build](#develop-and-build)
3. [Building from source](#building-from-source)
4. [Building for Docker](#building-for-docker)
### Varia
12. [Support our project](#support-our-project)
13. [What is new?](#what-is-new)
14. [Contributors](#contributors)
1. [Support our project](#support-our-project)
1. [What is new?](#what-is-new)
1. [Contributors](#contributors)
## Quickstart - Docker
@@ -78,12 +85,19 @@ If you want to connect to an USB or Raspberry Pi camera, [you'll need to run our
## Quickstart - Balena
Run Kerberos Agent with Balena super powers. Monitor your agent with seamless remote access, and an encrypted https endpoint.
Checkout our fleet on [Balena Hub](https://hub.balena.io/fleets?0%5B0%5D%5Bn%5D=any&0%5B0%5D%5Bo%5D=full_text_search&0%5B0%5D%5Bv%5D=agent), and add your agent.
Run Kerberos Agent with [Balena Cloud](https://www.balena.io/) super powers. Monitor your Kerberos Agent with seamless remote access, over the air updates, an encrypted public `https` endpoint and many more. Checkout our application `video-surveillance` on [Balena Hub](https://hub.balena.io/apps/2064752/video-surveillance), and create your first or fleet of Kerberos Agent(s).
[![balena deploy button](https://www.balena.io/deploy.svg)](https://dashboard.balena-cloud.com/deploy?repoUrl=https://github.com/kerberos-io/agent)
[![deploy with balena](https://balena.io/deploy.svg)](https://dashboard.balena-cloud.com/deploy?repoUrl=https://github.com/kerberos-io/balena-agent)
**_Work In Progress_** - Currently we only support IP and USB Cameras, we have [an approach for leveraging the Raspberry Pi camera](https://github.com/kerberos-io/camera-to-rtsp), but this isn't working as expected with Balena. If you require this, you'll need to use the traditional Docker deployment with sidecar as mentioned above.
## Quickstart - Snap
Run Kerberos Agent with our [Snapcraft package](https://snapcraft.io/kerberosio).
snap install kerberosio
Once installed you can find your Kerberos Agent configration at `/var/snap/kerberosio/common`. Run the Kerberos Agent as following
sudo kerberosio.agent -action=run -port=80
## A world of Kerberos Agents
@@ -95,17 +109,21 @@ This repository contains everything you'll need to know about our core product,
- Low memory and CPU usage.
- Simplified and modern user interface.
- Multi architecture (ARMv7, ARMv8, amd64, etc).
- Multi camera support: IP Cameras (H264), USB cameras and Raspberry Pi Cameras [through a RTSP proxy](https://github.com/kerberos-io/camera-to-rtsp).
- Multi architecture (ARMv7, ARMv8, amd64, etc).).
- Multi stream, for example recording in H265, live streaming and motion detection in H264.
- Multi camera support: IP Cameras (H264 and H265), USB cameras and Raspberry Pi Cameras [through a RTSP proxy](https://github.com/kerberos-io/camera-to-rtsp).
- Single camera per instance (e.g. one container per camera).
- Primary and secondary stream setup (record full-res, stream low-res).
- Low resolution streaming through MQTT and full resolution streaming through WebRTC.
- Ability to specifiy conditions: offline mode, motion region, time table, continuous recording, etc.
- Post- and pre-recording on motion detection.
- Ability to create fragmented recordings, and streaming though HLS fMP4.
- Low resolution streaming through MQTT and high resolution streaming through WebRTC (only supports H264/PCM).
- Backchannel audio from Kerberos Hub to IP camera (requires PCM ULAW codec)
- Audio (AAC) and video (H264/H265) recording in MP4 container.
- End-to-end encryption through MQTT using RSA and AES (livestreaming, ONVIF, remote configuration, etc)
- Conditional recording: offline mode, motion region, time table, continuous recording, webhook condition etc.
- Post- and pre-recording for motion detection.
- Encryption at rest using AES-256-CBC.
- Ability to create fragmented recordings, and streaming through HLS fMP4.
- [Deploy where you want](#how-to-run-and-deploy-a-kerberos-agent) with the tools you use: `docker`, `docker compose`, `ansible`, `terraform`, `kubernetes`, etc.
- Cloud storage/persistance: Kerberos Hub, Kerberos Vault and Dropbox. [(WIP: Minio, Storj, Google Drive, FTP etc.)](https://github.com/kerberos-io/agent/issues/95)
- WIP: Integrations (Webhooks, MQTT, Script, etc).
- Outputs: trigger an integration (Webhooks, MQTT, Script, etc) when a specific event (motion detection or start recording ) occurs
- REST API access and documentation through Swagger (trigger recording, update configuration, etc).
- MIT License
@@ -122,6 +140,8 @@ We have documented the different deployment models [in the `deployments` directo
- [Red Hat OpenShift with Ansible](https://github.com/kerberos-io/agent/tree/master/deployments#4-red-hat-ansible-and-openshift)
- [Terraform](https://github.com/kerberos-io/agent/tree/master/deployments#5-terraform)
- [Salt](https://github.com/kerberos-io/agent/tree/master/deployments#6-salt)
- [Balena](https://github.com/kerberos-io/agent/tree/master/deployments#8-balena)
- [Snap](https://github.com/kerberos-io/agent/tree/master/deployments#9-snap)
By default your Kerberos Agents will store all its configuration and recordings inside the container. To help you automate and have a more consistent data governance, you can attach volumes to configure and persist data of your Kerberos Agents, and/or configure each Kerberos Agent through environment variables.
@@ -165,7 +185,10 @@ Next to attaching the configuration file, it is also possible to override the co
| Name | Description | Default Value |
| --------------------------------------- | ----------------------------------------------------------------------------------------------- | ------------------------------ |
| `LOG_LEVEL` | Level for logging, could be "info", "warning", "debug", "error" or "fatal". | "info" |
| `LOG_OUTPUT` | Logging output format "json" or "text". | "text" |
| `AGENT_MODE` | You can choose to run this in 'release' for production, and or 'demo' for showcasing. | "release" |
| `AGENT_TLS_INSECURE` | Specify if you want to use `InsecureSkipVerify` for the internal HTTP client. | "false" |
| `AGENT_USERNAME` | The username used to authenticate against the Kerberos Agent login page. | "root" |
| `AGENT_PASSWORD` | The password used to authenticate against the Kerberos Agent login page. | "root" |
| `AGENT_KEY` | A unique identifier for your Kerberos Agent, this is auto-generated but can be overriden. | "" |
@@ -184,8 +207,11 @@ Next to attaching the configuration file, it is also possible to override the co
| `AGENT_CAPTURE_IPCAMERA_ONVIF_XADDR` | ONVIF endpoint/address running on the camera. | "" |
| `AGENT_CAPTURE_IPCAMERA_ONVIF_USERNAME` | ONVIF username to authenticate against. | "" |
| `AGENT_CAPTURE_IPCAMERA_ONVIF_PASSWORD` | ONVIF password to authenticate against. | "" |
| `AGENT_CAPTURE_MOTION` | Toggle for enabling or disabling motion. | "true" |
| `AGENT_CAPTURE_LIVEVIEW` | Toggle for enabling or disabling liveview. | "true" |
| `AGENT_CAPTURE_SNAPSHOTS` | Toggle for enabling or disabling snapshot generation. | "true" |
| `AGENT_CAPTURE_RECORDING` | Toggle for enabling making recordings. | "true" |
| `AGENT_CAPTURE_CONTINUOUS` | Toggle for enabling continuous or motion based recording. | "false" |
| `AGENT_CAPTURE_CONTINUOUS` | Toggle for enabling continuous "true" or motion "false". | "false" |
| `AGENT_CAPTURE_PRERECORDING` | If `CONTINUOUS` set to `false`, specify the recording time (seconds) before after motion event. | "10" |
| `AGENT_CAPTURE_POSTRECORDING` | If `CONTINUOUS` set to `false`, specify the recording time (seconds) after motion event. | "20" |
| `AGENT_CAPTURE_MAXLENGTH` | The maximum length of a single recording (seconds). | "30" |
@@ -212,6 +238,48 @@ Next to attaching the configuration file, it is also possible to override the co
| `AGENT_KERBEROSVAULT_DIRECTORY` | The directory, in the provider, where the recordings will be stored in. | "" |
| `AGENT_DROPBOX_ACCESS_TOKEN` | The Access Token from your Dropbox app, that is used to leverage the Dropbox SDK. | "" |
| `AGENT_DROPBOX_DIRECTORY` | The directory, in the provider, where the recordings will be stored in. | "" |
| `AGENT_ENCRYPTION` | Enable 'true' or disable 'false' end-to-end encryption for MQTT messages. | "false" |
| `AGENT_ENCRYPTION_RECORDINGS` | Enable 'true' or disable 'false' end-to-end encryption for recordings. | "false" |
| `AGENT_ENCRYPTION_FINGERPRINT` | The fingerprint of the keypair (public/private keys), so you know which one to use. | "" |
| `AGENT_ENCRYPTION_PRIVATE_KEY` | The private key (assymetric/RSA) to decryptand sign requests send over MQTT. | "" |
| `AGENT_ENCRYPTION_SYMMETRIC_KEY` | The symmetric key (AES) to encrypt and decrypt request send over MQTT. | "" |
## Encryption
You can encrypt your recordings and outgoing MQTT messages with your own AES and RSA keys by enabling the encryption settings. Once enabled all your recordings will be encrypted using AES-256-CBC and your symmetric key. You can either use the default `openssl` toolchain to decrypt the recordings with your AES key, as following:
openssl aes-256-cbc -d -md md5 -in encrypted.mp4 -out decrypted.mp4 -k your-key-96ab185xxxxxxxcxxxxxxxx6a59c62e8
, and additionally you can decrypt a folder of recordings, using the Kerberos Agent binary as following:
go run main.go -action decrypt ./data/recordings your-key-96ab185xxxxxxxcxxxxxxxx6a59c62e8
or for a single file:
go run main.go -action decrypt ./data/recordings/video.mp4 your-key-96ab185xxxxxxxcxxxxxxxx6a59c62e8
## H264 vs H265
If we talk about video encoders and decoders (codecs) there are 2 major video codecs on the market: H264 and H265. Taking into account your use case, you might use one over the other. We will provide an (not complete) overview of the advantages and disadvantages of each codec in the field of video surveillance and video analytics. If you would like to know more, you should look for additional resources on the internet (or if you like to read physical items, books still exists nowadays).
- H264 (also known as AVC or MPEG-4 Part 10)
- Is the most common one and most widely supported for IP cameras.
- Supported in the majority of browsers, operating system and third-party applications.
- Can be embedded in commercial and 3rd party applications.
- Different levels of compression (high, medium, low, ..)
- Better quality / compression ratio, shows less artifacts at medium compression ratios.
- Does support technologies such as WebRTC
- H265 (also known as HEVC)
- Is not supported on legacy cameras, though becoming rapidly available on "newer" IP cameras.
- Might not always be supported due to licensing. For example not supported in browers on a Linux distro.
- Requires licensing when embedding in a commercial product (be careful).
- Higher levels of compression (50% more than H264).
- H265 shows artifacts in motion based environments (which is less with H264).
- Recording the same video (resolution, duration and FPS) in H264 and H265 will result in approx 50% the file size.
- Not supported in technologies such as WebRTC
Conclusion: depending on the use case you might choose one over the other, and you can use both at the same time. For example you can use H264 (main stream) for livestreaming, and H265 (sub stream) for recording. If you wish to play recordings in a cross-platform and cross-browser environment, you might opt for H264 for better support.
## Contribute with Codespaces
@@ -234,9 +302,9 @@ On opening of the GitHub Codespace, some dependencies will be installed. Once th
const dev = {
ENV: 'dev',
HOSTNAME: externalHost,
//API_URL: `${protocol}//${hostname}:8080/api`,
//URL: `${protocol}//${hostname}:8080`,
//WS_URL: `${websocketprotocol}//${hostname}:8080/ws`,
//API_URL: `${protocol}//${hostname}:80/api`,
//URL: `${protocol}//${hostname}:80`,
//WS_URL: `${websocketprotocol}//${hostname}:80/ws`,
// Uncomment, and comment the above lines, when using codespaces or other special DNS names (which you can't control)
API_URL: `${protocol}//${externalHost}/api`,
@@ -249,7 +317,7 @@ Go and open two terminals one for the `ui` project and one for the `machinery` p
1. Terminal A:
cd machinery/
go run main.go run camera 80
go run main.go -action run -port 80
2. Terminal B:
@@ -290,7 +358,7 @@ You can simply run the `machinery` using following commands.
git clone https://github.com/kerberos-io/agent
cd machinery
go run main.go run mycameraname 80
go run main.go -action run -port 80
This will launch the Kerberos Agent and run a webserver on port `80`. You can change the port by your own preference. We strongly support the usage of [Goland](https://www.jetbrains.com/go/) or [Visual Studio Code](https://code.visualstudio.com/), as it comes with all the debugging and linting features builtin.

View File

@@ -14,6 +14,7 @@ We will discuss following deployment models.
- [5. Kerberos Factory](#5-kerberos-factory)
- [6. Terraform](#6-terraform)
- [7. Salt](#7-salt)
- [8. Balena](#8-balena)
## 0. Static binary
@@ -53,8 +54,26 @@ All of the previously deployments, `docker`, `kubernetes` and `openshift` are gr
## 6. Terraform
To be written
Terraform is a tool for infrastructure provisioning to build infrastructure through code, often called Infrastructure as Code. So, Terraform allows you to automate and manage your infrastructure, your platform, and the services that run on that platform. By using Terraform you can deploy your Kerberos Agents remotely at scale.
> Learn more [about Kerberos Agent with Terraform](https://github.com/kerberos-io/agent/tree/master/deployments/terraform).
## 7. Salt
To be written
## 8. Balena
Balena Cloud provide a seamless way of building and deploying applications at scale through the conceps of `blocks`, `apps` and `fleets`. Once you have your `app` deployed, for example our Kerberos Agent, you can benefit from features such as: remote access, over the air updates, an encrypted public `https` endpoint and many more.
Together with the Balena.io team we've build a Balena App, called [`video-surveillance`](https://hub.balena.io/apps/2064752/video-surveillance), which any can use to deploy a video surveillance system in a matter of minutes with all the expected management features you can think of.
> Learn more [about Kerberos Agent with Balena](https://github.com/kerberos-io/agent/tree/master/deployments/balena).
## 9. Snap
The Snap Store, also known as the Ubuntu Store , is a commercial centralized software store operated by Canonical. Similar to AppImage or Flatpak the Snap Store is able to provide up to date software no matter what version of Linux you are running and how old your libraries are.
We have published our own snap `Kerberos Agent` on the Snap Store, allowing you to seamless install a Kerberos Agent on your Linux devive.
> Learn more [about Kerberos Agent with Snap](https://github.com/kerberos-io/agent/tree/master/deployments/snap).

View File

@@ -82,7 +82,7 @@
initContainers:
- name: download-config
image: kerberos/agent:1b96d01
image: kerberos/agent:latest
volumeMounts:
- name: kerberos-data
mountPath: /home/agent/data/config
@@ -96,7 +96,7 @@
containers:
- name: agent
image: kerberos/agent:1b96d01
image: kerberos/agent:latest
volumeMounts:
- name: kerberos-data
mountPath: /home/agent/data/config

View File

@@ -0,0 +1,31 @@
# Deployment with Balena
Balena Cloud provide a seamless way of building and deploying applications at scale through the conceps of `blocks`, `apps` and `fleets`. Once you have your `app` deployed, for example our Kerberos Agent, you can benefit from features such as: remote access, over the air updates, an encrypted public `https` endpoint and many more.
We provide two mechanisms to deploy Kerberos Agent to a Balena Cloud fleet:
1. Use Kerberos Agent as [a block part of your application](https://github.com/kerberos-io/balena-agent-block).
2. Use Kerberos Agent as [a stand-alone application](https://github.com/kerberos-io/balena-agent).
## Block
Within Balena you can build the concept of a block, which is the equivalent of container image or a function in a typical programming language. The idea of blocks, you can find a more thorough explanation [here](https://docs.balena.io/learn/develop/blocks/), is that you can compose and combine multiple `blocks` to level up to the concept an `app`.
You as a developer can choose which `blocks` you would like to use, to build the desired `application` state you prefer. For example you can use the [Kerberos Agent block](https://hub.balena.io/blocks/2064662/agent) to compose a video surveillance system as part of your existing set of blocks.
You can the `Kerberos Agent` block by defining following elements in your `compose` file.
agent:
image: bh.cr/kerberos_io/agent
## App
Next to building individual `blocks` you as a developer can also decide to build up an application, composed of one or more `blocks` or third-party containers, and publish it as an `app` to the Balena Hub. This is exactly [what we've done..](https://hub.balena.io/apps/2064752/video-surveillance)
On Balena Hub we have created the []`video-surveillance` application](https://hub.balena.io/apps/2064752/video-surveillance) that utilises the [Kerberos Agent `block`](https://hub.balena.io/blocks/2064662/agent). The idea of this application is that utilises the foundation of our Kerberos Agent, but that it might include more `blocks` over time to increase and improve functionalities from other community projects.
To deploy the application you can simply press below `Deploy button` or you can navigate to the [Balena Hub apps page](https://hub.balena.io/apps/2064752/video-surveillance).
[![deploy with balena](https://balena.io/deploy.svg)](https://dashboard.balena-cloud.com/deploy?repoUrl=https://github.com/kerberos-io/agent)
You can find the source code, `balena.yaml` and `docker-compose.yaml` files in the [`balena-agent` repository](https://github.com/kerberos-io/balena-agent).

View File

@@ -9,7 +9,7 @@ Kerberos Agents are now also shipped as static binaries. Within the Docker image
You can run the binary as following on port `8080`:
main run cameraname 8080
main -action=run -port=80
## Systemd
@@ -18,7 +18,7 @@ When running on a Linux OS you might consider to auto-start the Kerberos Agent u
[Unit]
Wants=network.target
[Service]
ExecStart=/home/pi/agent/main run camera 80
ExecStart=/home/pi/agent/main -action=run -port=80
WorkingDirectory=/home/pi/agent/
[Install]
WantedBy=multi-user.target

View File

@@ -21,7 +21,7 @@ spec:
initContainers:
- name: download-config
image: kerberos/agent:1b96d01
image: kerberos/agent:latest
volumeMounts:
- name: kerberos-data
mountPath: /home/agent/data/config

View File

@@ -0,0 +1,15 @@
# Deployment with Snap Store
By browsing to the Snap Store, you'll be able [to find our own snap `Kerberos Agent`](https://snapcraft.io/kerberosio). You can either install the `Kerberos Agent` through the command line.
snap install kerberosio
Or use the Desktop client to have a visual interface.
![Kerberos Agent on Snap Store](./snapstore.png)
Once installed you can find your Kerberos Agent configration at `/var/snap/kerberosio/common`. Run the Kerberos Agent as following.
sudo kerberosio.agent -action=run -port=80
If successfull you'll be able to browse to port `80` or if you defined a different port. This will open the Kerberos Agent interface.

Binary file not shown.

After

Width:  |  Height:  |  Size: 616 KiB

View File

@@ -0,0 +1,41 @@
# Deployment with Terraform
If you are using Terraform as part of your DevOps stack, you might utilise it to deploy your Kerberos Agents. Within this deployment folder we have added an example Terraform file `docker.tf`, which installs the Kerberos Agent `docker` container on a remote system over `SSH`. We might create our own provider in the future, or add additional examples for example `snap`, `kubernetes`, etc.
For this example we will install Kerberos Agent using `docker` on a remote `linux` machine. Therefore we'll make sure we have the `TelkomIndonesia/linux` provider initialised.
terraform init
Once initialised you should see similar output:
Initializing the backend...
Initializing provider plugins...
- Reusing previous version of telkomindonesia/linux from the dependency lock file
- Using previously-installed telkomindonesia/linux v0.7.0
Go and open the `docker.tf` file and locate the `linux` provider, modify following credentials accordingly. Make sure they match for creating an `SSH` connection.
provider "linux" {
host = "x.y.z.u"
port = 22
user = "root"
password = "password"
}
Apply the `docker.tf` file, to install `docker` and the `kerberos/agent` docker container.
terraform apply
Once done you should see following output, and you should be able to reach the remote machine on port `80` or if configured differently the specified port you've defined.
Do you want to perform these actions?
Terraform will perform the actions described above.
Only 'yes' will be accepted to approve.
Enter a value: yes
linux_script.install_docker_kerberos_agent: Modifying... [id=a56cf7b0-db66-4f9b-beec-8a4dcef2a0c7]
linux_script.install_docker_kerberos_agent: Modifications complete after 3s [id=a56cf7b0-db66-4f9b-beec-8a4dcef2a0c7]
Apply complete! Resources: 0 added, 1 changed, 0 destroyed.

View File

@@ -0,0 +1,47 @@
terraform {
required_providers {
linux = {
source = "TelkomIndonesia/linux"
version = "0.7.0"
}
}
}
provider "linux" {
host = "x.y.z.u"
port = 22
user = "root"
password = "password"
}
locals {
image = "kerberos/agent"
version = "latest"
port = 80
}
resource "linux_script" "install_docker" {
lifecycle_commands {
create = "apt update && apt install -y $PACKAGE_NAME"
read = "apt-cache policy $PACKAGE_NAME | grep 'Installed:' | grep -v '(none)' | awk '{ print $2 }' | xargs | tr -d '\n'"
update = "apt update && apt install -y $PACKAGE_NAME"
delete = "apt remove -y $PACKAGE_NAME"
}
environment = {
PACKAGE_NAME = "docker"
}
}
resource "linux_script" "install_docker_kerberos_agent" {
lifecycle_commands {
create = "docker pull $IMAGE:$VERSION && docker run -d -p $PORT:80 --name agent $IMAGE:$VERSION"
read = "docker inspect agent"
update = "docker pull $IMAGE:$VERSION && docker rm agent --force && docker run -d -p $PORT:80 --name agent $IMAGE:$VERSION"
delete = "docker rm agent --force"
}
environment = {
IMAGE = local.image
VERSION = local.version
PORT = local.port
}
}

View File

@@ -10,7 +10,7 @@
"request": "launch",
"mode": "auto",
"program": "main.go",
"args": ["run", "cameraname", "8080"],
"args": ["-action", "run"],
"envFile": "${workspaceFolder}/.env",
"buildFlags": "--tags dynamic",
},

View File

@@ -111,5 +111,6 @@
"hub_key": "",
"hub_private_key": "",
"hub_site": "",
"condition_uri": ""
"condition_uri": "",
"encryption": {}
}

View File

@@ -29,7 +29,7 @@ const docTemplate = `{
"post": {
"description": "Will return the ONVIF capabilities for the specific camera.",
"tags": [
"camera"
"onvif"
],
"summary": "Will return the ONVIF capabilities for the specific camera.",
"operationId": "camera-onvif-capabilities",
@@ -54,11 +54,74 @@ const docTemplate = `{
}
}
},
"/api/camera/onvif/gotopreset": {
"post": {
"description": "Will activate the desired ONVIF preset.",
"tags": [
"onvif"
],
"summary": "Will activate the desired ONVIF preset.",
"operationId": "camera-onvif-gotopreset",
"parameters": [
{
"description": "OnvifPreset",
"name": "config",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/models.OnvifPreset"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/models.APIResponse"
}
}
}
}
},
"/api/camera/onvif/inputs": {
"post": {
"security": [
{
"Bearer": []
}
],
"description": "Will get the digital inputs from the ONVIF device.",
"tags": [
"onvif"
],
"summary": "Will get the digital inputs from the ONVIF device.",
"operationId": "get-digital-inputs",
"parameters": [
{
"description": "OnvifCredentials",
"name": "config",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/models.OnvifCredentials"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/models.APIResponse"
}
}
}
}
},
"/api/camera/onvif/login": {
"post": {
"description": "Try to login into ONVIF supported camera.",
"tags": [
"camera"
"onvif"
],
"summary": "Try to login into ONVIF supported camera.",
"operationId": "camera-onvif-login",
@@ -83,11 +146,86 @@ const docTemplate = `{
}
}
},
"/api/camera/onvif/outputs": {
"post": {
"security": [
{
"Bearer": []
}
],
"description": "Will get the relay outputs from the ONVIF device.",
"tags": [
"onvif"
],
"summary": "Will get the relay outputs from the ONVIF device.",
"operationId": "get-relay-outputs",
"parameters": [
{
"description": "OnvifCredentials",
"name": "config",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/models.OnvifCredentials"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/models.APIResponse"
}
}
}
}
},
"/api/camera/onvif/outputs/{output}": {
"post": {
"security": [
{
"Bearer": []
}
],
"description": "Will trigger the relay output from the ONVIF device.",
"tags": [
"onvif"
],
"summary": "Will trigger the relay output from the ONVIF device.",
"operationId": "trigger-relay-output",
"parameters": [
{
"description": "OnvifCredentials",
"name": "config",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/models.OnvifCredentials"
}
},
{
"type": "string",
"description": "Output",
"name": "output",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/models.APIResponse"
}
}
}
}
},
"/api/camera/onvif/pantilt": {
"post": {
"description": "Panning or/and tilting the camera using a direction (x,y).",
"tags": [
"camera"
"onvif"
],
"summary": "Panning or/and tilting the camera.",
"operationId": "camera-onvif-pantilt",
@@ -112,11 +250,74 @@ const docTemplate = `{
}
}
},
"/api/camera/onvif/presets": {
"post": {
"description": "Will return the ONVIF presets for the specific camera.",
"tags": [
"onvif"
],
"summary": "Will return the ONVIF presets for the specific camera.",
"operationId": "camera-onvif-presets",
"parameters": [
{
"description": "OnvifCredentials",
"name": "config",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/models.OnvifCredentials"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/models.APIResponse"
}
}
}
}
},
"/api/camera/onvif/verify": {
"post": {
"security": [
{
"Bearer": []
}
],
"description": "Will verify the ONVIF connectivity.",
"tags": [
"onvif"
],
"summary": "Will verify the ONVIF connectivity.",
"operationId": "verify-onvif",
"parameters": [
{
"description": "OnvifCredentials",
"name": "config",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/models.OnvifCredentials"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/models.APIResponse"
}
}
}
}
},
"/api/camera/onvif/zoom": {
"post": {
"description": "Zooming in or out the camera.",
"tags": [
"camera"
"onvif"
],
"summary": "Zooming in or out the camera.",
"operationId": "camera-onvif-zoom",
@@ -141,6 +342,90 @@ const docTemplate = `{
}
}
},
"/api/camera/record": {
"post": {
"description": "Make a recording.",
"tags": [
"camera"
],
"summary": "Make a recording.",
"operationId": "camera-record",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/models.APIResponse"
}
}
}
}
},
"/api/camera/restart": {
"post": {
"description": "Restart the agent.",
"tags": [
"camera"
],
"summary": "Restart the agent.",
"operationId": "camera-restart",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/models.APIResponse"
}
}
}
}
},
"/api/camera/snapshot/base64": {
"get": {
"description": "Get a snapshot from the camera in base64.",
"tags": [
"camera"
],
"summary": "Get a snapshot from the camera in base64.",
"operationId": "snapshot-base64",
"responses": {
"200": {
"description": ""
}
}
}
},
"/api/camera/snapshot/jpeg": {
"get": {
"description": "Get a snapshot from the camera in jpeg format.",
"tags": [
"camera"
],
"summary": "Get a snapshot from the camera in jpeg format.",
"operationId": "snapshot-jpeg",
"responses": {
"200": {
"description": ""
}
}
}
},
"/api/camera/stop": {
"post": {
"description": "Stop the agent.",
"tags": [
"camera"
],
"summary": "Stop the agent.",
"operationId": "camera-stop",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/models.APIResponse"
}
}
}
}
},
"/api/camera/verify/{streamType}": {
"post": {
"description": "This method will validate a specific profile connection from an RTSP camera, and try to get the codec.",
@@ -181,6 +466,75 @@ const docTemplate = `{
}
}
},
"/api/config": {
"get": {
"description": "Get the current configuration.",
"tags": [
"config"
],
"summary": "Get the current configuration.",
"operationId": "config",
"responses": {
"200": {
"description": ""
}
}
},
"post": {
"description": "Update the current configuration.",
"tags": [
"config"
],
"summary": "Update the current configuration.",
"operationId": "config",
"parameters": [
{
"description": "Configuration",
"name": "config",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/models.Config"
}
}
],
"responses": {
"200": {
"description": ""
}
}
}
},
"/api/dashboard": {
"get": {
"description": "Get all information showed on the dashboard.",
"tags": [
"general"
],
"summary": "Get all information showed on the dashboard.",
"operationId": "dashboard",
"responses": {
"200": {
"description": ""
}
}
}
},
"/api/days": {
"get": {
"description": "Get all days stored in the recordings directory.",
"tags": [
"general"
],
"summary": "Get all days stored in the recordings directory.",
"operationId": "days",
"responses": {
"200": {
"description": ""
}
}
}
},
"/api/hub/verify": {
"post": {
"security": [
@@ -190,7 +544,7 @@ const docTemplate = `{
],
"description": "Will verify the hub connectivity.",
"tags": [
"config"
"persistence"
],
"summary": "Will verify the hub connectivity.",
"operationId": "verify-hub",
@@ -215,6 +569,32 @@ const docTemplate = `{
}
}
},
"/api/latest-events": {
"post": {
"description": "Get the latest recordings (events) from the recordings directory.",
"tags": [
"general"
],
"summary": "Get the latest recordings (events) from the recordings directory.",
"operationId": "latest-events",
"parameters": [
{
"description": "Event filter",
"name": "eventFilter",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/models.EventFilter"
}
}
],
"responses": {
"200": {
"description": ""
}
}
}
},
"/api/login": {
"post": {
"description": "Get Authorization token.",
@@ -244,40 +624,6 @@ const docTemplate = `{
}
}
},
"/api/onvif/verify": {
"post": {
"security": [
{
"Bearer": []
}
],
"description": "Will verify the ONVIF connectivity.",
"tags": [
"config"
],
"summary": "Will verify the ONVIF connectivity.",
"operationId": "verify-onvif",
"parameters": [
{
"description": "Camera Config",
"name": "cameraConfig",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/models.IPCamera"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/models.APIResponse"
}
}
}
}
},
"/api/persistence/verify": {
"post": {
"security": [
@@ -287,7 +633,7 @@ const docTemplate = `{
],
"description": "Will verify the persistence.",
"tags": [
"config"
"persistence"
],
"summary": "Will verify the persistence.",
"operationId": "verify-persistence",
@@ -317,8 +663,15 @@ const docTemplate = `{
"models.APIResponse": {
"type": "object",
"properties": {
"can_pan_tilt": {
"type": "boolean"
},
"can_zoom": {
"type": "boolean"
},
"data": {},
"message": {}
"message": {},
"ptz_functions": {}
}
},
"models.Authentication": {
@@ -440,6 +793,9 @@ const docTemplate = `{
"dropbox": {
"$ref": "#/definitions/models.Dropbox"
},
"encryption": {
"$ref": "#/definitions/models.Encryption"
},
"friendly_name": {
"type": "string"
},
@@ -543,12 +899,49 @@ const docTemplate = `{
}
}
},
"models.Encryption": {
"type": "object",
"properties": {
"enabled": {
"type": "string"
},
"fingerprint": {
"type": "string"
},
"private_key": {
"type": "string"
},
"recordings": {
"type": "string"
},
"symmetric_key": {
"type": "string"
}
}
},
"models.EventFilter": {
"type": "object",
"properties": {
"number_of_elements": {
"type": "integer"
},
"timestamp_offset_end": {
"type": "integer"
},
"timestamp_offset_start": {
"type": "integer"
}
}
},
"models.IPCamera": {
"type": "object",
"properties": {
"fps": {
"type": "string"
},
"height": {
"type": "integer"
},
"onvif": {
"type": "string"
},
@@ -566,6 +959,9 @@ const docTemplate = `{
},
"sub_rtsp": {
"type": "string"
},
"width": {
"type": "integer"
}
}
},
@@ -621,6 +1017,17 @@ const docTemplate = `{
}
}
},
"models.OnvifPreset": {
"type": "object",
"properties": {
"onvif_credentials": {
"$ref": "#/definitions/models.OnvifCredentials"
},
"preset": {
"type": "string"
}
}
},
"models.OnvifZoom": {
"type": "object",
"properties": {

View File

@@ -21,7 +21,7 @@
"post": {
"description": "Will return the ONVIF capabilities for the specific camera.",
"tags": [
"camera"
"onvif"
],
"summary": "Will return the ONVIF capabilities for the specific camera.",
"operationId": "camera-onvif-capabilities",
@@ -46,11 +46,74 @@
}
}
},
"/api/camera/onvif/gotopreset": {
"post": {
"description": "Will activate the desired ONVIF preset.",
"tags": [
"onvif"
],
"summary": "Will activate the desired ONVIF preset.",
"operationId": "camera-onvif-gotopreset",
"parameters": [
{
"description": "OnvifPreset",
"name": "config",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/models.OnvifPreset"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/models.APIResponse"
}
}
}
}
},
"/api/camera/onvif/inputs": {
"post": {
"security": [
{
"Bearer": []
}
],
"description": "Will get the digital inputs from the ONVIF device.",
"tags": [
"onvif"
],
"summary": "Will get the digital inputs from the ONVIF device.",
"operationId": "get-digital-inputs",
"parameters": [
{
"description": "OnvifCredentials",
"name": "config",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/models.OnvifCredentials"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/models.APIResponse"
}
}
}
}
},
"/api/camera/onvif/login": {
"post": {
"description": "Try to login into ONVIF supported camera.",
"tags": [
"camera"
"onvif"
],
"summary": "Try to login into ONVIF supported camera.",
"operationId": "camera-onvif-login",
@@ -75,11 +138,86 @@
}
}
},
"/api/camera/onvif/outputs": {
"post": {
"security": [
{
"Bearer": []
}
],
"description": "Will get the relay outputs from the ONVIF device.",
"tags": [
"onvif"
],
"summary": "Will get the relay outputs from the ONVIF device.",
"operationId": "get-relay-outputs",
"parameters": [
{
"description": "OnvifCredentials",
"name": "config",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/models.OnvifCredentials"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/models.APIResponse"
}
}
}
}
},
"/api/camera/onvif/outputs/{output}": {
"post": {
"security": [
{
"Bearer": []
}
],
"description": "Will trigger the relay output from the ONVIF device.",
"tags": [
"onvif"
],
"summary": "Will trigger the relay output from the ONVIF device.",
"operationId": "trigger-relay-output",
"parameters": [
{
"description": "OnvifCredentials",
"name": "config",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/models.OnvifCredentials"
}
},
{
"type": "string",
"description": "Output",
"name": "output",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/models.APIResponse"
}
}
}
}
},
"/api/camera/onvif/pantilt": {
"post": {
"description": "Panning or/and tilting the camera using a direction (x,y).",
"tags": [
"camera"
"onvif"
],
"summary": "Panning or/and tilting the camera.",
"operationId": "camera-onvif-pantilt",
@@ -104,11 +242,74 @@
}
}
},
"/api/camera/onvif/presets": {
"post": {
"description": "Will return the ONVIF presets for the specific camera.",
"tags": [
"onvif"
],
"summary": "Will return the ONVIF presets for the specific camera.",
"operationId": "camera-onvif-presets",
"parameters": [
{
"description": "OnvifCredentials",
"name": "config",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/models.OnvifCredentials"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/models.APIResponse"
}
}
}
}
},
"/api/camera/onvif/verify": {
"post": {
"security": [
{
"Bearer": []
}
],
"description": "Will verify the ONVIF connectivity.",
"tags": [
"onvif"
],
"summary": "Will verify the ONVIF connectivity.",
"operationId": "verify-onvif",
"parameters": [
{
"description": "OnvifCredentials",
"name": "config",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/models.OnvifCredentials"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/models.APIResponse"
}
}
}
}
},
"/api/camera/onvif/zoom": {
"post": {
"description": "Zooming in or out the camera.",
"tags": [
"camera"
"onvif"
],
"summary": "Zooming in or out the camera.",
"operationId": "camera-onvif-zoom",
@@ -133,6 +334,90 @@
}
}
},
"/api/camera/record": {
"post": {
"description": "Make a recording.",
"tags": [
"camera"
],
"summary": "Make a recording.",
"operationId": "camera-record",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/models.APIResponse"
}
}
}
}
},
"/api/camera/restart": {
"post": {
"description": "Restart the agent.",
"tags": [
"camera"
],
"summary": "Restart the agent.",
"operationId": "camera-restart",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/models.APIResponse"
}
}
}
}
},
"/api/camera/snapshot/base64": {
"get": {
"description": "Get a snapshot from the camera in base64.",
"tags": [
"camera"
],
"summary": "Get a snapshot from the camera in base64.",
"operationId": "snapshot-base64",
"responses": {
"200": {
"description": ""
}
}
}
},
"/api/camera/snapshot/jpeg": {
"get": {
"description": "Get a snapshot from the camera in jpeg format.",
"tags": [
"camera"
],
"summary": "Get a snapshot from the camera in jpeg format.",
"operationId": "snapshot-jpeg",
"responses": {
"200": {
"description": ""
}
}
}
},
"/api/camera/stop": {
"post": {
"description": "Stop the agent.",
"tags": [
"camera"
],
"summary": "Stop the agent.",
"operationId": "camera-stop",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/models.APIResponse"
}
}
}
}
},
"/api/camera/verify/{streamType}": {
"post": {
"description": "This method will validate a specific profile connection from an RTSP camera, and try to get the codec.",
@@ -173,6 +458,75 @@
}
}
},
"/api/config": {
"get": {
"description": "Get the current configuration.",
"tags": [
"config"
],
"summary": "Get the current configuration.",
"operationId": "config",
"responses": {
"200": {
"description": ""
}
}
},
"post": {
"description": "Update the current configuration.",
"tags": [
"config"
],
"summary": "Update the current configuration.",
"operationId": "config",
"parameters": [
{
"description": "Configuration",
"name": "config",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/models.Config"
}
}
],
"responses": {
"200": {
"description": ""
}
}
}
},
"/api/dashboard": {
"get": {
"description": "Get all information showed on the dashboard.",
"tags": [
"general"
],
"summary": "Get all information showed on the dashboard.",
"operationId": "dashboard",
"responses": {
"200": {
"description": ""
}
}
}
},
"/api/days": {
"get": {
"description": "Get all days stored in the recordings directory.",
"tags": [
"general"
],
"summary": "Get all days stored in the recordings directory.",
"operationId": "days",
"responses": {
"200": {
"description": ""
}
}
}
},
"/api/hub/verify": {
"post": {
"security": [
@@ -182,7 +536,7 @@
],
"description": "Will verify the hub connectivity.",
"tags": [
"config"
"persistence"
],
"summary": "Will verify the hub connectivity.",
"operationId": "verify-hub",
@@ -207,6 +561,32 @@
}
}
},
"/api/latest-events": {
"post": {
"description": "Get the latest recordings (events) from the recordings directory.",
"tags": [
"general"
],
"summary": "Get the latest recordings (events) from the recordings directory.",
"operationId": "latest-events",
"parameters": [
{
"description": "Event filter",
"name": "eventFilter",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/models.EventFilter"
}
}
],
"responses": {
"200": {
"description": ""
}
}
}
},
"/api/login": {
"post": {
"description": "Get Authorization token.",
@@ -236,40 +616,6 @@
}
}
},
"/api/onvif/verify": {
"post": {
"security": [
{
"Bearer": []
}
],
"description": "Will verify the ONVIF connectivity.",
"tags": [
"config"
],
"summary": "Will verify the ONVIF connectivity.",
"operationId": "verify-onvif",
"parameters": [
{
"description": "Camera Config",
"name": "cameraConfig",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/models.IPCamera"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/models.APIResponse"
}
}
}
}
},
"/api/persistence/verify": {
"post": {
"security": [
@@ -279,7 +625,7 @@
],
"description": "Will verify the persistence.",
"tags": [
"config"
"persistence"
],
"summary": "Will verify the persistence.",
"operationId": "verify-persistence",
@@ -309,8 +655,15 @@
"models.APIResponse": {
"type": "object",
"properties": {
"can_pan_tilt": {
"type": "boolean"
},
"can_zoom": {
"type": "boolean"
},
"data": {},
"message": {}
"message": {},
"ptz_functions": {}
}
},
"models.Authentication": {
@@ -432,6 +785,9 @@
"dropbox": {
"$ref": "#/definitions/models.Dropbox"
},
"encryption": {
"$ref": "#/definitions/models.Encryption"
},
"friendly_name": {
"type": "string"
},
@@ -535,12 +891,49 @@
}
}
},
"models.Encryption": {
"type": "object",
"properties": {
"enabled": {
"type": "string"
},
"fingerprint": {
"type": "string"
},
"private_key": {
"type": "string"
},
"recordings": {
"type": "string"
},
"symmetric_key": {
"type": "string"
}
}
},
"models.EventFilter": {
"type": "object",
"properties": {
"number_of_elements": {
"type": "integer"
},
"timestamp_offset_end": {
"type": "integer"
},
"timestamp_offset_start": {
"type": "integer"
}
}
},
"models.IPCamera": {
"type": "object",
"properties": {
"fps": {
"type": "string"
},
"height": {
"type": "integer"
},
"onvif": {
"type": "string"
},
@@ -558,6 +951,9 @@
},
"sub_rtsp": {
"type": "string"
},
"width": {
"type": "integer"
}
}
},
@@ -613,6 +1009,17 @@
}
}
},
"models.OnvifPreset": {
"type": "object",
"properties": {
"onvif_credentials": {
"$ref": "#/definitions/models.OnvifCredentials"
},
"preset": {
"type": "string"
}
}
},
"models.OnvifZoom": {
"type": "object",
"properties": {

View File

@@ -2,8 +2,13 @@ basePath: /
definitions:
models.APIResponse:
properties:
can_pan_tilt:
type: boolean
can_zoom:
type: boolean
data: {}
message: {}
ptz_functions: {}
type: object
models.Authentication:
properties:
@@ -83,6 +88,8 @@ definitions:
type: string
dropbox:
$ref: '#/definitions/models.Dropbox'
encryption:
$ref: '#/definitions/models.Encryption'
friendly_name:
type: string
heartbeaturi:
@@ -151,10 +158,34 @@ definitions:
directory:
type: string
type: object
models.Encryption:
properties:
enabled:
type: string
fingerprint:
type: string
private_key:
type: string
recordings:
type: string
symmetric_key:
type: string
type: object
models.EventFilter:
properties:
number_of_elements:
type: integer
timestamp_offset_end:
type: integer
timestamp_offset_start:
type: integer
type: object
models.IPCamera:
properties:
fps:
type: string
height:
type: integer
onvif:
type: string
onvif_password:
@@ -167,6 +198,8 @@ definitions:
type: string
sub_rtsp:
type: string
width:
type: integer
type: object
models.KStorage:
properties:
@@ -202,6 +235,13 @@ definitions:
tilt:
type: number
type: object
models.OnvifPreset:
properties:
onvif_credentials:
$ref: '#/definitions/models.OnvifCredentials'
preset:
type: string
type: object
models.OnvifZoom:
properties:
onvif_credentials:
@@ -309,7 +349,47 @@ paths:
$ref: '#/definitions/models.APIResponse'
summary: Will return the ONVIF capabilities for the specific camera.
tags:
- camera
- onvif
/api/camera/onvif/gotopreset:
post:
description: Will activate the desired ONVIF preset.
operationId: camera-onvif-gotopreset
parameters:
- description: OnvifPreset
in: body
name: config
required: true
schema:
$ref: '#/definitions/models.OnvifPreset'
responses:
"200":
description: OK
schema:
$ref: '#/definitions/models.APIResponse'
summary: Will activate the desired ONVIF preset.
tags:
- onvif
/api/camera/onvif/inputs:
post:
description: Will get the digital inputs from the ONVIF device.
operationId: get-digital-inputs
parameters:
- description: OnvifCredentials
in: body
name: config
required: true
schema:
$ref: '#/definitions/models.OnvifCredentials'
responses:
"200":
description: OK
schema:
$ref: '#/definitions/models.APIResponse'
security:
- Bearer: []
summary: Will get the digital inputs from the ONVIF device.
tags:
- onvif
/api/camera/onvif/login:
post:
description: Try to login into ONVIF supported camera.
@@ -328,7 +408,54 @@ paths:
$ref: '#/definitions/models.APIResponse'
summary: Try to login into ONVIF supported camera.
tags:
- camera
- onvif
/api/camera/onvif/outputs:
post:
description: Will get the relay outputs from the ONVIF device.
operationId: get-relay-outputs
parameters:
- description: OnvifCredentials
in: body
name: config
required: true
schema:
$ref: '#/definitions/models.OnvifCredentials'
responses:
"200":
description: OK
schema:
$ref: '#/definitions/models.APIResponse'
security:
- Bearer: []
summary: Will get the relay outputs from the ONVIF device.
tags:
- onvif
/api/camera/onvif/outputs/{output}:
post:
description: Will trigger the relay output from the ONVIF device.
operationId: trigger-relay-output
parameters:
- description: OnvifCredentials
in: body
name: config
required: true
schema:
$ref: '#/definitions/models.OnvifCredentials'
- description: Output
in: path
name: output
required: true
type: string
responses:
"200":
description: OK
schema:
$ref: '#/definitions/models.APIResponse'
security:
- Bearer: []
summary: Will trigger the relay output from the ONVIF device.
tags:
- onvif
/api/camera/onvif/pantilt:
post:
description: Panning or/and tilting the camera using a direction (x,y).
@@ -347,7 +474,47 @@ paths:
$ref: '#/definitions/models.APIResponse'
summary: Panning or/and tilting the camera.
tags:
- camera
- onvif
/api/camera/onvif/presets:
post:
description: Will return the ONVIF presets for the specific camera.
operationId: camera-onvif-presets
parameters:
- description: OnvifCredentials
in: body
name: config
required: true
schema:
$ref: '#/definitions/models.OnvifCredentials'
responses:
"200":
description: OK
schema:
$ref: '#/definitions/models.APIResponse'
summary: Will return the ONVIF presets for the specific camera.
tags:
- onvif
/api/camera/onvif/verify:
post:
description: Will verify the ONVIF connectivity.
operationId: verify-onvif
parameters:
- description: OnvifCredentials
in: body
name: config
required: true
schema:
$ref: '#/definitions/models.OnvifCredentials'
responses:
"200":
description: OK
schema:
$ref: '#/definitions/models.APIResponse'
security:
- Bearer: []
summary: Will verify the ONVIF connectivity.
tags:
- onvif
/api/camera/onvif/zoom:
post:
description: Zooming in or out the camera.
@@ -366,6 +533,62 @@ paths:
$ref: '#/definitions/models.APIResponse'
summary: Zooming in or out the camera.
tags:
- onvif
/api/camera/record:
post:
description: Make a recording.
operationId: camera-record
responses:
"200":
description: OK
schema:
$ref: '#/definitions/models.APIResponse'
summary: Make a recording.
tags:
- camera
/api/camera/restart:
post:
description: Restart the agent.
operationId: camera-restart
responses:
"200":
description: OK
schema:
$ref: '#/definitions/models.APIResponse'
summary: Restart the agent.
tags:
- camera
/api/camera/snapshot/base64:
get:
description: Get a snapshot from the camera in base64.
operationId: snapshot-base64
responses:
"200":
description: ""
summary: Get a snapshot from the camera in base64.
tags:
- camera
/api/camera/snapshot/jpeg:
get:
description: Get a snapshot from the camera in jpeg format.
operationId: snapshot-jpeg
responses:
"200":
description: ""
summary: Get a snapshot from the camera in jpeg format.
tags:
- camera
/api/camera/stop:
post:
description: Stop the agent.
operationId: camera-stop
responses:
"200":
description: OK
schema:
$ref: '#/definitions/models.APIResponse'
summary: Stop the agent.
tags:
- camera
/api/camera/verify/{streamType}:
post:
@@ -395,6 +618,52 @@ paths:
summary: Validate a specific RTSP profile camera connection.
tags:
- camera
/api/config:
get:
description: Get the current configuration.
operationId: config
responses:
"200":
description: ""
summary: Get the current configuration.
tags:
- config
post:
description: Update the current configuration.
operationId: config
parameters:
- description: Configuration
in: body
name: config
required: true
schema:
$ref: '#/definitions/models.Config'
responses:
"200":
description: ""
summary: Update the current configuration.
tags:
- config
/api/dashboard:
get:
description: Get all information showed on the dashboard.
operationId: dashboard
responses:
"200":
description: ""
summary: Get all information showed on the dashboard.
tags:
- general
/api/days:
get:
description: Get all days stored in the recordings directory.
operationId: days
responses:
"200":
description: ""
summary: Get all days stored in the recordings directory.
tags:
- general
/api/hub/verify:
post:
description: Will verify the hub connectivity.
@@ -415,7 +684,24 @@ paths:
- Bearer: []
summary: Will verify the hub connectivity.
tags:
- config
- persistence
/api/latest-events:
post:
description: Get the latest recordings (events) from the recordings directory.
operationId: latest-events
parameters:
- description: Event filter
in: body
name: eventFilter
required: true
schema:
$ref: '#/definitions/models.EventFilter'
responses:
"200":
description: ""
summary: Get the latest recordings (events) from the recordings directory.
tags:
- general
/api/login:
post:
description: Get Authorization token.
@@ -435,27 +721,6 @@ paths:
summary: Get Authorization token.
tags:
- authentication
/api/onvif/verify:
post:
description: Will verify the ONVIF connectivity.
operationId: verify-onvif
parameters:
- description: Camera Config
in: body
name: cameraConfig
required: true
schema:
$ref: '#/definitions/models.IPCamera'
responses:
"200":
description: OK
schema:
$ref: '#/definitions/models.APIResponse'
security:
- Bearer: []
summary: Will verify the ONVIF connectivity.
tags:
- config
/api/persistence/verify:
post:
description: Will verify the persistence.
@@ -476,7 +741,7 @@ paths:
- Bearer: []
summary: Will verify the persistence.
tags:
- config
- persistence
securityDefinitions:
Bearer:
in: header

View File

@@ -1,41 +1,42 @@
module github.com/kerberos-io/agent/machinery
go 1.19
go 1.20
// replace github.com/kerberos-io/joy4 v1.0.57 => ../../../../github.com/kerberos-io/joy4
// replace github.com/kerberos-io/onvif v0.0.5 => ../../../../github.com/kerberos-io/onvif
//replace github.com/kerberos-io/joy4 v1.0.63 => ../../../../github.com/kerberos-io/joy4
//replace github.com/kerberos-io/onvif v0.0.10 => ../../../../github.com/kerberos-io/onvif
require (
github.com/InVisionApp/conjungo v1.1.0
github.com/appleboy/gin-jwt/v2 v2.9.1
github.com/asticode/go-astits v1.11.0
github.com/bluenviron/gortsplib/v3 v3.6.1
github.com/bluenviron/mediacommon v0.5.0
github.com/bluenviron/gortsplib/v4 v4.6.1
github.com/bluenviron/mediacommon v1.5.1
github.com/cedricve/go-onvif v0.0.0-20200222191200-567e8ce298f6
github.com/deepch/vdk v0.0.19
github.com/dropbox/dropbox-sdk-go-unofficial/v6 v6.0.5
github.com/eclipse/paho.mqtt.golang v1.4.2
github.com/elastic/go-sysinfo v1.9.0
github.com/gin-contrib/cors v1.4.0
github.com/gin-contrib/pprof v1.4.0
github.com/gin-gonic/contrib v0.0.0-20221130124618-7e01895a63f2
github.com/gin-gonic/gin v1.8.2
github.com/gin-gonic/gin v1.9.1
github.com/gofrs/uuid v4.4.0+incompatible
github.com/golang-jwt/jwt/v4 v4.4.3
github.com/golang-module/carbon/v2 v2.2.3
github.com/gorilla/websocket v1.5.0
github.com/kellydunn/golang-geo v0.7.0
github.com/kerberos-io/joy4 v1.0.58
github.com/kerberos-io/onvif v0.0.5
github.com/kerberos-io/joy4 v1.0.64
github.com/kerberos-io/onvif v0.0.14
github.com/minio/minio-go/v6 v6.0.57
github.com/nsmith5/mjpeg v0.0.0-20200913181537-54b8ada0e53e
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7
github.com/pion/rtp v1.7.13
github.com/pion/rtp v1.8.3
github.com/pion/webrtc/v3 v3.1.50
github.com/sirupsen/logrus v1.9.0
github.com/swaggo/files v1.0.0
github.com/swaggo/gin-swagger v1.5.3
github.com/swaggo/swag v1.8.9
github.com/tevino/abool v1.2.0
github.com/yapingcat/gomedia v0.0.0-20231203152327-9078d4068ce7
github.com/zaf/g711 v0.0.0-20220109202201-cf0017bf0359
go.mongodb.org/mongo-driver v1.7.5
gopkg.in/DataDog/dd-trace-go.v1 v1.46.0
gopkg.in/natefinch/lumberjack.v2 v2.0.0
@@ -53,49 +54,52 @@ require (
github.com/Microsoft/go-winio v0.5.1 // indirect
github.com/PuerkitoBio/purell v1.1.1 // indirect
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
github.com/asticode/go-astikit v0.30.0 // indirect
github.com/beevik/etree v1.1.0 // indirect
github.com/beevik/etree v1.2.0 // indirect
github.com/bytedance/sonic v1.9.1 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
github.com/clbanning/mxj v1.8.4 // indirect
github.com/clbanning/mxj/v2 v2.7.0 // indirect
github.com/dgraph-io/ristretto v0.1.0 // indirect
github.com/dustin/go-humanize v1.0.0 // indirect
github.com/elastic/go-windows v1.0.0 // indirect
github.com/elgs/gostrgen v0.0.0-20161222160715-9d61ae07eeae // indirect
github.com/elgs/gostrgen v0.0.0-20220325073726-0c3e00d082f6 // indirect
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 // indirect
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-openapi/jsonpointer v0.19.5 // indirect
github.com/go-openapi/jsonreference v0.19.6 // indirect
github.com/go-openapi/spec v0.20.4 // indirect
github.com/go-openapi/swag v0.19.15 // indirect
github.com/go-playground/locales v0.14.0 // indirect
github.com/go-playground/universal-translator v0.18.0 // indirect
github.com/go-playground/validator/v10 v10.11.1 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.14.0 // indirect
github.com/go-stack/stack v1.8.0 // indirect
github.com/goccy/go-json v0.10.0 // indirect
github.com/gofrs/uuid v3.2.0+incompatible // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/google/go-cmp v0.5.9 // indirect
github.com/google/pprof v0.0.0-20210423192551-a2663126120b // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/google/uuid v1.4.0 // indirect
github.com/joeshaw/multierror v0.0.0-20140124173710-69b34d4ec901 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.15.0 // indirect
github.com/klauspost/cpuid v1.2.3 // indirect
github.com/klauspost/cpuid/v2 v2.2.4 // indirect
github.com/kylelemons/go-gypsy v1.0.0 // indirect
github.com/leodido/go-urn v1.2.1 // indirect
github.com/leodido/go-urn v1.2.4 // indirect
github.com/lib/pq v1.10.7 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-isatty v0.0.16 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/minio/md5-simd v1.1.0 // indirect
github.com/minio/sha256-simd v0.1.1 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/onsi/gomega v1.27.4 // indirect
github.com/pelletier/go-toml/v2 v2.0.6 // indirect
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
github.com/philhofer/fwd v1.1.1 // indirect
github.com/pion/datachannel v1.5.5 // indirect
github.com/pion/dtls/v2 v2.1.5 // indirect
@@ -104,7 +108,7 @@ require (
github.com/pion/logging v0.2.2 // indirect
github.com/pion/mdns v0.0.5 // indirect
github.com/pion/randutil v0.1.0 // indirect
github.com/pion/rtcp v1.2.10 // indirect
github.com/pion/rtcp v1.2.12 // indirect
github.com/pion/sctp v1.8.5 // indirect
github.com/pion/sdp/v3 v3.0.6 // indirect
github.com/pion/srtp/v2 v2.0.10 // indirect
@@ -118,7 +122,8 @@ require (
github.com/secure-systems-lab/go-securesystemslib v0.4.0 // indirect
github.com/spaolacci/murmur3 v1.1.0 // indirect
github.com/tinylib/msgp v1.1.6 // indirect
github.com/ugorji/go/codec v1.2.7 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.11 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
github.com/xdg-go/scram v1.0.2 // indirect
github.com/xdg-go/stringprep v1.0.2 // indirect
@@ -126,20 +131,22 @@ require (
github.com/ziutek/mymysql v1.5.4 // indirect
go4.org/intern v0.0.0-20211027215823-ae77deb06f29 // indirect
go4.org/unsafe/assume-no-moving-gc v0.0.0-20220617031537-928513b29760 // indirect
golang.org/x/crypto v0.4.0 // indirect
golang.org/x/net v0.9.0 // indirect
golang.org/x/arch v0.3.0 // indirect
golang.org/x/crypto v0.16.0 // indirect
golang.org/x/net v0.19.0 // indirect
golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5 // indirect
golang.org/x/sync v0.1.0 // indirect
golang.org/x/sys v0.7.0 // indirect
golang.org/x/text v0.9.0 // indirect
golang.org/x/sys v0.15.0 // indirect
golang.org/x/text v0.14.0 // indirect
golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11 // indirect
golang.org/x/tools v0.7.0 // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
google.golang.org/appengine v1.6.6 // indirect
google.golang.org/grpc v1.32.0 // indirect
google.golang.org/protobuf v1.28.1 // indirect
google.golang.org/protobuf v1.30.0 // indirect
gopkg.in/ini.v1 v1.42.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
howett.net/plist v0.0.0-20181124034731-591f970eefbb // indirect
inet.af/netaddr v0.0.0-20220617031823-097006376321 // indirect
)

View File

@@ -64,27 +64,31 @@ github.com/appleboy/gin-jwt/v2 v2.9.1 h1:l29et8iLW6omcHltsOP6LLk4s3v4g2FbFs0koxG
github.com/appleboy/gin-jwt/v2 v2.9.1/go.mod h1:jwcPZJ92uoC9nOUTOKWoN/f6JZOgMSKlFSHw5/FrRUk=
github.com/appleboy/gofight/v2 v2.1.2 h1:VOy3jow4vIK8BRQJoC/I9muxyYlJ2yb9ht2hZoS3rf4=
github.com/appleboy/gofight/v2 v2.1.2/go.mod h1:frW+U1QZEdDgixycTj4CygQ48yLTUhplt43+Wczp3rw=
github.com/asticode/go-astikit v0.30.0 h1:DkBkRQRIxYcknlaU7W7ksNfn4gMFsB0tqMJflxkRsZA=
github.com/asticode/go-astikit v0.30.0/go.mod h1:h4ly7idim1tNhaVkdVBeXQZEE3L0xblP7fCWbgwipF0=
github.com/asticode/go-astits v1.11.0 h1:GTHUXht0ZXAJXsVbsLIcyfHr1Bchi4QQwMARw2ZWAng=
github.com/asticode/go-astits v1.11.0/go.mod h1:QSHmknZ51pf6KJdHKZHJTLlMegIrhega3LPWz3ND/iI=
github.com/beevik/etree v1.1.0 h1:T0xke/WvNtMoCqgzPhkX2r4rjY3GDZFi+FjpRZY2Jbs=
github.com/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A=
github.com/bluenviron/gortsplib/v3 v3.6.1 h1:+/kPiwmdRwUasU5thOBATJQ4/yD+vrIEutJyRTB/f+0=
github.com/bluenviron/gortsplib/v3 v3.6.1/go.mod h1:gc6Z8pBUMC9QBqYxcOY9eVxjDPOrmFcwVH61Xs3Gu2A=
github.com/bluenviron/mediacommon v0.5.0 h1:YsVFlEknaXWhZGfz+Y1QbuzXLMVSmHODc7OnRqZoITY=
github.com/bluenviron/mediacommon v0.5.0/go.mod h1:t0dqPsWUTchyvib0MhixIwXEgvDX4V9G+I0GzWLQRb8=
github.com/beevik/etree v1.2.0 h1:l7WETslUG/T+xOPs47dtd6jov2Ii/8/OjCldk5fYfQw=
github.com/beevik/etree v1.2.0/go.mod h1:aiPf89g/1k3AShMVAzriilpcE4R/Vuor90y83zVZWFc=
github.com/bluenviron/gortsplib/v4 v4.6.1 h1:+xI/hrNM/KX3qenqzKIG0MG8z+IHg0xu8OEoMfDZ+wg=
github.com/bluenviron/gortsplib/v4 v4.6.1/go.mod h1:dN1YjyPNMfy/NwC17Ga6MiIMiUoQfg5GL7LGsVHa0Jo=
github.com/bluenviron/mediacommon v1.5.1 h1:yYVF+ebqZOJh8yH+EeuPcAtTmWR66BqbJGmStxkScoI=
github.com/bluenviron/mediacommon v1.5.1/go.mod h1:Ij/kE1LEucSjryNBVTyPL/gBI0d6/Css3f5PyrM957w=
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
github.com/cedricve/go-onvif v0.0.0-20200222191200-567e8ce298f6 h1:bzFZYgZD5vf4PWaa2GjOh90HG88uKi2a+B6VnQcDlCA=
github.com/cedricve/go-onvif v0.0.0-20200222191200-567e8ce298f6/go.mod h1:nBrjN2nMHendp0Cvb/6GaJ1v92Qv/kzqxWtNBnKJEK0=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/clbanning/mxj v1.8.4 h1:HuhwZtbyvyOw+3Z1AowPkU87JkJUSv751ELWaiTpj8I=
github.com/clbanning/mxj v1.8.4/go.mod h1:BVjHeAH+rl9rs6f+QIpeRl0tfu10SXn1pUSa5PVGJng=
github.com/clbanning/mxj/v2 v2.7.0 h1:WA/La7UGCanFe5NpHF0Q3DNtnCsVoxbPKuyBNHWRyME=
github.com/clbanning/mxj/v2 v2.7.0/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb/go.mod h1:ZjrT6AXHbDs86ZSdt/osfBi5qfexBrKUdONk989Wnk4=
@@ -93,8 +97,6 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/deepch/vdk v0.0.19 h1:r6xYyBTtXEIEh+csO0XHT00sI7xLF+hQFkJE9/go5II=
github.com/deepch/vdk v0.0.19/go.mod h1:7ydHfSkflMZxBXfWR79dMjrT54xzvLxnPaByOa9Jpzg=
github.com/dgraph-io/ristretto v0.1.0 h1:Jv3CGQHp9OjuMBSne1485aDpUkTKEcUqF+jm/LuerPI=
github.com/dgraph-io/ristretto v0.1.0/go.mod h1:fux0lOrBhrVCJd3lcTHsIJhq1T2rokOu6v9Vcb3Q9ug=
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA=
@@ -110,8 +112,8 @@ github.com/elastic/go-sysinfo v1.9.0 h1:usICqY/Nw4Mpn9f4LdtpFrKxXroJDe81GaxxUlCc
github.com/elastic/go-sysinfo v1.9.0/go.mod h1:eBD1wEGVaRnRLGecc9iG1z8eOv5HnEdz9+nWd8UAxcE=
github.com/elastic/go-windows v1.0.0 h1:qLURgZFkkrYyTTkvYpsZIgf83AUsdIHfvlJaqaZ7aSY=
github.com/elastic/go-windows v1.0.0/go.mod h1:TsU0Nrp7/y3+VwE82FoZF8gC/XFg/Elz6CcloAxnPgU=
github.com/elgs/gostrgen v0.0.0-20161222160715-9d61ae07eeae h1:3KvK2DmA7TxQ6PZ2f0rWbdqjgJhRcqgbY70bBeE4clI=
github.com/elgs/gostrgen v0.0.0-20161222160715-9d61ae07eeae/go.mod h1:wruC5r2gHdr/JIUs5Rr1V45YtsAzKXZxAnn/5rPC97g=
github.com/elgs/gostrgen v0.0.0-20220325073726-0c3e00d082f6 h1:x9TA+vnGEyqmWY+eA9HfgxNRkOQqwiEpFE9IPXSGuEA=
github.com/elgs/gostrgen v0.0.0-20220325073726-0c3e00d082f6/go.mod h1:wruC5r2gHdr/JIUs5Rr1V45YtsAzKXZxAnn/5rPC97g=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
@@ -122,6 +124,8 @@ github.com/flynn/go-docopt v0.0.0-20140912013429-f6dd2ebbb31e/go.mod h1:HyVoz1Mz
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/gin-contrib/cors v1.4.0 h1:oJ6gwtUl3lqV0WEIwM/LxPF1QZ5qe2lGWdY2+bz7y0g=
github.com/gin-contrib/cors v1.4.0/go.mod h1:bs9pNM0x/UsmHPBWT2xZz9ROh8xYjYkiURUfmBoMlcs=
@@ -133,10 +137,9 @@ github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/contrib v0.0.0-20221130124618-7e01895a63f2 h1:dyuNlYlG1faymw39NdJddnzJICy6587tiGSVioWhYoE=
github.com/gin-gonic/contrib v0.0.0-20221130124618-7e01895a63f2/go.mod h1:iqneQ2Df3omzIVTkIfn7c1acsVnMGiSLn4XF5Blh3Yg=
github.com/gin-gonic/gin v1.6.2/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk=
github.com/gin-gonic/gin v1.8.2 h1:UzKToD9/PoFj/V4rvlKqTRKnQYyz8Sc1MJlv4JHPtvY=
github.com/gin-gonic/gin v1.8.2/go.mod h1:qw5AYuDrzRTnhvusDsrov+fDIxp9Dleuu12h8nfB398=
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
@@ -150,26 +153,27 @@ github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM=
github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos=
github.com/go-playground/validator/v10 v10.11.1 h1:prmOlTVv+YjZjmRmNSF3VmspqJIxJWXmqUsHwfTRRkQ=
github.com/go-playground/validator/v10 v10.11.1/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU=
github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js=
github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/goccy/go-json v0.10.0 h1:mXKd9Qw4NuzShiRlOXKews24ufknHO7gx30lsDyokKA=
github.com/goccy/go-json v0.10.0/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/gofrs/uuid v3.2.0+incompatible h1:y12jRkkFxsd7GpqdSZ+/KCs/fJbqpEXSGd4+jfEaewE=
github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA=
github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/golang-jwt/jwt/v4 v4.4.3 h1:Hxl6lhQFj4AnOX6MLrsCb/+7tCj7DxP7VA+2rDIq5AU=
github.com/golang-jwt/jwt/v4 v4.4.3/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang-module/carbon/v2 v2.2.3 h1:WvGIc5+qzq9drNzH+Gnjh1TZ0JgDY/IA+m2Dvk7Qm4Q=
@@ -236,8 +240,9 @@ github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLe
github.com/google/pprof v0.0.0-20210423192551-a2663126120b h1:l2YRhr+YLzmSp7KJMswRVk/lO5SwoFIcCLzJsVj+YPc=
github.com/google/pprof v0.0.0-20210423192551-a2663126120b/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4=
github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
@@ -264,16 +269,19 @@ github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/kellydunn/golang-geo v0.7.0 h1:A5j0/BvNgGwY6Yb6inXQxzYwlPHc6WVZR+MrarZYNNg=
github.com/kellydunn/golang-geo v0.7.0/go.mod h1:YYlQPJ+DPEzrHx8kT3oPHC/NjyvCCXE+IuKGKdrjrcU=
github.com/kerberos-io/joy4 v1.0.58 h1:R8EECSF+bG7o2yHC6cX/lF77Z+bDVGl6OioLZ3+5MN4=
github.com/kerberos-io/joy4 v1.0.58/go.mod h1:nZp4AjvKvTOXRrmDyAIOw+Da+JA5OcSo/JundGfOlFU=
github.com/kerberos-io/onvif v0.0.5 h1:kq9mnHZkih9Jl4DyIJ4Rzt++Y3DDKy3nI8S2ESEfZ5w=
github.com/kerberos-io/onvif v0.0.5/go.mod h1:Hr2dJOH2LM5SpYKk17gYZ1CMjhGhUl+QlT5kwYogrW0=
github.com/kerberos-io/joy4 v1.0.64 h1:gTUSotHSOhp9mNqEecgq88tQHvpj7TjmrvPUsPm0idg=
github.com/kerberos-io/joy4 v1.0.64/go.mod h1:nZp4AjvKvTOXRrmDyAIOw+Da+JA5OcSo/JundGfOlFU=
github.com/kerberos-io/onvif v0.0.14 h1:ZcpsIAFbuR/mEuTmMnyHM2sLX7OsnQ5sCjmhsgL33VI=
github.com/kerberos-io/onvif v0.0.14/go.mod h1:NAsn+VuMB/hvrm40xULWyiLJ/ArB5nAecX5hvDo5gcA=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/klauspost/compress v1.15.0 h1:xqfchp4whNFxn5A4XFyyYtitiWI8Hy5EW59jEwcyL6U=
github.com/klauspost/compress v1.15.0/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/klauspost/cpuid v1.2.3 h1:CCtW0xUnWGVINKvE/WWOYKdsPV6mawAtvQuSl8guwQs=
github.com/klauspost/cpuid v1.2.3/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
@@ -285,9 +293,9 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/go-gypsy v1.0.0 h1:7/wQ7A3UL1bnqRMnZ6T8cwCOArfZCxFmb1iTxaOOo1s=
github.com/kylelemons/go-gypsy v1.0.0/go.mod h1:chkXM0zjdpXOiqkCW1XcCHDfjfk14PH2KKkQWxfJUcU=
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
github.com/lib/pq v1.10.7 h1:p7ZhMD+KsSRozJr34udlUrhboJwWAgCg34+/ZZNvZZw=
github.com/lib/pq v1.10.7/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
@@ -295,10 +303,10 @@ github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
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/minio/md5-simd v1.1.0 h1:QPfiOqlZH+Cj9teu0t9b1nTBfPbyTl16Of5MeuShdK4=
github.com/minio/md5-simd v1.1.0/go.mod h1:XpBqgZULrMYD3R+M28PcmP0CkI7PEMzB3U77ZrKZ0Gw=
github.com/minio/minio-go/v6 v6.0.57 h1:ixPkbKkyD7IhnluRgQpGSpHdpvNVaW6OD5R9IAO/9Tw=
@@ -315,8 +323,6 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/nsmith5/mjpeg v0.0.0-20200913181537-54b8ada0e53e h1:bQo/jQ9qvcw7zqnovm8IbLsaOq3F+ELUQcxtxvalQvA=
github.com/nsmith5/mjpeg v0.0.0-20200913181537-54b8ada0e53e/go.mod h1:PW9xCZScEClMBP22n37i0SnN/8B9YzNXTNvOaIkLjv0=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
@@ -342,8 +348,9 @@ github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6
github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo=
github.com/otiai10/mint v1.3.3/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc=
github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo=
github.com/pelletier/go-toml/v2 v2.0.6 h1:nrzqCb7j9cDFj2coyLNLaZuJTLjWjlaz6nvTvIwycIU=
github.com/pelletier/go-toml/v2 v2.0.6/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek=
github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
github.com/philhofer/fwd v1.1.1 h1:GdGcTjf5RNAxwS4QLsiMzJYj5KEvPJD3Abr261yRQXQ=
github.com/philhofer/fwd v1.1.1/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU=
github.com/pion/datachannel v1.5.5 h1:10ef4kwdjije+M9d7Xm9im2Y3O6A6ccQb0zcqZcJew8=
@@ -361,10 +368,12 @@ github.com/pion/mdns v0.0.5/go.mod h1:UgssrvdD3mxpi8tMxAXbsppL3vJ4Jipw1mTCW+al01
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.9/go.mod h1:qVPhiCzAm4D/rxb6XzKeyZiQK69yJpbUDJSF7TgrqNo=
github.com/pion/rtcp v1.2.10 h1:nkr3uj+8Sp97zyItdN60tE/S6vk4al5CPRR6Gejsdjc=
github.com/pion/rtcp v1.2.10/go.mod h1:ztfEwXZNLGyF1oQDttz/ZKIBaeeg/oWbRYqzBM9TL1I=
github.com/pion/rtp v1.7.13 h1:qcHwlmtiI50t1XivvoawdCGTP4Uiypzfrsap+bijcoA=
github.com/pion/rtcp v1.2.12 h1:bKWiX93XKgDZENEXCijvHRU/wRifm6JV5DGcH6twtSM=
github.com/pion/rtcp v1.2.12/go.mod h1:sn6qjxvnwyAkkPzPULIbVqSKI5Dv54Rv7VG0kNxh9L4=
github.com/pion/rtp v1.7.13/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko=
github.com/pion/rtp v1.8.3 h1:VEHxqzSVQxCkKDSHro5/4IUUG1ea+MFdqR2R3xSpNU8=
github.com/pion/rtp v1.8.3/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU=
github.com/pion/sctp v1.8.5 h1:JCc25nghnXWOlSn3OVtEnA9PjQ2JsxQbG+CXZ1UkJKQ=
github.com/pion/sctp v1.8.5/go.mod h1:SUFFfDpViyKejTAdwD1d/HQsCu+V/40cCs2nZIvC3s0=
github.com/pion/sdp/v3 v3.0.6 h1:WuDLhtuFUUVpTfus9ILC4HRyHsW6TdugjEX/QY9OiUw=
@@ -388,7 +397,6 @@ github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsK
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
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/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
@@ -430,7 +438,10 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.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/swaggo/files v0.0.0-20220728132757-551d4a08d97a/go.mod h1:lKJPbtWzJ9JhsTN1k1gZgleJWY/cqq0psdoMmaThG3w=
github.com/swaggo/files v1.0.0 h1:1gGXVIeUFCS/dta17rnP0iOpr6CXFwKD7EO5ID233e4=
github.com/swaggo/files v1.0.0/go.mod h1:N59U6URJLyU1PQgFqPM7wXLMhJx7QAolnvfQkqO13kc=
@@ -451,11 +462,12 @@ github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tinylib/msgp v1.1.6 h1:i+SbKraHhnrf9M5MYmvQhFnbLhAXSDWF8WWsuyRdocw=
github.com/tinylib/msgp v1.1.6/go.mod h1:75BAfg2hauQhs3qedfdDZmWAPcFMAvJE5b9rGOMufyw=
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M=
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0=
github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
@@ -463,6 +475,8 @@ github.com/xdg-go/scram v1.0.2 h1:akYIkZ28e6A96dkWNJQu3nmCzH3YfwMPQExUYDaRv7w=
github.com/xdg-go/scram v1.0.2/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+hCSs=
github.com/xdg-go/stringprep v1.0.2 h1:6iq84/ryjjeRmMJwxutI51F2GIPlP5BfTvXHeYjyhBc=
github.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM=
github.com/yapingcat/gomedia v0.0.0-20231203152327-9078d4068ce7 h1:CDxRmG9/kGMMHbKuJezAM7Bp40P7EH2MqBn3qqf0bok=
github.com/yapingcat/gomedia v0.0.0-20231203152327-9078d4068ce7/go.mod h1:WSZ59bidJOO40JSJmLqlkBJrjZCtjbKKkygEMfzY/kc=
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d h1:splanxYIlg+5LfHAM6xpdFEAYOk8iySO56hMFq6uLyA=
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
@@ -471,6 +485,8 @@ github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/zaf/g711 v0.0.0-20220109202201-cf0017bf0359 h1:P9yeMx2iNJxJqXEwLtMjSwWcD2a0AlFmFByeosMZhLM=
github.com/zaf/g711 v0.0.0-20220109202201-cf0017bf0359/go.mod h1:ySLGJD8AQluMQuu5JDvfJrwsBra+8iX1jFsKS8KfB2I=
github.com/ziutek/mymysql v1.5.4 h1:GB0qdRGsTwQSBVYuVShFBKaXSnSnYYC2d9knnE1LHFs=
github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0=
go.mongodb.org/mongo-driver v1.7.5 h1:ny3p0reEpgsR2cfA5cjgwFZg3Cv/ofFh/8jbhGtz9VI=
@@ -485,6 +501,9 @@ go4.org/intern v0.0.0-20211027215823-ae77deb06f29/go.mod h1:cS2ma+47FKrLPdXFpr7C
go4.org/unsafe/assume-no-moving-gc v0.0.0-20211027215541-db492cf91b37/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E=
go4.org/unsafe/assume-no-moving-gc v0.0.0-20220617031537-928513b29760 h1:FyBZqvoA/jbNzuAWLQE2kG820zMAkcilx6BMjGbL/E4=
go4.org/unsafe/assume-no-moving-gc v0.0.0-20220617031537-928513b29760/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190513172903-22d7a77e9e5f/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
@@ -498,8 +517,9 @@ golang.org/x/crypto v0.0.0-20211117183948-ae814b36b871/go.mod h1:IxCIyHEi3zRg3s0
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20221010152910-d6f0a8c073c2/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.4.0 h1:UVQgzMY87xqpKNgb+kDsll2Igd33HszWHFLmpaRMq/8=
golang.org/x/crypto v0.4.0/go.mod h1:3quD/ATkf6oY+rnes5c3ExXTbLc8mueNue5/DoinL80=
golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY=
golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -581,8 +601,8 @@ golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM=
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -622,7 +642,6 @@ golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -652,14 +671,16 @@ golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220608164250-635b8c9b7f68/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/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.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
golang.org/x/sys v0.15.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=
@@ -675,8 +696,8 @@ 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.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@@ -817,8 +838,9 @@ google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp0
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/DataDog/dd-trace-go.v1 v1.46.0 h1:h/SbNfGfDMhBkB+/zzCWKPOlLcdd0Fc+QBAnZm009XM=
gopkg.in/DataDog/dd-trace-go.v1 v1.46.0/go.mod h1:kaa8caaECrtY0V/MUtPQAh1lx/euFzPJwrY1taTx3O4=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@@ -839,7 +861,6 @@ gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
@@ -861,5 +882,6 @@ howett.net/plist v0.0.0-20181124034731-591f970eefbb/go.mod h1:vMygbs4qMhSZSc4lCU
inet.af/netaddr v0.0.0-20220617031823-097006376321 h1:B4dC8ySKTQXasnjDTMsoCMf1sQG4WsMej0WXaHxunmU=
inet.af/netaddr v0.0.0-20220617031823-097006376321/go.mod h1:OIezDfdzOgFhuw4HuWapWq2e9l0H9tK4F1j+ETRtF3k=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=

View File

@@ -2,12 +2,17 @@ package main
import (
"context"
"flag"
"os"
"time"
"github.com/kerberos-io/agent/machinery/src/capture"
"github.com/kerberos-io/agent/machinery/src/components"
"github.com/kerberos-io/agent/machinery/src/log"
"github.com/kerberos-io/agent/machinery/src/models"
"github.com/kerberos-io/agent/machinery/src/onvif"
configService "github.com/kerberos-io/agent/machinery/src/config"
"github.com/kerberos-io/agent/machinery/src/routers"
"github.com/kerberos-io/agent/machinery/src/utils"
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer"
@@ -49,25 +54,66 @@ func main() {
}
// Start the show ;)
action := os.Args[1]
// We'll parse the flags (named variables), and start the agent.
var action string
var configDirectory string
var name string
var port string
var timeout string
flag.StringVar(&action, "action", "version", "Tell us what you want do 'run' or 'version'")
flag.StringVar(&configDirectory, "config", ".", "Where is the configuration stored")
flag.StringVar(&name, "name", "agent", "Provide a name for the agent")
flag.StringVar(&port, "port", "80", "On which port should the agent run")
flag.StringVar(&timeout, "timeout", "2000", "Number of milliseconds to wait for the ONVIF discovery to complete")
flag.Parse()
// Specify the level of loggin: "info", "warning", "debug", "error" or "fatal."
logLevel := os.Getenv("LOG_LEVEL")
if logLevel == "" {
logLevel = "info"
}
// Specify the output formatter of the log: "text" or "json".
logOutput := os.Getenv("LOG_OUTPUT")
if logOutput == "" {
logOutput = "text"
}
// Specify the timezone of the log: "UTC" or "Local".
timezone, _ := time.LoadLocation("CET")
log.Log.Init(timezone)
log.Log.Init(logLevel, logOutput, configDirectory, timezone)
switch action {
case "version":
log.Log.Info("You are currrently running Kerberos Agent " + VERSION)
log.Log.Info("main.Main(): You are currrently running Kerberos Agent " + VERSION)
case "discover":
timeout := os.Args[2]
log.Log.Info(timeout)
// Convert duration to int
timeout, err := time.ParseDuration(timeout + "ms")
if err != nil {
log.Log.Fatal("main.Main(): could not parse timeout: " + err.Error())
return
}
onvif.Discover(timeout)
case "decrypt":
log.Log.Info("main.Main(): Decrypting: " + flag.Arg(0) + " with key: " + flag.Arg(1))
symmetricKey := []byte(flag.Arg(1))
if symmetricKey == nil || len(symmetricKey) == 0 {
log.Log.Fatal("main.Main(): symmetric key should not be empty")
return
}
if len(symmetricKey) != 32 {
log.Log.Fatal("main.Main(): symmetric key should be 32 bytes")
return
}
utils.Decrypt(flag.Arg(0), symmetricKey)
case "run":
{
name := os.Args[2]
port := os.Args[3]
// Print Kerberos.io ASCII art
utils.PrintASCIIArt()
@@ -82,32 +128,32 @@ func main() {
configuration.Port = port
// Open this configuration either from Kerberos Agent or Kerberos Factory.
components.OpenConfig(&configuration)
configService.OpenConfig(configDirectory, &configuration)
// We will override the configuration with the environment variables
components.OverrideWithEnvironmentVariables(&configuration)
configService.OverrideWithEnvironmentVariables(&configuration)
// Printing final configuration
utils.PrintConfiguration(&configuration)
// Check the folder permissions, it might be that we do not have permissions to write
// recordings, update the configuration or save snapshots.
utils.CheckDataDirectoryPermissions()
utils.CheckDataDirectoryPermissions(configDirectory)
// Set timezone
timezone, _ := time.LoadLocation(configuration.Config.Timezone)
log.Log.Init(timezone)
log.Log.Init(logLevel, logOutput, configDirectory, timezone)
// Check if we have a device Key or not, if not
// we will generate one.
if configuration.Config.Key == "" {
key := utils.RandStringBytesMaskImpr(30)
configuration.Config.Key = key
err := components.StoreConfig(configuration.Config)
err := configService.StoreConfig(configDirectory, configuration.Config)
if err == nil {
log.Log.Info("Main: updated unique key for agent to: " + key)
log.Log.Info("main.Main(): updated unique key for agent to: " + key)
} else {
log.Log.Info("Main: something went wrong while trying to store key: " + key)
log.Log.Info("main.Main(): something went wrong while trying to store key: " + key)
}
}
@@ -115,18 +161,26 @@ func main() {
// This is used to restart the agent when the configuration is updated.
ctx, cancel := context.WithCancel(context.Background())
// We create a capture object, this will contain all the streaming clients.
// And allow us to extract media from within difference places in the agent.
capture := capture.Capture{
RTSPClient: nil,
RTSPSubClient: nil,
}
// Bootstrapping the agent
communication := models.Communication{
Context: &ctx,
CancelContext: &cancel,
HandleBootstrap: make(chan string, 1),
}
go components.Bootstrap(&configuration, &communication)
go components.Bootstrap(configDirectory, &configuration, &communication, &capture)
// Start the REST API.
routers.StartWebserver(&configuration, &communication)
routers.StartWebserver(configDirectory, &configuration, &communication, &capture)
}
default:
log.Log.Error("Main: Sorry I don't understand :(")
log.Log.Error("main.Main(): Sorry I don't understand :(")
}
}

View File

@@ -0,0 +1,966 @@
package capture
// #cgo pkg-config: libavcodec libavutil libswscale
// #include <libavcodec/avcodec.h>
// #include <libavutil/imgutils.h>
// #include <libswscale/swscale.h>
import "C"
import (
"context"
"errors"
"fmt"
"image"
"reflect"
"strconv"
"sync"
"time"
"unsafe"
"github.com/bluenviron/gortsplib/v4"
"github.com/bluenviron/gortsplib/v4/pkg/base"
"github.com/bluenviron/gortsplib/v4/pkg/description"
"github.com/bluenviron/gortsplib/v4/pkg/format"
"github.com/bluenviron/gortsplib/v4/pkg/format/rtph264"
"github.com/bluenviron/gortsplib/v4/pkg/format/rtph265"
"github.com/bluenviron/gortsplib/v4/pkg/format/rtplpcm"
"github.com/bluenviron/gortsplib/v4/pkg/format/rtpmpeg4audio"
"github.com/bluenviron/gortsplib/v4/pkg/format/rtpsimpleaudio"
"github.com/bluenviron/mediacommon/pkg/codecs/h264"
"github.com/bluenviron/mediacommon/pkg/codecs/h265"
"github.com/bluenviron/mediacommon/pkg/codecs/mpeg4audio"
"github.com/kerberos-io/agent/machinery/src/log"
"github.com/kerberos-io/agent/machinery/src/models"
"github.com/kerberos-io/agent/machinery/src/packets"
"github.com/pion/rtp"
)
// Implements the RTSPClient interface.
type Golibrtsp struct {
RTSPClient
Url string
Client gortsplib.Client
VideoDecoderMutex *sync.Mutex
VideoH264Index int8
VideoH264Media *description.Media
VideoH264Forma *format.H264
VideoH264Decoder *rtph264.Decoder
VideoH264FrameDecoder *Decoder
VideoH265Index int8
VideoH265Media *description.Media
VideoH265Forma *format.H265
VideoH265Decoder *rtph265.Decoder
VideoH265FrameDecoder *Decoder
AudioLPCMIndex int8
AudioLPCMMedia *description.Media
AudioLPCMForma *format.LPCM
AudioLPCMDecoder *rtplpcm.Decoder
AudioG711Index int8
AudioG711Media *description.Media
AudioG711Forma *format.G711
AudioG711Decoder *rtpsimpleaudio.Decoder
HasBackChannel bool
AudioG711IndexBackChannel int8
AudioG711MediaBackChannel *description.Media
AudioG711FormaBackChannel *format.G711
AudioMPEG4Index int8
AudioMPEG4Media *description.Media
AudioMPEG4Forma *format.MPEG4Audio
AudioMPEG4Decoder *rtpmpeg4audio.Decoder
Streams []packets.Stream
}
// Connect to the RTSP server.
func (g *Golibrtsp) Connect(ctx context.Context) (err error) {
transport := gortsplib.TransportTCP
g.Client = gortsplib.Client{
RequestBackChannels: false,
Transport: &transport,
}
// parse URL
u, err := base.ParseURL(g.Url)
if err != nil {
log.Log.Debug("capture.golibrtsp.Connect(ParseURL): " + err.Error())
return
}
// connect to the server
err = g.Client.Start(u.Scheme, u.Host)
if err != nil {
log.Log.Debug("capture.golibrtsp.Connect(Start): " + err.Error())
}
// find published medias
desc, _, err := g.Client.Describe(u)
if err != nil {
log.Log.Debug("capture.golibrtsp.Connect(Describe): " + err.Error())
return
}
// Iniatlise the mutex.
g.VideoDecoderMutex = &sync.Mutex{}
// find the H264 media and format
var formaH264 *format.H264
mediH264 := desc.FindFormat(&formaH264)
g.VideoH264Media = mediH264
g.VideoH264Forma = formaH264
if mediH264 == nil {
log.Log.Debug("capture.golibrtsp.Connect(H264): " + "video media not found")
} else {
// setup a video media
_, err = g.Client.Setup(desc.BaseURL, mediH264, 0, 0)
if err != nil {
// Something went wrong .. Do something
log.Log.Error("capture.golibrtsp.Connect(H264): " + err.Error())
} else {
// Get SPS from the SDP
// Calculate the width and height of the video
var sps h264.SPS
err = sps.Unmarshal(formaH264.SPS)
if err != nil {
log.Log.Debug("capture.golibrtsp.Connect(H264): " + err.Error())
return
}
g.Streams = append(g.Streams, packets.Stream{
Name: formaH264.Codec(),
IsVideo: true,
IsAudio: false,
SPS: formaH264.SPS,
PPS: formaH264.PPS,
Width: sps.Width(),
Height: sps.Height(),
FPS: sps.FPS(),
IsBackChannel: false,
})
// Set the index for the video
g.VideoH264Index = int8(len(g.Streams)) - 1
// setup RTP/H264 -> H264 decoder
rtpDec, err := formaH264.CreateDecoder()
if err != nil {
// Something went wrong .. Do something
}
g.VideoH264Decoder = rtpDec
// setup H264 -> raw frames decoder
frameDec, err := newDecoder("H264")
if err != nil {
// Something went wrong .. Do something
}
g.VideoH264FrameDecoder = frameDec
}
}
// find the H265 media and format
var formaH265 *format.H265
mediH265 := desc.FindFormat(&formaH265)
g.VideoH265Media = mediH265
g.VideoH265Forma = formaH265
if mediH265 == nil {
log.Log.Debug("capture.golibrtsp.Connect(H265): " + "video media not found")
} else {
// setup a video media
_, err = g.Client.Setup(desc.BaseURL, mediH265, 0, 0)
if err != nil {
// Something went wrong .. Do something
log.Log.Error("capture.golibrtsp.Connect(H265): " + err.Error())
} else {
// Get SPS from the SDP
// Calculate the width and height of the video
var sps h265.SPS
err = sps.Unmarshal(formaH265.SPS)
if err != nil {
log.Log.Info("capture.golibrtsp.Connect(H265): " + err.Error())
return
}
g.Streams = append(g.Streams, packets.Stream{
Name: formaH265.Codec(),
IsVideo: true,
IsAudio: false,
SPS: formaH265.SPS,
PPS: formaH265.PPS,
VPS: formaH265.VPS,
Width: sps.Width(),
Height: sps.Height(),
FPS: sps.FPS(),
IsBackChannel: false,
})
// Set the index for the video
g.VideoH265Index = int8(len(g.Streams)) - 1
// setup RTP/H265 -> H265 decoder
rtpDec, err := formaH265.CreateDecoder()
if err != nil {
// Something went wrong .. Do something
}
g.VideoH265Decoder = rtpDec
// setup H265 -> raw frames decoder
frameDec, err := newDecoder("H265")
if err != nil {
// Something went wrong .. Do something
}
g.VideoH265FrameDecoder = frameDec
}
}
// Look for audio stream.
// find the G711 media and format
audioForma, audioMedi := FindPCMU(desc, false)
g.AudioG711Media = audioMedi
g.AudioG711Forma = audioForma
if audioMedi == nil {
log.Log.Debug("capture.golibrtsp.Connect(G711): " + "audio media not found")
} else {
// setup a audio media
_, err = g.Client.Setup(desc.BaseURL, audioMedi, 0, 0)
if err != nil {
// Something went wrong .. Do something
log.Log.Error("capture.golibrtsp.Connect(G711): " + err.Error())
} else {
// create decoder
audiortpDec, err := audioForma.CreateDecoder()
if err != nil {
// Something went wrong .. Do something
log.Log.Error("capture.golibrtsp.Connect(G711): " + err.Error())
} else {
g.AudioG711Decoder = audiortpDec
g.Streams = append(g.Streams, packets.Stream{
Name: "PCM_MULAW",
IsVideo: false,
IsAudio: true,
IsBackChannel: false,
})
// Set the index for the audio
g.AudioG711Index = int8(len(g.Streams)) - 1
}
}
}
// Look for audio stream.
// find the AAC media and format
audioFormaMPEG4, audioMediMPEG4 := FindMPEG4Audio(desc, false)
g.AudioMPEG4Media = audioMediMPEG4
g.AudioMPEG4Forma = audioFormaMPEG4
if audioMediMPEG4 == nil {
log.Log.Debug("capture.golibrtsp.Connect(MPEG4): " + "audio media not found")
} else {
// setup a audio media
_, err = g.Client.Setup(desc.BaseURL, audioMediMPEG4, 0, 0)
if err != nil {
// Something went wrong .. Do something
log.Log.Error("capture.golibrtsp.Connect(MPEG4): " + err.Error())
} else {
g.Streams = append(g.Streams, packets.Stream{
Name: "AAC",
IsVideo: false,
IsAudio: true,
IsBackChannel: false,
})
// Set the index for the audio
g.AudioMPEG4Index = int8(len(g.Streams)) - 1
// create decoder
audiortpDec, err := audioFormaMPEG4.CreateDecoder()
if err != nil {
// Something went wrong .. Do something
log.Log.Error("capture.golibrtsp.Connect(MPEG4): " + err.Error())
}
g.AudioMPEG4Decoder = audiortpDec
}
}
return
}
func (g *Golibrtsp) ConnectBackChannel(ctx context.Context) (err error) {
// Transport TCP
transport := gortsplib.TransportTCP
g.Client = gortsplib.Client{
RequestBackChannels: true,
Transport: &transport,
}
// parse URL
u, err := base.ParseURL(g.Url)
if err != nil {
log.Log.Error("capture.golibrtsp.ConnectBackChannel(): " + err.Error())
return
}
// connect to the server
err = g.Client.Start(u.Scheme, u.Host)
if err != nil {
log.Log.Error("capture.golibrtsp.ConnectBackChannel(): " + err.Error())
}
// find published medias
desc, _, err := g.Client.Describe(u)
if err != nil {
log.Log.Error("capture.golibrtsp.ConnectBackChannel(): " + err.Error())
return
}
// Look for audio back channel.
g.HasBackChannel = false
// find the LPCM media and format
audioFormaBackChannel, audioMediBackChannel := FindPCMU(desc, true)
g.AudioG711MediaBackChannel = audioMediBackChannel
g.AudioG711FormaBackChannel = audioFormaBackChannel
if audioMediBackChannel == nil {
log.Log.Error("capture.golibrtsp.ConnectBackChannel(): audio backchannel not found, not a real error, however you might expect a backchannel. One of the reasons might be that the device already has an active client connected to the backchannel.")
err = errors.New("no audio backchannel found")
} else {
// setup a audio media
_, err = g.Client.Setup(desc.BaseURL, audioMediBackChannel, 0, 0)
if err != nil {
// Something went wrong .. Do something
log.Log.Error("capture.golibrtsp.ConnectBackChannel(): " + err.Error())
g.HasBackChannel = false
} else {
g.HasBackChannel = true
g.Streams = append(g.Streams, packets.Stream{
Name: "PCM_MULAW",
IsVideo: false,
IsAudio: true,
IsBackChannel: true,
})
// Set the index for the audio
g.AudioG711IndexBackChannel = int8(len(g.Streams)) - 1
}
}
return
}
// Start the RTSP client, and start reading packets.
func (g *Golibrtsp) Start(ctx context.Context, queue *packets.Queue, configuration *models.Configuration, communication *models.Communication) (err error) {
log.Log.Debug("capture.golibrtsp.Start(): started")
// called when a MULAW audio RTP packet arrives
if g.AudioG711Media != nil && g.AudioG711Forma != nil {
g.Client.OnPacketRTP(g.AudioG711Media, g.AudioG711Forma, func(rtppkt *rtp.Packet) {
// decode timestamp
pts, ok := g.Client.PacketPTS(g.AudioG711Media, rtppkt)
if !ok {
log.Log.Debug("capture.golibrtsp.Start(): " + "unable to get PTS")
return
}
// extract LPCM samples from RTP packets
op, err := g.AudioG711Decoder.Decode(rtppkt)
if err != nil {
log.Log.Error("capture.golibrtsp.Start(): " + err.Error())
return
}
pkt := packets.Packet{
IsKeyFrame: false,
Packet: rtppkt,
Data: op,
Time: pts,
CompositionTime: pts,
Idx: g.AudioG711Index,
IsVideo: false,
IsAudio: true,
Codec: "PCM_MULAW",
}
queue.WritePacket(pkt)
})
}
// called when a AAC audio RTP packet arrives
if g.AudioMPEG4Media != nil && g.AudioMPEG4Forma != nil {
g.Client.OnPacketRTP(g.AudioMPEG4Media, g.AudioMPEG4Forma, func(rtppkt *rtp.Packet) {
// decode timestamp
pts, ok := g.Client.PacketPTS(g.AudioMPEG4Media, rtppkt)
if !ok {
log.Log.Error("capture.golibrtsp.Start(): " + "unable to get PTS")
return
}
// Encode the AAC samples from RTP packets
// extract access units from RTP packets
aus, err := g.AudioMPEG4Decoder.Decode(rtppkt)
if err != nil {
log.Log.Error("capture.golibrtsp.Start(): " + err.Error())
return
}
enc, err := WriteMPEG4Audio(g.AudioMPEG4Forma, aus)
if err != nil {
log.Log.Error("capture.golibrtsp.Start(): " + err.Error())
return
}
pkt := packets.Packet{
IsKeyFrame: false,
Packet: rtppkt,
Data: enc,
Time: pts,
CompositionTime: pts,
Idx: g.AudioG711Index,
IsVideo: false,
IsAudio: true,
Codec: "AAC",
}
queue.WritePacket(pkt)
})
}
// called when a video RTP packet arrives for H264
var filteredAU [][]byte
if g.VideoH264Media != nil && g.VideoH264Forma != nil {
g.Client.OnPacketRTP(g.VideoH264Media, g.VideoH264Forma, func(rtppkt *rtp.Packet) {
// This will check if we need to stop the thread,
// because of a reconfiguration.
select {
case <-communication.HandleStream:
return
default:
}
if len(rtppkt.Payload) > 0 {
// decode timestamp
pts, ok := g.Client.PacketPTS(g.VideoH264Media, rtppkt)
if !ok {
log.Log.Debug("capture.golibrtsp.Start(): " + "unable to get PTS")
return
}
// Extract access units from RTP packets
// We need to do this, because the decoder expects a full
// access unit. Once we have a full access unit, we can
// decode it, and know if it's a keyframe or not.
au, errDecode := g.VideoH264Decoder.Decode(rtppkt)
if errDecode != nil {
if errDecode != rtph264.ErrNonStartingPacketAndNoPrevious && errDecode != rtph264.ErrMorePacketsNeeded {
log.Log.Error("capture.golibrtsp.Start(): " + errDecode.Error())
}
return
}
// We'll need to read out a few things.
// prepend an AUD. This is required by some players
filteredAU = [][]byte{
{byte(h264.NALUTypeAccessUnitDelimiter), 240},
}
// Check if we have a keyframe.
nonIDRPresent := false
idrPresent := false
for _, nalu := range au {
typ := h264.NALUType(nalu[0] & 0x1F)
switch typ {
case h264.NALUTypeAccessUnitDelimiter:
continue
case h264.NALUTypeIDR:
idrPresent = true
case h264.NALUTypeNonIDR:
nonIDRPresent = true
}
filteredAU = append(filteredAU, nalu)
}
if len(filteredAU) <= 1 || (!nonIDRPresent && !idrPresent) {
return
}
// Convert to packet.
enc, err := h264.AnnexBMarshal(filteredAU)
if err != nil {
log.Log.Error("capture.golibrtsp.Start(): " + err.Error())
return
}
pkt := packets.Packet{
IsKeyFrame: idrPresent,
Packet: rtppkt,
Data: enc,
Time: pts,
CompositionTime: pts,
Idx: g.VideoH264Index,
IsVideo: true,
IsAudio: false,
Codec: "H264",
}
pkt.Data = pkt.Data[4:]
if pkt.IsKeyFrame {
annexbNALUStartCode := func() []byte { return []byte{0x00, 0x00, 0x00, 0x01} }
pkt.Data = append(annexbNALUStartCode(), pkt.Data...)
pkt.Data = append(g.VideoH264Forma.PPS, pkt.Data...)
pkt.Data = append(annexbNALUStartCode(), pkt.Data...)
pkt.Data = append(g.VideoH264Forma.SPS, pkt.Data...)
pkt.Data = append(annexbNALUStartCode(), pkt.Data...)
}
queue.WritePacket(pkt)
// This will check if we need to stop the thread,
// because of a reconfiguration.
select {
case <-communication.HandleStream:
return
default:
}
if idrPresent {
// Increment packets, so we know the device
// is not blocking.
r := communication.PackageCounter.Load().(int64)
log.Log.Debug("capture.golibrtsp.Start(): packet size " + strconv.Itoa(len(pkt.Data)))
communication.PackageCounter.Store((r + 1) % 1000)
communication.LastPacketTimer.Store(time.Now().Unix())
}
}
})
}
// called when a video RTP packet arrives for H265
if g.VideoH265Media != nil && g.VideoH265Forma != nil {
g.Client.OnPacketRTP(g.VideoH265Media, g.VideoH265Forma, func(rtppkt *rtp.Packet) {
// This will check if we need to stop the thread,
// because of a reconfiguration.
select {
case <-communication.HandleStream:
return
default:
}
if len(rtppkt.Payload) > 0 {
// decode timestamp
pts, ok := g.Client.PacketPTS(g.VideoH265Media, rtppkt)
if !ok {
log.Log.Debug("capture.golibrtsp.Start(): " + "unable to get PTS")
return
}
// Extract access units from RTP packets
// We need to do this, because the decoder expects a full
// access unit. Once we have a full access unit, we can
// decode it, and know if it's a keyframe or not.
au, errDecode := g.VideoH265Decoder.Decode(rtppkt)
if errDecode != nil {
if errDecode != rtph265.ErrNonStartingPacketAndNoPrevious && errDecode != rtph265.ErrMorePacketsNeeded {
log.Log.Error("capture.golibrtsp.Start(): " + errDecode.Error())
}
return
}
filteredAU = [][]byte{
{byte(h265.NALUType_AUD_NUT) << 1, 1, 0x50},
}
isRandomAccess := false
for _, nalu := range au {
typ := h265.NALUType((nalu[0] >> 1) & 0b111111)
switch typ {
/*case h265.NALUType_VPS_NUT:
continue*/
case h265.NALUType_SPS_NUT:
continue
case h265.NALUType_PPS_NUT:
continue
case h265.NALUType_AUD_NUT:
continue
case h265.NALUType_IDR_W_RADL, h265.NALUType_IDR_N_LP, h265.NALUType_CRA_NUT:
isRandomAccess = true
}
filteredAU = append(filteredAU, nalu)
}
au = filteredAU
if len(au) <= 1 {
return
}
// add VPS, SPS and PPS before random access access unit
if isRandomAccess {
au = append([][]byte{
g.VideoH265Forma.VPS,
g.VideoH265Forma.SPS,
g.VideoH265Forma.PPS}, au...)
}
enc, err := h264.AnnexBMarshal(au)
if err != nil {
log.Log.Error("capture.golibrtsp.Start(): " + err.Error())
return
}
pkt := packets.Packet{
IsKeyFrame: isRandomAccess,
Packet: rtppkt,
Data: enc,
Time: pts,
CompositionTime: pts,
Idx: g.VideoH265Index,
IsVideo: true,
IsAudio: false,
Codec: "H265",
}
queue.WritePacket(pkt)
// This will check if we need to stop the thread,
// because of a reconfiguration.
select {
case <-communication.HandleStream:
return
default:
}
if isRandomAccess {
// Increment packets, so we know the device
// is not blocking.
r := communication.PackageCounter.Load().(int64)
log.Log.Debug("capture.golibrtsp.Start(): packet size " + strconv.Itoa(len(pkt.Data)))
communication.PackageCounter.Store((r + 1) % 1000)
communication.LastPacketTimer.Store(time.Now().Unix())
}
}
})
}
// Wait for a second, so we can be sure the stream is playing.
time.Sleep(1 * time.Second)
// Play the stream.
_, err = g.Client.Play(nil)
if err != nil {
log.Log.Error("capture.golibrtsp.Start(): " + err.Error())
}
return
}
// Start the RTSP client, and start reading packets.
func (g *Golibrtsp) StartBackChannel(ctx context.Context) (err error) {
log.Log.Info("capture.golibrtsp.StartBackChannel(): started")
// Wait for a second, so we can be sure the stream is playing.
time.Sleep(1 * time.Second)
// Play the stream.
_, err = g.Client.Play(nil)
if err != nil {
log.Log.Error("capture.golibrtsp.StartBackChannel(): " + err.Error())
}
return
}
func (g *Golibrtsp) WritePacket(pkt packets.Packet) error {
if g.HasBackChannel && g.AudioG711MediaBackChannel != nil {
err := g.Client.WritePacketRTP(g.AudioG711MediaBackChannel, pkt.Packet)
if err != nil {
log.Log.Debug("capture.golibrtsp.WritePacket(): " + err.Error())
return err
}
}
return nil
}
// Decode a packet to an image.
func (g *Golibrtsp) DecodePacket(pkt packets.Packet) (image.YCbCr, error) {
var img image.YCbCr
var err error
g.VideoDecoderMutex.Lock()
if len(pkt.Data) == 0 {
err = errors.New("TSPClient(Golibrtsp).DecodePacket(): empty frame")
} else if g.VideoH264Decoder != nil {
img, err = g.VideoH264FrameDecoder.decode(pkt.Data)
} else if g.VideoH265Decoder != nil {
img, err = g.VideoH265FrameDecoder.decode(pkt.Data)
} else {
err = errors.New("TSPClient(Golibrtsp).DecodePacket(): no decoder found, might already be closed")
}
g.VideoDecoderMutex.Unlock()
if err != nil {
log.Log.Error("capture.golibrtsp.DecodePacket(): " + err.Error())
return image.YCbCr{}, err
}
if img.Bounds().Empty() {
log.Log.Debug("capture.golibrtsp.DecodePacket(): empty frame")
return image.YCbCr{}, errors.New("Empty image")
}
return img, nil
}
// Decode a packet to a Gray image.
func (g *Golibrtsp) DecodePacketRaw(pkt packets.Packet) (image.Gray, error) {
var img image.Gray
var err error
g.VideoDecoderMutex.Lock()
if len(pkt.Data) == 0 {
err = errors.New("capture.golibrtsp.DecodePacketRaw(): empty frame")
} else if g.VideoH264Decoder != nil {
img, err = g.VideoH264FrameDecoder.decodeRaw(pkt.Data)
} else if g.VideoH265Decoder != nil {
img, err = g.VideoH265FrameDecoder.decodeRaw(pkt.Data)
} else {
err = errors.New("capture.golibrtsp.DecodePacketRaw(): no decoder found, might already be closed")
}
g.VideoDecoderMutex.Unlock()
if err != nil {
log.Log.Error("capture.golibrtsp.DecodePacketRaw(): " + err.Error())
return image.Gray{}, err
}
if img.Bounds().Empty() {
log.Log.Debug("capture.golibrtsp.DecodePacketRaw(): empty image")
return image.Gray{}, errors.New("Empty image")
}
// Do a deep copy of the image
imgDeepCopy := image.NewGray(img.Bounds())
imgDeepCopy.Stride = img.Stride
copy(imgDeepCopy.Pix, img.Pix)
return *imgDeepCopy, err
}
// Get a list of streams from the RTSP server.
func (j *Golibrtsp) GetStreams() ([]packets.Stream, error) {
return j.Streams, nil
}
// Get a list of video streams from the RTSP server.
func (g *Golibrtsp) GetVideoStreams() ([]packets.Stream, error) {
var videoStreams []packets.Stream
for _, stream := range g.Streams {
if stream.IsVideo {
videoStreams = append(videoStreams, stream)
}
}
return videoStreams, nil
}
// Get a list of audio streams from the RTSP server.
func (g *Golibrtsp) GetAudioStreams() ([]packets.Stream, error) {
var audioStreams []packets.Stream
for _, stream := range g.Streams {
if stream.IsAudio {
audioStreams = append(audioStreams, stream)
}
}
return audioStreams, nil
}
// Close the connection to the RTSP server.
func (g *Golibrtsp) Close() error {
// Close the demuxer.
g.Client.Close()
if g.VideoH264Decoder != nil {
g.VideoH264FrameDecoder.Close()
}
if g.VideoH265FrameDecoder != nil {
g.VideoH265FrameDecoder.Close()
}
return nil
}
func frameData(frame *C.AVFrame) **C.uint8_t {
return (**C.uint8_t)(unsafe.Pointer(&frame.data[0]))
}
func frameLineSize(frame *C.AVFrame) *C.int {
return (*C.int)(unsafe.Pointer(&frame.linesize[0]))
}
// h264Decoder is a wrapper around FFmpeg's H264 decoder.
type Decoder struct {
codecCtx *C.AVCodecContext
srcFrame *C.AVFrame
}
// newH264Decoder allocates a new h264Decoder.
func newDecoder(codecName string) (*Decoder, error) {
codec := C.avcodec_find_decoder(C.AV_CODEC_ID_H264)
if codecName == "H265" {
codec = C.avcodec_find_decoder(C.AV_CODEC_ID_H265)
}
if codec == nil {
return nil, fmt.Errorf("avcodec_find_decoder() failed")
}
codecCtx := C.avcodec_alloc_context3(codec)
if codecCtx == nil {
return nil, fmt.Errorf("avcodec_alloc_context3() failed")
}
res := C.avcodec_open2(codecCtx, codec, nil)
if res < 0 {
C.avcodec_close(codecCtx)
return nil, fmt.Errorf("avcodec_open2() failed")
}
srcFrame := C.av_frame_alloc()
if srcFrame == nil {
C.avcodec_close(codecCtx)
return nil, fmt.Errorf("av_frame_alloc() failed")
}
return &Decoder{
codecCtx: codecCtx,
srcFrame: srcFrame,
}, nil
}
// close closes the decoder.
func (d *Decoder) Close() {
if d.srcFrame != nil {
C.av_frame_free(&d.srcFrame)
}
C.av_frame_free(&d.srcFrame)
C.avcodec_close(d.codecCtx)
}
func (d *Decoder) decode(nalu []byte) (image.YCbCr, error) {
nalu = append([]uint8{0x00, 0x00, 0x00, 0x01}, []uint8(nalu)...)
// send NALU to decoder
var avPacket C.AVPacket
avPacket.data = (*C.uint8_t)(C.CBytes(nalu))
defer C.free(unsafe.Pointer(avPacket.data))
avPacket.size = C.int(len(nalu))
res := C.avcodec_send_packet(d.codecCtx, &avPacket)
if res < 0 {
return image.YCbCr{}, nil
}
// receive frame if available
res = C.avcodec_receive_frame(d.codecCtx, d.srcFrame)
if res < 0 {
return image.YCbCr{}, nil
}
if res == 0 {
fr := d.srcFrame
w := int(fr.width)
h := int(fr.height)
ys := int(fr.linesize[0])
cs := int(fr.linesize[1])
return image.YCbCr{
Y: fromCPtr(unsafe.Pointer(fr.data[0]), ys*h),
Cb: fromCPtr(unsafe.Pointer(fr.data[1]), cs*h/2),
Cr: fromCPtr(unsafe.Pointer(fr.data[2]), cs*h/2),
YStride: ys,
CStride: cs,
SubsampleRatio: image.YCbCrSubsampleRatio420,
Rect: image.Rect(0, 0, w, h),
}, nil
}
return image.YCbCr{}, nil
}
func (d *Decoder) decodeRaw(nalu []byte) (image.Gray, error) {
nalu = append([]uint8{0x00, 0x00, 0x00, 0x01}, []uint8(nalu)...)
// send NALU to decoder
var avPacket C.AVPacket
avPacket.data = (*C.uint8_t)(C.CBytes(nalu))
defer C.free(unsafe.Pointer(avPacket.data))
avPacket.size = C.int(len(nalu))
res := C.avcodec_send_packet(d.codecCtx, &avPacket)
if res < 0 {
return image.Gray{}, nil
}
// receive frame if available
res = C.avcodec_receive_frame(d.codecCtx, d.srcFrame)
if res < 0 {
return image.Gray{}, nil
}
if res == 0 {
fr := d.srcFrame
w := int(fr.width)
h := int(fr.height)
ys := int(fr.linesize[0])
return image.Gray{
Pix: fromCPtr(unsafe.Pointer(fr.data[0]), w*h),
Stride: ys,
Rect: image.Rect(0, 0, w, h),
}, nil
}
return image.Gray{}, nil
}
func fromCPtr(buf unsafe.Pointer, size int) (ret []uint8) {
hdr := (*reflect.SliceHeader)((unsafe.Pointer(&ret)))
hdr.Cap = size
hdr.Len = size
hdr.Data = uintptr(buf)
return
}
func FindPCMU(desc *description.Session, isBackChannel bool) (*format.G711, *description.Media) {
for _, media := range desc.Medias {
if media.IsBackChannel == isBackChannel {
for _, forma := range media.Formats {
if g711, ok := forma.(*format.G711); ok {
if g711.MULaw {
return g711, media
}
}
}
}
}
return nil, nil
}
func FindMPEG4Audio(desc *description.Session, isBackChannel bool) (*format.MPEG4Audio, *description.Media) {
for _, media := range desc.Medias {
if media.IsBackChannel == isBackChannel {
for _, forma := range media.Formats {
if mpeg4, ok := forma.(*format.MPEG4Audio); ok {
return mpeg4, media
}
}
}
}
return nil, nil
}
// WriteMPEG4Audio writes MPEG-4 Audio access units.
func WriteMPEG4Audio(forma *format.MPEG4Audio, aus [][]byte) ([]byte, error) {
pkts := make(mpeg4audio.ADTSPackets, len(aus))
for i, au := range aus {
pkts[i] = &mpeg4audio.ADTSPacket{
Type: forma.Config.Type,
SampleRate: forma.Config.SampleRate,
ChannelCount: forma.Config.ChannelCount,
AU: au,
}
}
enc, err := pkts.Marshal()
if err != nil {
return nil, err
}
return enc, nil
}

View File

@@ -1,150 +0,0 @@
package capture
import (
"context"
"strconv"
"sync"
"time"
"github.com/kerberos-io/agent/machinery/src/log"
"github.com/kerberos-io/agent/machinery/src/models"
"github.com/kerberos-io/joy4/av/pubsub"
"github.com/kerberos-io/joy4/av"
"github.com/kerberos-io/joy4/av/avutil"
"github.com/kerberos-io/joy4/cgo/ffmpeg"
"github.com/kerberos-io/joy4/format"
)
func OpenRTSP(ctx context.Context, url string) (av.DemuxCloser, []av.CodecData, error) {
format.RegisterAll()
infile, err := avutil.Open(ctx, url)
if err == nil {
streams, errstreams := infile.Streams()
return infile, streams, errstreams
}
return nil, []av.CodecData{}, err
}
func GetVideoStream(streams []av.CodecData) (av.CodecData, error) {
var videoStream av.CodecData
for _, stream := range streams {
if stream.Type().IsAudio() {
//astream := stream.(av.AudioCodecData)
} else if stream.Type().IsVideo() {
videoStream = stream
}
}
return videoStream, nil
}
func GetVideoDecoder(decoder *ffmpeg.VideoDecoder, streams []av.CodecData) {
// Load video codec
var vstream av.VideoCodecData
for _, stream := range streams {
if stream.Type().IsAudio() {
//astream := stream.(av.AudioCodecData)
} else if stream.Type().IsVideo() {
vstream = stream.(av.VideoCodecData)
}
}
err := ffmpeg.NewVideoDecoder(decoder, vstream)
if err != nil {
log.Log.Error("GetVideoDecoder: " + err.Error())
}
}
func DecodeImage(frame *ffmpeg.VideoFrame, pkt av.Packet, decoder *ffmpeg.VideoDecoder, decoderMutex *sync.Mutex) (*ffmpeg.VideoFrame, error) {
decoderMutex.Lock()
img, err := decoder.Decode(frame, pkt.Data)
decoderMutex.Unlock()
return img, err
}
func HandleStream(infile av.DemuxCloser, queue *pubsub.Queue, communication *models.Communication) { //, wg *sync.WaitGroup) {
log.Log.Debug("HandleStream: started")
var err error
loop:
for {
// This will check if we need to stop the thread,
// because of a reconfiguration.
select {
case <-communication.HandleStream:
break loop
default:
}
var pkt av.Packet
if pkt, err = infile.ReadPacket(); err != nil { // sometimes this throws an end of file..
log.Log.Error("HandleStream: " + err.Error())
time.Sleep(1 * time.Second)
}
// Could be that a decode is throwing errors.
if len(pkt.Data) > 0 {
queue.WritePacket(pkt)
// This will check if we need to stop the thread,
// because of a reconfiguration.
select {
case <-communication.HandleStream:
break loop
default:
}
if pkt.IsKeyFrame {
// Increment packets, so we know the device
// is not blocking.
r := communication.PackageCounter.Load().(int64)
log.Log.Info("HandleStream: packet size " + strconv.Itoa(len(pkt.Data)))
communication.PackageCounter.Store((r + 1) % 1000)
communication.LastPacketTimer.Store(time.Now().Unix())
}
}
}
queue.Close()
log.Log.Debug("HandleStream: finished")
}
func HandleSubStream(infile av.DemuxCloser, queue *pubsub.Queue, communication *models.Communication) { //, wg *sync.WaitGroup) {
log.Log.Debug("HandleSubStream: started")
var err error
loop:
for {
// This will check if we need to stop the thread,
// because of a reconfiguration.
select {
case <-communication.HandleSubStream:
break loop
default:
}
var pkt av.Packet
if pkt, err = infile.ReadPacket(); err != nil { // sometimes this throws an end of file..
log.Log.Error("HandleSubStream: " + err.Error())
time.Sleep(1 * time.Second)
}
// Could be that a decode is throwing errors.
if len(pkt.Data) > 0 {
queue.WritePacket(pkt)
// This will check if we need to stop the thread,
// because of a reconfiguration.
select {
case <-communication.HandleSubStream:
break loop
default:
}
}
}
queue.Close()
log.Log.Debug("HandleSubStream: finished")
}

View File

@@ -0,0 +1,72 @@
package capture
import (
"context"
"image"
"github.com/kerberos-io/agent/machinery/src/models"
"github.com/kerberos-io/agent/machinery/src/packets"
)
type Capture struct {
RTSPClient *Golibrtsp
RTSPSubClient *Golibrtsp
RTSPBackChannelClient *Golibrtsp
}
func (c *Capture) SetMainClient(rtspUrl string) *Golibrtsp {
c.RTSPClient = &Golibrtsp{
Url: rtspUrl,
}
return c.RTSPClient
}
func (c *Capture) SetSubClient(rtspUrl string) *Golibrtsp {
c.RTSPSubClient = &Golibrtsp{
Url: rtspUrl,
}
return c.RTSPSubClient
}
func (c *Capture) SetBackChannelClient(rtspUrl string) *Golibrtsp {
c.RTSPBackChannelClient = &Golibrtsp{
Url: rtspUrl,
}
return c.RTSPBackChannelClient
}
// RTSPClient is a interface that abstracts the RTSP client implementation.
type RTSPClient interface {
// Connect to the RTSP server.
Connect(ctx context.Context) error
// Connect to a backchannel RTSP server.
ConnectBackChannel(ctx context.Context) error
// Start the RTSP client, and start reading packets.
Start(ctx context.Context, queue *packets.Queue, configuration *models.Configuration, communication *models.Communication) error
// Start the RTSP client, and start reading packets.
StartBackChannel(ctx context.Context) (err error)
// Decode a packet into a image.
DecodePacket(pkt packets.Packet) (image.YCbCr, error)
// Decode a packet into a image.
DecodePacketRaw(pkt packets.Packet) (image.Gray, error)
// Write a packet to the RTSP server.
WritePacket(pkt packets.Packet) error
// Close the connection to the RTSP server.
Close() error
// Get a list of streams from the RTSP server.
GetStreams() ([]packets.Stream, error)
// Get a list of video streams from the RTSP server.
GetVideoStreams() ([]packets.Stream, error)
// Get a list of audio streams from the RTSP server.
GetAudioStreams() ([]packets.Stream, error)
}

View File

@@ -3,21 +3,23 @@ package capture
import (
"context"
"encoding/base64"
"image"
"os"
"strconv"
"time"
"github.com/gin-gonic/gin"
"github.com/kerberos-io/agent/machinery/src/conditions"
"github.com/kerberos-io/agent/machinery/src/encryption"
"github.com/kerberos-io/agent/machinery/src/log"
"github.com/kerberos-io/agent/machinery/src/models"
"github.com/kerberos-io/agent/machinery/src/packets"
"github.com/kerberos-io/agent/machinery/src/utils"
"github.com/kerberos-io/joy4/av/pubsub"
"github.com/kerberos-io/joy4/format/mp4"
"github.com/kerberos-io/joy4/av"
"github.com/yapingcat/gomedia/go-mp4"
)
func CleanupRecordingDirectory(configuration *models.Configuration) {
func CleanupRecordingDirectory(configDirectory string, configuration *models.Configuration) {
autoClean := configuration.Config.AutoClean
if autoClean == "true" {
maxSize := configuration.Config.MaxDirectorySize
@@ -25,7 +27,7 @@ func CleanupRecordingDirectory(configuration *models.Configuration) {
maxSize = 300
}
// Total size of the recording directory.
recordingsDirectory := "./data/recordings"
recordingsDirectory := configDirectory + "/data/recordings"
size, err := utils.DirSize(recordingsDirectory)
if err == nil {
sizeInMB := size / 1000 / 1000
@@ -51,14 +53,15 @@ func CleanupRecordingDirectory(configuration *models.Configuration) {
}
}
func HandleRecordStream(queue *pubsub.Queue, configuration *models.Configuration, communication *models.Communication, streams []av.CodecData) {
func HandleRecordStream(queue *packets.Queue, configDirectory string, configuration *models.Configuration, communication *models.Communication, rtspClient RTSPClient) {
config := configuration.Config
loc, _ := time.LoadLocation(config.Timezone)
if config.Capture.Recording == "false" {
log.Log.Info("HandleRecordStream: disabled, we will not record anything.")
log.Log.Info("capture.main.HandleRecordStream(): disabled, we will not record anything.")
} else {
log.Log.Debug("HandleRecordStream: started")
log.Log.Debug("capture.main.HandleRecordStream(): started")
recordingPeriod := config.Capture.PostRecording // number of seconds to record.
maxRecordingPeriod := config.Capture.MaxLengthRecording // maximum number of seconds to record.
@@ -68,20 +71,24 @@ func HandleRecordStream(queue *pubsub.Queue, configuration *models.Configuration
startRecording := now
timestamp := now
// For continuous and motion based recording we will use a single file.
var file *os.File
// Check if continuous recording.
if config.Capture.Continuous == "true" {
// Do not do anything!
log.Log.Info("HandleRecordStream: Start continuous recording ")
//var cws *cacheWriterSeeker
var myMuxer *mp4.Movmuxer
var videoTrack uint32
var audioTrack uint32
var name string
// Do not do anything!
log.Log.Info("capture.main.HandleRecordStream(continuous): start recording")
loc, _ := time.LoadLocation(config.Timezone)
now = time.Now().Unix()
timestamp = now
start := false
var name string
var myMuxer *mp4.Muxer
var file *os.File
var err error
// If continuous record the full length
recordingPeriod = maxRecordingPeriod
@@ -89,10 +96,9 @@ func HandleRecordStream(queue *pubsub.Queue, configuration *models.Configuration
fullName := ""
// Get as much packets we need.
//for pkt := range packets {
var cursorError error
var pkt av.Packet
var nextPkt av.Packet
var pkt packets.Packet
var nextPkt packets.Packet
recordingStatus := "idle"
recordingCursor := queue.Oldest()
@@ -110,21 +116,31 @@ func HandleRecordStream(queue *pubsub.Queue, configuration *models.Configuration
nextPkt.IsKeyFrame && (timestamp+recordingPeriod-now <= 0 || now-startRecording >= maxRecordingPeriod) {
// Write the last packet
if err := myMuxer.WritePacket(pkt); err != nil {
log.Log.Error(err.Error())
ttime := convertPTS(pkt.Time)
if pkt.IsVideo {
if err := myMuxer.Write(videoTrack, pkt.Data, ttime, ttime); err != nil {
log.Log.Error("capture.main.HandleRecordStream(continuous): " + err.Error())
}
} else if pkt.IsAudio {
if pkt.Codec == "AAC" {
if err := myMuxer.Write(audioTrack, pkt.Data, ttime, ttime); err != nil {
log.Log.Error("capture.main.HandleRecordStream(continuous): " + err.Error())
}
} else if pkt.Codec == "PCM_MULAW" {
// TODO: transcode to AAC, some work to do..
log.Log.Debug("capture.main.HandleRecordStream(continuous): no AAC audio codec detected, skipping audio track.")
}
}
// This will write the trailer a well.
if err := myMuxer.WriteTrailerWithPacket(nextPkt); err != nil {
log.Log.Error(err.Error())
if err := myMuxer.WriteTrailer(); err != nil {
log.Log.Error("capture.main.HandleRecordStream(continuous): " + err.Error())
}
log.Log.Info("HandleRecordStream: Recording finished: file save: " + name)
log.Log.Info("capture.main.HandleRecordStream(continuous): recording finished: file save: " + name)
// Cleanup muxer
start = false
myMuxer.Close()
myMuxer = nil
file.Close()
file = nil
@@ -133,42 +149,47 @@ func HandleRecordStream(queue *pubsub.Queue, configuration *models.Configuration
utils.CreateFragmentedMP4(fullName, config.Capture.FragmentedDuration)
}
// Check if we need to encrypt the recording.
if config.Encryption != nil && config.Encryption.Enabled == "true" && config.Encryption.Recordings == "true" && config.Encryption.SymmetricKey != "" {
// reopen file into memory 'fullName'
contents, err := os.ReadFile(fullName)
if err == nil {
// encrypt
encryptedContents, err := encryption.AesEncrypt(contents, config.Encryption.SymmetricKey)
if err == nil {
// write back to file
err := os.WriteFile(fullName, []byte(encryptedContents), 0644)
if err != nil {
log.Log.Error("capture.main.HandleRecordStream(continuous): error writing file: " + err.Error())
}
} else {
log.Log.Error("capture.main.HandleRecordStream(continuous): error encrypting file: " + err.Error())
}
} else {
log.Log.Error("capture.main.HandleRecordStream(continuous): error reading file: " + err.Error())
}
}
// Create a symbol link.
fc, _ := os.Create("./data/cloud/" + name)
fc, _ := os.Create(configDirectory + "/data/cloud/" + name)
fc.Close()
recordingStatus = "idle"
// Clean up the recording directory if necessary.
CleanupRecordingDirectory(configuration)
CleanupRecordingDirectory(configDirectory, configuration)
}
// If not yet started and a keyframe, let's make a recording
if !start && pkt.IsKeyFrame {
// Check if within time interval
nowInTimezone := time.Now().In(loc)
weekday := nowInTimezone.Weekday()
hour := nowInTimezone.Hour()
minute := nowInTimezone.Minute()
second := nowInTimezone.Second()
timeEnabled := config.Time
timeInterval := config.Timetable[int(weekday)]
if timeEnabled == "true" && timeInterval != nil {
start1 := timeInterval.Start1
end1 := timeInterval.End1
start2 := timeInterval.Start2
end2 := timeInterval.End2
currentTimeInSeconds := hour*60*60 + minute*60 + second
if (currentTimeInSeconds >= start1 && currentTimeInSeconds <= end1) ||
(currentTimeInSeconds >= start2 && currentTimeInSeconds <= end2) {
} else {
log.Log.Debug("HandleRecordStream: Disabled: no continuous recording at this moment. Not within specified time interval.")
time.Sleep(5 * time.Second)
continue
}
// We might have different conditions enabled such as time window or uri response.
// We'll validate those conditions and if not valid we'll not do anything.
valid, err := conditions.Validate(loc, configuration)
if !valid && err != nil {
log.Log.Debug("capture.main.HandleRecordStream(continuous): " + err.Error() + ".")
time.Sleep(5 * time.Second)
continue
}
start = true
@@ -192,42 +213,59 @@ func HandleRecordStream(queue *pubsub.Queue, configuration *models.Configuration
"769"
name = s + ".mp4"
fullName = "./data/recordings/" + name
fullName = configDirectory + "/data/recordings/" + name
// Running...
log.Log.Info("Recording started")
log.Log.Info("capture.main.HandleRecordStream(continuous): recording started")
file, err = os.Create(fullName)
if err == nil {
myMuxer = mp4.NewMuxer(file)
//cws = newCacheWriterSeeker(4096)
myMuxer, _ = mp4.CreateMp4Muxer(file)
// We choose between H264 and H265
if pkt.Codec == "H264" {
videoTrack = myMuxer.AddVideoTrack(mp4.MP4_CODEC_H264)
} else if pkt.Codec == "H265" {
videoTrack = myMuxer.AddVideoTrack(mp4.MP4_CODEC_H265)
}
// For an MP4 container, AAC is the only audio codec supported.
audioTrack = myMuxer.AddAudioTrack(mp4.MP4_CODEC_AAC)
} else {
log.Log.Error("capture.main.HandleRecordStream(continuous): " + err.Error())
}
log.Log.Info("HandleRecordStream: composing recording")
log.Log.Info("HandleRecordStream: write header")
// Creating the file, might block sometimes.
if err := myMuxer.WriteHeader(streams); err != nil {
log.Log.Error(err.Error())
}
if err := myMuxer.WritePacket(pkt); err != nil {
log.Log.Error(err.Error())
ttime := convertPTS(pkt.Time)
if pkt.IsVideo {
if err := myMuxer.Write(videoTrack, pkt.Data, ttime, ttime); err != nil {
log.Log.Error("capture.main.HandleRecordStream(continuous): " + err.Error())
}
} else if pkt.IsAudio {
if pkt.Codec == "AAC" {
if err := myMuxer.Write(audioTrack, pkt.Data, ttime, ttime); err != nil {
log.Log.Error("capture.main.HandleRecordStream(continuous): " + err.Error())
}
} else if pkt.Codec == "PCM_MULAW" {
// TODO: transcode to AAC, some work to do..
log.Log.Debug("capture.main.HandleRecordStream(continuous): no AAC audio codec detected, skipping audio track.")
}
}
recordingStatus = "started"
} else if start {
if err := myMuxer.WritePacket(pkt); err != nil {
log.Log.Error(err.Error())
}
// We will sync to file every keyframe.
if pkt.IsKeyFrame {
err := file.Sync()
if err != nil {
log.Log.Error(err.Error())
} else {
log.Log.Info("HandleRecordStream: Synced file: " + name)
ttime := convertPTS(pkt.Time)
if pkt.IsVideo {
if err := myMuxer.Write(videoTrack, pkt.Data, ttime, ttime); err != nil {
log.Log.Error("capture.main.HandleRecordStream(continuous): " + err.Error())
}
} else if pkt.IsAudio {
if pkt.Codec == "AAC" {
if err := myMuxer.Write(audioTrack, pkt.Data, ttime, ttime); err != nil {
log.Log.Error("capture.main.HandleRecordStream(continuous): " + err.Error())
}
} else if pkt.Codec == "PCM_MULAW" {
// TODO: transcode to AAC, some work to do..
log.Log.Debug("capture.main.HandleRecordStream(continuous): no AAC audio codec detected, skipping audio track.")
}
}
}
@@ -239,17 +277,15 @@ func HandleRecordStream(queue *pubsub.Queue, configuration *models.Configuration
// If this happens we need to check to properly close the recording.
if cursorError != nil {
if recordingStatus == "started" {
// This will write the trailer a well.
if err := myMuxer.WriteTrailer(); err != nil {
log.Log.Error(err.Error())
}
log.Log.Info("HandleRecordStream: Recording finished: file save: " + name)
log.Log.Info("capture.main.HandleRecordStream(continuous): Recording finished: file save: " + name)
// Cleanup muxer
start = false
myMuxer.Close()
myMuxer = nil
file.Close()
file = nil
@@ -258,24 +294,49 @@ func HandleRecordStream(queue *pubsub.Queue, configuration *models.Configuration
utils.CreateFragmentedMP4(fullName, config.Capture.FragmentedDuration)
}
// Check if we need to encrypt the recording.
if config.Encryption != nil && config.Encryption.Enabled == "true" && config.Encryption.Recordings == "true" && config.Encryption.SymmetricKey != "" {
// reopen file into memory 'fullName'
contents, err := os.ReadFile(fullName)
if err == nil {
// encrypt
encryptedContents, err := encryption.AesEncrypt(contents, config.Encryption.SymmetricKey)
if err == nil {
// write back to file
err := os.WriteFile(fullName, []byte(encryptedContents), 0644)
if err != nil {
log.Log.Error("capture.main.HandleRecordStream(motiondetection): error writing file: " + err.Error())
}
} else {
log.Log.Error("capture.main.HandleRecordStream(motiondetection): error encrypting file: " + err.Error())
}
} else {
log.Log.Error("capture.main.HandleRecordStream(motiondetection): error reading file: " + err.Error())
}
}
// Create a symbol link.
fc, _ := os.Create("./data/cloud/" + name)
fc, _ := os.Create(configDirectory + "/data/cloud/" + name)
fc.Close()
recordingStatus = "idle"
// Clean up the recording directory if necessary.
CleanupRecordingDirectory(configDirectory, configuration)
}
}
} else {
log.Log.Info("HandleRecordStream: Start motion based recording ")
var myMuxer *mp4.Muxer
var file *os.File
var err error
log.Log.Info("capture.main.HandleRecordStream(motiondetection): Start motion based recording ")
var lastDuration time.Duration
var lastRecordingTime int64
//var cws *cacheWriterSeeker
var myMuxer *mp4.Movmuxer
var videoTrack uint32
var audioTrack uint32
for motion := range communication.HandleMotion {
timestamp = time.Now().Unix()
@@ -315,29 +376,31 @@ func HandleRecordStream(queue *pubsub.Queue, configuration *models.Configuration
"769"
name := s + ".mp4"
fullName := "./data/recordings/" + name
fullName := configDirectory + "/data/recordings/" + name
// Running...
log.Log.Info("HandleRecordStream: Recording started")
file, err = os.Create(fullName)
if err == nil {
myMuxer = mp4.NewMuxer(file)
}
log.Log.Info("capture.main.HandleRecordStream(motiondetection): recording started")
file, _ = os.Create(fullName)
myMuxer, _ = mp4.CreateMp4Muxer(file)
// Check which video codec we need to use.
videoSteams, _ := rtspClient.GetVideoStreams()
for _, stream := range videoSteams {
if stream.Name == "H264" {
videoTrack = myMuxer.AddVideoTrack(mp4.MP4_CODEC_H264)
} else if stream.Name == "H265" {
videoTrack = myMuxer.AddVideoTrack(mp4.MP4_CODEC_H265)
}
}
// For an MP4 container, AAC is the only audio codec supported.
audioTrack = myMuxer.AddAudioTrack(mp4.MP4_CODEC_AAC)
start := false
log.Log.Info("HandleRecordStream: composing recording")
log.Log.Info("HandleRecordStream: write header")
// Creating the file, might block sometimes.
if err := myMuxer.WriteHeader(streams); err != nil {
log.Log.Error(err.Error())
}
// Get as much packets we need.
var cursorError error
var pkt av.Packet
var nextPkt av.Packet
recordingCursor := queue.DelayedGopCount(int(config.Capture.PreRecording))
var pkt packets.Packet
var nextPkt packets.Packet
recordingCursor := queue.DelayedGopCount(int(config.Capture.PreRecording + 1))
if cursorError == nil {
pkt, cursorError = recordingCursor.ReadPacket()
@@ -347,39 +410,52 @@ func HandleRecordStream(queue *pubsub.Queue, configuration *models.Configuration
nextPkt, cursorError = recordingCursor.ReadPacket()
if cursorError != nil {
log.Log.Error("HandleRecordStream: " + cursorError.Error())
log.Log.Error("capture.main.HandleRecordStream(motiondetection): " + cursorError.Error())
}
now := time.Now().Unix()
select {
case motion := <-communication.HandleMotion:
timestamp = now
log.Log.Info("HandleRecordStream: motion detected while recording. Expanding recording.")
log.Log.Info("capture.main.HandleRecordStream(motiondetection): motion detected while recording. Expanding recording.")
numberOfChanges = motion.NumberOfChanges
log.Log.Info("Received message with recording data, detected changes to save: " + strconv.Itoa(numberOfChanges))
log.Log.Info("capture.main.HandleRecordStream(motiondetection): Received message with recording data, detected changes to save: " + strconv.Itoa(numberOfChanges))
default:
}
if (timestamp+recordingPeriod-now < 0 || now-startRecording > maxRecordingPeriod) && nextPkt.IsKeyFrame {
log.Log.Info("HandleRecordStream: closing recording (timestamp: " + strconv.FormatInt(timestamp, 10) + ", recordingPeriod: " + strconv.FormatInt(recordingPeriod, 10) + ", now: " + strconv.FormatInt(now, 10) + ", startRecording: " + strconv.FormatInt(startRecording, 10) + ", maxRecordingPeriod: " + strconv.FormatInt(maxRecordingPeriod, 10))
log.Log.Info("capture.main.HandleRecordStream(motiondetection): closing recording (timestamp: " + strconv.FormatInt(timestamp, 10) + ", recordingPeriod: " + strconv.FormatInt(recordingPeriod, 10) + ", now: " + strconv.FormatInt(now, 10) + ", startRecording: " + strconv.FormatInt(startRecording, 10) + ", maxRecordingPeriod: " + strconv.FormatInt(maxRecordingPeriod, 10))
break
}
if pkt.IsKeyFrame && !start && pkt.Time >= lastDuration {
log.Log.Info("HandleRecordStream: write frames")
log.Log.Debug("capture.main.HandleRecordStream(motiondetection): write frames")
start = true
}
if start {
if err := myMuxer.WritePacket(pkt); err != nil {
log.Log.Error(err.Error())
ttime := convertPTS(pkt.Time)
if pkt.IsVideo {
if err := myMuxer.Write(videoTrack, pkt.Data, ttime, ttime); err != nil {
log.Log.Error("capture.main.HandleRecordStream(motiondetection): " + err.Error())
}
} else if pkt.IsAudio {
if pkt.Codec == "AAC" {
if err := myMuxer.Write(audioTrack, pkt.Data, ttime, ttime); err != nil {
log.Log.Error("capture.main.HandleRecordStream(motiondetection): " + err.Error())
}
} else if pkt.Codec == "PCM_MULAW" {
// TODO: transcode to AAC, some work to do..
log.Log.Debug("capture.main.HandleRecordStream(motiondetection): no AAC audio codec detected, skipping audio track.")
}
}
// We will sync to file every keyframe.
if pkt.IsKeyFrame {
err := file.Sync()
if err != nil {
log.Log.Error(err.Error())
log.Log.Error("capture.main.HandleRecordStream(motiondetection): " + err.Error())
} else {
log.Log.Info("HandleRecordStream: Synced file: " + name)
log.Log.Debug("capture.main.HandleRecordStream(motiondetection): synced file " + name)
}
}
}
@@ -387,16 +463,13 @@ func HandleRecordStream(queue *pubsub.Queue, configuration *models.Configuration
pkt = nextPkt
}
// This will write the trailer as well.
myMuxer.WriteTrailerWithPacket(nextPkt)
log.Log.Info("HandleRecordStream: file save: " + name)
// This will write the trailer a well.
myMuxer.WriteTrailer()
log.Log.Info("capture.main.HandleRecordStream(motiondetection): file save: " + name)
lastDuration = pkt.Time
lastRecordingTime = time.Now().Unix()
// Cleanup muxer
myMuxer.Close()
myMuxer = nil
file.Close()
file = nil
@@ -405,16 +478,37 @@ func HandleRecordStream(queue *pubsub.Queue, configuration *models.Configuration
utils.CreateFragmentedMP4(fullName, config.Capture.FragmentedDuration)
}
// Check if we need to encrypt the recording.
if config.Encryption != nil && config.Encryption.Enabled == "true" && config.Encryption.Recordings == "true" && config.Encryption.SymmetricKey != "" {
// reopen file into memory 'fullName'
contents, err := os.ReadFile(fullName)
if err == nil {
// encrypt
encryptedContents, err := encryption.AesEncrypt(contents, config.Encryption.SymmetricKey)
if err == nil {
// write back to file
err := os.WriteFile(fullName, []byte(encryptedContents), 0644)
if err != nil {
log.Log.Error("capture.main.HandleRecordStream(motiondetection): error writing file: " + err.Error())
}
} else {
log.Log.Error("capture.main.HandleRecordStream(motiondetection): error encrypting file: " + err.Error())
}
} else {
log.Log.Error("capture.main.HandleRecordStream(motiondetection): error reading file: " + err.Error())
}
}
// Create a symbol linc.
fc, _ := os.Create("./data/cloud/" + name)
fc, _ := os.Create(configDirectory + "/data/cloud/" + name)
fc.Close()
// Clean up the recording directory if necessary.
CleanupRecordingDirectory(configuration)
CleanupRecordingDirectory(configDirectory, configuration)
}
}
log.Log.Debug("HandleRecordStream: finished")
log.Log.Debug("capture.main.HandleRecordStream(): finished")
}
}
@@ -447,30 +541,46 @@ func VerifyCamera(c *gin.Context) {
if streamType == "secondary" {
rtspUrl = cameraStreams.SubRTSP
}
_, codecs, err := OpenRTSP(ctx, rtspUrl)
// Currently only support H264 encoded cameras, this will change.
// Establishing the camera connection without backchannel if no substream
rtspClient := &Golibrtsp{
Url: rtspUrl,
}
err := rtspClient.Connect(ctx)
if err == nil {
// Get the streams from the rtsp client.
streams, _ := rtspClient.GetStreams()
videoIdx := -1
audioIdx := -1
for i, codec := range codecs {
if codec.Type().String() == "H264" && videoIdx < 0 {
for i, stream := range streams {
if (stream.Name == "H264" || stream.Name == "H265") && videoIdx < 0 {
videoIdx = i
} else if codec.Type().String() == "PCM_MULAW" && audioIdx < 0 {
} else if stream.Name == "PCM_MULAW" && audioIdx < 0 {
audioIdx = i
}
}
if videoIdx > -1 {
c.JSON(200, models.APIResponse{
Message: "All good, detected a H264 codec.",
Data: codecs,
})
err := rtspClient.Close()
if err == nil {
if videoIdx > -1 {
c.JSON(200, models.APIResponse{
Message: "All good, detected a H264 codec.",
Data: streams,
})
} else {
c.JSON(400, models.APIResponse{
Message: "Stream doesn't have a H264 codec, we only support H264 so far.",
})
}
} else {
c.JSON(400, models.APIResponse{
Message: "Stream doesn't have a H264 codec, we only support H264 so far.",
Message: "Something went wrong while closing the connection " + err.Error(),
})
}
} else {
c.JSON(400, models.APIResponse{
Message: err.Error(),
@@ -482,3 +592,83 @@ func VerifyCamera(c *gin.Context) {
})
}
}
func Base64Image(captureDevice *Capture, communication *models.Communication) string {
// We'll try to get a snapshot from the camera.
var queue *packets.Queue
var cursor *packets.QueueCursor
// We'll pick the right client and decoder.
rtspClient := captureDevice.RTSPSubClient
if rtspClient != nil {
queue = communication.SubQueue
cursor = queue.Latest()
} else {
rtspClient = captureDevice.RTSPClient
queue = communication.Queue
cursor = queue.Latest()
}
// We'll try to have a keyframe, if not we'll return an empty string.
var encodedImage string
for {
if queue != nil && cursor != nil && rtspClient != nil {
pkt, err := cursor.ReadPacket()
if err == nil {
if !pkt.IsKeyFrame {
continue
}
var img image.YCbCr
img, err = (*rtspClient).DecodePacket(pkt)
if err == nil {
bytes, _ := utils.ImageToBytes(&img)
encodedImage = base64.StdEncoding.EncodeToString(bytes)
break
}
break
}
} else {
break
}
}
return encodedImage
}
func JpegImage(captureDevice *Capture, communication *models.Communication) image.YCbCr {
// We'll try to get a snapshot from the camera.
var queue *packets.Queue
var cursor *packets.QueueCursor
// We'll pick the right client and decoder.
rtspClient := captureDevice.RTSPSubClient
if rtspClient != nil {
queue = communication.SubQueue
cursor = queue.Latest()
} else {
rtspClient = captureDevice.RTSPClient
queue = communication.Queue
cursor = queue.Latest()
}
// We'll try to have a keyframe, if not we'll return an empty string.
var image image.YCbCr
for {
if queue != nil && cursor != nil && rtspClient != nil {
pkt, err := cursor.ReadPacket()
if err == nil {
if !pkt.IsKeyFrame {
continue
}
image, _ = (*rtspClient).DecodePacket(pkt)
break
}
} else {
break
}
}
return image
}
func convertPTS(v time.Duration) uint64 {
return uint64(v.Milliseconds())
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,7 @@
package cloud
import (
"crypto/tls"
"errors"
"io/ioutil"
"net/http"
@@ -43,7 +44,7 @@ func UploadKerberosHub(configuration *models.Configuration, fileName string) (bo
if err != nil {
err := "UploadKerberosHub: Upload Failed, file doesn't exists anymore."
log.Log.Info(err)
return false, true, errors.New(err)
return false, false, errors.New(err)
}
// Check if we are allowed to upload to the hub with these credentials.
@@ -62,7 +63,15 @@ func UploadKerberosHub(configuration *models.Configuration, fileName string) (bo
req.Header.Set("X-Kerberos-Hub-PrivateKey", config.HubPrivateKey)
req.Header.Set("X-Kerberos-Hub-Region", config.S3.Region)
client := &http.Client{}
var client *http.Client
if os.Getenv("AGENT_TLS_INSECURE") == "true" {
tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
client = &http.Client{Transport: tr}
} else {
client = &http.Client{}
}
resp, err := client.Do(req)
if resp != nil {
@@ -96,9 +105,6 @@ func UploadKerberosHub(configuration *models.Configuration, fileName string) (bo
req.Header.Set("X-Kerberos-Hub-PublicKey", config.HubKey)
req.Header.Set("X-Kerberos-Hub-PrivateKey", config.HubPrivateKey)
req.Header.Set("X-Kerberos-Hub-Region", config.S3.Region)
client = &http.Client{}
resp, err = client.Do(req)
if resp != nil {
defer resp.Body.Close()

View File

@@ -1,6 +1,7 @@
package cloud
import (
"crypto/tls"
"errors"
"io/ioutil"
"net/http"
@@ -43,7 +44,7 @@ func UploadKerberosVault(configuration *models.Configuration, fileName string) (
if err != nil {
err := "UploadKerberosVault: Upload Failed, file doesn't exists anymore."
log.Log.Info(err)
return false, true, errors.New(err)
return false, false, errors.New(err)
}
publicKey := config.KStorage.CloudKey
@@ -67,7 +68,16 @@ func UploadKerberosVault(configuration *models.Configuration, fileName string) (
req.Header.Set("X-Kerberos-Storage-Device", config.Key)
req.Header.Set("X-Kerberos-Storage-Capture", "IPCamera")
req.Header.Set("X-Kerberos-Storage-Directory", config.KStorage.Directory)
client := &http.Client{}
var client *http.Client
if os.Getenv("AGENT_TLS_INSECURE") == "true" {
tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
client = &http.Client{Transport: tr}
} else {
client = &http.Client{}
}
resp, err := client.Do(req)
if resp != nil {

View File

@@ -2,29 +2,29 @@ package components
import (
"context"
"runtime"
"os"
"strconv"
"sync"
"sync/atomic"
"time"
mqtt "github.com/eclipse/paho.mqtt.golang"
"github.com/kerberos-io/joy4/cgo/ffmpeg"
"github.com/gin-gonic/gin"
"github.com/kerberos-io/agent/machinery/src/capture"
"github.com/kerberos-io/agent/machinery/src/cloud"
"github.com/kerberos-io/agent/machinery/src/computervision"
configService "github.com/kerberos-io/agent/machinery/src/config"
"github.com/kerberos-io/agent/machinery/src/log"
"github.com/kerberos-io/agent/machinery/src/models"
"github.com/kerberos-io/agent/machinery/src/onvif"
"github.com/kerberos-io/agent/machinery/src/packets"
routers "github.com/kerberos-io/agent/machinery/src/routers/mqtt"
"github.com/kerberos-io/joy4/av"
"github.com/kerberos-io/joy4/av/pubsub"
"github.com/kerberos-io/agent/machinery/src/utils"
"github.com/tevino/abool"
)
func Bootstrap(configuration *models.Configuration, communication *models.Communication) {
log.Log.Debug("Bootstrap: started")
func Bootstrap(configDirectory string, configuration *models.Configuration, communication *models.Communication, captureDevice *capture.Capture) {
log.Log.Debug("components.Kerberos.Bootstrap(): bootstrapping the kerberos agent.")
// We will keep track of the Kerberos Agent up time
// This is send to Kerberos Hub in a heartbeat.
@@ -55,47 +55,45 @@ func Bootstrap(configuration *models.Configuration, communication *models.Commun
communication.HandleLiveSD = make(chan int64, 1)
communication.HandleLiveHDKeepalive = make(chan string, 1)
communication.HandleLiveHDPeers = make(chan string, 1)
communication.HandleONVIF = make(chan models.OnvifAction, 1)
communication.IsConfiguring = abool.New()
cameraSettings := &models.Camera{}
// Before starting the agent, we have a control goroutine, that might
// do several checks to see if the agent is still operational.
go ControlAgent(communication)
// Create some global variables
decoder := &ffmpeg.VideoDecoder{}
subDecoder := &ffmpeg.VideoDecoder{}
cameraSettings := &models.Camera{}
// Handle heartbeats
go cloud.HandleHeartBeat(configuration, communication, uptimeStart)
// We'll create a MQTT handler, which will be used to communicate with Kerberos Hub.
// Configure a MQTT client which helps for a bi-directional communication
mqttClient := routers.ConfigureMQTT(configuration, communication)
mqttClient := routers.ConfigureMQTT(configDirectory, configuration, communication)
// Run the agent and fire up all the other
// goroutines which do image capture, motion detection, onvif, etc.
for {
// This will blocking until receiving a signal to be restarted, reconfigured, stopped, etc.
status := RunAgent(configuration, communication, mqttClient, uptimeStart, cameraSettings, decoder, subDecoder)
status := RunAgent(configDirectory, configuration, communication, mqttClient, uptimeStart, cameraSettings, captureDevice)
if status == "stop" {
break
log.Log.Info("components.Kerberos.Bootstrap(): shutting down the agent in 3 seconds.")
time.Sleep(time.Second * 3)
os.Exit(0)
}
if status == "not started" {
// We will re open the configuration, might have changed :O!
OpenConfig(configuration)
configService.OpenConfig(configDirectory, configuration)
// We will override the configuration with the environment variables
OverrideWithEnvironmentVariables(configuration)
configService.OverrideWithEnvironmentVariables(configuration)
}
// Reset the MQTT client, might have provided new information, so we need to reconnect.
if routers.HasMQTTClientModified(configuration) {
routers.DisconnectMQTT(mqttClient, &configuration.Config)
mqttClient = routers.ConfigureMQTT(configuration, communication)
mqttClient = routers.ConfigureMQTT(configDirectory, configuration, communication)
}
// We will create a new cancelable context, which will be used to cancel and restart.
@@ -104,236 +102,300 @@ func Bootstrap(configuration *models.Configuration, communication *models.Commun
communication.Context = &ctx
communication.CancelContext = &cancel
}
log.Log.Debug("Bootstrap: finished")
}
func RunAgent(configuration *models.Configuration, communication *models.Communication, mqttClient mqtt.Client, uptimeStart time.Time, cameraSettings *models.Camera, decoder *ffmpeg.VideoDecoder, subDecoder *ffmpeg.VideoDecoder) string {
func RunAgent(configDirectory string, configuration *models.Configuration, communication *models.Communication, mqttClient mqtt.Client, uptimeStart time.Time, cameraSettings *models.Camera, captureDevice *capture.Capture) string {
log.Log.Debug("RunAgent: bootstrapping agent")
log.Log.Info("components.Kerberos.RunAgent(): Creating camera and processing threads.")
config := configuration.Config
status := "not started"
// Currently only support H264 encoded cameras, this will change.
// Establishing the camera connection
// Establishing the camera connection without backchannel if no substream
rtspUrl := config.Capture.IPCamera.RTSP
infile, streams, err := capture.OpenRTSP(context.Background(), rtspUrl)
// We will initialise the camera settings object
// so we can check if the camera settings have changed, and we need
// to reload the decoders.
videoStream, _ := capture.GetVideoStream(streams)
if videoStream == nil {
log.Log.Error("RunAgent: no video stream found, might be the wrong codec (we only support H264 for the moment)")
rtspClient := captureDevice.SetMainClient(rtspUrl)
if rtspUrl != "" {
err := rtspClient.Connect(context.Background())
if err != nil {
log.Log.Error("components.Kerberos.RunAgent(): error connecting to RTSP stream: " + err.Error())
rtspClient.Close()
rtspClient = nil
time.Sleep(time.Second * 3)
return status
}
} else {
log.Log.Error("components.Kerberos.RunAgent(): no rtsp url found in config, please provide one.")
rtspClient = nil
time.Sleep(time.Second * 3)
return status
}
num, denum := videoStream.(av.VideoCodecData).Framerate()
width := videoStream.(av.VideoCodecData).Width()
height := videoStream.(av.VideoCodecData).Height()
var queue *pubsub.Queue
var subQueue *pubsub.Queue
var decoderMutex sync.Mutex
var subDecoderMutex sync.Mutex
if err == nil {
log.Log.Info("RunAgent: opened RTSP stream: " + rtspUrl)
// We might have a secondary rtsp url, so we might need to use that.
var subInfile av.DemuxCloser
var subStreams []av.CodecData
subStreamEnabled := false
subRtspUrl := config.Capture.IPCamera.SubRTSP
if subRtspUrl != "" && subRtspUrl != rtspUrl {
subInfile, subStreams, err = capture.OpenRTSP(context.Background(), subRtspUrl)
if err == nil {
log.Log.Info("RunAgent: opened RTSP sub stream " + subRtspUrl)
subStreamEnabled = true
}
videoStream, _ := capture.GetVideoStream(subStreams)
if videoStream == nil {
log.Log.Error("RunAgent: no video substream found, might be the wrong codec (we only support H264 for the moment)")
time.Sleep(time.Second * 3)
return status
}
}
if cameraSettings.RTSP != rtspUrl || cameraSettings.SubRTSP != subRtspUrl || cameraSettings.Width != width || cameraSettings.Height != height || cameraSettings.Num != num || cameraSettings.Denum != denum || cameraSettings.Codec != videoStream.(av.VideoCodecData).Type() {
if cameraSettings.Initialized {
decoder.Close()
if subStreamEnabled {
subDecoder.Close()
}
}
// At some routines we will need to decode the image.
// Make sure its properly locked as we only have a single decoder.
log.Log.Info("RunAgent: camera settings changed, reloading decoder")
capture.GetVideoDecoder(decoder, streams)
if subStreamEnabled {
capture.GetVideoDecoder(subDecoder, subStreams)
}
cameraSettings.RTSP = rtspUrl
cameraSettings.SubRTSP = subRtspUrl
cameraSettings.Width = width
cameraSettings.Height = height
cameraSettings.Framerate = float64(num) / float64(denum)
cameraSettings.Num = num
cameraSettings.Denum = denum
cameraSettings.Codec = videoStream.(av.VideoCodecData).Type()
cameraSettings.Initialized = true
} else {
log.Log.Info("RunAgent: camera settings did not change, keeping decoder")
}
communication.Decoder = decoder
communication.SubDecoder = subDecoder
communication.DecoderMutex = &decoderMutex
communication.SubDecoderMutex = &subDecoderMutex
// Create a packet queue, which is filled by the HandleStream routing
// and consumed by all other routines: motion, livestream, etc.
if config.Capture.PreRecording <= 0 {
config.Capture.PreRecording = 1
log.Log.Warning("RunAgent: Prerecording value not found in config or invalid value! Found: " + strconv.FormatInt(config.Capture.PreRecording, 10))
}
// We are creating a queue to store the RTSP frames in, these frames will be
// processed by the different consumers: motion detection, recording, etc.
queue = pubsub.NewQueue()
communication.Queue = queue
queue.SetMaxGopCount(int(config.Capture.PreRecording) + 1) // GOP time frame is set to prerecording (we'll add 2 gops to leave some room).
log.Log.Info("RunAgent: SetMaxGopCount was set with: " + strconv.Itoa(int(config.Capture.PreRecording)+1))
queue.WriteHeader(streams)
// We might have a substream, if so we'll create a seperate queue.
if subStreamEnabled {
log.Log.Info("RunAgent: Creating sub stream queue with SetMaxGopCount set to " + strconv.Itoa(int(1)))
subQueue = pubsub.NewQueue()
communication.SubQueue = subQueue
subQueue.SetMaxGopCount(1)
subQueue.WriteHeader(subStreams)
}
// Handle the camera stream
go capture.HandleStream(infile, queue, communication)
// Handle the substream if enabled
if subStreamEnabled {
go capture.HandleSubStream(subInfile, subQueue, communication)
}
// Handle processing of motion
communication.HandleMotion = make(chan models.MotionDataPartial, 1)
if subStreamEnabled {
motionCursor := subQueue.Latest()
go computervision.ProcessMotion(motionCursor, configuration, communication, mqttClient, subDecoder, &subDecoderMutex)
} else {
motionCursor := queue.Latest()
go computervision.ProcessMotion(motionCursor, configuration, communication, mqttClient, decoder, &decoderMutex)
}
// Handle livestream SD (low resolution over MQTT)
if subStreamEnabled {
livestreamCursor := subQueue.Latest()
go cloud.HandleLiveStreamSD(livestreamCursor, configuration, communication, mqttClient, subDecoder, &subDecoderMutex)
} else {
livestreamCursor := queue.Latest()
go cloud.HandleLiveStreamSD(livestreamCursor, configuration, communication, mqttClient, decoder, &decoderMutex)
}
// Handle livestream HD (high resolution over WEBRTC)
communication.HandleLiveHDHandshake = make(chan models.SDPPayload, 1)
if subStreamEnabled {
livestreamHDCursor := subQueue.Latest()
go cloud.HandleLiveStreamHD(livestreamHDCursor, configuration, communication, mqttClient, subStreams, subDecoder, &decoderMutex)
} else {
livestreamHDCursor := queue.Latest()
go cloud.HandleLiveStreamHD(livestreamHDCursor, configuration, communication, mqttClient, streams, decoder, &decoderMutex)
}
// Handle recording, will write an mp4 to disk.
go capture.HandleRecordStream(queue, configuration, communication, streams)
// Handle Upload to cloud provider (Kerberos Hub, Kerberos Vault and others)
go cloud.HandleUpload(configuration, communication)
// Handle ONVIF actions
go onvif.HandleONVIFActions(configuration, communication)
// If we reach this point, we have a working RTSP connection.
communication.CameraConnected = true
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
// This will go into a blocking state, once this channel is triggered
// the agent will cleanup and restart.
status = <-communication.HandleBootstrap
// If we reach this point, we are stopping the stream.
communication.CameraConnected = false
// Cancel the main context, this will stop all the other goroutines.
(*communication.CancelContext)()
// We will re open the configuration, might have changed :O!
OpenConfig(configuration)
// We will override the configuration with the environment variables
OverrideWithEnvironmentVariables(configuration)
// Here we are cleaning up everything!
if configuration.Config.Offline != "true" {
communication.HandleUpload <- "stop"
}
communication.HandleStream <- "stop"
if subStreamEnabled {
communication.HandleSubStream <- "stop"
}
log.Log.Info("components.Kerberos.RunAgent(): opened RTSP stream: " + rtspUrl)
// Get the video streams from the RTSP server.
videoStreams, err := rtspClient.GetVideoStreams()
if err != nil || len(videoStreams) == 0 {
log.Log.Error("components.Kerberos.RunAgent(): no video stream found, might be the wrong codec (we only support H264 for the moment)")
rtspClient.Close()
time.Sleep(time.Second * 3)
infile.Close()
infile = nil
queue.Close()
queue = nil
communication.Queue = nil
if subStreamEnabled {
subInfile.Close()
subInfile = nil
subQueue.Close()
subQueue = nil
communication.SubQueue = nil
}
close(communication.HandleMotion)
communication.HandleMotion = nil
// Waiting for some seconds to make sure everything is properly closed.
log.Log.Info("RunAgent: waiting 3 seconds to make sure everything is properly closed.")
time.Sleep(time.Second * 3)
} else {
log.Log.Error("Something went wrong while opening RTSP: " + err.Error())
time.Sleep(time.Second * 3)
return status
}
log.Log.Debug("RunAgent: finished")
// Get the video stream from the RTSP server.
videoStream := videoStreams[0]
// Clean up, force garbage collection
runtime.GC()
// Get some information from the video stream.
width := videoStream.Width
height := videoStream.Height
// Set config values as well
configuration.Config.Capture.IPCamera.Width = width
configuration.Config.Capture.IPCamera.Height = height
var queue *packets.Queue
var subQueue *packets.Queue
// Create a packet queue, which is filled by the HandleStream routing
// and consumed by all other routines: motion, livestream, etc.
if config.Capture.PreRecording <= 0 {
config.Capture.PreRecording = 1
log.Log.Warning("components.Kerberos.RunAgent(): Prerecording value not found in config or invalid value! Found: " + strconv.FormatInt(config.Capture.PreRecording, 10))
}
// We might have a secondary rtsp url, so we might need to use that for livestreaming let us check first!
subStreamEnabled := false
subRtspUrl := config.Capture.IPCamera.SubRTSP
var videoSubStreams []packets.Stream
if subRtspUrl != "" && subRtspUrl != rtspUrl {
// For the sub stream we will not enable backchannel.
subStreamEnabled = true
rtspSubClient := captureDevice.SetSubClient(subRtspUrl)
captureDevice.RTSPSubClient = rtspSubClient
err := rtspSubClient.Connect(context.Background())
if err != nil {
log.Log.Error("components.Kerberos.RunAgent(): error connecting to RTSP sub stream: " + err.Error())
time.Sleep(time.Second * 3)
return status
}
log.Log.Info("components.Kerberos.RunAgent(): opened RTSP sub stream: " + rtspUrl)
// Get the video streams from the RTSP server.
videoSubStreams, err = rtspSubClient.GetVideoStreams()
if err != nil || len(videoSubStreams) == 0 {
log.Log.Error("components.Kerberos.RunAgent(): no video sub stream found, might be the wrong codec (we only support H264 for the moment)")
rtspSubClient.Close()
time.Sleep(time.Second * 3)
return status
}
// Get the video stream from the RTSP server.
videoSubStream := videoSubStreams[0]
width := videoSubStream.Width
height := videoSubStream.Height
// Set config values as well
configuration.Config.Capture.IPCamera.Width = width
configuration.Config.Capture.IPCamera.Height = height
}
if cameraSettings.RTSP != rtspUrl ||
cameraSettings.SubRTSP != subRtspUrl ||
cameraSettings.Width != width ||
cameraSettings.Height != height {
// TODO: this condition is used to reset the decoder when the camera settings change.
// The main idea is that you only set the decoder once, and then reuse it on each restart (no new memory allocation).
// However the stream settings of the camera might have been changed, and so the decoder might need to be reloaded.
// .... Not used for the moment ....
if cameraSettings.RTSP != "" && cameraSettings.SubRTSP != "" && cameraSettings.Initialized {
//decoder.Close()
//if subStreamEnabled {
// subDecoder.Close()
//}
}
// At some routines we will need to decode the image.
// Make sure its properly locked as we only have a single decoder.
log.Log.Info("components.Kerberos.RunAgent(): camera settings changed, reloading decoder")
//capture.GetVideoDecoder(decoder, streams)
//if subStreamEnabled {
// capture.GetVideoDecoder(subDecoder, subStreams)
//}
cameraSettings.RTSP = rtspUrl
cameraSettings.SubRTSP = subRtspUrl
cameraSettings.Width = width
cameraSettings.Height = height
cameraSettings.Initialized = true
} else {
log.Log.Info("components.Kerberos.RunAgent(): camera settings did not change, keeping decoder")
}
// We are creating a queue to store the RTSP frames in, these frames will be
// processed by the different consumers: motion detection, recording, etc.
queue = packets.NewQueue()
communication.Queue = queue
// Set the maximum GOP count, this is used to determine the pre-recording time.
log.Log.Info("components.Kerberos.RunAgent(): SetMaxGopCount was set with: " + strconv.Itoa(int(config.Capture.PreRecording)+1))
queue.SetMaxGopCount(int(config.Capture.PreRecording) + 1) // GOP time frame is set to prerecording (we'll add 2 gops to leave some room).
queue.WriteHeader(videoStreams)
go rtspClient.Start(context.Background(), queue, configuration, communication)
// Try to create backchannel
rtspBackChannelClient := captureDevice.SetBackChannelClient(rtspUrl)
err = rtspBackChannelClient.ConnectBackChannel(context.Background())
if err == nil {
log.Log.Info("components.Kerberos.RunAgent(): opened RTSP backchannel stream: " + rtspUrl)
go rtspBackChannelClient.StartBackChannel(context.Background())
}
rtspSubClient := captureDevice.RTSPSubClient
if subStreamEnabled && rtspSubClient != nil {
subQueue = packets.NewQueue()
communication.SubQueue = subQueue
subQueue.SetMaxGopCount(1) // GOP time frame is set to prerecording (we'll add 2 gops to leave some room).
subQueue.WriteHeader(videoSubStreams)
go rtspSubClient.Start(context.Background(), subQueue, configuration, communication)
}
// Handle livestream SD (low resolution over MQTT)
if subStreamEnabled {
livestreamCursor := subQueue.Latest()
go cloud.HandleLiveStreamSD(livestreamCursor, configuration, communication, mqttClient, rtspSubClient)
} else {
livestreamCursor := queue.Latest()
go cloud.HandleLiveStreamSD(livestreamCursor, configuration, communication, mqttClient, rtspClient)
}
// Handle livestream HD (high resolution over WEBRTC)
communication.HandleLiveHDHandshake = make(chan models.RequestHDStreamPayload, 1)
if subStreamEnabled {
livestreamHDCursor := subQueue.Latest()
go cloud.HandleLiveStreamHD(livestreamHDCursor, configuration, communication, mqttClient, rtspSubClient)
} else {
livestreamHDCursor := queue.Latest()
go cloud.HandleLiveStreamHD(livestreamHDCursor, configuration, communication, mqttClient, rtspClient)
}
// Handle recording, will write an mp4 to disk.
go capture.HandleRecordStream(queue, configDirectory, configuration, communication, rtspClient)
// Handle processing of motion
communication.HandleMotion = make(chan models.MotionDataPartial, 1)
if subStreamEnabled {
motionCursor := subQueue.Latest()
go computervision.ProcessMotion(motionCursor, configuration, communication, mqttClient, rtspSubClient)
} else {
motionCursor := queue.Latest()
go computervision.ProcessMotion(motionCursor, configuration, communication, mqttClient, rtspClient)
}
// Handle Upload to cloud provider (Kerberos Hub, Kerberos Vault and others)
go cloud.HandleUpload(configDirectory, configuration, communication)
// Handle ONVIF actions
communication.HandleONVIF = make(chan models.OnvifAction, 1)
go onvif.HandleONVIFActions(configuration, communication)
communication.HandleAudio = make(chan models.AudioDataPartial, 1)
if rtspBackChannelClient.HasBackChannel {
communication.HasBackChannel = true
go WriteAudioToBackchannel(communication, rtspBackChannelClient)
}
// If we reach this point, we have a working RTSP connection.
communication.CameraConnected = true
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
// This will go into a blocking state, once this channel is triggered
// the agent will cleanup and restart.
status = <-communication.HandleBootstrap
// If we reach this point, we are stopping the stream.
communication.CameraConnected = false
// Cancel the main context, this will stop all the other goroutines.
(*communication.CancelContext)()
// We will re open the configuration, might have changed :O!
configService.OpenConfig(configDirectory, configuration)
// We will override the configuration with the environment variables
configService.OverrideWithEnvironmentVariables(configuration)
// Here we are cleaning up everything!
if configuration.Config.Offline != "true" {
communication.HandleUpload <- "stop"
}
communication.HandleStream <- "stop"
// We use the steam channel to stop both main and sub stream.
//if subStreamEnabled {
// communication.HandleSubStream <- "stop"
//}
time.Sleep(time.Second * 3)
err = rtspClient.Close()
if err != nil {
log.Log.Error("components.Kerberos.RunAgent(): error closing RTSP stream: " + err.Error())
time.Sleep(time.Second * 3)
return status
}
queue.Close()
queue = nil
communication.Queue = nil
if subStreamEnabled {
err = rtspSubClient.Close()
if err != nil {
log.Log.Error("components.Kerberos.RunAgent(): error closing RTSP sub stream: " + err.Error())
time.Sleep(time.Second * 3)
return status
}
subQueue.Close()
subQueue = nil
communication.SubQueue = nil
}
err = rtspBackChannelClient.Close()
if err != nil {
log.Log.Error("components.Kerberos.RunAgent(): error closing RTSP backchannel stream: " + err.Error())
}
time.Sleep(time.Second * 3)
close(communication.HandleLiveHDHandshake)
communication.HandleLiveHDHandshake = nil
close(communication.HandleMotion)
communication.HandleMotion = nil
close(communication.HandleAudio)
communication.HandleAudio = nil
close(communication.HandleONVIF)
communication.HandleONVIF = nil
// Waiting for some seconds to make sure everything is properly closed.
log.Log.Info("components.Kerberos.RunAgent(): waiting 3 seconds to make sure everything is properly closed.")
time.Sleep(time.Second * 3)
return status
}
// ControlAgent will check if the camera is still connected, if not it will restart the agent.
// In the other thread we are keeping track of the number of packets received, and particular the keyframe packets.
// Once we are not receiving any packets anymore, we will restart the agent.
func ControlAgent(communication *models.Communication) {
log.Log.Debug("ControlAgent: started")
log.Log.Debug("components.Kerberos.ControlAgent(): started")
packageCounter := communication.PackageCounter
go func() {
// A channel to check the camera activity
@@ -354,11 +416,11 @@ func ControlAgent(communication *models.Communication) {
occurence = 0
}
log.Log.Info("ControlAgent: Number of packets read " + strconv.FormatInt(packetsR, 10))
log.Log.Info("components.Kerberos.ControlAgent(): Number of packets read " + strconv.FormatInt(packetsR, 10))
// After 15 seconds without activity this is thrown..
if occurence == 3 {
log.Log.Info("Main: Restarting machinery.")
log.Log.Info("components.Kerberos.ControlAgent(): Restarting machinery.")
communication.HandleBootstrap <- "restart"
time.Sleep(2 * time.Second)
occurence = 0
@@ -369,5 +431,255 @@ func ControlAgent(communication *models.Communication) {
time.Sleep(5 * time.Second)
}
}()
log.Log.Debug("ControlAgent: finished")
log.Log.Debug("components.Kerberos.ControlAgent(): finished")
}
// GetDashboard godoc
// @Router /api/dashboard [get]
// @ID dashboard
// @Tags general
// @Summary Get all information showed on the dashboard.
// @Description Get all information showed on the dashboard.
// @Success 200
func GetDashboard(c *gin.Context, configDirectory string, configuration *models.Configuration, communication *models.Communication) {
// Check if camera is online.
cameraIsOnline := communication.CameraConnected
// If an agent is properly setup with Kerberos Hub, we will send
// a ping to Kerberos Hub every 15seconds. On receiving a positive response
// it will update the CloudTimestamp value.
cloudIsOnline := false
if communication.CloudTimestamp != nil && communication.CloudTimestamp.Load() != nil {
timestamp := communication.CloudTimestamp.Load().(int64)
if timestamp > 0 {
cloudIsOnline = true
}
}
// The total number of recordings stored in the directory.
recordingDirectory := configDirectory + "/data/recordings"
numberOfRecordings := utils.NumberOfMP4sInDirectory(recordingDirectory)
// All days stored in this agent.
days := []string{}
latestEvents := []models.Media{}
files, err := utils.ReadDirectory(recordingDirectory)
if err == nil {
events := utils.GetSortedDirectory(files)
// Get All days
days = utils.GetDays(events, recordingDirectory, configuration)
// Get all latest events
var eventFilter models.EventFilter
eventFilter.NumberOfElements = 5
latestEvents = utils.GetMediaFormatted(events, recordingDirectory, configuration, eventFilter) // will get 5 latest recordings.
}
c.JSON(200, gin.H{
"offlineMode": configuration.Config.Offline,
"cameraOnline": cameraIsOnline,
"cloudOnline": cloudIsOnline,
"numberOfRecordings": numberOfRecordings,
"days": days,
"latestEvents": latestEvents,
})
}
// GetLatestEvents godoc
// @Router /api/latest-events [post]
// @ID latest-events
// @Tags general
// @Param eventFilter body models.EventFilter true "Event filter"
// @Summary Get the latest recordings (events) from the recordings directory.
// @Description Get the latest recordings (events) from the recordings directory.
// @Success 200
func GetLatestEvents(c *gin.Context, configDirectory string, configuration *models.Configuration, communication *models.Communication) {
var eventFilter models.EventFilter
err := c.BindJSON(&eventFilter)
if err == nil {
// Default to 10 if no limit is set.
if eventFilter.NumberOfElements == 0 {
eventFilter.NumberOfElements = 10
}
recordingDirectory := configDirectory + "/data/recordings"
files, err := utils.ReadDirectory(recordingDirectory)
if err == nil {
events := utils.GetSortedDirectory(files)
// We will get all recordings from the directory (as defined by the filter).
fileObjects := utils.GetMediaFormatted(events, recordingDirectory, configuration, eventFilter)
c.JSON(200, gin.H{
"events": fileObjects,
})
} else {
c.JSON(400, gin.H{
"data": "Something went wrong: " + err.Error(),
})
}
} else {
c.JSON(400, gin.H{
"data": "Something went wrong: " + err.Error(),
})
}
}
// GetDays godoc
// @Router /api/days [get]
// @ID days
// @Tags general
// @Summary Get all days stored in the recordings directory.
// @Description Get all days stored in the recordings directory.
// @Success 200
func GetDays(c *gin.Context, configDirectory string, configuration *models.Configuration, communication *models.Communication) {
recordingDirectory := configDirectory + "/data/recordings"
files, err := utils.ReadDirectory(recordingDirectory)
if err == nil {
events := utils.GetSortedDirectory(files)
days := utils.GetDays(events, recordingDirectory, configuration)
c.JSON(200, gin.H{
"events": days,
})
} else {
c.JSON(400, gin.H{
"data": "Something went wrong: " + err.Error(),
})
}
}
// StopAgent godoc
// @Router /api/camera/stop [post]
// @ID camera-stop
// @Tags camera
// @Summary Stop the agent.
// @Description Stop the agent.
// @Success 200 {object} models.APIResponse
func StopAgent(c *gin.Context, communication *models.Communication) {
log.Log.Info("components.Kerberos.StopAgent(): sending signal to stop agent, this will os.Exit(0).")
communication.HandleBootstrap <- "stop"
c.JSON(200, gin.H{
"stopped": true,
})
}
// RestartAgent godoc
// @Router /api/camera/restart [post]
// @ID camera-restart
// @Tags camera
// @Summary Restart the agent.
// @Description Restart the agent.
// @Success 200 {object} models.APIResponse
func RestartAgent(c *gin.Context, communication *models.Communication) {
log.Log.Info("components.Kerberos.RestartAgent(): sending signal to restart agent.")
communication.HandleBootstrap <- "restart"
c.JSON(200, gin.H{
"restarted": true,
})
}
// MakeRecording godoc
// @Router /api/camera/record [post]
// @ID camera-record
// @Tags camera
// @Summary Make a recording.
// @Description Make a recording.
// @Success 200 {object} models.APIResponse
func MakeRecording(c *gin.Context, communication *models.Communication) {
log.Log.Info("components.Kerberos.MakeRecording(): sending signal to start recording.")
dataToPass := models.MotionDataPartial{
Timestamp: time.Now().Unix(),
NumberOfChanges: 100000000, // hack set the number of changes to a high number to force recording
}
communication.HandleMotion <- dataToPass //Save data to the channel
c.JSON(200, gin.H{
"recording": true,
})
}
// GetSnapshotBase64 godoc
// @Router /api/camera/snapshot/base64 [get]
// @ID snapshot-base64
// @Tags camera
// @Summary Get a snapshot from the camera in base64.
// @Description Get a snapshot from the camera in base64.
// @Success 200
func GetSnapshotBase64(c *gin.Context, captureDevice *capture.Capture, configuration *models.Configuration, communication *models.Communication) {
// We'll try to get a snapshot from the camera.
base64Image := capture.Base64Image(captureDevice, communication)
if base64Image != "" {
communication.Image = base64Image
}
c.JSON(200, gin.H{
"base64": communication.Image,
})
}
// GetSnapshotJpeg godoc
// @Router /api/camera/snapshot/jpeg [get]
// @ID snapshot-jpeg
// @Tags camera
// @Summary Get a snapshot from the camera in jpeg format.
// @Description Get a snapshot from the camera in jpeg format.
// @Success 200
func GetSnapshotRaw(c *gin.Context, captureDevice *capture.Capture, configuration *models.Configuration, communication *models.Communication) {
// We'll try to get a snapshot from the camera.
image := capture.JpegImage(captureDevice, communication)
// encode image to jpeg
bytes, _ := utils.ImageToBytes(&image)
// Return image/jpeg
c.Data(200, "image/jpeg", bytes)
}
// GetConfig godoc
// @Router /api/config [get]
// @ID config
// @Tags config
// @Summary Get the current configuration.
// @Description Get the current configuration.
// @Success 200
func GetConfig(c *gin.Context, captureDevice *capture.Capture, configuration *models.Configuration, communication *models.Communication) {
// We'll try to get a snapshot from the camera.
base64Image := capture.Base64Image(captureDevice, communication)
if base64Image != "" {
communication.Image = base64Image
}
c.JSON(200, gin.H{
"config": configuration.Config,
"custom": configuration.CustomConfig,
"global": configuration.GlobalConfig,
"snapshot": communication.Image,
})
}
// UpdateConfig godoc
// @Router /api/config [post]
// @ID config
// @Tags config
// @Param config body models.Config true "Configuration"
// @Summary Update the current configuration.
// @Description Update the current configuration.
// @Success 200
func UpdateConfig(c *gin.Context, configDirectory string, configuration *models.Configuration, communication *models.Communication) {
var config models.Config
err := c.BindJSON(&config)
if err == nil {
err := configService.SaveConfig(configDirectory, config, configuration, communication)
if err == nil {
c.JSON(200, gin.H{
"data": "☄ Reconfiguring",
})
} else {
c.JSON(200, gin.H{
"data": "☄ Reconfiguring",
})
}
} else {
c.JSON(400, gin.H{
"data": "Something went wrong: " + err.Error(),
})
}
}

View File

@@ -1,25 +0,0 @@
package components
import (
"time"
"github.com/cedricve/go-onvif"
"github.com/kerberos-io/agent/machinery/src/log"
)
func Discover(timeout time.Duration) {
log.Log.Info("Discovering devices")
log.Log.Info("Waiting for " + (timeout * time.Second).String())
devices, err := onvif.StartDiscovery(timeout * time.Second)
if err != nil {
log.Log.Error(err.Error())
} else {
for _, device := range devices {
hostname, _ := device.GetHostname()
log.Log.Info(hostname.Name)
}
if len(devices) == 0 {
log.Log.Info("No devices descovered\n")
}
}
}

View File

@@ -1,93 +0,0 @@
package components
import (
"fmt"
"image"
"image/jpeg"
"log"
"time"
"github.com/deepch/vdk/av"
"github.com/deepch/vdk/codec/h264parser"
"github.com/deepch/vdk/format/rtsp"
"github.com/nsmith5/mjpeg"
)
type Stream struct {
Name string
Url string
Debug bool
Codecs string
}
func CreateStream(name string, url string) *Stream {
return &Stream{
Name: name,
Url: url,
}
}
func (s Stream) Open() *rtsp.Client {
// Enable debugging
if s.Debug {
rtsp.DebugRtsp = true
}
fmt.Println("Dialing in to " + s.Url)
session, err := rtsp.Dial(s.Url)
if err != nil {
log.Println("Something went wrong dialing into stream: ", err)
time.Sleep(5 * time.Second)
}
session.RtpKeepAliveTimeout = 10 * time.Second
return session
}
func (s Stream) Close(session *rtsp.Client) {
fmt.Println("Closing RTSP session.")
err := session.Close()
if err != nil {
log.Println("Something went wrong while closing your RTSP session: ", err)
}
}
func (s Stream) GetCodecs() []av.CodecData {
session := s.Open()
codec, err := session.Streams()
log.Println("Reading codecs from stream: ", codec)
if err != nil {
log.Println("Something went wrong while reading codecs from stream: ", err)
time.Sleep(5 * time.Second)
}
s.Close(session)
return codec
}
func (s Stream) ReadPackets(packetChannel chan av.Packet) {
session := s.Open()
for {
packet, err := session.ReadPacket()
if err != nil {
break
}
if len(packetChannel) < cap(packetChannel) {
packetChannel <- packet
}
}
s.Close(session)
}
func GetSPSFromCodec(codecs []av.CodecData) ([]byte, []byte) {
sps := codecs[0].(h264parser.CodecData).SPS()
pps := codecs[0].(h264parser.CodecData).PPS()
return sps, pps
}
func StartMotionJPEG(imageFunction func() (image.Image, error), quality int) mjpeg.Handler {
stream := mjpeg.Handler{
Next: imageFunction,
Options: &jpeg.Options{Quality: quality},
}
return stream
}

View File

@@ -0,0 +1,96 @@
package components
import (
"bufio"
"fmt"
"os"
"time"
"github.com/kerberos-io/agent/machinery/src/capture"
"github.com/kerberos-io/agent/machinery/src/log"
"github.com/kerberos-io/agent/machinery/src/models"
"github.com/kerberos-io/agent/machinery/src/packets"
"github.com/kerberos-io/joy4/av"
"github.com/pion/rtp"
"github.com/zaf/g711"
)
func GetBackChannelAudioCodec(streams []av.CodecData, communication *models.Communication) av.AudioCodecData {
for _, stream := range streams {
if stream.Type().IsAudio() {
if stream.Type().String() == "PCM_MULAW" {
pcmuCodec := stream.(av.AudioCodecData)
if pcmuCodec.IsBackChannel() {
communication.HasBackChannel = true
return pcmuCodec
}
}
}
}
return nil
}
func WriteAudioToBackchannel(communication *models.Communication, rtspClient capture.RTSPClient) {
log.Log.Info("Audio.WriteAudioToBackchannel(): writing to backchannel audio codec")
length := uint32(0)
sequenceNumber := uint16(0)
for audio := range communication.HandleAudio {
// Encode PCM to MULAW
var bufferUlaw []byte
for _, v := range audio.Data {
b := g711.EncodeUlawFrame(v)
bufferUlaw = append(bufferUlaw, b)
}
pkt := packets.Packet{
Packet: &rtp.Packet{
Header: rtp.Header{
Version: 2,
Marker: true, // should be true
PayloadType: 0, //packet.PayloadType, // will be owerwriten
SequenceNumber: sequenceNumber,
Timestamp: uint32(length),
SSRC: 1293847657,
},
Payload: bufferUlaw,
},
}
err := rtspClient.WritePacket(pkt)
if err != nil {
log.Log.Error("Audio.WriteAudioToBackchannel(): error writing packet to backchannel")
}
length = (length + uint32(len(bufferUlaw))) % 65536
sequenceNumber = (sequenceNumber + 1) % 65535
time.Sleep(128 * time.Millisecond)
}
log.Log.Info("Audio.WriteAudioToBackchannel(): finished")
}
func WriteFileToBackChannel(infile av.DemuxCloser) {
// Do the warmup!
file, err := os.Open("./audiofile.bye")
if err != nil {
fmt.Println("WriteFileToBackChannel: error opening audiofile.bye file")
}
defer file.Close()
// Read file into buffer
reader := bufio.NewReader(file)
buffer := make([]byte, 1024)
count := 0
for {
_, err := reader.Read(buffer)
if err != nil {
break
}
// Send to backchannel
fmt.Println(buffer)
infile.Write(buffer, 2, uint32(count))
count = count + 1024
time.Sleep(128 * time.Millisecond)
}
}

View File

@@ -1,28 +1,23 @@
package computervision
import (
"bufio"
"bytes"
"encoding/base64"
"image"
"image/jpeg"
"sync"
"time"
mqtt "github.com/eclipse/paho.mqtt.golang"
geo "github.com/kellydunn/golang-geo"
"github.com/kerberos-io/agent/machinery/src/capture"
"github.com/kerberos-io/agent/machinery/src/conditions"
"github.com/kerberos-io/agent/machinery/src/log"
"github.com/kerberos-io/agent/machinery/src/models"
"github.com/kerberos-io/joy4/av"
"github.com/kerberos-io/joy4/av/pubsub"
"github.com/kerberos-io/joy4/cgo/ffmpeg"
"github.com/kerberos-io/agent/machinery/src/packets"
)
func ProcessMotion(motionCursor *pubsub.QueueCursor, configuration *models.Configuration, communication *models.Communication, mqttClient mqtt.Client, decoder *ffmpeg.VideoDecoder, decoderMutex *sync.Mutex) { //, wg *sync.WaitGroup) {
func ProcessMotion(motionCursor *packets.QueueCursor, configuration *models.Configuration, communication *models.Communication, mqttClient mqtt.Client, rtspClient capture.RTSPClient) {
log.Log.Debug("ProcessMotion: started")
log.Log.Debug("computervision.main.ProcessMotion(): start motion detection")
config := configuration.Config
loc, _ := time.LoadLocation(config.Timezone)
var isPixelChangeThresholdReached = false
var changesToReturn = 0
@@ -35,16 +30,14 @@ func ProcessMotion(motionCursor *pubsub.QueueCursor, configuration *models.Confi
if config.Capture.Continuous == "true" {
log.Log.Info("ProcessMotion: Continuous recording, so no motion detection.")
log.Log.Info("computervision.main.ProcessMotion(): you've enabled continuous recording, so no motion detection required.")
} else {
log.Log.Info("ProcessMotion: Motion detection enabled.")
log.Log.Info("computervision.main.ProcessMotion(): motion detected is enabled, so starting the motion detection.")
key := config.HubKey
// Allocate a VideoFrame
frame := ffmpeg.AllocVideoFrame()
hubKey := config.HubKey
deviceKey := config.Key
// Initialise first 2 elements
var imageArray [3]*image.Gray
@@ -52,15 +45,15 @@ func ProcessMotion(motionCursor *pubsub.QueueCursor, configuration *models.Confi
j := 0
var cursorError error
var pkt av.Packet
var pkt packets.Packet
for cursorError == nil {
pkt, cursorError = motionCursor.ReadPacket()
// Check If valid package.
if len(pkt.Data) > 0 && pkt.IsKeyFrame {
grayImage, err := GetGrayImage(frame, pkt, decoder, decoderMutex)
grayImage, err := rtspClient.DecodePacketRaw(pkt)
if err == nil {
imageArray[j] = grayImage
imageArray[j] = &grayImage
j++
}
}
@@ -69,34 +62,33 @@ func ProcessMotion(motionCursor *pubsub.QueueCursor, configuration *models.Confi
}
}
img := imageArray[0]
if img != nil {
// Calculate mask
var polyObjects []geo.Polygon
// Calculate mask
var polyObjects []geo.Polygon
if config.Region != nil {
for _, polygon := range config.Region.Polygon {
coords := polygon.Coordinates
poly := geo.Polygon{}
for _, c := range coords {
x := c.X
y := c.Y
p := geo.NewPoint(x, y)
if !poly.Contains(p) {
poly.Add(p)
}
if config.Region != nil {
for _, polygon := range config.Region.Polygon {
coords := polygon.Coordinates
poly := geo.Polygon{}
for _, c := range coords {
x := c.X
y := c.Y
p := geo.NewPoint(x, y)
if !poly.Contains(p) {
poly.Add(p)
}
polyObjects = append(polyObjects, poly)
}
polyObjects = append(polyObjects, poly)
}
}
img := imageArray[0]
var coordinatesToCheck []int
if img != nil {
bounds := img.Bounds()
rows := bounds.Dy()
cols := bounds.Dx()
// Make fixed size array of uinty8
var coordinatesToCheck []int
for y := 0; y < rows; y++ {
for x := 0; x < cols; x++ {
for _, poly := range polyObjects {
@@ -107,10 +99,13 @@ func ProcessMotion(motionCursor *pubsub.QueueCursor, configuration *models.Confi
}
}
}
}
// If no region is set, we'll skip the motion detection
if len(coordinatesToCheck) > 0 {
// Start the motion detection
i := 0
loc, _ := time.LoadLocation(config.Timezone)
for cursorError == nil {
pkt, cursorError = motionCursor.ReadPacket()
@@ -120,67 +115,58 @@ func ProcessMotion(motionCursor *pubsub.QueueCursor, configuration *models.Confi
continue
}
grayImage, err := GetGrayImage(frame, pkt, decoder, decoderMutex)
grayImage, err := rtspClient.DecodePacketRaw(pkt)
if err == nil {
imageArray[2] = grayImage
imageArray[2] = &grayImage
}
// Store snapshots (jpg) for hull.
if config.Capture.Snapshots != "false" {
StoreSnapshot(communication, frame, pkt, decoder, decoderMutex)
}
// Check if within time interval
detectMotion := true
timeEnabled := config.Time
if timeEnabled != "false" {
now := time.Now().In(loc)
weekday := now.Weekday()
hour := now.Hour()
minute := now.Minute()
second := now.Second()
if config.Timetable != nil && len(config.Timetable) > 0 {
timeInterval := config.Timetable[int(weekday)]
if timeInterval != nil {
start1 := timeInterval.Start1
end1 := timeInterval.End1
start2 := timeInterval.Start2
end2 := timeInterval.End2
currentTimeInSeconds := hour*60*60 + minute*60 + second
if (currentTimeInSeconds >= start1 && currentTimeInSeconds <= end1) ||
(currentTimeInSeconds >= start2 && currentTimeInSeconds <= end2) {
} else {
detectMotion = false
log.Log.Info("ProcessMotion: Time interval not valid, disabling motion detection.")
}
}
}
// We might have different conditions enabled such as time window or uri response.
// We'll validate those conditions and if not valid we'll not do anything.
detectMotion, err := conditions.Validate(loc, configuration)
if !detectMotion && err != nil {
log.Log.Debug("computervision.main.ProcessMotion(): " + err.Error() + ".")
}
if config.Capture.Motion != "false" {
// Remember additional information about the result of findmotion
isPixelChangeThresholdReached, changesToReturn = FindMotion(imageArray, coordinatesToCheck, pixelThreshold)
if detectMotion && isPixelChangeThresholdReached {
if detectMotion {
// If offline mode is disabled, send a message to the hub
if config.Offline == "false" {
if mqttClient != nil {
if key != "" {
mqttClient.Publish("kerberos/"+key+"/device/"+config.Key+"/motion", 2, false, "motion")
} else {
mqttClient.Publish("kerberos/device/"+config.Key+"/motion", 2, false, "motion")
// Remember additional information about the result of findmotion
isPixelChangeThresholdReached, changesToReturn = FindMotion(imageArray, coordinatesToCheck, pixelThreshold)
if isPixelChangeThresholdReached {
// If offline mode is disabled, send a message to the hub
if config.Offline != "true" {
if mqttClient != nil {
if hubKey != "" {
message := models.Message{
Payload: models.Payload{
Action: "motion",
DeviceId: configuration.Config.Key,
Value: map[string]interface{}{
"timestamp": time.Now().Unix(),
},
},
}
payload, err := models.PackageMQTTMessage(configuration, message)
if err == nil {
mqttClient.Publish("kerberos/hub/"+hubKey, 0, false, payload)
} else {
log.Log.Info("computervision.main.ProcessMotion(): failed to package MQTT message: " + err.Error())
}
} else {
mqttClient.Publish("kerberos/agent/"+deviceKey, 2, false, "motion")
}
}
}
}
if config.Capture.Recording != "false" {
dataToPass := models.MotionDataPartial{
Timestamp: time.Now().Unix(),
NumberOfChanges: changesToReturn,
if config.Capture.Recording != "false" {
dataToPass := models.MotionDataPartial{
Timestamp: time.Now().Unix(),
NumberOfChanges: changesToReturn,
}
communication.HandleMotion <- dataToPass //Save data to the channel
}
communication.HandleMotion <- dataToPass //Save data to the channel
}
}
@@ -194,11 +180,9 @@ func ProcessMotion(motionCursor *pubsub.QueueCursor, configuration *models.Confi
img = nil
}
}
frame.Free()
}
log.Log.Debug("ProcessMotion: finished")
log.Log.Debug("computervision.main.ProcessMotion(): stop the motion detection.")
}
func FindMotion(imageArray [3]*image.Gray, coordinatesToCheck []int, pixelChangeThreshold int) (thresholdReached bool, changesDetected int) {
@@ -210,29 +194,6 @@ func FindMotion(imageArray [3]*image.Gray, coordinatesToCheck []int, pixelChange
return changes > pixelChangeThreshold, changes
}
func GetGrayImage(frame *ffmpeg.VideoFrame, pkt av.Packet, dec *ffmpeg.VideoDecoder, decoderMutex *sync.Mutex) (*image.Gray, error) {
_, err := capture.DecodeImage(frame, pkt, dec, decoderMutex)
// Do a deep copy of the image
imgDeepCopy := image.NewGray(frame.ImageGray.Bounds())
imgDeepCopy.Stride = frame.ImageGray.Stride
copy(imgDeepCopy.Pix, frame.ImageGray.Pix)
return imgDeepCopy, err
}
func GetRawImage(frame *ffmpeg.VideoFrame, pkt av.Packet, dec *ffmpeg.VideoDecoder, decoderMutex *sync.Mutex) (*ffmpeg.VideoFrame, error) {
_, err := capture.DecodeImage(frame, pkt, dec, decoderMutex)
return frame, err
}
func ImageToBytes(img image.Image) ([]byte, error) {
buffer := new(bytes.Buffer)
w := bufio.NewWriter(buffer)
err := jpeg.Encode(w, img, &jpeg.Options{Quality: 15})
return buffer.Bytes(), err
}
func AbsDiffBitwiseAndThreshold(img1 *image.Gray, img2 *image.Gray, img3 *image.Gray, threshold int, coordinatesToCheck []int) int {
changes := 0
for i := 0; i < len(coordinatesToCheck); i++ {
@@ -245,16 +206,3 @@ func AbsDiffBitwiseAndThreshold(img1 *image.Gray, img2 *image.Gray, img3 *image.
}
return changes
}
func StoreSnapshot(communication *models.Communication, frame *ffmpeg.VideoFrame, pkt av.Packet, decoder *ffmpeg.VideoDecoder, decoderMutex *sync.Mutex) {
rgbImage, err := GetRawImage(frame, pkt, decoder, decoderMutex)
if err == nil {
buffer := new(bytes.Buffer)
w := bufio.NewWriter(buffer)
err := jpeg.Encode(w, &rgbImage.Image, &jpeg.Options{Quality: 15})
if err == nil {
snapshot := base64.StdEncoding.EncodeToString(buffer.Bytes())
communication.Image = snapshot
}
}
}

View File

@@ -0,0 +1,28 @@
package conditions
import (
"errors"
"time"
"github.com/kerberos-io/agent/machinery/src/models"
)
func Validate(loc *time.Location, configuration *models.Configuration) (valid bool, err error) {
valid = true
err = nil
withinTimeInterval := IsWithinTimeInterval(loc, configuration)
if !withinTimeInterval {
valid = false
err = errors.New("time interval not valid")
return
}
validUriResponse := IsValidUriResponse(configuration)
if !validUriResponse {
valid = false
err = errors.New("uri response not valid")
return
}
return
}

View File

@@ -0,0 +1,39 @@
package conditions
import (
"time"
"github.com/kerberos-io/agent/machinery/src/log"
"github.com/kerberos-io/agent/machinery/src/models"
)
func IsWithinTimeInterval(loc *time.Location, configuration *models.Configuration) (enabled bool) {
config := configuration.Config
timeEnabled := config.Time
enabled = true
if timeEnabled != "false" {
now := time.Now().In(loc)
weekday := now.Weekday()
hour := now.Hour()
minute := now.Minute()
second := now.Second()
if config.Timetable != nil && len(config.Timetable) > 0 {
timeInterval := config.Timetable[int(weekday)]
if timeInterval != nil {
start1 := timeInterval.Start1
end1 := timeInterval.End1
start2 := timeInterval.Start2
end2 := timeInterval.End2
currentTimeInSeconds := hour*60*60 + minute*60 + second
if (currentTimeInSeconds >= start1 && currentTimeInSeconds <= end1) ||
(currentTimeInSeconds >= start2 && currentTimeInSeconds <= end2) {
log.Log.Debug("conditions.timewindow.IsWithinTimeInterval(): time interval valid, enabling recording.")
} else {
log.Log.Info("conditions.timewindow.IsWithinTimeInterval(): time interval not valid, disabling recording.")
enabled = false
}
}
}
}
return
}

View File

@@ -0,0 +1,59 @@
package conditions
import (
"bytes"
"crypto/tls"
"fmt"
"net/http"
"os"
"time"
"github.com/kerberos-io/agent/machinery/src/log"
"github.com/kerberos-io/agent/machinery/src/models"
)
func IsValidUriResponse(configuration *models.Configuration) (enabled bool) {
config := configuration.Config
conditionURI := config.ConditionURI
enabled = true
if conditionURI != "" {
// We will send a POST request to the conditionURI, and expect a 200 response.
// In the payload we will send some information, so the other end can decide
// if it should enable or disable recording.
var client *http.Client
if os.Getenv("AGENT_TLS_INSECURE") == "true" {
tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
client = &http.Client{Transport: tr}
} else {
client = &http.Client{}
}
var object = fmt.Sprintf(`{
"camera_id" : "%s",
"camera_name" : "%s",
"site_id" : "%s",
"hub_key" : "%s",
"timestamp" : "%s",
}`, config.Key, config.FriendlyName, config.HubSite, config.HubKey, time.Now().Format("2006-01-02 15:04:05"))
var jsonStr = []byte(object)
buffy := bytes.NewBuffer(jsonStr)
req, _ := http.NewRequest("POST", conditionURI, buffy)
req.Header.Set("Content-Type", "application/json")
resp, err := client.Do(req)
if resp != nil {
resp.Body.Close()
}
if err == nil && resp.StatusCode == 200 {
log.Log.Info("conditions.uri.IsValidUriResponse(): response 200, enabling recording.")
} else {
log.Log.Info("conditions.uri.IsValidUriResponse(): response not 200, disabling recording.")
enabled = false
}
}
return
}

View File

@@ -1,14 +1,12 @@
package components
package config
import (
"context"
"encoding/json"
"errors"
"image"
"io/ioutil"
"os"
"reflect"
"sort"
"strconv"
"strings"
"time"
@@ -20,33 +18,14 @@ import (
"go.mongodb.org/mongo-driver/bson"
)
func GetImageFromFilePath() (image.Image, error) {
snapshotDirectory := "./data/snapshots"
files, err := ioutil.ReadDir(snapshotDirectory)
if err == nil && len(files) > 1 {
sort.Slice(files, func(i, j int) bool {
return files[i].ModTime().Before(files[j].ModTime())
})
filePath := "./data/snapshots/" + files[1].Name()
f, err := os.Open(filePath)
if err != nil {
return nil, err
}
defer f.Close()
image, _, err := image.Decode(f)
return image, err
}
return nil, errors.New("Could not find a snapshot in " + snapshotDirectory)
}
// ReadUserConfig Reads the user configuration of the Kerberos Open Source instance.
// This will return a models.User struct including the username, password,
// selected language, and if the installation was completed or not.
func ReadUserConfig() (userConfig models.User) {
func ReadUserConfig(configDirectory string) (userConfig models.User) {
for {
jsonFile, err := os.Open("./data/config/user.json")
jsonFile, err := os.Open(configDirectory + "/data/config/user.json")
if err != nil {
log.Log.Error("Config file is not found " + "./data/config/user.json, trying again in 5s: " + err.Error())
log.Log.Error("Config file is not found " + configDirectory + "/data/config/user.json, trying again in 5s: " + err.Error())
time.Sleep(5 * time.Second)
} else {
log.Log.Info("Successfully Opened user.json")
@@ -66,7 +45,7 @@ func ReadUserConfig() (userConfig models.User) {
return
}
func OpenConfig(configuration *models.Configuration) {
func OpenConfig(configDirectory string, configuration *models.Configuration) {
// We are checking which deployment this is running, so we can load
// into the configuration as expected.
@@ -84,23 +63,44 @@ func OpenConfig(configuration *models.Configuration) {
collection := db.Collection("configuration")
var globalConfig models.Config
err := collection.FindOne(context.Background(), bson.M{
res := collection.FindOne(context.Background(), bson.M{
"type": "global",
}).Decode(&globalConfig)
})
if res.Err() != nil {
log.Log.Error("Could not find global configuration, using default configuration.")
panic("Could not find global configuration, using default configuration.")
}
err := res.Decode(&globalConfig)
if err != nil {
log.Log.Error("Could not find global configuration, using default configuration.")
panic("Could not find global configuration, using default configuration.")
}
if globalConfig.Type != "global" {
log.Log.Error("Could not find global configuration, might missed the mongodb connection.")
panic("Could not find global configuration, might missed the mongodb connection.")
}
configuration.GlobalConfig = globalConfig
var customConfig models.Config
deploymentName := os.Getenv("DEPLOYMENT_NAME")
err = collection.FindOne(context.Background(), bson.M{
res = collection.FindOne(context.Background(), bson.M{
"type": "config",
"name": deploymentName,
}).Decode(&customConfig)
})
if res.Err() != nil {
log.Log.Error("Could not find configuration for " + deploymentName + ", using global configuration.")
}
err = res.Decode(&customConfig)
if err != nil {
log.Log.Error("Could not find configuration for " + deploymentName + ", using global configuration.")
}
if customConfig.Type != "config" {
log.Log.Error("Could not find custom configuration, might missed the mongodb connection.")
panic("Could not find custom configuration, might missed the mongodb connection.")
}
configuration.CustomConfig = customConfig
// We will merge both configs in a single config file.
@@ -120,8 +120,13 @@ func OpenConfig(configuration *models.Configuration) {
},
)
// Merge Config toplevel
// Reset main configuration Config.
configuration.Config = models.Config{}
// Merge the global settings in the main config
conjungo.Merge(&configuration.Config, configuration.GlobalConfig, opts)
// Now we might override some settings with the custom config
conjungo.Merge(&configuration.Config, configuration.CustomConfig, opts)
// Merge Kerberos Vault settings
@@ -136,6 +141,15 @@ func OpenConfig(configuration *models.Configuration) {
conjungo.Merge(&s3, configuration.CustomConfig.S3, opts)
configuration.Config.S3 = &s3
// Merge Encryption settings
var encryption models.Encryption
conjungo.Merge(&encryption, configuration.GlobalConfig.Encryption, opts)
conjungo.Merge(&encryption, configuration.CustomConfig.Encryption, opts)
configuration.Config.Encryption = &encryption
// Merge timetable manually because it's an array
configuration.Config.Timetable = configuration.CustomConfig.Timetable
// Cleanup
opts = nil
@@ -146,9 +160,9 @@ func OpenConfig(configuration *models.Configuration) {
// Open device config
for {
jsonFile, err := os.Open("./data/config/config.json")
jsonFile, err := os.Open(configDirectory + "/data/config/config.json")
if err != nil {
log.Log.Error("Config file is not found " + "./data/config/config.json" + ", trying again in 5s.")
log.Log.Error("Config file is not found " + configDirectory + "/data/config/config.json" + ", trying again in 5s.")
time.Sleep(5 * time.Second)
} else {
log.Log.Info("Successfully Opened config.json from " + configuration.Name)
@@ -189,7 +203,7 @@ func OverrideWithEnvironmentVariables(configuration *models.Configuration) {
configuration.Config.Key = value
break
case "AGENT_NAME":
configuration.Config.Name = value
configuration.Config.FriendlyName = value
break
case "AGENT_TIMEZONE":
configuration.Config.Timezone = value
@@ -432,16 +446,34 @@ func OverrideWithEnvironmentVariables(configuration *models.Configuration) {
case "AGENT_DROPBOX_DIRECTORY":
configuration.Config.Dropbox.Directory = value
break
/* When encryption is enabled */
case "AGENT_ENCRYPTION":
configuration.Config.Encryption.Enabled = value
break
case "AGENT_ENCRYPTION_RECORDINGS":
configuration.Config.Encryption.Recordings = value
break
case "AGENT_ENCRYPTION_FINGERPRINT":
configuration.Config.Encryption.Fingerprint = value
break
case "AGENT_ENCRYPTION_PRIVATE_KEY":
encryptionPrivateKey := strings.ReplaceAll(value, "\\n", "\n")
configuration.Config.Encryption.PrivateKey = encryptionPrivateKey
break
case "AGENT_ENCRYPTION_SYMMETRIC_KEY":
configuration.Config.Encryption.SymmetricKey = value
break
}
}
}
}
func SaveConfig(config models.Config, configuration *models.Configuration, communication *models.Communication) error {
func SaveConfig(configDirectory string, config models.Config, configuration *models.Configuration, communication *models.Communication) error {
if !communication.IsConfiguring.IsSet() {
communication.IsConfiguring.Set()
err := StoreConfig(config)
err := StoreConfig(configDirectory, config)
if err != nil {
communication.IsConfiguring.UnSet()
return err
@@ -462,7 +494,16 @@ func SaveConfig(config models.Config, configuration *models.Configuration, commu
}
}
func StoreConfig(config models.Config) error {
func StoreConfig(configDirectory string, config models.Config) error {
// Encryption key can be set wrong.
if config.Encryption != nil {
encryptionPrivateKey := config.Encryption.PrivateKey
// Replace \\n by \n
encryptionPrivateKey = strings.ReplaceAll(encryptionPrivateKey, "\\n", "\n")
config.Encryption.PrivateKey = encryptionPrivateKey
}
// Save into database
if os.Getenv("DEPLOYMENT") == "factory" || os.Getenv("MACHINERY_ENVIRONMENT") == "kubernetes" {
// Write to mongodb
@@ -484,7 +525,7 @@ func StoreConfig(config models.Config) error {
// Save into file
} else if os.Getenv("DEPLOYMENT") == "" || os.Getenv("DEPLOYMENT") == "agent" {
res, _ := json.MarshalIndent(config, "", "\t")
err := ioutil.WriteFile("./data/config/config.json", res, 0644)
err := ioutil.WriteFile(configDirectory+"/data/config/config.json", res, 0644)
return err
}

View File

@@ -28,10 +28,10 @@ func New() *mongo.Client {
password := os.Getenv("MONGODB_PASSWORD")
authentication := "SCRAM-SHA-256"
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
_init_ctx.Do(func() {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
_instance = new(DB)
mongodbURI := fmt.Sprintf("mongodb://%s:%s@%s", username, password, host)
if replicaset != "" {

View File

@@ -0,0 +1,126 @@
package encryption
import (
"bytes"
"crypto"
"crypto/aes"
"crypto/cipher"
"crypto/md5"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"encoding/base64"
"errors"
"hash"
)
func DecryptWithPrivateKey(ciphertext string, privateKey *rsa.PrivateKey) ([]byte, error) {
cipheredValue, _ := base64.StdEncoding.DecodeString(ciphertext)
out, err := rsa.DecryptPKCS1v15(nil, privateKey, cipheredValue)
return out, err
}
func SignWithPrivateKey(data []byte, privateKey *rsa.PrivateKey) ([]byte, error) {
hashed := sha256.Sum256(data)
signature, err := rsa.SignPKCS1v15(nil, privateKey, crypto.SHA256, hashed[:])
return signature, err
}
func AesEncrypt(content []byte, password string) ([]byte, error) {
salt := make([]byte, 8)
_, err := rand.Read(salt)
if err != nil {
return nil, err
}
key, iv, err := DefaultEvpKDF([]byte(password), salt)
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
mode := cipher.NewCBCEncrypter(block, iv)
cipherBytes := PKCS5Padding(content, aes.BlockSize)
mode.CryptBlocks(cipherBytes, cipherBytes)
cipherText := make([]byte, 16+len(cipherBytes))
copy(cipherText[:8], []byte("Salted__"))
copy(cipherText[8:16], salt)
copy(cipherText[16:], cipherBytes)
return cipherText, nil
}
func AesDecrypt(cipherText []byte, password string) ([]byte, error) {
if string(cipherText[:8]) != "Salted__" {
return nil, errors.New("invalid crypto js aes encryption")
}
salt := cipherText[8:16]
cipherBytes := cipherText[16:]
key, iv, err := DefaultEvpKDF([]byte(password), salt)
if err != nil {
return nil, err
}
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
mode := cipher.NewCBCDecrypter(block, iv)
mode.CryptBlocks(cipherBytes, cipherBytes)
result := PKCS5UnPadding(cipherBytes)
return result, nil
}
func EvpKDF(password []byte, salt []byte, keySize int, iterations int, hashAlgorithm string) ([]byte, error) {
var block []byte
var hasher hash.Hash
derivedKeyBytes := make([]byte, 0)
switch hashAlgorithm {
case "md5":
hasher = md5.New()
default:
return []byte{}, errors.New("not implement hasher algorithm")
}
for len(derivedKeyBytes) < keySize*4 {
if len(block) > 0 {
hasher.Write(block)
}
hasher.Write(password)
hasher.Write(salt)
block = hasher.Sum([]byte{})
hasher.Reset()
for i := 1; i < iterations; i++ {
hasher.Write(block)
block = hasher.Sum([]byte{})
hasher.Reset()
}
derivedKeyBytes = append(derivedKeyBytes, block...)
}
return derivedKeyBytes[:keySize*4], nil
}
func DefaultEvpKDF(password []byte, salt []byte) (key []byte, iv []byte, err error) {
keySize := 256 / 32
ivSize := 128 / 32
derivedKeyBytes, err := EvpKDF(password, salt, keySize+ivSize, 1, "md5")
if err != nil {
return []byte{}, []byte{}, err
}
return derivedKeyBytes[:keySize*4], derivedKeyBytes[keySize*4:], nil
}
func PKCS5UnPadding(src []byte) []byte {
length := len(src)
unpadding := int(src[length-1])
return src[:(length - unpadding)]
}
func PKCS5Padding(src []byte, blockSize int) []byte {
padding := blockSize - len(src)%blockSize
padtext := bytes.Repeat([]byte{byte(padding)}, padding)
return append(src, padtext...)
}

View File

@@ -12,7 +12,6 @@ import (
// The logging library being used everywhere.
var Log = Logging{
Logger: "logrus",
Level: "debug",
}
// -----------------
@@ -21,7 +20,7 @@ var Log = Logging{
var gologging = logging.MustGetLogger("gologger")
func ConfigureGoLogging(timezone *time.Location) {
func ConfigureGoLogging(configDirectory string, timezone *time.Location) {
// Logging
var format = logging.MustStringFormatter(
`%{color}%{time:15:04:05.000} %{shortfunc} ▶ %{level:.4s} %{id:03x}%{color:reset} %{message}`,
@@ -32,7 +31,7 @@ func ConfigureGoLogging(timezone *time.Location) {
stdBackend := logging.NewLogBackend(os.Stderr, "", 0)
stdBackendLeveled := logging.NewBackendFormatter(stdBackend, format)
fileBackend := logging.NewLogBackend(&lumberjack.Logger{
Filename: "./data/log/machinery.txt",
Filename: configDirectory + "/data/log/machinery.txt",
MaxSize: 2, // megabytes
Compress: true, // disabled by default
}, "", 0)
@@ -45,19 +44,45 @@ func ConfigureGoLogging(timezone *time.Location) {
// This a logrus
// -> github.com/sirupsen/logrus
func ConfigureLogrus(timezone *time.Location) {
// Log as JSON instead of the default ASCII formatter.
logrus.SetFormatter(LocalTimeZoneFormatter{
Timezone: timezone,
Formatter: &logrus.JSONFormatter{},
}) // Use local timezone for providing datetime in logs!
func ConfigureLogrus(level string, output string, timezone *time.Location) {
if output == "json" {
// Log as JSON instead of the default ASCII formatter.
logrus.SetFormatter(LocalTimeZoneFormatter{
Timezone: timezone,
Formatter: &logrus.JSONFormatter{},
})
} else if output == "text" {
// Log as text with colors.
formatter := logrus.TextFormatter{
ForceColors: true,
FullTimestamp: true,
}
logrus.SetFormatter(LocalTimeZoneFormatter{
Timezone: timezone,
Formatter: &formatter,
})
}
// Use local timezone for providing datetime in logs!
// Output to stdout instead of the default stderr
// Can be any io.Writer, see below for File example
logrus.SetOutput(os.Stdout)
// Only log the warning severity or above.
logrus.SetLevel(logrus.InfoLevel)
logLevel := logrus.InfoLevel
if level == "error" {
logLevel = logrus.ErrorLevel
} else if level == "debug" {
logLevel = logrus.DebugLevel
logrus.SetReportCaller(true)
} else if level == "fatal" {
logLevel = logrus.FatalLevel
} else if level == "warning" {
logLevel = logrus.WarnLevel
} // Add this line for logging filename and line number!
logrus.SetLevel(logLevel)
}
type LocalTimeZoneFormatter struct {
@@ -72,15 +97,14 @@ func (u LocalTimeZoneFormatter) Format(e *logrus.Entry) ([]byte, error) {
type Logging struct {
Logger string
Level string
}
func (self *Logging) Init(timezone *time.Location) {
func (self *Logging) Init(level string, logoutput string, configDirectory string, timezone *time.Location) {
switch self.Logger {
case "go-logging":
ConfigureGoLogging(timezone)
ConfigureGoLogging(configDirectory, timezone)
case "logrus":
ConfigureLogrus(timezone)
ConfigureLogrus(level, logoutput, timezone)
default:
}
}

View File

@@ -0,0 +1,6 @@
package models
type AudioDataPartial struct {
Timestamp int64 `json:"timestamp" bson:"timestamp"`
Data []int16 `json:"data" bson:"data"`
}

View File

@@ -2,11 +2,9 @@ package models
import (
"context"
"sync"
"sync/atomic"
"github.com/kerberos-io/joy4/av/pubsub"
"github.com/kerberos-io/joy4/cgo/ffmpeg"
"github.com/kerberos-io/agent/machinery/src/packets"
"github.com/tevino/abool"
)
@@ -22,20 +20,18 @@ type Communication struct {
HandleStream chan string
HandleSubStream chan string
HandleMotion chan MotionDataPartial
HandleAudio chan AudioDataPartial
HandleUpload chan string
HandleHeartBeat chan string
HandleLiveSD chan int64
HandleLiveHDKeepalive chan string
HandleLiveHDHandshake chan SDPPayload
HandleLiveHDHandshake chan RequestHDStreamPayload
HandleLiveHDPeers chan string
HandleONVIF chan OnvifAction
IsConfiguring *abool.AtomicBool
Queue *pubsub.Queue
SubQueue *pubsub.Queue
DecoderMutex *sync.Mutex
SubDecoderMutex *sync.Mutex
Decoder *ffmpeg.VideoDecoder
SubDecoder *ffmpeg.VideoDecoder
Queue *packets.Queue
SubQueue *packets.Queue
Image string
CameraConnected bool
HasBackChannel bool
}

View File

@@ -21,7 +21,7 @@ type Config struct {
AutoClean string `json:"auto_clean"`
RemoveAfterUpload string `json:"remove_after_upload"`
MaxDirectorySize int64 `json:"max_directory_size"`
Timezone string `json:"timezone,omitempty" bson:"timezone,omitempty"`
Timezone string `json:"timezone"`
Capture Capture `json:"capture"`
Timetable []*Timetable `json:"timetable"`
Region *Region `json:"region"`
@@ -42,6 +42,7 @@ type Config struct {
HubPrivateKey string `json:"hub_private_key" bson:"hub_private_key"`
HubSite string `json:"hub_site" bson:"hub_site"`
ConditionURI string `json:"condition_uri" bson:"condition_uri"`
Encryption *Encryption `json:"encryption,omitempty" bson:"encryption,omitempty"`
}
// Capture defines which camera type (Id) you are using (IP, USB or Raspberry Pi camera),
@@ -70,13 +71,15 @@ type Capture struct {
// IPCamera configuration, such as the RTSP url of the IPCamera and the FPS.
// Also includes ONVIF integration
type IPCamera struct {
Width int `json:"width"`
Height int `json:"height"`
FPS string `json:"fps"`
RTSP string `json:"rtsp"`
SubRTSP string `json:"sub_rtsp"`
FPS string `json:"fps"`
ONVIF string `json:"onvif,omitempty" bson:"onvif"`
ONVIFXAddr string `json:"onvif_xaddr,omitempty" bson:"onvif_xaddr"`
ONVIFUsername string `json:"onvif_username,omitempty" bson:"onvif_username"`
ONVIFPassword string `json:"onvif_password,omitempty" bson:"onvif_password"`
ONVIFXAddr string `json:"onvif_xaddr" bson:"onvif_xaddr"`
ONVIFUsername string `json:"onvif_username" bson:"onvif_username"`
ONVIFPassword string `json:"onvif_password" bson:"onvif_password"`
}
// USBCamera configuration, such as the device path (/dev/video*)
@@ -155,3 +158,12 @@ type Dropbox struct {
AccessToken string `json:"access_token,omitempty" bson:"access_token,omitempty"`
Directory string `json:"directory,omitempty" bson:"directory,omitempty"`
}
// Encryption
type Encryption struct {
Enabled string `json:"enabled" bson:"enabled"`
Recordings string `json:"recordings" bson:"recordings"`
Fingerprint string `json:"fingerprint" bson:"fingerprint"`
PrivateKey string `json:"private_key" bson:"private_key"`
SymmetricKey string `json:"symmetric_key" bson:"symmetric_key"`
}

View File

@@ -0,0 +1,190 @@
package models
import (
"crypto/rsa"
"crypto/x509"
"encoding/base64"
"encoding/json"
"encoding/pem"
"io"
"strings"
"time"
"github.com/gofrs/uuid"
"github.com/kerberos-io/agent/machinery/src/encryption"
"github.com/kerberos-io/agent/machinery/src/log"
)
func PackageMQTTMessage(configuration *Configuration, msg Message) ([]byte, error) {
// Create a Version 4 UUID.
u2, err := uuid.NewV4()
if err != nil {
log.Log.Error("failed to generate UUID: " + err.Error())
}
// We'll generate an unique id, and encrypt / decrypt it using the private key if available.
msg.Mid = u2.String()
msg.DeviceId = msg.Payload.DeviceId
msg.Timestamp = time.Now().Unix()
// We'll hide the message (by default in latest version)
// We will encrypt using the Kerberos Hub private key if set.
/*msg.Hidden = false
if configuration.Config.HubPrivateKey != "" {
msg.Hidden = true
pload := msg.Payload
// Pload to base64
data, err := json.Marshal(pload)
if err != nil {
msg.Hidden = false
} else {
k := configuration.Config.Encryption.SymmetricKey
encryptedValue, err := encryption.AesEncrypt(data, k)
if err == nil {
data := base64.StdEncoding.EncodeToString(encryptedValue)
msg.Payload.HiddenValue = data
msg.Payload.Value = make(map[string]interface{})
}
}
}*/
// Next to hiding the message, we can also encrypt it using your own private key.
// Which is not stored in a remote environment (hence you are the only one owning it).
msg.Encrypted = false
if configuration.Config.Encryption != nil && configuration.Config.Encryption.Enabled == "true" {
msg.Encrypted = true
}
msg.PublicKey = ""
msg.Fingerprint = ""
if msg.Encrypted {
pload := msg.Payload
// Pload to base64
data, err := json.Marshal(pload)
if err != nil {
log.Log.Error("models.mqtt.PackageMQTTMessage(): failed to marshal payload: " + err.Error())
}
// Encrypt the value
privateKey := configuration.Config.Encryption.PrivateKey
r := strings.NewReader(privateKey)
pemBytes, _ := io.ReadAll(r)
block, _ := pem.Decode(pemBytes)
if block == nil {
log.Log.Error("models.mqtt.PackageMQTTMessage(): error decoding PEM block containing private key")
} else {
// Parse private key
b := block.Bytes
key, err := x509.ParsePKCS8PrivateKey(b)
if err != nil {
log.Log.Error("models.mqtt.PackageMQTTMessage(): error parsing private key: " + err.Error())
}
// Conver key to *rsa.PrivateKey
rsaKey, _ := key.(*rsa.PrivateKey)
// Create a 16bit key random
k := configuration.Config.Encryption.SymmetricKey
encryptedValue, err := encryption.AesEncrypt(data, k)
if err == nil {
data := base64.StdEncoding.EncodeToString(encryptedValue)
// Sign the encrypted value
signature, err := encryption.SignWithPrivateKey([]byte(data), rsaKey)
if err == nil {
base64Signature := base64.StdEncoding.EncodeToString(signature)
msg.Payload.EncryptedValue = data
msg.Payload.Signature = base64Signature
msg.Payload.Value = make(map[string]interface{})
}
}
}
}
payload, err := json.Marshal(msg)
return payload, err
}
// The message structure which is used to send over
// and receive messages from the MQTT broker
type Message struct {
Mid string `json:"mid"`
DeviceId string `json:"device_id"`
Timestamp int64 `json:"timestamp"`
Encrypted bool `json:"encrypted"`
Hidden bool `json:"hidden"`
PublicKey string `json:"public_key"`
Fingerprint string `json:"fingerprint"`
Payload Payload `json:"payload"`
}
// The payload structure which is used to send over
// and receive messages from the MQTT broker
type Payload struct {
Action string `json:"action"`
DeviceId string `json:"device_id"`
Signature string `json:"signature"`
EncryptedValue string `json:"encrypted_value"`
HiddenValue string `json:"hidden_value"`
Value map[string]interface{} `json:"value"`
}
// We received a audio input
type AudioPayload struct {
Timestamp int64 `json:"timestamp"` // timestamp of the recording request.
Data []int16 `json:"data"`
}
// We received a recording request, we'll send it to the motion handler.
type RecordPayload struct {
Timestamp int64 `json:"timestamp"` // timestamp of the recording request.
}
// We received a preset position request, we'll request it through onvif and send it back.
type PTZPositionPayload struct {
Timestamp int64 `json:"timestamp"` // timestamp of the preset request.
}
// We received a request config request, we'll fetch the current config and send it back.
type RequestConfigPayload struct {
Timestamp int64 `json:"timestamp"` // timestamp of the preset request.
}
// We received a update config request, we'll update the current config and send a confirmation back.
type UpdateConfigPayload struct {
Timestamp int64 `json:"timestamp"` // timestamp of the preset request.
Config Config `json:"config"`
}
// We received a request SD stream request
type RequestSDStreamPayload struct {
Timestamp int64 `json:"timestamp"` // timestamp
}
// We received a request HD stream request
type RequestHDStreamPayload struct {
Timestamp int64 `json:"timestamp"` // timestamp
HubKey string `json:"hub_key"` // hub key
SessionID string `json:"session_id"` // session id
SessionDescription string `json:"session_description"` // session description
}
// We received a receive HD candidates request
type ReceiveHDCandidatesPayload struct {
Timestamp int64 `json:"timestamp"` // timestamp
SessionID string `json:"session_id"` // session id
Candidate string `json:"candidate"` // candidate
}
type NavigatePTZPayload struct {
Timestamp int64 `json:"timestamp"` // timestamp
DeviceId string `json:"device_id"` // device id
Action string `json:"action"` // action
}
type TriggerRelay struct {
Timestamp int64 `json:"timestamp"` // timestamp
DeviceId string `json:"device_id"` // device id
Token string `json:"token"` // token
}

View File

@@ -12,4 +12,13 @@ type OnvifActionPTZ struct {
Down int `json:"down" bson:"down"`
Center int `json:"center" bson:"center"`
Zoom float64 `json:"zoom" bson:"zoom"`
X float64 `json:"x" bson:"x"`
Y float64 `json:"y" bson:"y"`
Z float64 `json:"z" bson:"z"`
Preset string `json:"preset" bson:"preset"`
}
type OnvifActionPreset struct {
Name string `json:"name" bson:"name"`
Token string `json:"token" bson:"token"`
}

View File

@@ -29,3 +29,8 @@ type OnvifZoom struct {
OnvifCredentials OnvifCredentials `json:"onvif_credentials,omitempty" bson:"onvif_credentials"`
Zoom float64 `json:"zoom,omitempty" bson:"zoom"`
}
type OnvifPreset struct {
OnvifCredentials OnvifCredentials `json:"onvif_credentials,omitempty" bson:"onvif_credentials"`
Preset string `json:"preset,omitempty" bson:"preset"`
}

View File

@@ -0,0 +1,15 @@
package models
import "time"
// The OutputMessage contains the relevant information
// to specify the type of triggers we want to execute.
type OutputMessage struct {
Name string
Outputs []string
Trigger string
Timestamp time.Time
File string
CameraId string
SiteId string
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,59 @@
package outputs
import (
"github.com/kerberos-io/agent/machinery/src/log"
"github.com/kerberos-io/agent/machinery/src/models"
)
type Output interface {
// Triggers the integration
Trigger(message models.OutputMessage) error
}
func Execute(message *models.OutputMessage) (err error) {
err = nil
outputs := message.Outputs
for _, output := range outputs {
switch output {
case "slack":
slack := &SlackOutput{}
err := slack.Trigger(message)
if err == nil {
log.Log.Debug("outputs.main.Execute(slack): message was processed by output.")
} else {
log.Log.Error("outputs.main.Execute(slack): " + err.Error())
}
break
case "webhook":
webhook := &WebhookOutput{}
err := webhook.Trigger(message)
if err == nil {
log.Log.Debug("outputs.main.Execute(webhook): message was processed by output.")
} else {
log.Log.Error("outputs.main.Execute(webhook): " + err.Error())
}
break
case "onvif_relay":
onvif := &OnvifRelayOutput{}
err := onvif.Trigger(message)
if err == nil {
log.Log.Debug("outputs.main.Execute(onvif): message was processed by output.")
} else {
log.Log.Error("outputs.main.Execute(onvif): " + err.Error())
}
break
case "script":
script := &ScriptOutput{}
err := script.Trigger(message)
if err == nil {
log.Log.Debug("outputs.main.Execute(script): message was processed by output.")
} else {
log.Log.Error("outputs.main.Execute(script): " + err.Error())
}
break
}
}
return err
}

View File

@@ -0,0 +1,12 @@
package outputs
import "github.com/kerberos-io/agent/machinery/src/models"
type OnvifRelayOutput struct {
Output
}
func (o *OnvifRelayOutput) Trigger(message *models.OutputMessage) (err error) {
err = nil
return err
}

View File

@@ -0,0 +1,12 @@
package outputs
import "github.com/kerberos-io/agent/machinery/src/models"
type ScriptOutput struct {
Output
}
func (scr *ScriptOutput) Trigger(message *models.OutputMessage) (err error) {
err = nil
return err
}

View File

@@ -0,0 +1,12 @@
package outputs
import "github.com/kerberos-io/agent/machinery/src/models"
type SlackOutput struct {
Output
}
func (s *SlackOutput) Trigger(message *models.OutputMessage) (err error) {
err = nil
return err
}

View File

@@ -0,0 +1,12 @@
package outputs
import "github.com/kerberos-io/agent/machinery/src/models"
type WebhookOutput struct {
Output
}
func (w *WebhookOutput) Trigger(message *models.OutputMessage) (err error) {
err = nil
return err
}

View File

@@ -0,0 +1,69 @@
package packets
type Buf struct {
Head, Tail BufPos
pkts []Packet
Size int
Count int
}
func NewBuf() *Buf {
return &Buf{
pkts: make([]Packet, 64),
}
}
func (self *Buf) Pop() Packet {
if self.Count == 0 {
panic("pktque.Buf: Pop() when count == 0")
}
i := int(self.Head) & (len(self.pkts) - 1)
pkt := self.pkts[i]
self.pkts[i] = Packet{}
self.Size -= len(pkt.Data)
self.Head++
self.Count--
return pkt
}
func (self *Buf) grow() {
newpkts := make([]Packet, len(self.pkts)*2)
for i := self.Head; i.LT(self.Tail); i++ {
newpkts[int(i)&(len(newpkts)-1)] = self.pkts[int(i)&(len(self.pkts)-1)]
}
self.pkts = newpkts
}
func (self *Buf) Push(pkt Packet) {
if self.Count == len(self.pkts) {
self.grow()
}
self.pkts[int(self.Tail)&(len(self.pkts)-1)] = pkt
self.Tail++
self.Count++
self.Size += len(pkt.Data)
}
func (self *Buf) Get(pos BufPos) Packet {
return self.pkts[int(pos)&(len(self.pkts)-1)]
}
func (self *Buf) IsValidPos(pos BufPos) bool {
return pos.GE(self.Head) && pos.LT(self.Tail)
}
type BufPos int
func (self BufPos) LT(pos BufPos) bool {
return self-pos < 0
}
func (self BufPos) GE(pos BufPos) bool {
return self-pos >= 0
}
func (self BufPos) GT(pos BufPos) bool {
return self-pos > 0
}

View File

@@ -0,0 +1,20 @@
package packets
import (
"time"
"github.com/pion/rtp"
)
// Packet represents an RTP Packet
type Packet struct {
Packet *rtp.Packet
IsAudio bool // packet is audio
IsVideo bool // packet is video
IsKeyFrame bool // video packet is key frame
Idx int8 // stream index in container format
Codec string // codec name
CompositionTime time.Duration // packet presentation time minus decode time for H264 B-Frame
Time time.Duration // packet decode time
Data []byte // packet data
}

View File

@@ -0,0 +1,225 @@
// Packege pubsub implements publisher-subscribers model used in multi-channel streaming.
package packets
import (
"io"
"sync"
"time"
)
// time
// ----------------->
//
// V-A-V-V-A-V-V-A-V-V
// | |
// 0 5 10
// head tail
// oldest latest
//
// One publisher and multiple subscribers thread-safe packet buffer queue.
type Queue struct {
buf *Buf
head, tail int
lock *sync.RWMutex
cond *sync.Cond
curgopcount, maxgopcount int
streams []Stream
videoidx int
closed bool
}
func NewQueue() *Queue {
q := &Queue{}
q.buf = NewBuf()
q.maxgopcount = 2
q.lock = &sync.RWMutex{}
q.cond = sync.NewCond(q.lock.RLocker())
q.videoidx = -1
return q
}
func (self *Queue) SetMaxGopCount(n int) {
self.lock.Lock()
self.maxgopcount = n
self.lock.Unlock()
return
}
func (self *Queue) WriteHeader(streams []Stream) error {
self.lock.Lock()
self.streams = streams
for i, stream := range streams {
if stream.IsVideo {
self.videoidx = i
}
}
self.cond.Broadcast()
self.lock.Unlock()
return nil
}
func (self *Queue) WriteTrailer() error {
return nil
}
// After Close() called, all QueueCursor's ReadPacket will return io.EOF.
func (self *Queue) Close() (err error) {
self.lock.Lock()
self.closed = true
self.cond.Broadcast()
// Close all QueueCursor's ReadPacket
for i := 0; i < self.buf.Size; i++ {
pkt := self.buf.Pop()
pkt.Data = nil
}
self.lock.Unlock()
return
}
func (self *Queue) GetSize() int {
return self.buf.Count
}
// Put packet into buffer, old packets will be discared.
func (self *Queue) WritePacket(pkt Packet) (err error) {
self.lock.Lock()
self.buf.Push(pkt)
if pkt.Idx == int8(self.videoidx) && pkt.IsKeyFrame {
self.curgopcount++
}
for self.curgopcount >= self.maxgopcount && self.buf.Count > 1 {
pkt := self.buf.Pop()
if pkt.Idx == int8(self.videoidx) && pkt.IsKeyFrame {
self.curgopcount--
}
if self.curgopcount < self.maxgopcount {
break
}
}
//println("shrink", self.curgopcount, self.maxgopcount, self.buf.Head, self.buf.Tail, "count", self.buf.Count, "size", self.buf.Size)
self.cond.Broadcast()
self.lock.Unlock()
return
}
type QueueCursor struct {
que *Queue
pos BufPos
gotpos bool
init func(buf *Buf, videoidx int) BufPos
}
func (self *Queue) newCursor() *QueueCursor {
return &QueueCursor{
que: self,
}
}
// Create cursor position at latest packet.
func (self *Queue) Latest() *QueueCursor {
cursor := self.newCursor()
cursor.init = func(buf *Buf, videoidx int) BufPos {
return buf.Tail
}
return cursor
}
// Create cursor position at oldest buffered packet.
func (self *Queue) Oldest() *QueueCursor {
cursor := self.newCursor()
cursor.init = func(buf *Buf, videoidx int) BufPos {
return buf.Head
}
return cursor
}
// Create cursor position at specific time in buffered packets.
func (self *Queue) DelayedTime(dur time.Duration) *QueueCursor {
cursor := self.newCursor()
cursor.init = func(buf *Buf, videoidx int) BufPos {
i := buf.Tail - 1
if buf.IsValidPos(i) {
end := buf.Get(i)
for buf.IsValidPos(i) {
if end.Time-buf.Get(i).Time > dur {
break
}
i--
}
}
return i
}
return cursor
}
// Create cursor position at specific delayed GOP count in buffered packets.
func (self *Queue) DelayedGopCount(n int) *QueueCursor {
cursor := self.newCursor()
cursor.init = func(buf *Buf, videoidx int) BufPos {
i := buf.Tail - 1
if videoidx != -1 {
for gop := 0; buf.IsValidPos(i) && gop < n; i-- {
pkt := buf.Get(i)
if pkt.Idx == int8(self.videoidx) && pkt.IsKeyFrame {
gop++
}
}
}
return i
}
return cursor
}
func (self *QueueCursor) Streams() (streams []Stream, err error) {
self.que.cond.L.Lock()
for self.que.streams == nil && !self.que.closed {
self.que.cond.Wait()
}
if self.que.streams != nil {
streams = self.que.streams
} else {
err = io.EOF
}
self.que.cond.L.Unlock()
return
}
// ReadPacket will not consume packets in Queue, it's just a cursor.
func (self *QueueCursor) ReadPacket() (pkt Packet, err error) {
self.que.cond.L.Lock()
buf := self.que.buf
if !self.gotpos {
self.pos = self.init(buf, self.que.videoidx)
self.gotpos = true
}
for {
if self.pos.LT(buf.Head) {
self.pos = buf.Head
} else if self.pos.GT(buf.Tail) {
self.pos = buf.Tail
}
if buf.IsValidPos(self.pos) {
pkt = buf.Get(self.pos)
self.pos++
break
}
if self.que.closed {
err = io.EOF
break
}
self.que.cond.Wait()
}
self.que.cond.L.Unlock()
return
}

View File

@@ -0,0 +1,42 @@
package packets
type Stream struct {
// The name of the stream.
Name string
// The URL of the stream.
URL string
// Is the stream a video stream.
IsVideo bool
// Is the stream a audio stream.
IsAudio bool
// The width of the stream.
Width int
// The height of the stream.
Height int
// Num is the numerator of the framerate.
Num int
// Denum is the denominator of the framerate.
Denum int
// FPS is the framerate of the stream.
FPS float64
// For H264, this is the sps.
SPS []byte
// For H264, this is the pps.
PPS []byte
// For H265, this is the vps.
VPS []byte
// IsBackChannel is true if this stream is a back channel.
IsBackChannel bool
}

View File

@@ -0,0 +1,60 @@
package packets
import (
"time"
)
/*
pop push
seg seg seg
|--------| |---------| |---|
20ms 40ms 5ms
----------------- time -------------------->
headtm tailtm
*/
type tlSeg struct {
tm, dur time.Duration
}
type Timeline struct {
segs []tlSeg
headtm time.Duration
}
func (self *Timeline) Push(tm time.Duration, dur time.Duration) {
if len(self.segs) > 0 {
tail := self.segs[len(self.segs)-1]
diff := tm - (tail.tm + tail.dur)
if diff < 0 {
tm -= diff
}
}
self.segs = append(self.segs, tlSeg{tm, dur})
}
func (self *Timeline) Pop(dur time.Duration) (tm time.Duration) {
if len(self.segs) == 0 {
return self.headtm
}
tm = self.segs[0].tm
for dur > 0 && len(self.segs) > 0 {
seg := &self.segs[0]
sub := dur
if seg.dur < sub {
sub = seg.dur
}
seg.dur -= sub
dur -= sub
seg.tm += sub
self.headtm += sub
if seg.dur == 0 {
copy(self.segs[0:], self.segs[1:])
self.segs = self.segs[:len(self.segs)-1]
}
}
return
}

View File

@@ -1,252 +0,0 @@
package http
import (
"github.com/gin-gonic/gin"
"github.com/kerberos-io/agent/machinery/src/models"
"github.com/kerberos-io/agent/machinery/src/onvif"
)
// Login godoc
// @Router /api/login [post]
// @ID login
// @Tags authentication
// @Summary Get Authorization token.
// @Description Get Authorization token.
// @Param credentials body models.Authentication true "Credentials"
// @Success 200 {object} models.Authorization
func Login() {}
// LoginToOnvif godoc
// @Router /api/camera/onvif/login [post]
// @ID camera-onvif-login
// @Tags camera
// @Param config body models.OnvifCredentials true "OnvifCredentials"
// @Summary Try to login into ONVIF supported camera.
// @Description Try to login into ONVIF supported camera.
// @Success 200 {object} models.APIResponse
func LoginToOnvif(c *gin.Context) {
var onvifCredentials models.OnvifCredentials
err := c.BindJSON(&onvifCredentials)
if err == nil && onvifCredentials.ONVIFXAddr != "" {
configuration := &models.Configuration{
Config: models.Config{
Capture: models.Capture{
IPCamera: models.IPCamera{
ONVIFXAddr: onvifCredentials.ONVIFXAddr,
ONVIFUsername: onvifCredentials.ONVIFUsername,
ONVIFPassword: onvifCredentials.ONVIFPassword,
},
},
},
}
cameraConfiguration := configuration.Config.Capture.IPCamera
device, err := onvif.ConnectToOnvifDevice(&cameraConfiguration)
if err == nil {
c.JSON(200, gin.H{
"device": device,
})
} else {
c.JSON(400, gin.H{
"data": "Something went wrong: " + err.Error(),
})
}
} else {
c.JSON(400, gin.H{
"data": "Something went wrong: " + err.Error(),
})
}
}
// GetOnvifCapabilities godoc
// @Router /api/camera/onvif/capabilities [post]
// @ID camera-onvif-capabilities
// @Tags camera
// @Param config body models.OnvifCredentials true "OnvifCredentials"
// @Summary Will return the ONVIF capabilities for the specific camera.
// @Description Will return the ONVIF capabilities for the specific camera.
// @Success 200 {object} models.APIResponse
func GetOnvifCapabilities(c *gin.Context) {
var onvifCredentials models.OnvifCredentials
err := c.BindJSON(&onvifCredentials)
if err == nil && onvifCredentials.ONVIFXAddr != "" {
configuration := &models.Configuration{
Config: models.Config{
Capture: models.Capture{
IPCamera: models.IPCamera{
ONVIFXAddr: onvifCredentials.ONVIFXAddr,
ONVIFUsername: onvifCredentials.ONVIFUsername,
ONVIFPassword: onvifCredentials.ONVIFPassword,
},
},
},
}
cameraConfiguration := configuration.Config.Capture.IPCamera
device, err := onvif.ConnectToOnvifDevice(&cameraConfiguration)
if err == nil {
c.JSON(200, gin.H{
"capabilities": onvif.GetCapabilitiesFromDevice(device),
})
} else {
c.JSON(400, gin.H{
"data": "Something went wrong: " + err.Error(),
})
}
} else {
c.JSON(400, gin.H{
"data": "Something went wrong: " + err.Error(),
})
}
}
// DoOnvifPanTilt godoc
// @Router /api/camera/onvif/pantilt [post]
// @ID camera-onvif-pantilt
// @Tags camera
// @Param panTilt body models.OnvifPanTilt true "OnvifPanTilt"
// @Summary Panning or/and tilting the camera.
// @Description Panning or/and tilting the camera using a direction (x,y).
// @Success 200 {object} models.APIResponse
func DoOnvifPanTilt(c *gin.Context) {
var onvifPanTilt models.OnvifPanTilt
err := c.BindJSON(&onvifPanTilt)
if err == nil && onvifPanTilt.OnvifCredentials.ONVIFXAddr != "" {
configuration := &models.Configuration{
Config: models.Config{
Capture: models.Capture{
IPCamera: models.IPCamera{
ONVIFXAddr: onvifPanTilt.OnvifCredentials.ONVIFXAddr,
ONVIFUsername: onvifPanTilt.OnvifCredentials.ONVIFUsername,
ONVIFPassword: onvifPanTilt.OnvifCredentials.ONVIFPassword,
},
},
},
}
cameraConfiguration := configuration.Config.Capture.IPCamera
device, err := onvif.ConnectToOnvifDevice(&cameraConfiguration)
if err == nil {
// Get token from the first profile
token, err := onvif.GetTokenFromProfile(device, 0)
if err == nil {
// Get the configurations from the device
ptzConfigurations, err := onvif.GetPTZConfigurationsFromDevice(device)
if err == nil {
pan := onvifPanTilt.Pan
tilt := onvifPanTilt.Tilt
err := onvif.ContinuousPanTilt(device, ptzConfigurations, token, pan, tilt)
if err == nil {
c.JSON(200, models.APIResponse{
Message: "Successfully pan/tilted the camera",
})
} else {
c.JSON(400, models.APIResponse{
Message: "Something went wrong: " + err.Error(),
})
}
} else {
c.JSON(400, models.APIResponse{
Message: "Something went wrong: " + err.Error(),
})
}
} else {
c.JSON(400, models.APIResponse{
Message: "Something went wrong: " + err.Error(),
})
}
} else {
c.JSON(400, models.APIResponse{
Message: "Something went wrong: " + err.Error(),
})
}
} else {
c.JSON(400, models.APIResponse{
Message: "Something went wrong: " + err.Error(),
})
}
}
// DoOnvifZoom godoc
// @Router /api/camera/onvif/zoom [post]
// @ID camera-onvif-zoom
// @Tags camera
// @Param zoom body models.OnvifZoom true "OnvifZoom"
// @Summary Zooming in or out the camera.
// @Description Zooming in or out the camera.
// @Success 200 {object} models.APIResponse
func DoOnvifZoom(c *gin.Context) {
var onvifZoom models.OnvifZoom
err := c.BindJSON(&onvifZoom)
if err == nil && onvifZoom.OnvifCredentials.ONVIFXAddr != "" {
configuration := &models.Configuration{
Config: models.Config{
Capture: models.Capture{
IPCamera: models.IPCamera{
ONVIFXAddr: onvifZoom.OnvifCredentials.ONVIFXAddr,
ONVIFUsername: onvifZoom.OnvifCredentials.ONVIFUsername,
ONVIFPassword: onvifZoom.OnvifCredentials.ONVIFPassword,
},
},
},
}
cameraConfiguration := configuration.Config.Capture.IPCamera
device, err := onvif.ConnectToOnvifDevice(&cameraConfiguration)
if err == nil {
// Get token from the first profile
token, err := onvif.GetTokenFromProfile(device, 0)
if err == nil {
// Get the PTZ configurations from the device
ptzConfigurations, err := onvif.GetPTZConfigurationsFromDevice(device)
if err == nil {
zoom := onvifZoom.Zoom
err := onvif.ContinuousZoom(device, ptzConfigurations, token, zoom)
if err == nil {
c.JSON(200, models.APIResponse{
Message: "Successfully zoomed the camera",
})
} else {
c.JSON(400, models.APIResponse{
Message: "Something went wrong: " + err.Error(),
})
}
} else {
c.JSON(400, models.APIResponse{
Message: "Something went wrong: " + err.Error(),
})
}
} else {
c.JSON(400, models.APIResponse{
Message: "Something went wrong: " + err.Error(),
})
}
} else {
c.JSON(400, models.APIResponse{
Message: "Something went wrong: " + err.Error(),
})
}
} else {
c.JSON(400, models.APIResponse{
Message: "Something went wrong: " + err.Error(),
})
}
}

View File

@@ -1,240 +0,0 @@
package http
import (
"image"
"time"
jwt "github.com/appleboy/gin-jwt/v2"
"github.com/gin-gonic/gin"
"github.com/kerberos-io/agent/machinery/src/capture"
"github.com/kerberos-io/agent/machinery/src/onvif"
"github.com/kerberos-io/agent/machinery/src/routers/websocket"
"github.com/kerberos-io/agent/machinery/src/cloud"
"github.com/kerberos-io/agent/machinery/src/components"
"github.com/kerberos-io/agent/machinery/src/log"
"github.com/kerberos-io/agent/machinery/src/models"
"github.com/kerberos-io/agent/machinery/src/utils"
)
func AddRoutes(r *gin.Engine, authMiddleware *jwt.GinJWTMiddleware, configuration *models.Configuration, communication *models.Communication) *gin.RouterGroup {
r.GET("/ws", func(c *gin.Context) {
websocket.WebsocketHandler(c, communication)
})
// This is legacy should be removed in future! Now everything
// lives under the /api prefix.
r.GET("/config", func(c *gin.Context) {
c.JSON(200, gin.H{
"config": configuration.Config,
"custom": configuration.CustomConfig,
"global": configuration.GlobalConfig,
"snapshot": communication.Image,
})
})
// This is legacy should be removed in future! Now everything
// lives under the /api prefix.
r.POST("/config", func(c *gin.Context) {
var config models.Config
err := c.BindJSON(&config)
if err == nil {
err := components.SaveConfig(config, configuration, communication)
if err == nil {
c.JSON(200, gin.H{
"data": "☄ Reconfiguring",
})
} else {
c.JSON(400, gin.H{
"data": "Something went wrong: " + err.Error(),
})
}
} else {
c.JSON(400, gin.H{
"data": "Something went wrong: " + err.Error(),
})
}
})
api := r.Group("/api")
{
api.POST("/login", authMiddleware.LoginHandler)
api.GET("/dashboard", func(c *gin.Context) {
// Check if camera is online.
cameraIsOnline := communication.CameraConnected
// If an agent is properly setup with Kerberos Hub, we will send
// a ping to Kerberos Hub every 15seconds. On receiving a positive response
// it will update the CloudTimestamp value.
cloudIsOnline := false
if communication.CloudTimestamp != nil && communication.CloudTimestamp.Load() != nil {
timestamp := communication.CloudTimestamp.Load().(int64)
if timestamp > 0 {
cloudIsOnline = true
}
}
// The total number of recordings stored in the directory.
recordingDirectory := "./data/recordings"
numberOfRecordings := utils.NumberOfMP4sInDirectory(recordingDirectory)
// All days stored in this agent.
days := []string{}
latestEvents := []models.Media{}
files, err := utils.ReadDirectory(recordingDirectory)
if err == nil {
events := utils.GetSortedDirectory(files)
// Get All days
days = utils.GetDays(events, recordingDirectory, configuration)
// Get all latest events
var eventFilter models.EventFilter
eventFilter.NumberOfElements = 5
latestEvents = utils.GetMediaFormatted(events, recordingDirectory, configuration, eventFilter) // will get 5 latest recordings.
}
c.JSON(200, gin.H{
"offlineMode": configuration.Config.Offline,
"cameraOnline": cameraIsOnline,
"cloudOnline": cloudIsOnline,
"numberOfRecordings": numberOfRecordings,
"days": days,
"latestEvents": latestEvents,
})
})
api.POST("/latest-events", func(c *gin.Context) {
var eventFilter models.EventFilter
err := c.BindJSON(&eventFilter)
if err == nil {
// Default to 10 if no limit is set.
if eventFilter.NumberOfElements == 0 {
eventFilter.NumberOfElements = 10
}
recordingDirectory := "./data/recordings"
files, err := utils.ReadDirectory(recordingDirectory)
if err == nil {
events := utils.GetSortedDirectory(files)
// We will get all recordings from the directory (as defined by the filter).
fileObjects := utils.GetMediaFormatted(events, recordingDirectory, configuration, eventFilter)
c.JSON(200, gin.H{
"events": fileObjects,
})
} else {
c.JSON(400, gin.H{
"data": "Something went wrong: " + err.Error(),
})
}
} else {
c.JSON(400, gin.H{
"data": "Something went wrong: " + err.Error(),
})
}
})
api.GET("/days", func(c *gin.Context) {
recordingDirectory := "./data/recordings"
files, err := utils.ReadDirectory(recordingDirectory)
if err == nil {
events := utils.GetSortedDirectory(files)
days := utils.GetDays(events, recordingDirectory, configuration)
c.JSON(200, gin.H{
"events": days,
})
} else {
c.JSON(400, gin.H{
"data": "Something went wrong: " + err.Error(),
})
}
})
api.GET("/config", func(c *gin.Context) {
c.JSON(200, gin.H{
"config": configuration.Config,
"custom": configuration.CustomConfig,
"global": configuration.GlobalConfig,
"snapshot": communication.Image,
})
})
api.POST("/config", func(c *gin.Context) {
var config models.Config
err := c.BindJSON(&config)
if err == nil {
err := components.SaveConfig(config, configuration, communication)
if err == nil {
c.JSON(200, gin.H{
"data": "☄ Reconfiguring",
})
} else {
c.JSON(200, gin.H{
"data": "☄ Reconfiguring",
})
}
} else {
c.JSON(400, gin.H{
"data": "Something went wrong: " + err.Error(),
})
}
})
api.GET("/restart", func(c *gin.Context) {
communication.HandleBootstrap <- "restart"
c.JSON(200, gin.H{
"restarted": true,
})
})
api.GET("/stop", func(c *gin.Context) {
communication.HandleBootstrap <- "stop"
c.JSON(200, gin.H{
"stopped": true,
})
})
api.POST("/onvif/verify", func(c *gin.Context) {
onvif.VerifyOnvifConnection(c)
})
api.POST("/hub/verify", func(c *gin.Context) {
cloud.VerifyHub(c)
})
api.POST("/persistence/verify", func(c *gin.Context) {
cloud.VerifyPersistence(c)
})
// Streaming handler
api.GET("/stream", func(c *gin.Context) {
// TODO add a token validation!
imageFunction := func() (image.Image, error) {
// We will only send an image once per second.
time.Sleep(time.Second * 1)
log.Log.Info("AddRoutes (/stream): reading from MJPEG stream")
img, err := components.GetImageFromFilePath()
return img, err
}
h := components.StartMotionJPEG(imageFunction, 80)
h.ServeHTTP(c.Writer, c.Request)
})
// Camera specific methods. Doesn't require any authorization.
// These are available for anyone, but require the agent, to reach
// the camera.
api.POST("/camera/onvif/login", LoginToOnvif)
api.POST("/camera/onvif/capabilities", GetOnvifCapabilities)
api.POST("/camera/onvif/pantilt", DoOnvifPanTilt)
api.POST("/camera/onvif/zoom", DoOnvifZoom)
api.POST("/camera/verify/:streamType", capture.VerifyCamera)
// Secured endpoints..
api.Use(authMiddleware.MiddlewareFunc())
{
}
}
return api
}

View File

@@ -1,7 +1,9 @@
package http
import (
"io"
"os"
"strconv"
jwt "github.com/appleboy/gin-jwt/v2"
"github.com/gin-contrib/pprof"
@@ -12,6 +14,8 @@ import (
"log"
_ "github.com/kerberos-io/agent/machinery/docs"
"github.com/kerberos-io/agent/machinery/src/capture"
"github.com/kerberos-io/agent/machinery/src/encryption"
"github.com/kerberos-io/agent/machinery/src/models"
swaggerFiles "github.com/swaggo/files"
ginSwagger "github.com/swaggo/gin-swagger"
@@ -35,7 +39,10 @@ import (
// @in header
// @name Authorization
func StartServer(configuration *models.Configuration, communication *models.Communication) {
func StartServer(configDirectory string, configuration *models.Configuration, communication *models.Communication, captureDevice *capture.Capture) {
// Set release mode
gin.SetMode(gin.ReleaseMode)
// Initialize REST API
r := gin.Default()
@@ -57,12 +64,12 @@ func StartServer(configuration *models.Configuration, communication *models.Comm
}
// Add all routes
AddRoutes(r, authMiddleware, configuration, communication)
AddRoutes(r, authMiddleware, configDirectory, configuration, communication, captureDevice)
// Update environment variables
environmentVariables := "./www/env.js"
environmentVariables := configDirectory + "/www/env.js"
if os.Getenv("AGENT_MODE") == "demo" {
demoEnvironmentVariables := "./www/env.demo.js"
demoEnvironmentVariables := configDirectory + "/www/env.demo.js"
// Move demo environment variables to environment variables
err := os.Rename(demoEnvironmentVariables, environmentVariables)
if err != nil {
@@ -71,12 +78,14 @@ func StartServer(configuration *models.Configuration, communication *models.Comm
}
// Add static routes to UI
r.Use(static.Serve("/", static.LocalFile("./www", true)))
r.Use(static.Serve("/dashboard", static.LocalFile("./www", true)))
r.Use(static.Serve("/media", static.LocalFile("./www", true)))
r.Use(static.Serve("/settings", static.LocalFile("./www", true)))
r.Use(static.Serve("/login", static.LocalFile("./www", true)))
r.Handle("GET", "/file/*filepath", Files)
r.Use(static.Serve("/", static.LocalFile(configDirectory+"/www", true)))
r.Use(static.Serve("/dashboard", static.LocalFile(configDirectory+"/www", true)))
r.Use(static.Serve("/media", static.LocalFile(configDirectory+"/www", true)))
r.Use(static.Serve("/settings", static.LocalFile(configDirectory+"/www", true)))
r.Use(static.Serve("/login", static.LocalFile(configDirectory+"/www", true)))
r.Handle("GET", "/file/*filepath", func(c *gin.Context) {
Files(c, configDirectory, configuration)
})
// Run the api on port
err = r.Run(":" + configuration.Port)
@@ -85,8 +94,51 @@ func StartServer(configuration *models.Configuration, communication *models.Comm
}
}
func Files(c *gin.Context) {
c.Header("Access-Control-Allow-Origin", "*")
c.Header("Content-Type", "video/mp4")
c.File("./data/recordings" + c.Param("filepath"))
func Files(c *gin.Context, configDirectory string, configuration *models.Configuration) {
// Get File
filePath := configDirectory + "/data/recordings" + c.Param("filepath")
_, err := os.Open(filePath)
if err != nil {
c.JSON(404, gin.H{"error": "File not found"})
return
}
contents, err := os.ReadFile(filePath)
if err == nil {
// Get symmetric key
symmetricKey := configuration.Config.Encryption.SymmetricKey
encryptedRecordings := configuration.Config.Encryption.Recordings
// Decrypt file
if encryptedRecordings == "true" && symmetricKey != "" {
// Read file
if err != nil {
c.JSON(404, gin.H{"error": "File not found"})
return
}
// Decrypt file
contents, err = encryption.AesDecrypt(contents, symmetricKey)
if err != nil {
c.JSON(404, gin.H{"error": "File not found"})
return
}
}
// Get fileSize from contents
fileSize := len(contents)
// Send file to gin
c.Header("Access-Control-Allow-Origin", "*")
c.Header("Content-Disposition", "attachment; filename="+filePath)
c.Header("Content-Type", "video/mp4")
c.Header("Content-Length", strconv.Itoa(fileSize))
// Send contents to gin
io.WriteString(c.Writer, string(contents))
} else {
c.JSON(404, gin.H{"error": "File not found"})
return
}
}

View File

@@ -0,0 +1,570 @@
package http
import (
"github.com/gin-gonic/gin"
"github.com/kerberos-io/agent/machinery/src/log"
"github.com/kerberos-io/agent/machinery/src/models"
"github.com/kerberos-io/agent/machinery/src/onvif"
)
// Login godoc
// @Router /api/login [post]
// @ID login
// @Tags authentication
// @Summary Get Authorization token.
// @Description Get Authorization token.
// @Param credentials body models.Authentication true "Credentials"
// @Success 200 {object} models.Authorization
func Login() {}
// LoginToOnvif godoc
// @Router /api/camera/onvif/login [post]
// @ID camera-onvif-login
// @Tags onvif
// @Param config body models.OnvifCredentials true "OnvifCredentials"
// @Summary Try to login into ONVIF supported camera.
// @Description Try to login into ONVIF supported camera.
// @Success 200 {object} models.APIResponse
func LoginToOnvif(c *gin.Context) {
var onvifCredentials models.OnvifCredentials
err := c.BindJSON(&onvifCredentials)
if err == nil && onvifCredentials.ONVIFXAddr != "" {
configuration := &models.Configuration{
Config: models.Config{
Capture: models.Capture{
IPCamera: models.IPCamera{
ONVIFXAddr: onvifCredentials.ONVIFXAddr,
ONVIFUsername: onvifCredentials.ONVIFUsername,
ONVIFPassword: onvifCredentials.ONVIFPassword,
},
},
},
}
cameraConfiguration := configuration.Config.Capture.IPCamera
device, capabilities, err := onvif.ConnectToOnvifDevice(&cameraConfiguration)
if err == nil {
// Get token from the first profile
token, err := onvif.GetTokenFromProfile(device, 0)
if err == nil {
c.JSON(200, gin.H{
"device": device,
"capabilities": capabilities,
"token": token,
})
} else {
c.JSON(400, gin.H{
"data": "Something went wrong: " + err.Error(),
})
}
} else {
c.JSON(400, gin.H{
"data": "Something went wrong: " + err.Error(),
})
}
} else {
c.JSON(400, gin.H{
"data": "Something went wrong: " + err.Error(),
})
}
}
// GetOnvifCapabilities godoc
// @Router /api/camera/onvif/capabilities [post]
// @ID camera-onvif-capabilities
// @Tags onvif
// @Param config body models.OnvifCredentials true "OnvifCredentials"
// @Summary Will return the ONVIF capabilities for the specific camera.
// @Description Will return the ONVIF capabilities for the specific camera.
// @Success 200 {object} models.APIResponse
func GetOnvifCapabilities(c *gin.Context) {
var onvifCredentials models.OnvifCredentials
err := c.BindJSON(&onvifCredentials)
if err == nil && onvifCredentials.ONVIFXAddr != "" {
configuration := &models.Configuration{
Config: models.Config{
Capture: models.Capture{
IPCamera: models.IPCamera{
ONVIFXAddr: onvifCredentials.ONVIFXAddr,
ONVIFUsername: onvifCredentials.ONVIFUsername,
ONVIFPassword: onvifCredentials.ONVIFPassword,
},
},
},
}
cameraConfiguration := configuration.Config.Capture.IPCamera
_, capabilities, err := onvif.ConnectToOnvifDevice(&cameraConfiguration)
if err == nil {
c.JSON(200, gin.H{
"capabilities": capabilities,
})
} else {
c.JSON(400, gin.H{
"data": "Something went wrong: " + err.Error(),
})
}
} else {
c.JSON(400, gin.H{
"data": "Something went wrong: " + err.Error(),
})
}
}
// DoOnvifPanTilt godoc
// @Router /api/camera/onvif/pantilt [post]
// @ID camera-onvif-pantilt
// @Tags onvif
// @Param panTilt body models.OnvifPanTilt true "OnvifPanTilt"
// @Summary Panning or/and tilting the camera.
// @Description Panning or/and tilting the camera using a direction (x,y).
// @Success 200 {object} models.APIResponse
func DoOnvifPanTilt(c *gin.Context) {
var onvifPanTilt models.OnvifPanTilt
err := c.BindJSON(&onvifPanTilt)
if err == nil && onvifPanTilt.OnvifCredentials.ONVIFXAddr != "" {
configuration := &models.Configuration{
Config: models.Config{
Capture: models.Capture{
IPCamera: models.IPCamera{
ONVIFXAddr: onvifPanTilt.OnvifCredentials.ONVIFXAddr,
ONVIFUsername: onvifPanTilt.OnvifCredentials.ONVIFUsername,
ONVIFPassword: onvifPanTilt.OnvifCredentials.ONVIFPassword,
},
},
},
}
cameraConfiguration := configuration.Config.Capture.IPCamera
device, _, err := onvif.ConnectToOnvifDevice(&cameraConfiguration)
if err == nil {
// Get token from the first profile
token, err := onvif.GetTokenFromProfile(device, 0)
if err == nil {
// Get the configurations from the device
ptzConfigurations, err := onvif.GetPTZConfigurationsFromDevice(device)
if err == nil {
pan := onvifPanTilt.Pan
tilt := onvifPanTilt.Tilt
err := onvif.ContinuousPanTilt(device, ptzConfigurations, token, pan, tilt)
if err == nil {
c.JSON(200, models.APIResponse{
Message: "Successfully pan/tilted the camera",
})
} else {
c.JSON(400, models.APIResponse{
Message: "Something went wrong: " + err.Error(),
})
}
} else {
c.JSON(400, models.APIResponse{
Message: "Something went wrong: " + err.Error(),
})
}
} else {
c.JSON(400, models.APIResponse{
Message: "Something went wrong: " + err.Error(),
})
}
} else {
c.JSON(400, models.APIResponse{
Message: "Something went wrong: " + err.Error(),
})
}
} else {
c.JSON(400, models.APIResponse{
Message: "Something went wrong: " + err.Error(),
})
}
}
// DoOnvifZoom godoc
// @Router /api/camera/onvif/zoom [post]
// @ID camera-onvif-zoom
// @Tags onvif
// @Param zoom body models.OnvifZoom true "OnvifZoom"
// @Summary Zooming in or out the camera.
// @Description Zooming in or out the camera.
// @Success 200 {object} models.APIResponse
func DoOnvifZoom(c *gin.Context) {
var onvifZoom models.OnvifZoom
err := c.BindJSON(&onvifZoom)
if err == nil && onvifZoom.OnvifCredentials.ONVIFXAddr != "" {
configuration := &models.Configuration{
Config: models.Config{
Capture: models.Capture{
IPCamera: models.IPCamera{
ONVIFXAddr: onvifZoom.OnvifCredentials.ONVIFXAddr,
ONVIFUsername: onvifZoom.OnvifCredentials.ONVIFUsername,
ONVIFPassword: onvifZoom.OnvifCredentials.ONVIFPassword,
},
},
},
}
cameraConfiguration := configuration.Config.Capture.IPCamera
device, _, err := onvif.ConnectToOnvifDevice(&cameraConfiguration)
if err == nil {
// Get token from the first profile
token, err := onvif.GetTokenFromProfile(device, 0)
if err == nil {
// Get the PTZ configurations from the device
ptzConfigurations, err := onvif.GetPTZConfigurationsFromDevice(device)
if err == nil {
zoom := onvifZoom.Zoom
err := onvif.ContinuousZoom(device, ptzConfigurations, token, zoom)
if err == nil {
c.JSON(200, models.APIResponse{
Message: "Successfully zoomed the camera",
})
} else {
c.JSON(400, models.APIResponse{
Message: "Something went wrong: " + err.Error(),
})
}
} else {
c.JSON(400, models.APIResponse{
Message: "Something went wrong: " + err.Error(),
})
}
} else {
c.JSON(400, models.APIResponse{
Message: "Something went wrong: " + err.Error(),
})
}
} else {
c.JSON(400, models.APIResponse{
Message: "Something went wrong: " + err.Error(),
})
}
} else {
c.JSON(400, models.APIResponse{
Message: "Something went wrong: " + err.Error(),
})
}
}
// GetOnvifPresets godoc
// @Router /api/camera/onvif/presets [post]
// @ID camera-onvif-presets
// @Tags onvif
// @Param config body models.OnvifCredentials true "OnvifCredentials"
// @Summary Will return the ONVIF presets for the specific camera.
// @Description Will return the ONVIF presets for the specific camera.
// @Success 200 {object} models.APIResponse
func GetOnvifPresets(c *gin.Context) {
var onvifCredentials models.OnvifCredentials
err := c.BindJSON(&onvifCredentials)
if err == nil && onvifCredentials.ONVIFXAddr != "" {
configuration := &models.Configuration{
Config: models.Config{
Capture: models.Capture{
IPCamera: models.IPCamera{
ONVIFXAddr: onvifCredentials.ONVIFXAddr,
ONVIFUsername: onvifCredentials.ONVIFUsername,
ONVIFPassword: onvifCredentials.ONVIFPassword,
},
},
},
}
cameraConfiguration := configuration.Config.Capture.IPCamera
device, _, err := onvif.ConnectToOnvifDevice(&cameraConfiguration)
if err == nil {
presets, err := onvif.GetPresetsFromDevice(device)
if err == nil {
c.JSON(200, gin.H{
"presets": presets,
})
} else {
c.JSON(400, gin.H{
"data": "Something went wrong: " + err.Error(),
})
}
} else {
c.JSON(400, gin.H{
"data": "Something went wrong: " + err.Error(),
})
}
} else {
c.JSON(400, gin.H{
"data": "Something went wrong: " + err.Error(),
})
}
}
// GoToOnvifPReset godoc
// @Router /api/camera/onvif/gotopreset [post]
// @ID camera-onvif-gotopreset
// @Tags onvif
// @Param config body models.OnvifPreset true "OnvifPreset"
// @Summary Will activate the desired ONVIF preset.
// @Description Will activate the desired ONVIF preset.
// @Success 200 {object} models.APIResponse
func GoToOnvifPreset(c *gin.Context) {
var onvifPreset models.OnvifPreset
err := c.BindJSON(&onvifPreset)
if err == nil && onvifPreset.OnvifCredentials.ONVIFXAddr != "" {
configuration := &models.Configuration{
Config: models.Config{
Capture: models.Capture{
IPCamera: models.IPCamera{
ONVIFXAddr: onvifPreset.OnvifCredentials.ONVIFXAddr,
ONVIFUsername: onvifPreset.OnvifCredentials.ONVIFUsername,
ONVIFPassword: onvifPreset.OnvifCredentials.ONVIFPassword,
},
},
},
}
cameraConfiguration := configuration.Config.Capture.IPCamera
device, _, err := onvif.ConnectToOnvifDevice(&cameraConfiguration)
if err == nil {
err := onvif.GoToPresetFromDevice(device, onvifPreset.Preset)
if err == nil {
c.JSON(200, gin.H{
"data": "Camera preset activated: " + onvifPreset.Preset,
})
} else {
c.JSON(400, gin.H{
"data": "Something went wrong: " + err.Error(),
})
}
} else {
c.JSON(400, gin.H{
"data": "Something went wrong: " + err.Error(),
})
}
} else {
c.JSON(400, gin.H{
"data": "Something went wrong: " + err.Error(),
})
}
}
// DoGetDigitalInputs godoc
// @Router /api/camera/onvif/inputs [post]
// @ID get-digital-inputs
// @Security Bearer
// @securityDefinitions.apikey Bearer
// @in header
// @name Authorization
// @Tags onvif
// @Param config body models.OnvifCredentials true "OnvifCredentials"
// @Summary Will get the digital inputs from the ONVIF device.
// @Description Will get the digital inputs from the ONVIF device.
// @Success 200 {object} models.APIResponse
func DoGetDigitalInputs(c *gin.Context) {
var onvifCredentials models.OnvifCredentials
err := c.BindJSON(&onvifCredentials)
if err == nil && onvifCredentials.ONVIFXAddr != "" {
configuration := &models.Configuration{
Config: models.Config{
Capture: models.Capture{
IPCamera: models.IPCamera{
ONVIFXAddr: onvifCredentials.ONVIFXAddr,
ONVIFUsername: onvifCredentials.ONVIFUsername,
ONVIFPassword: onvifCredentials.ONVIFPassword,
},
},
},
}
cameraConfiguration := configuration.Config.Capture.IPCamera
_, _, err := onvif.ConnectToOnvifDevice(&cameraConfiguration)
if err == nil {
// Get the digital inputs and outputs from the device
inputOutputs, err := onvif.GetInputOutputs()
if err == nil {
if err == nil {
// Get the digital outputs from the device
var inputs []onvif.ONVIFEvents
for _, event := range inputOutputs {
if event.Type == "input" {
inputs = append(inputs, event)
}
}
c.JSON(200, gin.H{
"data": inputs,
})
} else {
c.JSON(400, gin.H{
"data": "Something went wrong: " + err.Error(),
})
}
} else {
c.JSON(400, gin.H{
"data": "Something went wrong: " + err.Error(),
})
}
} else {
c.JSON(400, gin.H{
"data": "Something went wrong: " + err.Error(),
})
}
} else {
c.JSON(400, gin.H{
"data": "Something went wrong: " + err.Error(),
})
}
}
// DoGetRelayOutputs godoc
// @Router /api/camera/onvif/outputs [post]
// @ID get-relay-outputs
// @Security Bearer
// @securityDefinitions.apikey Bearer
// @in header
// @name Authorization
// @Tags onvif
// @Param config body models.OnvifCredentials true "OnvifCredentials"
// @Summary Will get the relay outputs from the ONVIF device.
// @Description Will get the relay outputs from the ONVIF device.
// @Success 200 {object} models.APIResponse
func DoGetRelayOutputs(c *gin.Context) {
var onvifCredentials models.OnvifCredentials
err := c.BindJSON(&onvifCredentials)
if err == nil && onvifCredentials.ONVIFXAddr != "" {
configuration := &models.Configuration{
Config: models.Config{
Capture: models.Capture{
IPCamera: models.IPCamera{
ONVIFXAddr: onvifCredentials.ONVIFXAddr,
ONVIFUsername: onvifCredentials.ONVIFUsername,
ONVIFPassword: onvifCredentials.ONVIFPassword,
},
},
},
}
cameraConfiguration := configuration.Config.Capture.IPCamera
_, _, err := onvif.ConnectToOnvifDevice(&cameraConfiguration)
if err == nil {
// Get the digital inputs and outputs from the device
inputOutputs, err := onvif.GetInputOutputs()
if err == nil {
if err == nil {
// Get the digital outputs from the device
var outputs []onvif.ONVIFEvents
for _, event := range inputOutputs {
if event.Type == "output" {
outputs = append(outputs, event)
}
}
c.JSON(200, gin.H{
"data": outputs,
})
} else {
c.JSON(400, gin.H{
"data": "Something went wrong: " + err.Error(),
})
}
} else {
c.JSON(400, gin.H{
"data": "Something went wrong: " + err.Error(),
})
}
} else {
c.JSON(400, gin.H{
"data": "Something went wrong: " + err.Error(),
})
}
} else {
c.JSON(400, gin.H{
"data": "Something went wrong: " + err.Error(),
})
}
}
// DoTriggerRelayOutput godoc
// @Router /api/camera/onvif/outputs/{output} [post]
// @ID trigger-relay-output
// @Security Bearer
// @securityDefinitions.apikey Bearer
// @in header
// @name Authorization
// @Tags onvif
// @Param config body models.OnvifCredentials true "OnvifCredentials"
// @Param output path string true "Output"
// @Summary Will trigger the relay output from the ONVIF device.
// @Description Will trigger the relay output from the ONVIF device.
// @Success 200 {object} models.APIResponse
func DoTriggerRelayOutput(c *gin.Context) {
var onvifCredentials models.OnvifCredentials
err := c.BindJSON(&onvifCredentials)
// Get the output from the url
output := c.Param("output")
if err == nil && onvifCredentials.ONVIFXAddr != "" && output != "" {
configuration := &models.Configuration{
Config: models.Config{
Capture: models.Capture{
IPCamera: models.IPCamera{
ONVIFXAddr: onvifCredentials.ONVIFXAddr,
ONVIFUsername: onvifCredentials.ONVIFUsername,
ONVIFPassword: onvifCredentials.ONVIFPassword,
},
},
},
}
cameraConfiguration := configuration.Config.Capture.IPCamera
device, _, err := onvif.ConnectToOnvifDevice(&cameraConfiguration)
if err == nil {
err := onvif.TriggerRelayOutput(device, output)
if err == nil {
msg := "relay output triggered: " + output
log.Log.Info("routers.http.methods.DoTriggerRelayOutput(): " + msg)
c.JSON(200, gin.H{
"data": msg,
})
} else {
msg := "something went wrong: " + err.Error()
log.Log.Error("routers.http.methods.DoTriggerRelayOutput(): " + msg)
c.JSON(400, gin.H{
"data": msg,
})
}
} else {
msg := "something went wrong: " + err.Error()
log.Log.Error("routers.http.methods.DoTriggerRelayOutput(): " + msg)
c.JSON(400, gin.H{
"data": msg,
})
}
} else {
msg := "something went wrong: " + err.Error()
log.Log.Error("routers.http.methods.DoTriggerRelayOutput(): " + msg)
c.JSON(400, gin.H{
"data": msg,
})
}
}

View File

@@ -0,0 +1,111 @@
package http
import (
jwt "github.com/appleboy/gin-jwt/v2"
"github.com/gin-gonic/gin"
"github.com/kerberos-io/agent/machinery/src/capture"
"github.com/kerberos-io/agent/machinery/src/components"
"github.com/kerberos-io/agent/machinery/src/onvif"
"github.com/kerberos-io/agent/machinery/src/routers/websocket"
"github.com/kerberos-io/agent/machinery/src/cloud"
"github.com/kerberos-io/agent/machinery/src/models"
)
func AddRoutes(r *gin.Engine, authMiddleware *jwt.GinJWTMiddleware, configDirectory string, configuration *models.Configuration, communication *models.Communication, captureDevice *capture.Capture) *gin.RouterGroup {
r.GET("/ws", func(c *gin.Context) {
websocket.WebsocketHandler(c, communication, captureDevice)
})
// This is legacy should be removed in future! Now everything
// lives under the /api prefix.
r.GET("/config", func(c *gin.Context) {
components.GetConfig(c, captureDevice, configuration, communication)
})
// This is legacy should be removed in future! Now everything
// lives under the /api prefix.
r.POST("/config", func(c *gin.Context) {
components.UpdateConfig(c, configDirectory, configuration, communication)
})
api := r.Group("/api")
{
api.POST("/login", authMiddleware.LoginHandler)
api.GET("/dashboard", func(c *gin.Context) {
components.GetDashboard(c, configDirectory, configuration, communication)
})
api.POST("/latest-events", func(c *gin.Context) {
components.GetLatestEvents(c, configDirectory, configuration, communication)
})
api.GET("/days", func(c *gin.Context) {
components.GetDays(c, configDirectory, configuration, communication)
})
api.GET("/config", func(c *gin.Context) {
components.GetConfig(c, captureDevice, configuration, communication)
})
api.POST("/config", func(c *gin.Context) {
components.UpdateConfig(c, configDirectory, configuration, communication)
})
// Will verify the current hub settings.
api.POST("/hub/verify", func(c *gin.Context) {
cloud.VerifyHub(c)
})
// Will verify the current persistence settings.
api.POST("/persistence/verify", func(c *gin.Context) {
cloud.VerifyPersistence(c, configDirectory)
})
// Camera specific methods. Doesn't require any authorization.
// These are available for anyone, but require the agent, to reach
// the camera.
api.POST("/camera/restart", func(c *gin.Context) {
components.RestartAgent(c, communication)
})
api.POST("/camera/stop", func(c *gin.Context) {
components.StopAgent(c, communication)
})
api.POST("/camera/record", func(c *gin.Context) {
components.MakeRecording(c, communication)
})
api.GET("/camera/snapshot/jpeg", func(c *gin.Context) {
components.GetSnapshotRaw(c, captureDevice, configuration, communication)
})
api.GET("/camera/snapshot/base64", func(c *gin.Context) {
components.GetSnapshotBase64(c, captureDevice, configuration, communication)
})
// Onvif specific methods. Doesn't require any authorization.
// Will verify the current onvif settings.
api.POST("/camera/onvif/verify", onvif.VerifyOnvifConnection)
api.POST("/camera/onvif/login", LoginToOnvif)
api.POST("/camera/onvif/capabilities", GetOnvifCapabilities)
api.POST("/camera/onvif/presets", GetOnvifPresets)
api.POST("/camera/onvif/gotopreset", GoToOnvifPreset)
api.POST("/camera/onvif/pantilt", DoOnvifPanTilt)
api.POST("/camera/onvif/zoom", DoOnvifZoom)
api.POST("/camera/onvif/inputs", DoGetDigitalInputs)
api.POST("/camera/onvif/outputs", DoGetRelayOutputs)
api.POST("/camera/onvif/outputs/:output", DoTriggerRelayOutput)
api.POST("/camera/verify/:streamType", capture.VerifyCamera)
// Secured endpoints..
api.Use(authMiddleware.MiddlewareFunc())
{
}
}
return api
}

View File

@@ -1,10 +1,11 @@
package routers
import (
"github.com/kerberos-io/agent/machinery/src/capture"
"github.com/kerberos-io/agent/machinery/src/models"
"github.com/kerberos-io/agent/machinery/src/routers/http"
)
func StartWebserver(configuration *models.Configuration, communication *models.Communication) {
http.StartServer(configuration, communication)
func StartWebserver(configDirectory string, configuration *models.Configuration, communication *models.Communication, captureDevice *capture.Capture) {
http.StartServer(configDirectory, configuration, communication, captureDevice)
}

View File

@@ -1,15 +1,24 @@
package mqtt
import (
"crypto/rsa"
"crypto/x509"
"encoding/base64"
"encoding/json"
"encoding/pem"
"fmt"
"io/ioutil"
"math/rand"
"strconv"
"strings"
"time"
mqtt "github.com/eclipse/paho.mqtt.golang"
configService "github.com/kerberos-io/agent/machinery/src/config"
"github.com/kerberos-io/agent/machinery/src/encryption"
"github.com/kerberos-io/agent/machinery/src/log"
"github.com/kerberos-io/agent/machinery/src/models"
"github.com/kerberos-io/agent/machinery/src/onvif"
"github.com/kerberos-io/agent/machinery/src/webrtc"
)
@@ -34,7 +43,18 @@ func HasMQTTClientModified(configuration *models.Configuration) bool {
return false
}
func ConfigureMQTT(configuration *models.Configuration, communication *models.Communication) mqtt.Client {
// Configuring MQTT to subscribe for various bi-directional messaging
// Listen and reply (a generic method to share and retrieve information)
//
// - [SUBSCRIPTION] kerberos/agent/{hubkey} (hub -> agent)
// - [PUBLISH] kerberos/hub/{hubkey} (agent -> hub)
//
// !!! LEGACY METHODS BELOW, WE SHOULD LEVERAGE THE ABOVE METHOD!
// [PUBlISH]
// Next to subscribing to various topics, we'll also publish messages to various topics, find a list of available Publish methods.
// - kerberos/{hubkey}/device/{devicekey}/motion: a motion signal
func ConfigureMQTT(configDirectory string, configuration *models.Configuration, communication *models.Communication) mqtt.Client {
config := configuration.Config
@@ -46,7 +66,7 @@ func ConfigureMQTT(configuration *models.Configuration, communication *models.Co
PREV_AgentKey = configuration.Config.Key
if config.Offline == "true" {
log.Log.Info("ConfigureMQTT: not starting as running in Offline mode.")
log.Log.Info("routers.mqtt.main.ConfigureMQTT(): not starting as running in Offline mode.")
} else {
opts := mqtt.NewClientOptions()
@@ -55,7 +75,7 @@ func ConfigureMQTT(configuration *models.Configuration, communication *models.Co
// and share and receive messages to/from.
mqttURL := config.MQTTURI
opts.AddBroker(mqttURL)
log.Log.Info("ConfigureMQTT: Set broker uri " + mqttURL)
log.Log.Debug("routers.mqtt.main.ConfigureMQTT(): Set broker uri " + mqttURL)
// Our MQTT broker can have username/password credentials
// to protect it from the outside.
@@ -64,8 +84,8 @@ func ConfigureMQTT(configuration *models.Configuration, communication *models.Co
if mqtt_username != "" || mqtt_password != "" {
opts.SetUsername(mqtt_username)
opts.SetPassword(mqtt_password)
log.Log.Info("ConfigureMQTT: Set username " + mqtt_username)
log.Log.Info("ConfigureMQTT: Set password " + mqtt_password)
log.Log.Debug("routers.mqtt.main.ConfigureMQTT(): Set username " + mqtt_username)
log.Log.Debug("routers.mqtt.main.ConfigureMQTT(): Set password " + mqtt_password)
}
// Some extra options to make sure the connection behaves
@@ -101,37 +121,22 @@ func ConfigureMQTT(configuration *models.Configuration, communication *models.Co
}
opts.SetClientID(mqttClientID)
log.Log.Info("ConfigureMQTT: Set ClientID " + mqttClientID)
log.Log.Info("routers.mqtt.main.ConfigureMQTT(): Set ClientID " + mqttClientID)
rand.Seed(time.Now().UnixNano())
webrtc.CandidateArrays = make(map[string](chan string))
opts.OnConnect = func(c mqtt.Client) {
// We managed to connect to the MQTT broker, hurray!
log.Log.Info("ConfigureMQTT: " + mqttClientID + " connected to " + mqttURL)
log.Log.Info("routers.mqtt.main.ConfigureMQTT(): " + mqttClientID + " connected to " + mqttURL)
// Create a subscription to know if send out a livestream or not.
MQTTListenerHandleLiveSD(c, hubKey, configuration, communication)
// Create a subscription for the WEBRTC livestream.
MQTTListenerHandleLiveHDHandshake(c, hubKey, configuration, communication)
// Create a subscription for keeping alive the WEBRTC livestream.
MQTTListenerHandleLiveHDKeepalive(c, hubKey, configuration, communication)
// Create a subscription to listen to the number of WEBRTC peers.
MQTTListenerHandleLiveHDPeers(c, hubKey, configuration, communication)
// Create a subscription to listen for WEBRTC candidates.
MQTTListenerHandleLiveHDCandidates(c, hubKey, configuration, communication)
// Create a susbcription to listen for ONVIF actions: e.g. PTZ, Zoom, etc.
MQTTListenerHandleONVIF(c, hubKey, configuration, communication)
// Create a susbcription for listen and reply
MQTTListenerHandler(c, hubKey, configDirectory, configuration, communication)
}
}
mqc := mqtt.NewClient(opts)
if token := mqc.Connect(); token.WaitTimeout(3 * time.Second) {
if token.Error() != nil {
log.Log.Error("ConfigureMQTT: unable to establish mqtt broker connection, error was: " + token.Error().Error())
log.Log.Error("routers.mqtt.main.ConfigureMQTT(): unable to establish mqtt broker connection, error was: " + token.Error().Error())
}
}
return mqc
@@ -140,121 +145,405 @@ func ConfigureMQTT(configuration *models.Configuration, communication *models.Co
return nil
}
func MQTTListenerHandleLiveSD(mqttClient mqtt.Client, hubKey string, configuration *models.Configuration, communication *models.Communication) {
config := configuration.Config
topicRequest := "kerberos/" + hubKey + "/device/" + config.Key + "/request-live"
mqttClient.Subscribe(topicRequest, 0, func(c mqtt.Client, msg mqtt.Message) {
func MQTTListenerHandler(mqttClient mqtt.Client, hubKey string, configDirectory string, configuration *models.Configuration, communication *models.Communication) {
if hubKey == "" {
log.Log.Info("routers.mqtt.main.MQTTListenerHandler(): no hub key provided, not subscribing to kerberos/hub/{hubkey}")
} else {
agentListener := fmt.Sprintf("kerberos/agent/%s", hubKey)
mqttClient.Subscribe(agentListener, 1, func(c mqtt.Client, msg mqtt.Message) {
// Decode the message, we are expecting following format.
// {
// mid: string, "unique id for the message"
// timestamp: int64, "unix timestamp when the message was generated"
// encrypted: boolean,
// fingerprint: string, "fingerprint of the message to validate authenticity"
// payload: Payload, "a json object which might be encrypted"
// }
var message models.Message
json.Unmarshal(msg.Payload(), &message)
// We will receive all messages from our hub, so we'll need to filter to the relevant device.
if message.Mid != "" && message.Timestamp != 0 && message.DeviceId == configuration.Config.Key {
// Messages might be encrypted, if so we'll
// need to decrypt them.
var payload models.Payload
if message.Encrypted && configuration.Config.Encryption != nil && configuration.Config.Encryption.Enabled == "true" {
encryptedValue := message.Payload.EncryptedValue
if len(encryptedValue) > 0 {
symmetricKey := configuration.Config.Encryption.SymmetricKey
privateKey := configuration.Config.Encryption.PrivateKey
r := strings.NewReader(privateKey)
pemBytes, _ := ioutil.ReadAll(r)
block, _ := pem.Decode(pemBytes)
if block == nil {
log.Log.Error("routers.mqtt.main.MQTTListenerHandler(): error decoding PEM block containing private key")
return
} else {
// Parse private key
b := block.Bytes
key, err := x509.ParsePKCS8PrivateKey(b)
if err != nil {
log.Log.Error("routers.mqtt.main.MQTTListenerHandler(): error parsing private key: " + err.Error())
return
} else {
// Conver key to *rsa.PrivateKey
rsaKey, _ := key.(*rsa.PrivateKey)
// Get encrypted key from message, delimited by :::
encryptedKey := strings.Split(encryptedValue, ":::")[0] // encrypted with RSA
encryptedValue := strings.Split(encryptedValue, ":::")[1] // encrypted with AES
// Convert encrypted value to []byte
decryptedKey, err := encryption.DecryptWithPrivateKey(encryptedKey, rsaKey)
if decryptedKey != nil {
if string(decryptedKey) == symmetricKey {
// Decrypt value with decryptedKey
data, err := base64.StdEncoding.DecodeString(encryptedValue)
if err != nil {
return
}
decryptedValue, err := encryption.AesDecrypt(data, string(decryptedKey))
if err != nil {
log.Log.Error("routers.mqtt.main.MQTTListenerHandler(): error decrypting message: " + err.Error())
return
}
json.Unmarshal(decryptedValue, &payload)
} else {
log.Log.Error("routers.mqtt.main.MQTTListenerHandler(): error decrypting message, assymetric keys do not match.")
return
}
} else if err != nil {
log.Log.Error("routers.mqtt.main.MQTTListenerHandler(): error decrypting message: " + err.Error())
return
}
}
}
}
} else {
payload = message.Payload
}
// We'll find out which message we received, and act accordingly.
log.Log.Info("routers.mqtt.main.MQTTListenerHandler(): received message with action: " + payload.Action)
switch payload.Action {
case "record":
go HandleRecording(mqttClient, hubKey, payload, configuration, communication)
case "get-audio-backchannel":
go HandleAudio(mqttClient, hubKey, payload, configuration, communication)
case "get-ptz-position":
go HandleGetPTZPosition(mqttClient, hubKey, payload, configuration, communication)
case "update-ptz-position":
go HandleUpdatePTZPosition(mqttClient, hubKey, payload, configuration, communication)
case "navigate-ptz":
go HandleNavigatePTZ(mqttClient, hubKey, payload, configuration, communication)
case "request-config":
go HandleRequestConfig(mqttClient, hubKey, payload, configuration, communication)
case "update-config":
go HandleUpdateConfig(mqttClient, hubKey, payload, configDirectory, configuration, communication)
case "request-sd-stream":
go HandleRequestSDStream(mqttClient, hubKey, payload, configuration, communication)
case "request-hd-stream":
go HandleRequestHDStream(mqttClient, hubKey, payload, configuration, communication)
case "receive-hd-candidates":
go HandleReceiveHDCandidates(mqttClient, hubKey, payload, configuration, communication)
case "trigger-relay":
go HandleTriggerRelay(mqttClient, hubKey, payload, configuration, communication)
}
}
})
}
}
func HandleRecording(mqttClient mqtt.Client, hubKey string, payload models.Payload, configuration *models.Configuration, communication *models.Communication) {
value := payload.Value
// Convert map[string]interface{} to RecordPayload
jsonData, _ := json.Marshal(value)
var recordPayload models.RecordPayload
json.Unmarshal(jsonData, &recordPayload)
if recordPayload.Timestamp != 0 {
motionDataPartial := models.MotionDataPartial{
Timestamp: recordPayload.Timestamp,
}
communication.HandleMotion <- motionDataPartial
}
}
func HandleAudio(mqttClient mqtt.Client, hubKey string, payload models.Payload, configuration *models.Configuration, communication *models.Communication) {
value := payload.Value
// Convert map[string]interface{} to AudioPayload
jsonData, _ := json.Marshal(value)
var audioPayload models.AudioPayload
json.Unmarshal(jsonData, &audioPayload)
if audioPayload.Timestamp != 0 {
audioDataPartial := models.AudioDataPartial{
Timestamp: audioPayload.Timestamp,
Data: audioPayload.Data,
}
communication.HandleAudio <- audioDataPartial
}
}
func HandleGetPTZPosition(mqttClient mqtt.Client, hubKey string, payload models.Payload, configuration *models.Configuration, communication *models.Communication) {
value := payload.Value
// Convert map[string]interface{} to PTZPositionPayload
jsonData, _ := json.Marshal(value)
var positionPayload models.PTZPositionPayload
json.Unmarshal(jsonData, &positionPayload)
if positionPayload.Timestamp != 0 {
// Get Position from device
pos, err := onvif.GetPositionFromDevice(*configuration)
if err != nil {
log.Log.Error("routers.mqtt.main.HandlePTZPosition(): error getting position from device: " + err.Error())
} else {
// Needs to wrapped!
posString := fmt.Sprintf("%f,%f,%f", pos.PanTilt.X, pos.PanTilt.Y, pos.Zoom.X)
message := models.Message{
Payload: models.Payload{
Action: "ptz-position",
DeviceId: configuration.Config.Key,
Value: map[string]interface{}{
"timestamp": positionPayload.Timestamp,
"position": posString,
},
},
}
payload, err := models.PackageMQTTMessage(configuration, message)
if err == nil {
mqttClient.Publish("kerberos/hub/"+hubKey, 0, false, payload)
} else {
log.Log.Info("routers.mqtt.main.HandlePTZPosition(): something went wrong while sending position to hub: " + string(payload))
}
}
}
}
func HandleUpdatePTZPosition(mqttClient mqtt.Client, hubKey string, payload models.Payload, configuration *models.Configuration, communication *models.Communication) {
value := payload.Value
// Convert map[string]interface{} to PTZPositionPayload
jsonData, _ := json.Marshal(value)
var onvifAction models.OnvifAction
json.Unmarshal(jsonData, &onvifAction)
if onvifAction.Action != "" {
if communication.CameraConnected {
communication.HandleONVIF <- onvifAction
log.Log.Info("routers.mqtt.main.MQTTListenerHandleONVIF(): Received an action - " + onvifAction.Action)
} else {
log.Log.Info("routers.mqtt.main.MQTTListenerHandleONVIF(): received action, but camera is not connected.")
}
}
}
func HandleRequestConfig(mqttClient mqtt.Client, hubKey string, payload models.Payload, configuration *models.Configuration, communication *models.Communication) {
value := payload.Value
// Convert map[string]interface{} to RequestConfigPayload
jsonData, _ := json.Marshal(value)
var configPayload models.RequestConfigPayload
json.Unmarshal(jsonData, &configPayload)
if configPayload.Timestamp != 0 {
// Get Config from the device
key := configuration.Config.Key
name := configuration.Config.Name
if key != "" && name != "" {
// Copy the config, as we don't want to share the encryption part.
deepCopy := configuration.Config
var configMap map[string]interface{}
inrec, _ := json.Marshal(deepCopy)
json.Unmarshal(inrec, &configMap)
// Unset encryption part.
delete(configMap, "encryption")
message := models.Message{
Payload: models.Payload{
Action: "receive-config",
DeviceId: configuration.Config.Key,
Value: configMap,
},
}
payload, err := models.PackageMQTTMessage(configuration, message)
if err == nil {
mqttClient.Publish("kerberos/hub/"+hubKey, 0, false, payload)
} else {
log.Log.Info("routers.mqtt.main.HandleRequestConfig(): something went wrong while sending config to hub: " + string(payload))
}
} else {
log.Log.Info("routers.mqtt.main.HandleRequestConfig(): no config available")
}
log.Log.Info("routers.mqtt.main.HandleRequestConfig(): Received a request for the config")
}
}
func HandleUpdateConfig(mqttClient mqtt.Client, hubKey string, payload models.Payload, configDirectory string, configuration *models.Configuration, communication *models.Communication) {
value := payload.Value
// Convert map[string]interface{} to UpdateConfigPayload
jsonData, _ := json.Marshal(value)
var configPayload models.UpdateConfigPayload
json.Unmarshal(jsonData, &configPayload)
if configPayload.Timestamp != 0 {
config := configPayload.Config
// Make sure to remove Encryption part, as we don't want to save it.
config.Encryption = configuration.Config.Encryption
err := configService.SaveConfig(configDirectory, config, configuration, communication)
if err == nil {
log.Log.Info("routers.mqtt.main.HandleUpdateConfig(): Config updated")
message := models.Message{
Payload: models.Payload{
Action: "acknowledge-update-config",
DeviceId: configuration.Config.Key,
},
}
payload, err := models.PackageMQTTMessage(configuration, message)
if err == nil {
mqttClient.Publish("kerberos/hub/"+hubKey, 0, false, payload)
} else {
log.Log.Info("routers.mqtt.main.HandleUpdateConfig(): something went wrong while sending acknowledge config to hub: " + string(payload))
}
} else {
log.Log.Info("routers.mqtt.main.HandleUpdateConfig(): Config update failed")
}
}
}
func HandleRequestSDStream(mqttClient mqtt.Client, hubKey string, payload models.Payload, configuration *models.Configuration, communication *models.Communication) {
value := payload.Value
// Convert map[string]interface{} to RequestSDStreamPayload
jsonData, _ := json.Marshal(value)
var requestSDStreamPayload models.RequestSDStreamPayload
json.Unmarshal(jsonData, &requestSDStreamPayload)
if requestSDStreamPayload.Timestamp != 0 {
if communication.CameraConnected {
select {
case communication.HandleLiveSD <- time.Now().Unix():
default:
}
log.Log.Info("MQTTListenerHandleLiveSD: received request to livestream.")
log.Log.Info("routers.mqtt.main.HandleRequestSDStream(): received request to livestream.")
} else {
log.Log.Info("MQTTListenerHandleLiveSD: received request to livestream, but camera is not connected.")
log.Log.Info("routers.mqtt.main.HandleRequestSDStream(): received request to livestream, but camera is not connected.")
}
msg.Ack()
})
}
}
func MQTTListenerHandleLiveHDHandshake(mqttClient mqtt.Client, hubKey string, configuration *models.Configuration, communication *models.Communication) {
config := configuration.Config
topicRequestWebRtc := config.Key + "/register"
mqttClient.Subscribe(topicRequestWebRtc, 0, func(c mqtt.Client, msg mqtt.Message) {
func HandleRequestHDStream(mqttClient mqtt.Client, hubKey string, payload models.Payload, configuration *models.Configuration, communication *models.Communication) {
value := payload.Value
// Convert map[string]interface{} to RequestHDStreamPayload
jsonData, _ := json.Marshal(value)
var requestHDStreamPayload models.RequestHDStreamPayload
json.Unmarshal(jsonData, &requestHDStreamPayload)
if requestHDStreamPayload.Timestamp != 0 {
if communication.CameraConnected {
var sdp models.SDPPayload
json.Unmarshal(msg.Payload(), &sdp)
// Set the Hub key, so we can send back the answer.
requestHDStreamPayload.HubKey = hubKey
select {
case communication.HandleLiveHDHandshake <- sdp:
case communication.HandleLiveHDHandshake <- requestHDStreamPayload:
default:
}
log.Log.Info("MQTTListenerHandleLiveHDHandshake: received request to setup webrtc.")
log.Log.Info("routers.mqtt.main.HandleRequestHDStream(): received request to setup webrtc.")
} else {
log.Log.Info("MQTTListenerHandleLiveHDHandshake: received request to setup webrtc, but camera is not connected.")
log.Log.Info("routers.mqtt.main.HandleRequestHDStream(): received request to setup webrtc, but camera is not connected.")
}
msg.Ack()
})
}
}
func MQTTListenerHandleLiveHDKeepalive(mqttClient mqtt.Client, hubKey string, configuration *models.Configuration, communication *models.Communication) {
config := configuration.Config
topicKeepAlive := fmt.Sprintf("kerberos/webrtc/keepalivehub/%s", config.Key)
mqttClient.Subscribe(topicKeepAlive, 0, func(c mqtt.Client, msg mqtt.Message) {
func HandleReceiveHDCandidates(mqttClient mqtt.Client, hubKey string, payload models.Payload, configuration *models.Configuration, communication *models.Communication) {
value := payload.Value
// Convert map[string]interface{} to ReceiveHDCandidatesPayload
jsonData, _ := json.Marshal(value)
var receiveHDCandidatesPayload models.ReceiveHDCandidatesPayload
json.Unmarshal(jsonData, &receiveHDCandidatesPayload)
if receiveHDCandidatesPayload.Timestamp != 0 {
if communication.CameraConnected {
alive := string(msg.Payload())
communication.HandleLiveHDKeepalive <- alive
log.Log.Info("MQTTListenerHandleLiveHDKeepalive: Received keepalive: " + alive)
// Register candidate channel
key := configuration.Config.Key + "/" + receiveHDCandidatesPayload.SessionID
go webrtc.RegisterCandidates(key, receiveHDCandidatesPayload)
} else {
log.Log.Info("MQTTListenerHandleLiveHDKeepalive: received keepalive, but camera is not connected.")
log.Log.Info("routers.mqtt.main.HandleReceiveHDCandidates(): received candidate, but camera is not connected.")
}
})
}
}
func MQTTListenerHandleLiveHDPeers(mqttClient mqtt.Client, hubKey string, configuration *models.Configuration, communication *models.Communication) {
config := configuration.Config
topicPeers := fmt.Sprintf("kerberos/webrtc/peers/%s", config.Key)
mqttClient.Subscribe(topicPeers, 0, func(c mqtt.Client, msg mqtt.Message) {
if communication.CameraConnected {
peerCount := string(msg.Payload())
communication.HandleLiveHDPeers <- peerCount
log.Log.Info("MQTTListenerHandleLiveHDPeers: Number of peers listening: " + peerCount)
} else {
log.Log.Info("MQTTListenerHandleLiveHDPeers: received peer count, but camera is not connected.")
}
})
}
func HandleNavigatePTZ(mqttClient mqtt.Client, hubKey string, payload models.Payload, configuration *models.Configuration, communication *models.Communication) {
value := payload.Value
jsonData, _ := json.Marshal(value)
var navigatePTZPayload models.NavigatePTZPayload
json.Unmarshal(jsonData, &navigatePTZPayload)
func MQTTListenerHandleLiveHDCandidates(mqttClient mqtt.Client, hubKey string, configuration *models.Configuration, communication *models.Communication) {
config := configuration.Config
topicCandidates := "candidate/cloud"
mqttClient.Subscribe(topicCandidates, 0, func(c mqtt.Client, msg mqtt.Message) {
if communication.CameraConnected {
var candidate models.Candidate
json.Unmarshal(msg.Payload(), &candidate)
if candidate.CloudKey == config.Key {
key := candidate.CloudKey + "/" + candidate.Cuuid
candidatesExists := false
var channel chan string
for !candidatesExists {
webrtc.CandidatesMutex.Lock()
channel, candidatesExists = webrtc.CandidateArrays[key]
webrtc.CandidatesMutex.Unlock()
}
log.Log.Info("MQTTListenerHandleLiveHDCandidates: " + string(msg.Payload()))
channel <- string(msg.Payload())
}
} else {
log.Log.Info("MQTTListenerHandleLiveHDCandidates: received candidate, but camera is not connected.")
}
})
}
func MQTTListenerHandleONVIF(mqttClient mqtt.Client, hubKey string, configuration *models.Configuration, communication *models.Communication) {
config := configuration.Config
topicOnvif := fmt.Sprintf("kerberos/onvif/%s", config.Key)
mqttClient.Subscribe(topicOnvif, 0, func(c mqtt.Client, msg mqtt.Message) {
if navigatePTZPayload.Timestamp != 0 {
if communication.CameraConnected {
action := navigatePTZPayload.Action
var onvifAction models.OnvifAction
json.Unmarshal(msg.Payload(), &onvifAction)
json.Unmarshal([]byte(action), &onvifAction)
communication.HandleONVIF <- onvifAction
log.Log.Info("MQTTListenerHandleONVIF: Received an action - " + onvifAction.Action)
log.Log.Info("routers.mqtt.main.HandleNavigatePTZ(): Received an action - " + onvifAction.Action)
} else {
log.Log.Info("MQTTListenerHandleONVIF: received action, but camera is not connected.")
log.Log.Info("routers.mqtt.main.HandleNavigatePTZ(): received action, but camera is not connected.")
}
})
}
}
func HandleTriggerRelay(mqttClient mqtt.Client, hubKey string, payload models.Payload, configuration *models.Configuration, communication *models.Communication) {
value := payload.Value
jsonData, _ := json.Marshal(value)
var triggerRelayPayload models.TriggerRelay
json.Unmarshal(jsonData, &triggerRelayPayload)
if triggerRelayPayload.Timestamp != 0 {
if communication.CameraConnected {
// Get token (name of relay)
token := triggerRelayPayload.Token
// Connect to Onvif device
cameraConfiguration := configuration.Config.Capture.IPCamera
device, _, err := onvif.ConnectToOnvifDevice(&cameraConfiguration)
if err == nil {
// Trigger relay output
err := onvif.TriggerRelayOutput(device, token)
if err != nil {
log.Log.Error("routers.mqtt.main.HandleTriggerRelay(): error triggering relay: " + err.Error())
} else {
log.Log.Info("routers.mqtt.main.HandleTriggerRelay(): trigger (" + token + ") relay output.")
}
} else {
log.Log.Error("routers.mqtt.main.HandleTriggerRelay(): error connecting to device: " + err.Error())
}
} else {
log.Log.Info("routers.mqtt.main.HandleTriggerRelay(): received trigger, but camera is not connected.")
}
}
}
func DisconnectMQTT(mqttClient mqtt.Client, config *models.Config) {
if mqttClient != nil {
// Cleanup all subscriptions
mqttClient.Unsubscribe("kerberos/" + PREV_HubKey + "/device/" + PREV_AgentKey + "/request-live")
mqttClient.Unsubscribe(PREV_AgentKey + "/register")
mqttClient.Unsubscribe("kerberos/webrtc/keepalivehub/" + PREV_AgentKey)
mqttClient.Unsubscribe("kerberos/webrtc/peers/" + PREV_AgentKey)
mqttClient.Unsubscribe("candidate/cloud")
mqttClient.Unsubscribe("kerberos/onvif/" + PREV_AgentKey)
// New methods
mqttClient.Unsubscribe("kerberos/agent/" + PREV_HubKey)
mqttClient.Disconnect(1000)
mqttClient = nil
log.Log.Info("DisconnectMQTT: MQTT client disconnected.")
log.Log.Info("routers.mqtt.main.DisconnectMQTT(): MQTT client disconnected.")
}
}

View File

@@ -3,15 +3,17 @@ package websocket
import (
"context"
"encoding/base64"
"image"
"net/http"
"sync"
"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
"github.com/kerberos-io/agent/machinery/src/computervision"
"github.com/kerberos-io/agent/machinery/src/capture"
"github.com/kerberos-io/agent/machinery/src/log"
"github.com/kerberos-io/agent/machinery/src/models"
"github.com/kerberos-io/joy4/cgo/ffmpeg"
"github.com/kerberos-io/agent/machinery/src/packets"
"github.com/kerberos-io/agent/machinery/src/utils"
)
type Message struct {
@@ -47,7 +49,7 @@ var upgrader = websocket.Upgrader{
},
}
func WebsocketHandler(c *gin.Context, communication *models.Communication) {
func WebsocketHandler(c *gin.Context, communication *models.Communication, captureDevice *capture.Capture) {
w := c.Writer
r := c.Request
conn, err := upgrader.Upgrade(w, r, nil)
@@ -58,12 +60,17 @@ func WebsocketHandler(c *gin.Context, communication *models.Communication) {
var message Message
err = conn.ReadJSON(&message)
if err != nil {
log.Log.Error("routers.websocket.main.WebsocketHandler(): " + err.Error())
return
}
clientID := message.ClientID
if sockets[clientID] == nil {
connection := new(Connection)
connection.Socket = conn
sockets[clientID] = connection
sockets[clientID].Cancels = make(map[string]context.CancelFunc)
log.Log.Info("routers.websocket.main.WebsocketHandler(): " + clientID + ": connected.")
}
// Continuously read messages
@@ -85,14 +92,14 @@ func WebsocketHandler(c *gin.Context, communication *models.Communication) {
if exists {
sockets[clientID].Cancels["stream-sd"]()
} else {
log.Log.Error("Streaming sd does not exists for " + clientID)
log.Log.Error("routers.websocket.main.WebsocketHandler(): streaming sd does not exists for " + clientID)
}
case "stream-sd":
if communication.CameraConnected {
_, exists := sockets[clientID].Cancels["stream-sd"]
if exists {
log.Log.Info("Already streaming sd for " + clientID)
log.Log.Debug("routers.websocket.main.WebsocketHandler(): already streaming sd for " + clientID)
} else {
startStream := Message{
ClientID: clientID,
@@ -105,7 +112,7 @@ func WebsocketHandler(c *gin.Context, communication *models.Communication) {
ctx, cancel := context.WithCancel(context.Background())
sockets[clientID].Cancels["stream-sd"] = cancel
go ForwardSDStream(ctx, clientID, sockets[clientID], communication)
go ForwardSDStream(ctx, clientID, sockets[clientID], communication, captureDevice)
}
}
}
@@ -119,37 +126,44 @@ func WebsocketHandler(c *gin.Context, communication *models.Communication) {
_, exists := sockets[clientID]
if exists {
delete(sockets, clientID)
log.Log.Info("WebsocketHandler: " + clientID + ": terminated and disconnected websocket connection.")
log.Log.Info("routers.websocket.main.WebsocketHandler(): " + clientID + ": terminated and disconnected websocket connection.")
}
}
}
func ForwardSDStream(ctx context.Context, clientID string, connection *Connection, communication *models.Communication) {
func ForwardSDStream(ctx context.Context, clientID string, connection *Connection, communication *models.Communication, captureDevice *capture.Capture) {
queue := communication.Queue
cursor := queue.Latest()
decoder := communication.Decoder
decoderMutex := communication.DecoderMutex
var queue *packets.Queue
var cursor *packets.QueueCursor
// Allocate ffmpeg.VideoFrame
frame := ffmpeg.AllocVideoFrame()
// We'll pick the right client and decoder.
rtspClient := captureDevice.RTSPSubClient
if rtspClient != nil {
queue = communication.SubQueue
cursor = queue.Latest()
} else {
rtspClient = captureDevice.RTSPClient
queue = communication.Queue
cursor = queue.Latest()
}
logreader:
for {
var encodedImage string
if queue != nil && cursor != nil && decoder != nil {
if queue != nil && cursor != nil && rtspClient != nil {
pkt, err := cursor.ReadPacket()
if err == nil {
if !pkt.IsKeyFrame {
continue
}
img, err := computervision.GetRawImage(frame, pkt, decoder, decoderMutex)
var img image.YCbCr
img, err = (*rtspClient).DecodePacket(pkt)
if err == nil {
bytes, _ := computervision.ImageToBytes(&img.Image)
bytes, _ := utils.ImageToBytes(&img)
encodedImage = base64.StdEncoding.EncodeToString(bytes)
}
} else {
log.Log.Error("ForwardSDStream:" + err.Error())
log.Log.Error("routers.websocket.main.ForwardSDStream():" + err.Error())
break logreader
}
}
@@ -163,7 +177,7 @@ logreader:
}
err := connection.WriteJson(startStrean)
if err != nil {
log.Log.Error("ForwardSDStream:" + err.Error())
log.Log.Error("routers.websocket.main.ForwardSDStream():" + err.Error())
break logreader
}
select {
@@ -173,16 +187,14 @@ logreader:
}
}
frame.Free()
// Close socket for streaming
_, exists := connection.Cancels["stream-sd"]
if exists {
delete(connection.Cancels, "stream-sd")
} else {
log.Log.Error("Streaming sd does not exists for " + clientID)
log.Log.Error("routers.websocket.main.ForwardSDStream(): streaming sd does not exists for " + clientID)
}
// Send stop streaming message
log.Log.Info("ForwardSDStream: stop sending streaming over websocket")
log.Log.Info("routers.websocket.main.ForwardSDStream(): stop sending streaming over websocket")
}

View File

@@ -1,247 +0,0 @@
package rtsp
import (
"fmt"
"image"
"image/jpeg"
"log"
"os"
"strconv"
"time"
"github.com/bluenviron/gortsplib/v3"
"github.com/bluenviron/gortsplib/v3/pkg/base"
"github.com/bluenviron/gortsplib/v3/pkg/formats"
"github.com/bluenviron/gortsplib/v3/pkg/formats/rtph265"
"github.com/bluenviron/gortsplib/v3/pkg/url"
"github.com/bluenviron/mediacommon/pkg/codecs/h264"
"github.com/pion/rtp"
)
func CreateClient() {
c := &gortsplib.Client{
OnRequest: func(req *base.Request) {
//log.Log.Info(logger.Debug, "c->s %v", req)
},
OnResponse: func(res *base.Response) {
//s.Log(logger.Debug, "s->c %v", res)
},
OnTransportSwitch: func(err error) {
//s.Log(logger.Warn, err.Error())
},
OnPacketLost: func(err error) {
//s.Log(logger.Warn, err.Error())
},
OnDecodeError: func(err error) {
//s.Log(logger.Warn, err.Error())
},
}
u, err := url.Parse("rtsp://admin:admin@192.168.1.111") //"rtsp://seing:bud-edPTQc@109.159.199.103:554/rtsp/defaultPrimary?mtu=1440&streamType=m") //
if err != nil {
panic(err)
}
err = c.Start(u.Scheme, u.Host)
if err != nil {
//return err
}
defer c.Close()
medias, baseURL, _, err := c.Describe(u)
if err != nil {
//return err
}
fmt.Println(medias)
// find the H264 media and format
var forma *formats.H265
medi := medias.FindFormat(&forma)
if medi == nil {
panic("media not found")
}
// setup RTP/H264 -> H264 decoder
rtpDec := forma.CreateDecoder()
// setup H264 -> MPEG-TS muxer
//pegtsMuxer, err := newMPEGTSMuxer(forma.SPS, forma.PPS)
if err != nil {
panic(err)
}
// setup H264 -> raw frames decoder
/*h264RawDec, err := newH264Decoder()
if err != nil {
panic(err)
}
defer h264RawDec.close()
// if SPS and PPS are present into the SDP, send them to the decoder
if forma.SPS != nil {
h264RawDec.decode(forma.SPS)
}
if forma.PPS != nil {
h264RawDec.decode(forma.PPS)
}*/
readErr := make(chan error)
go func() {
readErr <- func() error {
// Get codecs
for _, medi := range medias {
for _, forma := range medi.Formats {
fmt.Println(forma)
}
}
err = c.SetupAll(medias, baseURL)
if err != nil {
return err
}
for _, medi := range medias {
for _, forma := range medi.Formats {
c.OnPacketRTP(medi, forma, func(pkt *rtp.Packet) {
au, pts, err := rtpDec.Decode(pkt)
if err != nil {
if err != rtph265.ErrNonStartingPacketAndNoPrevious && err != rtph265.ErrMorePacketsNeeded {
log.Printf("ERR: %v", err)
}
return
}
for _, nalu := range au {
log.Printf("received NALU with PTS %v and size %d\n", pts, len(nalu))
}
/*// extract access unit from RTP packets
// DecodeUntilMarker is necessary for the DTS extractor to work
if pkt.PayloadType == 96 {
au, pts, err := rtpDec.DecodeUntilMarker(pkt)
if err != nil {
if err != rtph264.ErrNonStartingPacketAndNoPrevious && err != rtph264.ErrMorePacketsNeeded {
log.Printf("ERR: %v", err)
}
return
}
// encode the access unit into MPEG-TS
mpegtsMuxer.encode(au, pts)
for _, nalu := range au {
// convert NALUs into RGBA frames
img, err := h264RawDec.decode(nalu)
if err != nil {
panic(err)
}
// wait for a frame
if img == nil {
continue
}
// convert frame to JPEG and save to file
err = saveToFile(img)
if err != nil {
panic(err)
}
}
}*/
})
}
}
_, err = c.Play(nil)
if err != nil {
return err
}
return c.Wait()
}()
}()
for {
select {
case err := <-readErr:
fmt.Println(err)
}
}
}
func saveToFile(img image.Image) error {
// create file
fname := strconv.FormatInt(time.Now().UnixNano()/int64(time.Millisecond), 10) + ".jpg"
f, err := os.Create(fname)
if err != nil {
panic(err)
}
defer f.Close()
log.Println("saving", fname)
// convert to jpeg
return jpeg.Encode(f, img, &jpeg.Options{
Quality: 60,
})
}
// extract SPS and PPS without decoding RTP packets
func rtpH264ExtractSPSPPS(pkt *rtp.Packet) ([]byte, []byte) {
if len(pkt.Payload) < 1 {
return nil, nil
}
typ := h264.NALUType(pkt.Payload[0] & 0x1F)
switch typ {
case h264.NALUTypeSPS:
return pkt.Payload, nil
case h264.NALUTypePPS:
return nil, pkt.Payload
case h264.NALUTypeSTAPA:
payload := pkt.Payload[1:]
var sps []byte
var pps []byte
for len(payload) > 0 {
if len(payload) < 2 {
break
}
size := uint16(payload[0])<<8 | uint16(payload[1])
payload = payload[2:]
if size == 0 {
break
}
if int(size) > len(payload) {
return nil, nil
}
nalu := payload[:size]
payload = payload[size:]
typ = h264.NALUType(nalu[0] & 0x1F)
switch typ {
case h264.NALUTypeSPS:
sps = nalu
case h264.NALUTypePPS:
pps = nalu
}
}
return sps, pps
default:
return nil, nil
}
}

View File

@@ -1,140 +0,0 @@
package rtsp
import (
"fmt"
"image"
"unsafe"
)
// #cgo pkg-config: libavcodec libavutil libswscale
// #include <libavcodec/avcodec.h>
// #include <libavutil/imgutils.h>
// #include <libswscale/swscale.h>
import "C"
func frameData(frame *C.AVFrame) **C.uint8_t {
return (**C.uint8_t)(unsafe.Pointer(&frame.data[0]))
}
func frameLineSize(frame *C.AVFrame) *C.int {
return (*C.int)(unsafe.Pointer(&frame.linesize[0]))
}
// h264Decoder is a wrapper around ffmpeg's H264 decoder.
type h264Decoder struct {
codecCtx *C.AVCodecContext
srcFrame *C.AVFrame
swsCtx *C.struct_SwsContext
dstFrame *C.AVFrame
dstFramePtr []uint8
}
// newH264Decoder allocates a new h264Decoder.
func newH264Decoder() (*h264Decoder, error) {
codec := C.avcodec_find_decoder(C.AV_CODEC_ID_H264)
if codec == nil {
return nil, fmt.Errorf("avcodec_find_decoder() failed")
}
codecCtx := C.avcodec_alloc_context3(codec)
if codecCtx == nil {
return nil, fmt.Errorf("avcodec_alloc_context3() failed")
}
res := C.avcodec_open2(codecCtx, codec, nil)
if res < 0 {
C.avcodec_close(codecCtx)
return nil, fmt.Errorf("avcodec_open2() failed")
}
srcFrame := C.av_frame_alloc()
if srcFrame == nil {
C.avcodec_close(codecCtx)
return nil, fmt.Errorf("av_frame_alloc() failed")
}
return &h264Decoder{
codecCtx: codecCtx,
srcFrame: srcFrame,
}, nil
}
// close closes the decoder.
func (d *h264Decoder) close() {
if d.dstFrame != nil {
C.av_frame_free(&d.dstFrame)
}
if d.swsCtx != nil {
C.sws_freeContext(d.swsCtx)
}
C.av_frame_free(&d.srcFrame)
C.avcodec_close(d.codecCtx)
}
func (d *h264Decoder) decode(nalu []byte) (image.Image, error) {
nalu = append([]uint8{0x00, 0x00, 0x00, 0x01}, []uint8(nalu)...)
// send frame to decoder
var avPacket C.AVPacket
avPacket.data = (*C.uint8_t)(C.CBytes(nalu))
defer C.free(unsafe.Pointer(avPacket.data))
avPacket.size = C.int(len(nalu))
res := C.avcodec_send_packet(d.codecCtx, &avPacket)
if res < 0 {
return nil, nil
}
// receive frame if available
res = C.avcodec_receive_frame(d.codecCtx, d.srcFrame)
if res < 0 {
return nil, nil
}
// if frame size has changed, allocate needed objects
if d.dstFrame == nil || d.dstFrame.width != d.srcFrame.width || d.dstFrame.height != d.srcFrame.height {
if d.dstFrame != nil {
C.av_frame_free(&d.dstFrame)
}
if d.swsCtx != nil {
C.sws_freeContext(d.swsCtx)
}
d.dstFrame = C.av_frame_alloc()
d.dstFrame.format = C.AV_PIX_FMT_RGBA
d.dstFrame.width = d.srcFrame.width
d.dstFrame.height = d.srcFrame.height
d.dstFrame.color_range = C.AVCOL_RANGE_JPEG
res = C.av_frame_get_buffer(d.dstFrame, 1)
if res < 0 {
return nil, fmt.Errorf("av_frame_get_buffer() err")
}
d.swsCtx = C.sws_getContext(d.srcFrame.width, d.srcFrame.height, C.AV_PIX_FMT_YUV420P,
d.dstFrame.width, d.dstFrame.height, (int32)(d.dstFrame.format), C.SWS_BILINEAR, nil, nil, nil)
if d.swsCtx == nil {
return nil, fmt.Errorf("sws_getContext() err")
}
dstFrameSize := C.av_image_get_buffer_size((int32)(d.dstFrame.format), d.dstFrame.width, d.dstFrame.height, 1)
d.dstFramePtr = (*[1 << 30]uint8)(unsafe.Pointer(d.dstFrame.data[0]))[:dstFrameSize:dstFrameSize]
}
// convert frame from YUV420 to RGB
res = C.sws_scale(d.swsCtx, frameData(d.srcFrame), frameLineSize(d.srcFrame),
0, d.srcFrame.height, frameData(d.dstFrame), frameLineSize(d.dstFrame))
if res < 0 {
return nil, fmt.Errorf("sws_scale() err")
}
// embed frame into an image.Image
return &image.RGBA{
Pix: d.dstFramePtr,
Stride: 4 * (int)(d.dstFrame.width),
Rect: image.Rectangle{
Max: image.Point{(int)(d.dstFrame.width), (int)(d.dstFrame.height)},
},
}, nil
}

View File

@@ -1,15 +0,0 @@
package rtsp
// mp4Muxer allows to save a H264 stream into a Mp4 file.
type mp4Muxer struct {
sps []byte
pps []byte
}
// newMp4Muxer allocates a mp4Muxer.
func newMp4Muxer(sps []byte, pps []byte) (*mp4Muxer, error) {
return &mp4Muxer{
sps: sps,
pps: pps,
}, nil
}

View File

@@ -1,173 +0,0 @@
package rtsp
import (
"bufio"
"context"
"log"
"os"
"time"
"github.com/asticode/go-astits"
"github.com/bluenviron/mediacommon/pkg/codecs/h264"
)
// mpegtsMuxer allows to save a H264 stream into a MPEG-TS file.
type mpegtsMuxer struct {
sps []byte
pps []byte
f *os.File
b *bufio.Writer
mux *astits.Muxer
dtsExtractor *h264.DTSExtractor
firstIDRReceived bool
startDTS time.Duration
}
// newMPEGTSMuxer allocates a mpegtsMuxer.
func newMPEGTSMuxer(sps []byte, pps []byte) (*mpegtsMuxer, error) {
f, err := os.Create("mystream.ts")
if err != nil {
return nil, err
}
b := bufio.NewWriter(f)
mux := astits.NewMuxer(context.Background(), b)
mux.AddElementaryStream(astits.PMTElementaryStream{
ElementaryPID: 256,
StreamType: astits.StreamTypeH264Video,
})
mux.SetPCRPID(256)
return &mpegtsMuxer{
sps: sps,
pps: pps,
f: f,
b: b,
mux: mux,
}, nil
}
// close closes all the mpegtsMuxer resources.
func (e *mpegtsMuxer) close() {
e.b.Flush()
e.f.Close()
}
// encode encodes a H264 access unit into MPEG-TS.
func (e *mpegtsMuxer) encode(au [][]byte, pts time.Duration) error {
// prepend an AUD. This is required by some players
filteredNALUs := [][]byte{
{byte(h264.NALUTypeAccessUnitDelimiter), 240},
}
nonIDRPresent := false
idrPresent := false
for _, nalu := range au {
typ := h264.NALUType(nalu[0] & 0x1F)
switch typ {
case h264.NALUTypeSPS:
e.sps = append([]byte(nil), nalu...)
continue
case h264.NALUTypePPS:
e.pps = append([]byte(nil), nalu...)
continue
case h264.NALUTypeAccessUnitDelimiter:
continue
case h264.NALUTypeIDR:
idrPresent = true
case h264.NALUTypeNonIDR:
nonIDRPresent = true
}
filteredNALUs = append(filteredNALUs, nalu)
}
au = filteredNALUs
if !nonIDRPresent && !idrPresent {
return nil
}
// add SPS and PPS before every group that contains an IDR
if idrPresent {
au = append([][]byte{e.sps, e.pps}, au...)
}
var dts time.Duration
if !e.firstIDRReceived {
// skip samples silently until we find one with a IDR
if !idrPresent {
return nil
}
e.firstIDRReceived = true
e.dtsExtractor = h264.NewDTSExtractor()
var err error
dts, err = e.dtsExtractor.Extract(au, pts)
if err != nil {
return err
}
e.startDTS = dts
dts = 0
pts -= e.startDTS
} else {
var err error
dts, err = e.dtsExtractor.Extract(au, pts)
if err != nil {
return err
}
dts -= e.startDTS
pts -= e.startDTS
}
oh := &astits.PESOptionalHeader{
MarkerBits: 2,
}
if dts == pts {
oh.PTSDTSIndicator = astits.PTSDTSIndicatorOnlyPTS
oh.PTS = &astits.ClockReference{Base: int64(pts.Seconds() * 90000)}
} else {
oh.PTSDTSIndicator = astits.PTSDTSIndicatorBothPresent
oh.DTS = &astits.ClockReference{Base: int64(dts.Seconds() * 90000)}
oh.PTS = &astits.ClockReference{Base: int64(pts.Seconds() * 90000)}
}
// encode into Annex-B
annexb, err := h264.AnnexBMarshal(au)
if err != nil {
return err
}
// write TS packet
_, err = e.mux.WriteData(&astits.MuxerData{
PID: 256,
AdaptationField: &astits.PacketAdaptationField{
RandomAccessIndicator: idrPresent,
},
PES: &astits.PESData{
Header: &astits.PESHeader{
OptionalHeader: oh,
StreamID: 224, // video
},
Data: annexb,
},
})
if err != nil {
return err
}
log.Println("wrote TS packet")
return nil
}

View File

@@ -1,9 +1,12 @@
package utils
import (
"bufio"
"bytes"
"errors"
"fmt"
"image"
"image/jpeg"
"io/ioutil"
"math/rand"
"os"
@@ -15,6 +18,7 @@ import (
"strings"
"time"
"github.com/kerberos-io/agent/machinery/src/encryption"
"github.com/kerberos-io/agent/machinery/src/log"
"github.com/kerberos-io/agent/machinery/src/models"
)
@@ -110,15 +114,15 @@ func CountDigits(i int64) (count int) {
return count
}
func CheckDataDirectoryPermissions() error {
recordingsDirectory := "./data/recordings"
configDirectory := "./data/config"
snapshotsDirectory := "./data/snapshots"
cloudDirectory := "./data/cloud"
func CheckDataDirectoryPermissions(configDirectory string) error {
recordingsDirectory := configDirectory + "/data/recordings"
configurationDirectory := configDirectory + "/data/config"
snapshotsDirectory := configDirectory + "/data/snapshots"
cloudDirectory := configDirectory + "/data/cloud"
err := CheckDirectoryPermissions(recordingsDirectory)
if err == nil {
err = CheckDirectoryPermissions(configDirectory)
err = CheckDirectoryPermissions(configurationDirectory)
if err == nil {
err = CheckDirectoryPermissions(snapshotsDirectory)
if err == nil {
@@ -330,3 +334,74 @@ func PrintConfiguration(configuration *models.Configuration) {
}
log.Log.Info("Printing our configuration (config.json): " + configurationVariables)
}
func Decrypt(directoryOrFile string, symmetricKey []byte) {
// Check if file or directory
fileInfo, err := os.Stat(directoryOrFile)
if err != nil {
log.Log.Fatal(err.Error())
return
}
var files []string
if fileInfo.IsDir() {
// Create decrypted directory
err = os.MkdirAll(directoryOrFile+"/decrypted", 0755)
if err != nil {
log.Log.Fatal(err.Error())
return
}
dir, err := os.ReadDir(directoryOrFile)
if err != nil {
log.Log.Fatal(err.Error())
return
}
for _, file := range dir {
// Check if file is not a directory
if !file.IsDir() {
// Check if an mp4 file
if strings.HasSuffix(file.Name(), ".mp4") {
files = append(files, directoryOrFile+"/"+file.Name())
}
}
}
} else {
files = append(files, directoryOrFile)
}
// We'll loop over all files and decrypt them one by one.
for _, file := range files {
// Read file
content, err := os.ReadFile(file)
if err != nil {
log.Log.Fatal(err.Error())
return
}
// Decrypt using AES key
decrypted, err := encryption.AesDecrypt(content, string(symmetricKey))
if err != nil {
log.Log.Fatal("Something went wrong while decrypting: " + err.Error())
return
}
// Write decrypted content to file with appended .decrypted
// Get filename split by / and get last element.
fileParts := strings.Split(file, "/")
fileName := fileParts[len(fileParts)-1]
pathToFile := strings.Join(fileParts[:len(fileParts)-1], "/")
err = os.WriteFile(pathToFile+"/decrypted/"+fileName, []byte(decrypted), 0644)
if err != nil {
log.Log.Fatal(err.Error())
return
}
}
}
func ImageToBytes(img image.Image) ([]byte, error) {
buffer := new(bytes.Buffer)
w := bufio.NewWriter(buffer)
err := jpeg.Encode(w, img, &jpeg.Options{Quality: 15})
return buffer.Bytes(), err
}

View File

@@ -3,21 +3,18 @@ package webrtc
import (
"encoding/base64"
"encoding/json"
"fmt"
"io"
"strconv"
"sync"
"sync/atomic"
"time"
"github.com/kerberos-io/agent/machinery/src/capture"
"github.com/kerberos-io/agent/machinery/src/log"
"github.com/kerberos-io/agent/machinery/src/models"
"github.com/kerberos-io/joy4/av/pubsub"
"github.com/kerberos-io/agent/machinery/src/packets"
mqtt "github.com/eclipse/paho.mqtt.golang"
av "github.com/kerberos-io/joy4/av"
"github.com/kerberos-io/joy4/cgo/ffmpeg"
h264parser "github.com/kerberos-io/joy4/codec/h264parser"
pionWebRTC "github.com/pion/webrtc/v3"
pionMedia "github.com/pion/webrtc/v3/pkg/media"
)
@@ -73,7 +70,7 @@ func CreateWebRTC(name string, stunServers []string, turnServers []string, turnS
func (w WebRTC) DecodeSessionDescription(data string) ([]byte, error) {
sd, err := base64.StdEncoding.DecodeString(data)
if err != nil {
log.Log.Error("DecodeString error: " + err.Error())
log.Log.Error("webrtc.main.DecodeSessionDescription(): " + err.Error())
return []byte{}, err
}
return sd, nil
@@ -87,25 +84,53 @@ func (w WebRTC) CreateOffer(sd []byte) pionWebRTC.SessionDescription {
return offer
}
func InitializeWebRTCConnection(configuration *models.Configuration, communication *models.Communication, mqttClient mqtt.Client, videoTrack *pionWebRTC.TrackLocalStaticSample, audioTrack *pionWebRTC.TrackLocalStaticSample, handshake models.SDPPayload, candidates chan string) {
func RegisterCandidates(key string, candidate models.ReceiveHDCandidatesPayload) {
// Set lock
CandidatesMutex.Lock()
_, ok := CandidateArrays[key]
if !ok {
CandidateArrays[key] = make(chan string)
}
log.Log.Info("webrtc.main.HandleReceiveHDCandidates(): " + candidate.Candidate)
select {
case CandidateArrays[key] <- candidate.Candidate:
default:
log.Log.Info("webrtc.main.HandleReceiveHDCandidates(): channel is full.")
}
CandidatesMutex.Unlock()
}
func InitializeWebRTCConnection(configuration *models.Configuration, communication *models.Communication, mqttClient mqtt.Client, videoTrack *pionWebRTC.TrackLocalStaticSample, audioTrack *pionWebRTC.TrackLocalStaticSample, handshake models.RequestHDStreamPayload) {
config := configuration.Config
name := config.Key
deviceKey := config.Key
stunServers := []string{config.STUNURI}
turnServers := []string{config.TURNURI}
turnServersUsername := config.TURNUsername
turnServersCredential := config.TURNPassword
// We create a channel which will hold the candidates for this session.
sessionKey := config.Key + "/" + handshake.SessionID
CandidatesMutex.Lock()
_, ok := CandidateArrays[sessionKey]
if !ok {
CandidateArrays[sessionKey] = make(chan string)
}
CandidatesMutex.Unlock()
// Set variables
hubKey := handshake.HubKey
sessionDescription := handshake.SessionDescription
// Create WebRTC object
w := CreateWebRTC(name, stunServers, turnServers, turnServersUsername, turnServersCredential)
sd, err := w.DecodeSessionDescription(handshake.Sdp)
w := CreateWebRTC(deviceKey, stunServers, turnServers, turnServersUsername, turnServersCredential)
sd, err := w.DecodeSessionDescription(sessionDescription)
if err == nil {
mediaEngine := &pionWebRTC.MediaEngine{}
if err := mediaEngine.RegisterDefaultCodecs(); err != nil {
log.Log.Error("InitializeWebRTCConnection: something went wrong registering codecs.")
log.Log.Error("webrtc.main.InitializeWebRTCConnection(): something went wrong registering codecs for media engine: " + err.Error())
}
api := pionWebRTC.NewAPI(pionWebRTC.WithMediaEngine(mediaEngine))
@@ -122,112 +147,147 @@ func InitializeWebRTCConnection(configuration *models.Configuration, communicati
Credential: w.TurnServersCredential,
},
},
//ICETransportPolicy: pionWebRTC.ICETransportPolicyRelay,
//ICETransportPolicy: pionWebRTC.ICETransportPolicyRelay, // This will force a relay server, we might make this configurable.
},
)
if err == nil && peerConnection != nil {
if _, err = peerConnection.AddTrack(videoTrack); err != nil {
panic(err)
log.Log.Error("webrtc.main.InitializeWebRTCConnection(): something went wrong while adding video track: " + err.Error())
}
if _, err = peerConnection.AddTrack(audioTrack); err != nil {
panic(err)
}
if err != nil {
panic(err)
log.Log.Error("webrtc.main.InitializeWebRTCConnection(): something went wrong while adding audio track: " + err.Error())
}
peerConnection.OnICEConnectionStateChange(func(connectionState pionWebRTC.ICEConnectionState) {
if connectionState == pionWebRTC.ICEConnectionStateDisconnected {
atomic.AddInt64(&peerConnectionCount, -1)
peerConnections[handshake.Cuuid] = nil
close(candidates)
// Set lock
CandidatesMutex.Lock()
peerConnections[handshake.SessionID] = nil
_, ok := CandidateArrays[sessionKey]
if ok {
close(CandidateArrays[sessionKey])
}
CandidatesMutex.Unlock()
close(w.PacketsCount)
if err := peerConnection.Close(); err != nil {
panic(err)
log.Log.Error("webrtc.main.InitializeWebRTCConnection(): something went wrong while closing peer connection: " + err.Error())
}
} else if connectionState == pionWebRTC.ICEConnectionStateConnected {
atomic.AddInt64(&peerConnectionCount, 1)
} else if connectionState == pionWebRTC.ICEConnectionStateChecking {
for candidate := range candidates {
log.Log.Info("InitializeWebRTCConnection: Received candidate.")
// Iterate over the candidates and send them to the remote client
// Non blocking channel
for candidate := range CandidateArrays[sessionKey] {
log.Log.Info("webrtc.main.InitializeWebRTCConnection(): Received candidate from channel: " + candidate)
if candidateErr := peerConnection.AddICECandidate(pionWebRTC.ICECandidateInit{Candidate: string(candidate)}); candidateErr != nil {
log.Log.Error("webrtc.main.InitializeWebRTCConnection(): something went wrong while adding candidate: " + candidateErr.Error())
}
}
} else if connectionState == pionWebRTC.ICEConnectionStateFailed {
log.Log.Info("webrtc.main.InitializeWebRTCConnection(): ICEConnectionStateFailed")
}
log.Log.Info("InitializeWebRTCConnection: connection state changed to: " + connectionState.String())
log.Log.Info("InitializeWebRTCConnection: Number of peers connected (" + strconv.FormatInt(peerConnectionCount, 10) + ")")
log.Log.Info("webrtc.main.InitializeWebRTCConnection(): connection state changed to: " + connectionState.String())
log.Log.Info("webrtc.main.InitializeWebRTCConnection(): Number of peers connected (" + strconv.FormatInt(peerConnectionCount, 10) + ")")
})
offer := w.CreateOffer(sd)
if err = peerConnection.SetRemoteDescription(offer); err != nil {
panic(err)
log.Log.Error("webrtc.main.InitializeWebRTCConnection(): something went wrong while setting remote description: " + err.Error())
}
//gatherCompletePromise := pionWebRTC.GatheringCompletePromise(peerConnection)
answer, err := peerConnection.CreateAnswer(nil)
if err != nil {
panic(err)
log.Log.Error("webrtc.main.InitializeWebRTCConnection(): something went wrong while creating answer: " + err.Error())
} else if err = peerConnection.SetLocalDescription(answer); err != nil {
panic(err)
log.Log.Error("webrtc.main.InitializeWebRTCConnection(): something went wrong while setting local description: " + err.Error())
}
// When an ICE candidate is available send to the other Pion instance
// the other Pion instance will add this candidate by calling AddICECandidate
var candidatesMux sync.Mutex
// When an ICE candidate is available send to the other peer using the signaling server (MQTT).
// The other peer will add this candidate by calling AddICECandidate
peerConnection.OnICECandidate(func(candidate *pionWebRTC.ICECandidate) {
if candidate == nil {
return
}
candidatesMux.Lock()
defer candidatesMux.Unlock()
topic := fmt.Sprintf("%s/%s/candidate/edge", name, handshake.Cuuid)
log.Log.Info("InitializeWebRTCConnection: Send candidate to " + topic)
candiInit := candidate.ToJSON()
// Create a config map
valueMap := make(map[string]interface{})
candateJSON := candidate.ToJSON()
sdpmid := "0"
candiInit.SDPMid = &sdpmid
candi, err := json.Marshal(candiInit)
candateJSON.SDPMid = &sdpmid
candateBinary, err := json.Marshal(candateJSON)
if err == nil {
log.Log.Info("InitializeWebRTCConnection:" + string(candi))
token := mqttClient.Publish(topic, 2, false, candi)
valueMap["candidate"] = string(candateBinary)
} else {
log.Log.Info("webrtc.main.InitializeWebRTCConnection(): something went wrong while marshalling candidate: " + err.Error())
}
// We'll send the candidate to the hub
message := models.Message{
Payload: models.Payload{
Action: "receive-hd-candidates",
DeviceId: configuration.Config.Key,
Value: valueMap,
},
}
payload, err := models.PackageMQTTMessage(configuration, message)
if err == nil {
token := mqttClient.Publish("kerberos/hub/"+hubKey, 2, false, payload)
token.Wait()
} else {
log.Log.Info("webrtc.main.InitializeWebRTCConnection(): while packaging mqtt message: " + err.Error())
}
})
peerConnections[handshake.Cuuid] = peerConnection
// Create a channel which will be used to send candidates to the other peer
peerConnections[handshake.SessionID] = peerConnection
if err == nil {
topic := fmt.Sprintf("%s/%s/answer", name, handshake.Cuuid)
log.Log.Info("InitializeWebRTCConnection: Send SDP answer to " + topic)
mqttClient.Publish(topic, 2, false, []byte(base64.StdEncoding.EncodeToString([]byte(answer.SDP))))
// Create a config map
valueMap := make(map[string]interface{})
valueMap["sdp"] = []byte(base64.StdEncoding.EncodeToString([]byte(answer.SDP)))
log.Log.Info("webrtc.main.InitializeWebRTCConnection(): Send SDP answer")
// We'll send the candidate to the hub
message := models.Message{
Payload: models.Payload{
Action: "receive-hd-answer",
DeviceId: configuration.Config.Key,
Value: valueMap,
},
}
payload, err := models.PackageMQTTMessage(configuration, message)
if err == nil {
token := mqttClient.Publish("kerberos/hub/"+hubKey, 2, false, payload)
token.Wait()
} else {
log.Log.Info("webrtc.main.InitializeWebRTCConnection(): while packaging mqtt message: " + err.Error())
}
}
}
} else {
log.Log.Error("InitializeWebRTCConnection: NewPeerConnection failed: " + err.Error())
log.Log.Error("Initializwebrtc.main.InitializeWebRTCConnection()eWebRTCConnection: NewPeerConnection failed: " + err.Error())
}
}
func NewVideoTrack(codecs []av.CodecData) *pionWebRTC.TrackLocalStaticSample {
var mimeType string
mimeType = pionWebRTC.MimeTypeH264
func NewVideoTrack(streams []packets.Stream) *pionWebRTC.TrackLocalStaticSample {
mimeType := pionWebRTC.MimeTypeH264
outboundVideoTrack, _ := pionWebRTC.NewTrackLocalStaticSample(pionWebRTC.RTPCodecCapability{MimeType: mimeType}, "video", "pion124")
return outboundVideoTrack
}
func NewAudioTrack(codecs []av.CodecData) *pionWebRTC.TrackLocalStaticSample {
func NewAudioTrack(streams []packets.Stream) *pionWebRTC.TrackLocalStaticSample {
var mimeType string
for _, codec := range codecs {
if codec.Type().String() == "OPUS" {
for _, stream := range streams {
if stream.Name == "OPUS" {
mimeType = pionWebRTC.MimeTypeOpus
} else if codec.Type().String() == "PCM_MULAW" {
} else if stream.Name == "PCM_MULAW" {
mimeType = pionWebRTC.MimeTypePCMU
} else if codec.Type().String() == "PCM_ALAW" {
} else if stream.Name == "PCM_ALAW" {
mimeType = pionWebRTC.MimeTypePCMA
}
}
@@ -235,7 +295,7 @@ func NewAudioTrack(codecs []av.CodecData) *pionWebRTC.TrackLocalStaticSample {
return outboundAudioTrack
}
func WriteToTrack(livestreamCursor *pubsub.QueueCursor, configuration *models.Configuration, communication *models.Communication, mqttClient mqtt.Client, videoTrack *pionWebRTC.TrackLocalStaticSample, audioTrack *pionWebRTC.TrackLocalStaticSample, codecs []av.CodecData, decoder *ffmpeg.VideoDecoder, decoderMutex *sync.Mutex) {
func WriteToTrack(livestreamCursor *packets.QueueCursor, configuration *models.Configuration, communication *models.Communication, mqttClient mqtt.Client, videoTrack *pionWebRTC.TrackLocalStaticSample, audioTrack *pionWebRTC.TrackLocalStaticSample, rtspClient capture.RTSPClient) {
config := configuration.Config
@@ -244,37 +304,32 @@ func WriteToTrack(livestreamCursor *pubsub.QueueCursor, configuration *models.Co
// Set the indexes for the video & audio streams
// Later when we read a packet we need to figure out which track to send it to.
videoIdx := -1
audioIdx := -1
for i, codec := range codecs {
if codec.Type().String() == "H264" && videoIdx < 0 {
videoIdx = i
} else if (codec.Type().String() == "OPUS" || codec.Type().String() == "PCM_MULAW" || codec.Type().String() == "PCM_ALAW") && audioIdx < 0 {
audioIdx = i
hasH264 := false
hasPCM_MULAW := false
streams, _ := rtspClient.GetStreams()
for _, stream := range streams {
if stream.Name == "H264" {
hasH264 = true
} else if stream.Name == "PCM_MULAW" {
hasPCM_MULAW = true
}
}
if videoIdx == -1 {
log.Log.Error("WriteToTrack: no video codec found.")
if !hasH264 && !hasPCM_MULAW {
log.Log.Error("webrtc.main.WriteToTrack(): no valid video codec and audio codec found.")
} else {
annexbNALUStartCode := func() []byte { return []byte{0x00, 0x00, 0x00, 0x01} }
if config.Capture.TranscodingWebRTC == "true" {
if videoIdx > -1 {
log.Log.Info("WriteToTrack: successfully using a transcoder.")
} else {
}
// Todo..
} else {
log.Log.Info("WriteToTrack: not using a transcoder.")
//log.Log.Info("webrtc.main.WriteToTrack(): not using a transcoder.")
}
var cursorError error
var pkt av.Packet
var pkt packets.Packet
var previousTime time.Duration
start := false
receivedKeyFrame := false
codecData := codecs[videoIdx]
lastKeepAlive := "0"
peerCount := "0"
@@ -324,71 +379,32 @@ func WriteToTrack(livestreamCursor *pubsub.QueueCursor, configuration *models.Co
}
}
if config.Capture.TranscodingWebRTC == "true" {
//if config.Capture.TranscodingWebRTC == "true" {
// We will transcode the video
// TODO..
//}
/*decoderMutex.Lock()
decoder.SetFramerate(30, 1)
frame, err := decoder.Decode(pkt.Data)
decoderMutex.Unlock()
if err == nil && frame != nil && frame.Width() > 0 && frame.Height() > 0 {
var _outpkts []av.Packet
transcodingResolution := config.Capture.TranscodingResolution
newWidth := frame.Width() * int(transcodingResolution) / 100
newHeight := frame.Height() * int(transcodingResolution) / 100
encoder.SetResolution(newWidth, newHeight)
if _outpkts, err = encoder.Encode(frame); err != nil {
}
if len(_outpkts) > 0 {
pkt = _outpkts[0]
codecData, _ = encoder.CodecData()
}
}*/
}
switch int(pkt.Idx) {
case videoIdx:
// For every key-frame pre-pend the SPS and PPS
pkt.Data = pkt.Data[4:]
if pkt.IsVideo {
// Start at the first keyframe
if pkt.IsKeyFrame {
start = true
pkt.Data = append(annexbNALUStartCode(), pkt.Data...)
pkt.Data = append(codecData.(h264parser.CodecData).PPS(), pkt.Data...)
pkt.Data = append(annexbNALUStartCode(), pkt.Data...)
pkt.Data = append(codecData.(h264parser.CodecData).SPS(), pkt.Data...)
pkt.Data = append(annexbNALUStartCode(), pkt.Data...)
log.Log.Info("WriteToTrack: Sending keyframe")
if config.Capture.ForwardWebRTC == "true" {
log.Log.Info("WriteToTrack: Sending keep a live to remote broker.")
topic := fmt.Sprintf("kerberos/webrtc/keepalive/%s", config.Key)
mqttClient.Publish(topic, 2, false, "1")
}
}
if start {
sample := pionMedia.Sample{Data: pkt.Data, Duration: bufferDuration}
if config.Capture.ForwardWebRTC == "true" {
samplePacket, err := json.Marshal(sample)
if err == nil {
// Write packets
topic := fmt.Sprintf("kerberos/webrtc/packets/%s", config.Key)
mqttClient.Publish(topic, 0, false, samplePacket)
} else {
log.Log.Info("WriteToTrack: Error marshalling frame, " + err.Error())
}
// We will send the video to a remote peer
// TODO..
} else {
if err := videoTrack.WriteSample(sample); err != nil && err != io.ErrClosedPipe {
log.Log.Error("WriteToTrack: something went wrong while writing sample: " + err.Error())
log.Log.Error("webrtc.main.WriteToTrack(): something went wrong while writing sample: " + err.Error())
}
}
}
case audioIdx:
} else if pkt.IsAudio {
// We will send the audio
sample := pionMedia.Sample{Data: pkt.Data, Duration: pkt.Time}
if err := audioTrack.WriteSample(sample); err != nil && err != io.ErrClosedPipe {
log.Log.Error("WriteToTrack: something went wrong while writing sample: " + err.Error())
log.Log.Error("webrtc.main.WriteToTrack(): something went wrong while writing sample: " + err.Error())
}
}
}
@@ -400,5 +416,5 @@ func WriteToTrack(livestreamCursor *pubsub.QueueCursor, configuration *models.Co
}
peerConnectionCount = 0
log.Log.Info("WriteToTrack: stop writing to track.")
log.Log.Info("webrtc.main.WriteToTrack(): stop writing to track.")
}

View File

@@ -25,6 +25,7 @@
"jsx-a11y/media-has-caption": "off",
"jsx-a11y/anchor-is-valid": "off",
"jsx-a11y/click-events-have-key-events": "off",
"jsx-a11y/control-has-associated-label": "off",
"jsx-a11y/no-noninteractive-element-interactions": "off",
"jsx-a11y/no-static-element-interactions": "off",
"jsx-a11y/label-has-associated-control": [

View File

@@ -85,14 +85,23 @@
"advanced_configuration": "Erweiterte Konfiguration",
"description_advanced_configuration": "Erweiterte Einstellungen um Funktionen des Kerberos Agent zu aktivieren oder deaktivieren",
"offline_mode": "Offline Modus",
"description_offline_mode": "Ausgehende Verbindungen deaktivieren"
"description_offline_mode": "Ausgehende Verbindungen deaktivieren",
"encryption": "Encryption",
"description_encryption": "Enable encryption for all outgoing traffic. MQTT messages and/or recordings will be encrypted using AES-256. A private key is used for signing.",
"encryption_enabled": "Enable MQTT encryption",
"description_encryption_enabled": "Enable encryption for all MQTT messages.",
"encryption_recordings_enabled": "Enable recording encryption",
"description_encryption_recordings_enabled": "Enable encryption for all recordings.",
"encryption_fingerprint": "Fingerprint",
"encryption_privatekey": "Private key",
"encryption_symmetrickey": "Symmetric key"
},
"camera": {
"camera": "Kamera",
"description_camera": "Diese Einstellungen sind notwendig um eine Verbindung mit der Kamera herzustellen",
"only_h264": "Aktuell werden nur H264 RTSP kompatible Kameras unterstützt",
"only_h264": "Aktuell werden nur H264/H265 RTSP kompatible Kameras unterstützt",
"rtsp_url": "RTSP URL",
"rtsp_h264": "H264 RTSP URL der Kamera",
"rtsp_h264": "H264/H265 RTSP URL der Kamera",
"sub_rtsp_url": "RTSP url für die Live Übertragung.",
"sub_rtsp_h264": "Ergänzende URL der Kamera mit geringerer Auflösung für die Live Übertragung.",
"onvif": "ONVIF",

View File

@@ -23,7 +23,7 @@
},
"dashboard": {
"title": "Dashboard",
"heading": "Overview of your video surveilance",
"heading": "Overview of your video surveillance",
"number_of_days": "Number of days",
"total_recordings": "Total recordings",
"connected": "Connected",
@@ -85,14 +85,23 @@
"advanced_configuration": "Advanced configuration",
"description_advanced_configuration": "Detailed configuration options to enable or disable specific parts of the Kerberos Agent",
"offline_mode": "Offline mode",
"description_offline_mode": "Disable all outgoing traffic"
"description_offline_mode": "Disable all outgoing traffic",
"encryption": "Encryption",
"description_encryption": "Enable encryption for all outgoing traffic. MQTT messages and/or recordings will be encrypted using AES-256. A private key is used for signing.",
"encryption_enabled": "Enable MQTT encryption",
"description_encryption_enabled": "Enable encryption for all MQTT messages.",
"encryption_recordings_enabled": "Enable recording encryption",
"description_encryption_recordings_enabled": "Enable encryption for all recordings.",
"encryption_fingerprint": "Fingerprint",
"encryption_privatekey": "Private key",
"encryption_symmetrickey": "Symmetric key"
},
"camera": {
"camera": "Camera",
"description_camera": "Camera settings are required to make a connection to your camera of choice.",
"only_h264": "Currently only H264 RTSP streams are supported.",
"only_h264": "Currently only H264/H265 RTSP streams are supported.",
"rtsp_url": "RTSP url",
"rtsp_h264": "A H264 RTSP connection to your camera.",
"rtsp_h264": "A H264/H265 RTSP connection to your camera.",
"sub_rtsp_url": "Sub RTSP url (used for livestreaming)",
"sub_rtsp_h264": "A secondary RTSP connection to the low resolution of your camera.",
"onvif": "ONVIF",
@@ -142,7 +151,7 @@
"stun_turn_description_webrtc": "Forward h264 stream through MQTT",
"stun_turn_transcode": "Transcode stream",
"stun_turn_description_transcode": "Convert stream to a lower resolution",
"stun_turn_downscale": "Downscale resolution (in % or original resolution)",
"stun_turn_downscale": "Downscale resolution (in % of original resolution)",
"mqtt": "MQTT",
"description_mqtt": "A MQTT broker is used to communicate from",
"description2_mqtt": "to the Kerberos Agent, to achieve for example livestreaming or ONVIF (PTZ) capabilities.",

View File

@@ -85,14 +85,23 @@
"advanced_configuration": "Advanced configuration",
"description_advanced_configuration": "Detailed configuration options to enable or disable specific parts of the Kerberos Agent",
"offline_mode": "Offline mode",
"description_offline_mode": "Disable all outgoing traffic"
"description_offline_mode": "Disable all outgoing traffic",
"encryption": "Encryption",
"description_encryption": "Enable encryption for all outgoing traffic. MQTT messages and/or recordings will be encrypted using AES-256. A private key is used for signing.",
"encryption_enabled": "Enable MQTT encryption",
"description_encryption_enabled": "Enable encryption for all MQTT messages.",
"encryption_recordings_enabled": "Enable recording encryption",
"description_encryption_recordings_enabled": "Enable encryption for all recordings.",
"encryption_fingerprint": "Fingerprint",
"encryption_privatekey": "Private key",
"encryption_symmetrickey": "Symmetric key"
},
"camera": {
"camera": "Camera",
"description_camera": "Camera settings are required to make a connection to your camera of choice.",
"only_h264": "Currently only H264 RTSP streams are supported.",
"only_h264": "Currently only H264/H265 RTSP streams are supported.",
"rtsp_url": "RTSP url",
"rtsp_h264": "A H264 RTSP connection to your camera.",
"rtsp_h264": "A H264/H265 RTSP connection to your camera.",
"sub_rtsp_url": "Sub RTSP url (used for livestreaming)",
"sub_rtsp_h264": "A secondary RTSP connection to the low resolution of your camera.",
"onvif": "ONVIF",

View File

@@ -84,14 +84,23 @@
"advanced_configuration": "Configuration avancée",
"description_advanced_configuration": "Les options de configuration détaillées pour activer ou désactiver des composants spécifiques de l'Agent Kerberos",
"offline_mode": "Mode hors-ligne",
"description_offline_mode": "Désactiver tout le trafic sortant"
"description_offline_mode": "Désactiver tout le trafic sortant",
"encryption": "Encryption",
"description_encryption": "Enable encryption for all outgoing traffic. MQTT messages and/or recordings will be encrypted using AES-256. A private key is used for signing.",
"encryption_enabled": "Enable MQTT encryption",
"description_encryption_enabled": "Enable encryption for all MQTT messages.",
"encryption_recordings_enabled": "Enable recording encryption",
"description_encryption_recordings_enabled": "Enable encryption for all recordings.",
"encryption_fingerprint": "Fingerprint",
"encryption_privatekey": "Private key",
"encryption_symmetrickey": "Symmetric key"
},
"camera": {
"camera": "Caméra",
"description_camera": "Les paramètres de la caméra sont requis pour établir une connexion à la caméra de votre choix.",
"only_h264": "Actuellement, seuls les flux RTSP H264 sont pris en charge.",
"only_h264": "Actuellement, seuls les flux RTSP H264/H265 sont pris en charge.",
"rtsp_url": "URL RTSP",
"rtsp_h264": "Une connexion RTSP H264 à votre caméra.",
"rtsp_h264": "Une connexion RTSP H264/H265 à votre caméra.",
"sub_rtsp_url": "URL RTSP secondaire (utilisé pour le direct)",
"sub_rtsp_h264": "Une connexion RTSP secondaire vers le flux basse résolution de votre caméra.",
"onvif": "ONVIF",

View File

@@ -0,0 +1,224 @@
{
"breadcrumb": {
"watch_recordings": "रिकॉर्डिंग देखें",
"configure": "कॉन्फ़िगर"
},
"buttons": {
"save": "सेव्ह",
"verify_connection": "कनेक्शन चेक करें"
},
"navigation": {
"profile": "प्रोफ़ाइल",
"admin": "व्यवस्थापक",
"management": "प्रबंध",
"dashboard": "डैशबोर्ड",
"recordings": "रिकॉर्डिंग",
"settings": "सेटिंग",
"help_support": "मदद",
"swagger": "स्वैगर एपीआई",
"documentation": "प्रलेखन",
"ui_library": "यूआई लाइब्रेरी",
"layout": "भाषा और लेआऊट",
"choose_language": "भाषा चुनें"
},
"dashboard": {
"title": "डैशबोर्ड",
"heading": "आपके वीडियो निगरानी का अवलोकन",
"number_of_days": "दिनों की संख्या",
"total_recordings": "कुल रिकॉर्डिंग",
"connected": "जुड़े है",
"not_connected": "जुड़े नहीं हैं",
"offline_mode": "ऑफ़लाइन मोड",
"latest_events": "नवीनतम घटनाए",
"configure_connection": "कनेक्शन कॉन्फ़िगर करें",
"no_events": "कोई घटनाए नहीं",
"no_events_description": "कोई रिकॉर्डिंग नहीं मिली, सुनिश्चित करें कि आपका Kerberos एजेंट ठीक से कॉन्फ़िगर किया गया है।",
"motion_detected": "मोशन का पता चला",
"live_view": "लाइव देखें",
"loading_live_view": "लाइव दृश्य लोड हो रहा है",
"loading_live_view_description": "रुकिए हम आपका लाइव व्यू यहां लोड कर रहे हैं। ",
"time": "समय",
"description": "विवरण",
"name": "नाम"
},
"recordings": {
"title": "रिकॉर्डिंग",
"heading": "आपकी सभी रिकॉर्डिंग एक ही स्थान पर",
"search_media": "मीडिया खोजें"
},
"settings": {
"title": "सेटिंग",
"heading": "अपना कैमरा ऑनबोर्ड करें",
"submenu": {
"all": "सभी",
"overview": "अवलोकन",
"camera": "कैमरा",
"recording": "रिकॉर्डिंग",
"streaming": "स्ट्रीमिंग",
"conditions": "कंडीशन",
"persistence": "परसीस्टेन्स"
},
"info": {
"kerberos_hub_demo": "Kerberos हब को क्रियाशील देखने के लिए हमारे Kerberos हब डेमो पर एक नज़र डालें!",
"configuration_updated_success": "आपका कॉन्फ़िगरेशन सफलतापूर्वक अपडेट कर दिया गया है.",
"configuration_updated_error": "सहेजते समय कुछ ग़लत हो गया.",
"verify_hub": "अपनी Kerberos हब सेटिंग सत्यापित की जा रही है।",
"verify_hub_success": "कर्बेरोस हब सेटिंग्स सफलतापूर्वक सत्यापित हो गईं।",
"verify_hub_error": "कर्बरोस हब का सत्यापन करते समय कुछ गलत हो गया",
"verify_persistence": "आपकी दृढ़ता सेटिंग सत्यापित की जा रही है.",
"verify_persistence_success": "दृढ़ता सेटिंग्स सफलतापूर्वक सत्यापित की गई हैं।",
"verify_persistence_error": "दृढ़ता की पुष्टि करते समय कुछ गलत हो गया",
"verify_camera": "अपनी कैमरा सेटिंग सत्यापित कर रहा है।",
"verify_camera_success": "कैमरा सेटिंग्स सफलतापूर्वक सत्यापित हो गईं।",
"verify_camera_error": "कैमरा सेटिंग्स सत्यापित करते समय कुछ गलत हो गया",
"verify_onvif": "अपनी ONVIF सेटिंग्स सत्यापित कर रहा हूँ।",
"verify_onvif_success": "ONVIF सेटिंग्स सफलतापूर्वक सत्यापित हो गईं।",
"verify_onvif_error": "ONVIF सेटिंग्स सत्यापित करते समय कुछ गलत हो गया"
},
"overview": {
"general": "सामान्य",
"description_general": "आपके Kerberos एजेंट के लिए सामान्य सेटिंग्स",
"key": "की",
"camera_name": "कैमरे का नाम",
"timezone": "समय क्षेत्र",
"select_timezone": "समयक्षेत्र चुनें",
"advanced_configuration": "एडवांस कॉन्फ़िगरेशन",
"description_advanced_configuration": "Kerberos एजेंट के विशिष्ट भागों को सक्षम या अक्षम करने के लिए विस्तृत कॉन्फ़िगरेशन विकल्प",
"offline_mode": "ऑफ़लाइन मोड",
"description_offline_mode": "सभी आउटगोइंग ट्रैफ़िक अक्षम करें",
"encryption": "Encryption",
"description_encryption": "Enable encryption for all outgoing traffic. MQTT messages and/or recordings will be encrypted using AES-256. A private key is used for signing.",
"encryption_enabled": "Enable MQTT encryption",
"description_encryption_enabled": "Enable encryption for all MQTT messages.",
"encryption_recordings_enabled": "Enable recording encryption",
"description_encryption_recordings_enabled": "Enable encryption for all recordings.",
"encryption_fingerprint": "Fingerprint",
"encryption_privatekey": "Private key",
"encryption_symmetrickey": "Symmetric key"
},
"camera": {
"camera": "कैमरा",
"description_camera": "आपकी पसंद के कैमरे से कनेक्शन बनाने के लिए कैमरा सेटिंग्स की आवश्यकता होती है।",
"only_h264": "वर्तमान में केवल H264/H265 RTSP स्ट्रीम समर्थित हैं।",
"rtsp_url": "RTSP URL",
"rtsp_h264": "आपके कैमरे से H264/H265 RTSP कनेक्शन।",
"sub_rtsp_url": "दुसरी RTSP URL (लाइवस्ट्रीमिंग के लिए प्रयुक्त)",
"sub_rtsp_h264": "आपके कैमरे के कम रिज़ॉल्यूशन के लिए एक दुसरी RTSP कनेक्शन।",
"onvif": "ONVIF",
"description_onvif": "ONVIF क्षमताओं के साथ संचार करने के लिए क्रेडेन्शियल। ",
"onvif_xaddr": "ONVIF xaddr",
"onvif_username": "ONVIF उपयोक्तानाम",
"onvif_password": "ओएनवीआईएफ पासवर्ड",
"verify_connection": "कनेक्शन सत्यापित करें",
"verify_sub_connection": "उप कनेक्शन सत्यापित करें"
},
"recording": {
"recording": "रिकॉर्डिंग",
"description_recording": "निर्दिष्ट करें कि आप रिकॉर्डिंग कैसे करना चाहेंगे. ",
"continuous_recording": "लगातार रिकॉर्डिंग",
"description_continuous_recording": "24/7 या गति आधारित रिकॉर्डिंग करें।",
"max_duration": "अधिकतम वीडियो अवधि (सेकंड)",
"description_max_duration": "रिकॉर्डिंग की अधिकतम अवधि.",
"pre_recording": "पूर्व रिकॉर्डिंग (key frames buffered)",
"description_pre_recording": "किसी घटना के घटित होने से सेकंड पहले.",
"post_recording": "पोस्ट रिकॉर्डिंग (सेकंड)",
"description_post_recording": "किसी घटना के घटित होने के सेकंड बाद.",
"threshold": "रिकॉर्डिंग सीमा (पिक्सेल)",
"description_threshold": "रिकॉर्ड करने के लिए पिक्सेल की संख्या बदल दी गई",
"autoclean": "अपने आप क्लीन करे",
"description_autoclean": "निर्दिष्ट करें कि क्या Kerberos एजेंट एक विशिष्ट क्षमता (एमबी) तक पहुंचने पर रिकॉर्डिंग को क्लीन कर सकता है। ",
"autoclean_enable": "स्वतः क्लीन सक्षम करें",
"autoclean_description_enable": "क्षमता पूरी होने पर सबसे पुरानी रिकॉर्डिंग हटा दें।",
"autoclean_max_directory_size": "अधिकतम डिरेक्टरी आकार (एमबी)",
"autoclean_description_max_directory_size": "संग्रहीत रिकॉर्डिंग की अधिकतम एमबी।",
"fragmentedrecordings": "खंडित रिकॉर्डिंग",
"description_fragmentedrecordings": "जब रिकॉर्डिंग खंडित हो जाती हैं तो वे HLS स्ट्रीम के लिए उपयुक्त होती हैं। ",
"fragmentedrecordings_enable": "विखंडन सक्षम करें",
"fragmentedrecordings_description_enable": "HLS के लिए खंडित रिकॉर्डिंग आवश्यक हैं।",
"fragmentedrecordings_duration": "खंड अवधि",
"fragmentedrecordings_description_duration": "एक टुकड़े की अवधि."
},
"streaming": {
"stun_turn": "WebRTC के लिए STUN/TURN",
"description_stun_turn": "पूर्ण-रिज़ॉल्यूशन लाइवस्ट्रीमिंग के लिए हम WebRTC की अवधारणा का उपयोग करते हैं। ",
"stun_server": "STUN server",
"turn_server": "TURN server",
"turn_username": "उपयोगकर्ता नाम",
"turn_password": "पासवर्ड",
"stun_turn_forward": "फोरवर्डींग और ट्रांसकोडिंग",
"stun_turn_description_forward": "TURN/STUN संचार के लिए अनुकूलन और संवर्द्धन।",
"stun_turn_webrtc": "WebRTC ब्रोकर को फोरवर्डींग किया जा रहा है",
"stun_turn_description_webrtc": "MQTT के माध्यम से h264 स्ट्रीम को फोरवर्डींग करें",
"stun_turn_transcode": "ट्रांसकोड स्ट्रीम",
"stun_turn_description_transcode": "स्ट्रीम को कम रिज़ॉल्यूशन में बदलें",
"stun_turn_downscale": "डाउनस्केल रिज़ॉल्यूशन (% या मूल रिज़ॉल्यूशन में)",
"mqtt": "MQTT",
"description_mqtt": "एक MQTT ब्रोकर का उपयोग काम्युनिकेट करने के लिए किया जाता है",
"description2_mqtt": "उदाहरण के लिए लाइवस्ट्रीमिंग या ONVIF (PTZ) क्षमताओं को प्राप्त करने के लिए Kerberos एजेंट को।",
"mqtt_brokeruri": "Broker Uri",
"mqtt_username": "उपयोगकर्ता नाम",
"mqtt_password": "पासवर्ड"
},
"conditions": {
"timeofinterest": "रुचि का समय",
"description_timeofinterest": "रिकॉर्डिंग केवल विशिष्ट समय अंतराल (समय क्षेत्र के आधार पर) के बीच करें।",
"timeofinterest_enabled": "सक्रिय",
"timeofinterest_description_enabled": "सक्षम होने पर आप समय विंडो निर्दिष्ट कर सकते हैं",
"sunday": "रविवार",
"monday": "सोमवार",
"tuesday": "मंगलवार",
"wednesday": "बुधवार",
"thursday": "गुरुवार",
"friday": "शुक्रवार",
"saturday": "शनिवार",
"externalcondition": "बाह्य स्थिति",
"description_externalcondition": "बाहरी वेबसेवा के आधार पर रिकॉर्डिंग को सक्षम या अक्षम किया जा सकता है।",
"regionofinterest": "दिलचस्पी के क्षेत्र",
"description_regionofinterest": "एक या अधिक क्षेत्रों को परिभाषित करने से, गति को केवल आपके द्वारा परिभाषित क्षेत्रों में ही ट्रैक किया जाएगा।"
},
"persistence": {
"kerberoshub": "Kerberos हब",
"description_kerberoshub": "Kerberos एजेंट दिल की धड़कनों को सेंट्रल में भेज सकते हैं",
"description2_kerberoshub": "आपके वीडियो परिदृश्य के बारे में वास्तविक समय की जानकारी दिखाने के लिए दिल की धड़कन और अन्य प्रासंगिक जानकारी को केर्बरोस हब से समन्वयित किया जाता है।",
"persistence": "अटलता",
"saasoffering": "Kerberos हब (SAAS offering)",
"description_persistence": "अपनी रिकॉर्डिंग संग्रहीत करने की क्षमता होना हर चीज़ की शुरुआत है। ",
"description2_persistence": ", या कोई तृतीय पक्ष प्रदाता",
"select_persistence": "एक दृढ़ता का चयन करें",
"kerberoshub_proxyurl": "Kerberos हब प्रॉक्सी URL",
"kerberoshub_description_proxyurl": "आपकी रिकॉर्डिंग अपलोड करने के लिए प्रॉक्सी एंडपॉइंट।",
"kerberoshub_apiurl": "Kerberos हब API URL",
"kerberoshub_description_apiurl": "आपकी रिकॉर्डिंग अपलोड करने के लिए API एंडपॉइंट।",
"kerberoshub_publickey": "सार्वजनिक की",
"kerberoshub_description_publickey": "आपके Kerberos हब खाते को दी गई सार्वजनिक की।",
"kerberoshub_privatekey": "निजी चाबी",
"kerberoshub_description_privatekey": "आपके Kerberos हब खाते को दी गई निजी की।",
"kerberoshub_site": "साइट",
"kerberoshub_description_site": "साइट आईडी Kerberos एजेंट Kerberos हब से संबंधित हैं।",
"kerberoshub_region": "क्षेत्र",
"kerberoshub_description_region": "जिस क्षेत्र में हम अपनी रिकॉर्डिंग संग्रहीत कर रहे हैं।",
"kerberoshub_bucket": "बकेट",
"kerberoshub_description_bucket": "जिस बकेट में हम अपनी रिकॉर्डिंग संग्रहीत कर रहे हैं।",
"kerberoshub_username": "उपयोगकर्ता नाम/निर्देशिका (Kerberos हब उपयोगकर्ता नाम से मेल खाना चाहिए)",
"kerberoshub_description_username": "आपके Kerberos हब खाते का उपयोगकर्ता नाम।",
"kerberosvault_apiurl": "Kerberos वॉल्ट API URL",
"kerberosvault_description_apiurl": "कर्बरोस वॉल्ट एपीआई",
"kerberosvault_provider": "प्रदाता",
"kerberosvault_description_provider": "वह प्रदाता जिसे आपकी रिकॉर्डिंग भेजी जाएगी।",
"kerberosvault_directory": "निर्देशिका (Kerberos हब उपयोगकर्ता नाम से मेल खाना चाहिए)",
"kerberosvault_description_directory": "उप निर्देशिका रिकॉर्डिंग आपके प्रदाता में संग्रहीत की जाएगी।",
"kerberosvault_accesskey": "प्रवेश की चाबी",
"kerberosvault_description_accesskey": "आपके Kerberos वॉल्ट खाते की एक्सेस की।",
"kerberosvault_secretkey": "गुप्त की",
"kerberosvault_description_secretkey": "आपके कर्बेरोस वॉल्ट खाते की गुप्त की।",
"dropbox_directory": "निर्देशिका",
"dropbox_description_directory": "वह उप निर्देशिका जहां रिकॉर्डिंग आपके ड्रॉपबॉक्स खाते में संग्रहीत की जाएगी।",
"dropbox_accesstoken": "एक्सेस टोकन",
"dropbox_description_accesstoken": "आपके ड्रॉपबॉक्स खाते/ऐप का एक्सेस टोकन।",
"verify_connection": "कनेक्शन सत्यापित करें",
"remove_after_upload": "एक बार जब रिकॉर्डिंग कुछ दृढ़ता पर अपलोड हो जाती है, तो हो सकता है कि आप उन्हें स्थानीय Kerberos एजेंट से हटाना चाहें।",
"remove_after_upload_description": "सफलतापूर्वक अपलोड होने के बाद रिकॉर्डिंग हटा दें।",
"remove_after_upload_enabled": "अपलोड पर डिलीट सक्षम"
}
}
}

View File

@@ -0,0 +1,224 @@
{
"breadcrumb": {
"watch_recordings": "Guarda registrazioni",
"configure": "Configura"
},
"buttons": {
"save": "Salva",
"verify_connection": "Verifica connessione"
},
"navigation": {
"profile": "Profilo",
"admin": "admin",
"management": "Gestione",
"dashboard": "Dashboard",
"recordings": "Registrazioni",
"settings": "Impostazioni",
"help_support": "Aiuto e supporto",
"swagger": "Swagger API",
"documentation": "Documentazione",
"ui_library": "Biblioteca UI",
"layout": "Lingua e layout",
"choose_language": "Scegli lingua"
},
"dashboard": {
"title": "Dashboard",
"heading": "Panoramica della videosorveglianza",
"number_of_days": "Numero di giorni",
"total_recordings": "Registrazioni totali",
"connected": "Connesso",
"not_connected": "Non connesso",
"offline_mode": "Modalità offline",
"latest_events": "Ultimi eventi",
"configure_connection": "Configura connessione",
"no_events": "Nessun evento",
"no_events_description": "Non sono state trovate registrazioni, assicurati che il Kerberos Agent sia configurato correttamente.",
"motion_detected": "Movimento rilevato",
"live_view": "Vista in diretta",
"loading_live_view": "Caricamento vista in diretta",
"loading_live_view_description": "Attendi mentre viene caricata la vista in diretta. Se non l'hai ancora fatto, configura la connessione con la videocamera nelle pagine di impostazione.",
"time": "Ora",
"description": "Descrizione",
"name": "Nome"
},
"recordings": {
"title": "Registrazioni",
"heading": "Tutte le tue registrazioni in un posto solo",
"search_media": "Cerca video"
},
"settings": {
"title": "Impostazioni",
"heading": "Panoramica impostazioni videocamera e Agent",
"submenu": {
"all": "All",
"overview": "Panoramica",
"camera": "Videocamera",
"recording": "Registrazione",
"streaming": "Streaming",
"conditions": "Criteri",
"persistence": "Persistenza"
},
"info": {
"kerberos_hub_demo": "Dai un'occhiata al nostro ambiente demo di Kerberos Hub per vederlo in azione!",
"configuration_updated_success": "La configurazione è stata aggiornata con successo.",
"configuration_updated_error": "Si è verificato un problema durante il salvataggio.",
"verify_hub": "Controllo delle impostazioni di Kerberos Hub.",
"verify_hub_success": "Impostazioni di Kerberos Hub verificate correttamente.",
"verify_hub_error": "Si è verificato un problema durante la verifica delle impostazioni di Kerberos Hub",
"verify_persistence": "Controlla le impostazioni della persistenza.",
"verify_persistence_success": "Impostazioni della persistenza verificate correttamente.",
"verify_persistence_error": "Si è verificato un problema durante la verifica delle impostazioni della persistenza",
"verify_camera": "Controlla le impostazioni della videocamera.",
"verify_camera_success": "Impostazioni videocamera verificate correttamente.",
"verify_camera_error": "Si è verificato un problema durante la verifica delle impostazioni della videocamera",
"verify_onvif": "Controlla le impostazioni ONVIF.",
"verify_onvif_success": "Impostazioni ONVIF verificate correttamente.",
"verify_onvif_error": "Si è verificato un problema durante la verifica delle impostazioni ONVIF"
},
"overview": {
"general": "Generale",
"description_general": "Impostazioni generali del Kerberos Agent",
"key": "Chiave",
"camera_name": "Nome videocamera",
"timezone": "Fuso orario",
"select_timezone": "Seleziona un fuso orario",
"advanced_configuration": "Configurazione avanzata",
"description_advanced_configuration": "Opzioni di configurazione dettagliate per abilitare o disabilitare parti specifiche del Kerberos Agent",
"offline_mode": "Modalità offline",
"description_offline_mode": "Disabilita traffico in uscita",
"encryption": "Encryption",
"description_encryption": "Enable encryption for all outgoing traffic. MQTT messages and/or recordings will be encrypted using AES-256. A private key is used for signing.",
"encryption_enabled": "Enable MQTT encryption",
"description_encryption_enabled": "Enable encryption for all MQTT messages.",
"encryption_recordings_enabled": "Enable recording encryption",
"description_encryption_recordings_enabled": "Enable encryption for all recordings.",
"encryption_fingerprint": "Fingerprint",
"encryption_privatekey": "Private key",
"encryption_symmetrickey": "Symmetric key"
},
"camera": {
"camera": "Videocamera",
"description_camera": "Le impostazioni della fotocamera sono necessarie per stabilire una connessione con la videocamera scelta.",
"only_h264": "Al momento sono supportati solo streams RTSP H264/H265.",
"rtsp_url": "Url RTSP",
"rtsp_h264": "Connessione RTSP H264/H265 alla videocamera.",
"sub_rtsp_url": "Sub-url RTSP (per lo streaming in diretta)",
"sub_rtsp_h264": "URL RTSP supplementare della videocamera con risoluzione inferiore per lo streaming in diretta.",
"onvif": "ONVIF",
"description_onvif": "Credenziali per interagire con le funzionalità ONVIF come PTZ o altre funzioni fornite dalla videocamera.",
"onvif_xaddr": "ONVIF xaddr",
"onvif_username": "ONVIF username",
"onvif_password": "ONVIF password",
"verify_connection": "Verifica connessione",
"verify_sub_connection": "Verifica sub-connessione"
},
"recording": {
"recording": "Registrazione",
"description_recording": "Specificare se effettuare le registrazioni con un'impostazione continua 24/7 oppure basata sulla rilevazione di movimento.",
"continuous_recording": "Registrazione continua",
"description_continuous_recording": "Effettuare registrazioni 24/7 o basate sul movimento.",
"max_duration": "massima durata video (in secondi)",
"description_max_duration": "Durata massima della registrazione.",
"pre_recording": "pre registrazione (buffering dei key frames)",
"description_pre_recording": "Secondi prima del verificarsi di un evento.",
"post_recording": "post registrazione (in)",
"description_post_recording": "Secondi dopo il verificarsi di un evento.",
"threshold": "Soglia di registrazione (in pixel)",
"description_threshold": "Numero di pixel modificati per avviare la registrazione",
"autoclean": "Cancellazione automatica",
"description_autoclean": "Specificare se l'Agente Kerberos può cancellare le registrazioni quando viene raggiunta una specifica capacità di archiviazione (in MB). Questo rimuoverà le registrazioni più vecchie quando la capacità viene raggiunta.",
"autoclean_enable": "Abilita cancellazione automatica",
"autoclean_description_enable": "Rimuovere la registrazione più vecchia al raggiungimento della capacità.",
"autoclean_max_directory_size": "Dimensione massima della cartella (in MB)",
"autoclean_description_max_directory_size": "Dimensione massima in MB delle registrazioni salvate.",
"fragmentedrecordings": "Registrazioni frammentate",
"description_fragmentedrecordings": "Quando le registrazioni sono frammentate, sono adatte ad uno stream HLS. Se attivato, il contenitore MP4 avrà un aspetto leggermente diverso.",
"fragmentedrecordings_enable": "Abilita frammentazione",
"fragmentedrecordings_description_enable": "Per utilizzare gli stream HLS sono necessarie registrazioni frammentate.",
"fragmentedrecordings_duration": "durata frammento",
"fragmentedrecordings_description_duration": "Durata del singolo frammento."
},
"streaming": {
"stun_turn": "STUN/TURN per WebRTC",
"description_stun_turn": "Per lo streaming in diretta a massima risoluzione viene impiegato WebRTC. Una delle sue funzionalità chiave è la ICE-candidate, che consente di attraversare il NAT utilizzando i concetti di STUN/TURN.",
"stun_server": "STUN server",
"turn_server": "TURN server",
"turn_username": "Username",
"turn_password": "Password",
"stun_turn_forward": "Inoltro e transcodifica",
"stun_turn_description_forward": "Ottimizzazioni e miglioramenti per la comunicazione TURN/STUN.",
"stun_turn_webrtc": "Inoltro al broker WebRTC",
"stun_turn_description_webrtc": "Inoltro dello stream h264 via MQTT",
"stun_turn_transcode": "Transcodifica stream",
"stun_turn_description_transcode": "Conversione dello stream in una risoluzione inferiore",
"stun_turn_downscale": "Riduzione della risoluzione (in % o risoluzione originale)",
"mqtt": "MQTT",
"description_mqtt": "Un broker MQTT è usato per comunicare da",
"description2_mqtt": "al Kerberos Agent, per ottenere, ad esempio, funzionalità di livestreaming o ONVIF (PTZ).",
"mqtt_brokeruri": "Uri Broker",
"mqtt_username": "Username",
"mqtt_password": "Password"
},
"conditions": {
"timeofinterest": "Periodo di interesse",
"description_timeofinterest": "Effettua registrazioni solamente all'interno di specifici intervalli orari (basato sul fuso orario).",
"timeofinterest_enabled": "Abilitato",
"timeofinterest_description_enabled": "Se abilitato, è possibile specificare una finestra temporale",
"sunday": "Domenica",
"monday": "Lunedì",
"tuesday": "Martedì",
"wednesday": "Mercoledì",
"thursday": "Giovedì",
"friday": "Venerdì",
"saturday": "Sabato",
"externalcondition": "Condizione esterna",
"description_externalcondition": "È possibile attivare o disattivare la dipendenza da un servizio esterno di registrazione.",
"regionofinterest": "Regione di interesse",
"description_regionofinterest": "Definendo una o più regioni, il movimento verrà tracciato solo al loro interno."
},
"persistence": {
"kerberoshub": "Kerberos Hub",
"description_kerberoshub": "Kerberos Agents can send heartbeats to a central",
"description2_kerberoshub": "installation. Heartbeats and other relevant information are synced to Kerberos Hub to show realtime information about your video landscape.",
"persistence": "Persistenza",
"saasoffering": "Kerberos Hub (soluzione SAAS)",
"description_persistence": "La possibilità di poter salvare le tue registrazioni rappresenta l'inizio di tutto. Puoi scegliere tra il nostro",
"description2_persistence": ", oppure un provider di terze parti",
"select_persistence": "Seleziona una persistenza",
"kerberoshub_proxyurl": "URL Proxy Kerberos Hub",
"kerberoshub_description_proxyurl": "Endpoint del Proxy per l'upload delle registrazioni.",
"kerberoshub_apiurl": "API URL Kerberos Hub",
"kerberoshub_description_apiurl": "Endpoint API per l'upload delle registrazioni.",
"kerberoshub_publickey": "Chiave pubblica",
"kerberoshub_description_publickey": "Chiave pubblica dell'account Kerberos Hub.",
"kerberoshub_privatekey": "Chiave privata",
"kerberoshub_description_privatekey": "Chiave privata dell'account Kerberos Hub.",
"kerberoshub_site": "Sito",
"kerberoshub_description_site": "ID del sito a cui appartengono i Kerberos Agents in Kerberos Hub.",
"kerberoshub_region": "Regione",
"kerberoshub_description_region": "La regione in cui memorizziamo le registrazioni.",
"kerberoshub_bucket": "Bucket",
"kerberoshub_description_bucket": "Bucket in cui memorizziamo le registrazioni.",
"kerberoshub_username": "Username/Cartella (dovrebbe essere uguale allo username di Kerberos Hub)",
"kerberoshub_description_username": "Username del tuo account Kerberos Hub.",
"kerberosvault_apiurl": "API URL Kerberos Vault",
"kerberosvault_description_apiurl": "API di Kerberos Vault",
"kerberosvault_provider": "Provider",
"kerberosvault_description_provider": "Provider al quale saranno inviate le registrazioni.",
"kerberosvault_directory": "Cartella (dovrebbe essere uguale allo username di Kerberos Hub)",
"kerberosvault_description_directory": "Sotto cartella in cui saranno memorizzate le tue registrazioni nel provider.",
"kerberosvault_accesskey": "Access key",
"kerberosvault_description_accesskey": "Access key del tuo account Kerberos Vault.",
"kerberosvault_secretkey": "Secret key",
"kerberosvault_description_secretkey": "Secret key del tuo account Kerberos Vault.",
"dropbox_directory": "Cartella",
"dropbox_description_directory": "Sottcartella dell'account Dropbox in cui saranno salvate le registrazioni.",
"dropbox_accesstoken": "Access token",
"dropbox_description_accesstoken": "Access token del tuo account/app Dropbox.",
"verify_connection": "Verifica connessione",
"remove_after_upload": "Una volta che le registrazioni sono state caricate su una certa persistenza, si potrebbe volerle rimuovere dal Kerberos Agent locale.",
"remove_after_upload_description": "Cancella le registrazioni dopo che sono state caricate correttamente.",
"remove_after_upload_enabled": "Abilita cancellazione al caricamento"
}
}
}

View File

@@ -85,14 +85,23 @@
"advanced_configuration": "詳細設定",
"description_advanced_configuration": "Kerberos エージェントの特定の部分を有効または無効にするための詳細な構成オプション",
"offline_mode": "オフラインモード",
"description_offline_mode": "すべての送信トラフィックを無効にする"
"description_offline_mode": "すべての送信トラフィックを無効にする",
"encryption": "Encryption",
"description_encryption": "Enable encryption for all outgoing traffic. MQTT messages and/or recordings will be encrypted using AES-256. A private key is used for signing.",
"encryption_enabled": "Enable MQTT encryption",
"description_encryption_enabled": "Enable encryption for all MQTT messages.",
"encryption_recordings_enabled": "Enable recording encryption",
"description_encryption_recordings_enabled": "Enable encryption for all recordings.",
"encryption_fingerprint": "Fingerprint",
"encryption_privatekey": "Private key",
"encryption_symmetrickey": "Symmetric key"
},
"camera": {
"camera": "カメラ",
"description_camera": "選択したカメラに接続するには、カメラの設定が必要です。",
"only_h264": "現在、H264 RTSP ストリームのみがサポートされています。",
"only_h264": "現在、H264/H265 RTSP ストリームのみがサポートされています。",
"rtsp_url": "RTSP URL",
"rtsp_h264": "カメラへの H264 RTSP 接続。",
"rtsp_h264": "カメラへの H264/H265 RTSP 接続。",
"sub_rtsp_url": "Sub RTSP url (ライブストリーミングに使用)",
"sub_rtsp_h264": "カメラの低解像度へのセカンダリ RTSP 接続。",
"onvif": "ONVIF",

View File

@@ -85,14 +85,23 @@
"advanced_configuration": "Geavanceerde instellingen",
"description_advanced_configuration": "Detail instellingen om bepaalde functionaliteiten van je Kerberos Agent aan en uit te zetten",
"offline_mode": "Offline modus",
"description_offline_mode": "Uitzetten van uitgaande connectiviteit"
"description_offline_mode": "Uitzetten van uitgaande connectiviteit",
"encryption": "Encrypteer",
"description_encryption": "Activeer encryptie voor alle uitgaande verkeer. MQTT berichten en/of opnames worden geencrypteerd met AES-256. Een private sleutel wordt gebruikt voor het ondertekenen.",
"encryption_enabled": "Activeer MQTT encryptie",
"description_encryption_enabled": "Activeer encryptie voor alle MQTT berichten.",
"encryption_recordings_enabled": "Activeer opname encryptie",
"description_encryption_recordings_enabled": "Activeer encryptie voor alle opnames.",
"encryption_fingerprint": "Vingerafdruk",
"encryption_privatekey": "Private sleutel",
"encryption_symmetrickey": "Symmetrische sleutel"
},
"camera": {
"camera": "Camera",
"description_camera": "Camera settings are required to make a connection to your camera of choice.",
"only_h264": "Momenteel worden enkel H264 RTSP streams gesupporteerd",
"only_h264": "Momenteel worden enkel H264/H265 RTSP streams gesupporteerd",
"rtsp_url": "RTSP url",
"rtsp_h264": "Een H264 RTSP connectie met jouw camera.",
"rtsp_h264": "Een H264/H265 RTSP connectie met jouw camera.",
"sub_rtsp_url": "Sub RTSP url (used for livestreaming)",
"sub_rtsp_h264": "A secondary RTSP connection to the low resolution of your camera.",
"onvif": "ONVIF",

View File

@@ -85,14 +85,23 @@
"advanced_configuration": "Advanced configuration",
"description_advanced_configuration": "Detailed configuration options to enable or disable specific parts of the Kerberos Agent",
"offline_mode": "Offline mode",
"description_offline_mode": "Disable all outgoing traffic"
"description_offline_mode": "Disable all outgoing traffic",
"encryption": "Encryption",
"description_encryption": "Enable encryption for all outgoing traffic. MQTT messages and/or recordings will be encrypted using AES-256. A private key is used for signing.",
"encryption_enabled": "Enable MQTT encryption",
"description_encryption_enabled": "Enable encryption for all MQTT messages.",
"encryption_recordings_enabled": "Enable recording encryption",
"description_encryption_recordings_enabled": "Enable encryption for all recordings.",
"encryption_fingerprint": "Fingerprint",
"encryption_privatekey": "Private key",
"encryption_symmetrickey": "Symmetric key"
},
"camera": {
"camera": "Camera",
"description_camera": "Camera settings are required to make a connection to your camera of choice.",
"only_h264": "Currently only H264 RTSP streams are supported.",
"only_h264": "Currently only H264/H265 RTSP streams are supported.",
"rtsp_url": "RTSP url",
"rtsp_h264": "A H264 RTSP connection to your camera.",
"rtsp_h264": "A /H265 RTSP connection to your camera.",
"sub_rtsp_url": "Sub RTSP url (used for livestreaming)",
"sub_rtsp_h264": "A secondary RTSP connection to the low resolution of your camera.",
"onvif": "ONVIF",

View File

@@ -85,14 +85,23 @@
"advanced_configuration": "Configurações avançadas",
"description_advanced_configuration": "Opções de configuração detalhadas para habilitar ou desabilitar partes específicas do Kerberos Agent",
"offline_mode": "Modo Offline",
"description_offline_mode": "Desative todo o tráfego de saída"
"description_offline_mode": "Desative todo o tráfego de saída",
"encryption": "Encryption",
"description_encryption": "Enable encryption for all outgoing traffic. MQTT messages and/or recordings will be encrypted using AES-256. A private key is used for signing.",
"encryption_enabled": "Enable MQTT encryption",
"description_encryption_enabled": "Enable encryption for all MQTT messages.",
"encryption_recordings_enabled": "Enable recording encryption",
"description_encryption_recordings_enabled": "Enable encryption for all recordings.",
"encryption_fingerprint": "Fingerprint",
"encryption_privatekey": "Private key",
"encryption_symmetrickey": "Symmetric key"
},
"camera": {
"camera": "Câmera",
"description_camera": "As configurações da câmera são necessárias para fazer uma conexão com a câmera de sua escolha.",
"only_h264": "Atualmente, apenas streams H264 RTSP são suportados.",
"only_h264": "Atualmente, apenas streams H264/H265 RTSP são suportados.",
"rtsp_url": "Url RTSP",
"rtsp_h264": "Uma conexão H264 RTSP para sua câmera.",
"rtsp_h264": "Uma conexão H264/H265 RTSP para sua câmera.",
"sub_rtsp_url": "Sub RTSP URL(usado para transmissão ao vivo)",
"sub_rtsp_h264": "Uma conexão RTSP secundária para a baixa resolução de sua câmera.",
"onvif": "ONVIF",

View File

@@ -0,0 +1,224 @@
{
"breadcrumb": {
"watch_recordings": "Смотреть записи",
"configure": "Настроить"
},
"buttons": {
"save": "Сохранить",
"verify_connection": "Проверить подключение"
},
"navigation": {
"profile": "Профиль",
"admin": "admin",
"management": "Управление",
"dashboard": "Панель",
"recordings": "Записи",
"settings": "Настройки",
"help_support": "Помощь & Поддержка",
"swagger": "Swagger API",
"documentation": "Документация",
"ui_library": "UI Библиотека",
"layout": "Язык & Макет ",
"choose_language": "Выбрать язык"
},
"dashboard": {
"title": "Панель",
"heading": "Обзор системы видеонаблюдения",
"number_of_days": "Количество дней",
"total_recordings": "Всего записей",
"connected": "Подключён",
"not_connected": "Не подключён",
"offline_mode": "Оффлайн режим",
"latest_events": "Последние события",
"configure_connection": "Настроить подключение",
"no_events": "Нет событий",
"no_events_description": "Записи не найдены, убедитесь, что ваш Kerberos Agent правильно настроен.",
"motion_detected": "Обнаружено движение",
"live_view": "Прямая трансляция",
"loading_live_view": "Загрузка трансляции",
"loading_live_view_description": "Подождите, мы загружаем сюда изображение в реальном времени. Если вы не настроили подключение камеры, обновите его на страницах настроек.",
"time": "Время",
"description": "Описание",
"name": "Название"
},
"recordings": {
"title": "Записи",
"heading": "Все ваши записи в одном месте",
"search_media": "Поиск записи"
},
"settings": {
"title": "Настройки",
"heading": "Обзор настроек камеры и агента",
"submenu": {
"all": "Все",
"overview": "Обзор",
"camera": "Камера",
"recording": "Запись",
"streaming": "Потоковое вещание",
"conditions": "Условия",
"persistence": "Хранилище"
},
"info": {
"kerberos_hub_demo": "Посмотрите на демо, чтобы увидеть Kerberos Hub в действии!",
"configuration_updated_success": "Настройки успешно обновлены.",
"configuration_updated_error": "При сохранении что-то пошло не так.",
"verify_hub": "Проверка настроек Kerberos Hub.",
"verify_hub_success": "Настройки Kerberos Hub успешно проверены.",
"verify_hub_error": "Что-то пошло не так при проверке концентратора Kerberos Hub",
"verify_persistence": "Проверка настроек хранилища.",
"verify_persistence_success": "Настройки хранилища успешно проверены.",
"verify_persistence_error": "Что-то пошло не так при проверке хранилища",
"verify_camera": "Проверка настроек камеры.",
"verify_camera_success": "Настройки камеры успешно проверены.",
"verify_camera_error": "Что-то пошло не так при проверке настроек камеры",
"verify_onvif": "Проверка настроек ONVIF.",
"verify_onvif_success": "Настройки ONVIF успешно проверены.",
"verify_onvif_error": "Что-то пошло не так при проверке настроек ONVIF"
},
"overview": {
"general": "Главная",
"description_general": "Общие настройки Kerberos Agent",
"key": "Ключ",
"camera_name": "Название камеры",
"timezone": "Часовой пояс",
"select_timezone": "Выберите часовой пояс",
"advanced_configuration": "Расширенные настройки",
"description_advanced_configuration": "Расширенные настройки для включения или отключения определенных частей Kerberos Agent",
"offline_mode": "Автономный режим",
"description_offline_mode": "Отключить весь исходящий трафик",
"encryption": "Шифрование",
"description_encryption": "Включите шифрование для всего исходящего трафика. MQTT-сообщения и/или записи будут зашифрованы с использованием AES-256. Для подписи используется закрытый ключ.",
"encryption_enabled": "Включить шифрование MQTT",
"description_encryption_enabled": "Включает шифрование для всех сообщений MQTT.",
"encryption_recordings_enabled": "Включить шифрование записей",
"description_encryption_recordings_enabled": "Включает шифрование для всех записей.",
"encryption_fingerprint": "Отпечаток",
"encryption_privatekey": "Закрытый ключ",
"encryption_symmetrickey": "Симметричный ключ"
},
"camera": {
"camera": "Камера",
"description_camera": "Настройки камеры необходимы для установки соединения с выбранной камерой.",
"only_h264": "В настоящее время поддерживаются только потоки H264/H265 RTSP.",
"rtsp_url": "Адрес основного потока RTSP",
"rtsp_h264": "Подключение к камере по протоколу H264/H265 RTSP.",
"sub_rtsp_url": "Адрес дополнительного потока RTSP (используется для прямой трансляции)",
"sub_rtsp_h264": "Дополнительное RTSP-соединение с низким разрешением камеры.",
"onvif": "ONVIF",
"description_onvif": "Учетные данные для связи по протоколу ONVIF. Они используются для PTZ или других возможностей, предоставляемых камерой.",
"onvif_xaddr": "ONVIF xaddr",
"onvif_username": "ONVIF пользователь",
"onvif_password": "ONVIF пароль",
"verify_connection": "Проверка основного соединения",
"verify_sub_connection": "Проверка дополнительного подключения"
},
"recording": {
"recording": "Запись",
"description_recording": "Укажите, как вы хотите вести запись. Непрерывная круглосуточная запись или запись по движению.",
"continuous_recording": "Непрерывная запись",
"description_continuous_recording": "Осуществлять 24/7 запись или запись по движению.",
"max_duration": "максимальная продолжительность видео (секунд)",
"description_max_duration": "Максимальная продолжительность записи.",
"pre_recording": "Предзапись (секунд)",
"description_pre_recording": "Секунд до наступления события.",
"post_recording": "Записывать после (секунд)",
"description_post_recording": "Секунд после наступления события.",
"threshold": "Уровень срабатывания записи (пикселей)",
"description_threshold": "Количество пикселей, измененных для записи",
"autoclean": "Автоочистка",
"description_autoclean": "Укажите, может ли Kerberos Agent очищать записи при достижении определенного объема памяти (МБ). При этом по достижении указанной емкости будут удаляться самые старые записи.",
"autoclean_enable": "Включить автоматическую очистку",
"autoclean_description_enable": "При достижении емкости удаляется самая старая запись.",
"autoclean_max_directory_size": "Максимальный размер каталога (МБ)",
"autoclean_description_max_directory_size": "Максимальное количество хранимых мегабайт записей.",
"fragmentedrecordings": "Фрагментированные записи",
"description_fragmentedrecordings": "Когда записи фрагментированы, они подходят для HLS-потока. При включении контейнер MP4 будет выглядеть несколько иначе.",
"fragmentedrecordings_enable": "Включить фрагментацию",
"fragmentedrecordings_description_enable": "Фрагментированные записи необходимы для HLS.",
"fragmentedrecordings_duration": "продолжительность фрагмента",
"fragmentedrecordings_description_duration": "Продолжительность одного фрагмента."
},
"streaming": {
"stun_turn": "STUN/TURN для WebRTC",
"description_stun_turn": "Для организации трансляций в полном разрешении мы используем технологию WebRTC. Одной из ключевых возможностей является функция ICE-candidate, которая позволяет обходить NAT, используя концепции STUN/TURN.",
"stun_server": "STUN сервер",
"turn_server": "TURN сервер",
"turn_username": "Имя пользователя",
"turn_password": "Пароль",
"stun_turn_forward": "Переадресация и транскодирование",
"stun_turn_description_forward": "Оптимизация и усовершенствование связи TURN/STUN.",
"stun_turn_webrtc": "Переадресация на WebRTC-брокера",
"stun_turn_description_webrtc": "Передача потока h264 через MQTT",
"stun_turn_transcode": "Транскодирование потока",
"stun_turn_description_transcode": "Преобразование потока в меньшее разрешение",
"stun_turn_downscale": "Уменьшение разрешения (в % от исходного разрешения)",
"mqtt": "MQTT",
"description_mqtt": "Брокер MQTT используется для обмена данными с",
"description2_mqtt": "к Kerberos Agent, чтобы, например, получить возможность трансляции видео или ONVIF (PTZ).",
"mqtt_brokeruri": "Адрес брокера",
"mqtt_username": "Имя пользователя",
"mqtt_password": "Пароль"
},
"conditions": {
"timeofinterest": "Время интереса",
"description_timeofinterest": "Производить запись только в определенные временные интервалы (в зависимости от часового пояса).",
"timeofinterest_enabled": "Включено",
"timeofinterest_description_enabled": "Если эта функция включена, то можно указать временные окна",
"sunday": "Воскресенье",
"monday": "Понедельник",
"tuesday": "Вторник",
"wednesday": "Среда",
"thursday": "Четверг",
"friday": "Пятница",
"saturday": "Суббота",
"externalcondition": "Внешнее условия",
"description_externalcondition": "В зависимости от внешнего веб-сервиса запись может быть включена или отключена.",
"regionofinterest": "Область интереса",
"description_regionofinterest": "Если задать одну или несколько областей, то движение будет отслеживаться только в заданных областях."
},
"persistence": {
"kerberoshub": "Kerberos Hub",
"description_kerberoshub": "Kerberos Agent'ы могут отправлять heartbeat сообщения в центральный",
"description2_kerberoshub": "узел. Heartbeat и другая необходимая информация синхронизируются с Kerberos Hub для отображения информации о видеоландшафте в реальном времени.",
"persistence": "Хранилище",
"saasoffering": "Kerberos Hub (SAAS предложение)",
"description_persistence": "Возможность хранения записей - это начало всего. Вы можете выбрать один из наших вариантов",
"description2_persistence": ", или стороннего провайдера",
"select_persistence": "Выберите хранилище",
"kerberoshub_proxyurl": "Kerberos Hub Proxy URL",
"kerberoshub_description_proxyurl": "Конечная точка Proxy для загрузки записей.",
"kerberoshub_apiurl": "Kerberos Hub API URL",
"kerberoshub_description_apiurl": "Конечная точка API для загрузки записей.",
"kerberoshub_publickey": "Открытый ключ",
"kerberoshub_description_publickey": "Открытый ключ, присвоенный вашей учетной записи Kerberos Hub.",
"kerberoshub_privatekey": "Закрытый ключ",
"kerberoshub_description_privatekey": "Закрытый ключ, присвоенный вашей учетной записи Kerberos Hub.",
"kerberoshub_site": "Сайт",
"kerberoshub_description_site": "Идентификатор сайта, к которому принадлежат агенты Kerberos (Agent) в Kerberos Hub.",
"kerberoshub_region": "Регион",
"kerberoshub_description_region": "Регион, в котором хранятся наши записи.",
"kerberoshub_bucket": "Bucket",
"kerberoshub_description_bucket": "Bucket, в котором мы храним наши записи.",
"kerberoshub_username": "Имя пользователя/каталог (должно соответствовать имени пользователя в Kerberos Hub)",
"kerberoshub_description_username": "Имя пользователя вашей учетной записи Kerberos Hub.",
"kerberosvault_apiurl": "Kerberos Vault API URL",
"kerberosvault_description_apiurl": "The Kerberos Vault API",
"kerberosvault_provider": "Провайдер",
"kerberosvault_description_provider": "Провайдер, которому будут отправляться ваши записи.",
"kerberosvault_directory": "Каталог (должен совпадать с именем пользователя в Kerberos Hub)",
"kerberosvault_description_directory": "Подкаталог, в котором будут храниться записи у вашего провайдера.",
"kerberosvault_accesskey": "Ключ доступа",
"kerberosvault_description_accesskey": "Ключ доступа вашей учетной записи Kerberos Vault.",
"kerberosvault_secretkey": "Секретный ключ",
"kerberosvault_description_secretkey": "Секретный ключ учетной записи Kerberos Vault.",
"dropbox_directory": "Каталог",
"dropbox_description_directory": "Подкаталог, в котором будут храниться записи в вашем аккаунте Dropbox.",
"dropbox_accesstoken": "Токен доступа",
"dropbox_description_accesstoken": "Токен доступа вашего аккаунта/приложения Dropbox.",
"verify_connection": "Проверка соединения",
"remove_after_upload": "Как только записи будут загружены на какой-либо сервер, вы, возможно, захотите удалить их из локального агента Kerberos.",
"remove_after_upload_description": "Удаление записей после их успешной загрузки.",
"remove_after_upload_enabled": "Включено удаление при выгрузке"
}
}
}

View File

@@ -85,14 +85,23 @@
"advanced_configuration": "高级配置",
"description_advanced_configuration": "启用或禁用 Kerberos Agent 特定部分详细配置选项",
"offline_mode": "离线模式",
"description_offline_mode": "禁用所有传出流量"
"description_offline_mode": "禁用所有传出流量",
"encryption": "Encryption",
"description_encryption": "Enable encryption for all outgoing traffic. MQTT messages and/or recordings will be encrypted using AES-256. A private key is used for signing.",
"encryption_enabled": "Enable MQTT encryption",
"description_encryption_enabled": "Enable encryption for all MQTT messages.",
"encryption_recordings_enabled": "Enable recording encryption",
"description_encryption_recordings_enabled": "Enable encryption for all recordings.",
"encryption_fingerprint": "Fingerprint",
"encryption_privatekey": "Private key",
"encryption_symmetrickey": "Symmetric key"
},
"camera": {
"camera": "相机",
"description_camera": "需要相机设置才能连接到您选择的相机。",
"only_h264": "目前仅支持 H264 RTSP 流。",
"only_h264": "目前仅支持 H264/H265 RTSP 流。",
"rtsp_url": "RTSP 网址",
"rtsp_h264": "与摄像机的 H264 RTSP 连接。",
"rtsp_h264": "与摄像机的 H264/H265 RTSP 连接。",
"sub_rtsp_url": "子 RTSP 网址(用于直播)",
"sub_rtsp_h264": "与低分辨率相机的辅助 RTSP 连接。",
"onvif": "ONVIF",

View File

@@ -53,9 +53,9 @@ export const verifyOnvif = (config, onSuccess, onError) => {
}
},
(error) => {
const { message } = error;
const { data } = error;
if (onError) {
onError(message);
onError(data);
}
}
);

View File

@@ -92,7 +92,7 @@ export function doVerifyHub(config, onSuccess, onError) {
}
export function doVerifyOnvif(config, onSuccess, onError) {
const endpoint = API.post(`onvif/verify`, {
const endpoint = API.post(`camera/onvif/verify`, {
...config,
});
endpoint

View File

@@ -7,6 +7,9 @@ import './ImageCanvas.css';
class ImageCanvas extends React.Component {
componentDidMount() {
this.width = 0;
this.height = 0;
this.loadImage = this.loadImage.bind(this);
this.generateRandomTagsDescriptor =
this.generateRandomTagsDescriptor.bind(this);
@@ -55,14 +58,27 @@ class ImageCanvas extends React.Component {
const { image } = this.props;
this.loadImage(image, (img) => {
this.loadData(img);
if (this.width !== img.width || this.height !== img.height) {
this.width = img.width;
this.height = img.height;
this.loadData(img);
} else {
this.editor.addContentSource(img);
}
});
}
componentDidUpdate() {
const { image } = this.props;
this.loadImage(image, (img) => {
this.loadData(img);
if (this.width !== img.width || this.height !== img.height) {
this.width = img.width;
this.height = img.height;
this.loadData(img);
} else {
// alert('ok');
this.editor.addContentSource(img);
}
});
}

View File

@@ -20,9 +20,12 @@ const LanguageSelect = () => {
fr: { label: 'Francais', dir: 'ltr', active: false },
pl: { label: 'Polski', dir: 'ltr', active: false },
de: { label: 'Deutsch', dir: 'ltr', active: false },
it: { label: 'Italiano', dir: 'ltr', active: false },
pt: { label: 'Português', dir: 'ltr', active: false },
es: { label: 'Español', dir: 'ltr', active: false },
ja: { label: '日本', dir: 'rlt', active: false },
hi: { label: 'हिंदी', dir: 'ltr', active: false },
ru: { label: 'Русский', dir: 'ltr', active: false },
};
if (!languageMap[selected]) {

View File

@@ -9,9 +9,9 @@ const dev = {
ENV: 'dev',
// Comment the below lines, when using codespaces or other special DNS names (which you can't control)
HOSTNAME: hostname,
API_URL: `${protocol}//${hostname}:8080/api`,
URL: `${protocol}//${hostname}:8080`,
WS_URL: `${websocketprotocol}//${hostname}:8080/ws`,
API_URL: `${protocol}//${hostname}:80/api`,
URL: `${protocol}//${hostname}:80`,
WS_URL: `${websocketprotocol}//${hostname}:80/ws`,
MODE: window['env']['mode'],
// Uncomment, and comment the above lines, when using codespaces or other special DNS names (which you can't control)
// HOSTNAME: externalHost,

View File

@@ -14,7 +14,7 @@ i18n
escapeValue: false,
},
load: 'languageOnly',
whitelist: ['de', 'en', 'nl', 'fr', 'pl', 'es', 'pt', 'ja'],
whitelist: ['de', 'en', 'nl', 'fr', 'pl', 'es', 'pt', 'ja', 'ru'],
});
export default i18n;

View File

@@ -19,6 +19,8 @@ import {
} from '@kerberos-io/ui';
import { Link, withRouter } from 'react-router-dom';
import { connect } from 'react-redux';
import { interval } from 'rxjs';
import { send } from '@giantmachines/redux-websocket';
import ImageCanvas from '../../components/ImageCanvas/ImageCanvas';
import './Settings.scss';
import timezones from './timezones';
@@ -121,6 +123,7 @@ class Settings extends React.Component {
this.onUpdateToggle = this.onUpdateToggle.bind(this);
this.onUpdateNumberField = this.onUpdateNumberField.bind(this);
this.onUpdateTimeline = this.onUpdateTimeline.bind(this);
this.initialiseLiveview = this.initialiseLiveview.bind(this);
this.verifyPersistenceSettings = this.verifyPersistenceSettings.bind(this);
this.verifyHubSettings = this.verifyHubSettings.bind(this);
this.verifyCameraSettings = this.verifyCameraSettings.bind(this);
@@ -144,11 +147,18 @@ class Settings extends React.Component {
}));
this.calculateTimetable(config.timetable);
});
this.initialiseLiveview();
}
componentWillUnmount() {
document.removeEventListener('keydown', this.escFunction, false);
clearInterval(this.interval);
const { dispatchSend } = this.props;
const message = {
message_type: 'stop-sd',
};
dispatchSend(message);
}
onAddRegion(device, id, polygon) {
@@ -227,6 +237,24 @@ class Settings extends React.Component {
]);
}
initialiseLiveview() {
const message = {
message_type: 'stream-sd',
};
const { connected, dispatchSend } = this.props;
if (connected) {
dispatchSend(message);
}
const requestStreamInterval = interval(2000);
this.requestStreamSubscription = requestStreamInterval.subscribe(() => {
const { connected: isConnected } = this.props;
if (isConnected) {
dispatchSend(message);
}
});
}
calculateTimetable(timetable) {
this.timetable = timetable;
if (this.timetable) {
@@ -521,8 +549,8 @@ class Settings extends React.Component {
loadingHub,
} = this.state;
const { config: c, t } = this.props;
const { config, snapshot } = c;
const { config: c, t, images } = this.props;
const { config } = c;
const snapshotBase64 = 'data:image/png;base64,';
// Determine which section(s) to be shown, depending on the searching criteria.
@@ -729,7 +757,7 @@ class Settings extends React.Component {
/>
)}
{verifyOnvifError && (
<InfoBar type="alert" message={`${verifyOnvifErrorMessage}`} />
<InfoBar type="alert" message={verifyOnvifErrorMessage} />
)}
{loadingHub && (
@@ -810,6 +838,24 @@ class Settings extends React.Component {
this.onUpdateDropdown('', 'timezone', value[0], config)
}
/>
<br />
<hr />
<p>
{t('settings.overview.description_advanced_configuration')}
</p>
<div className="toggle-wrapper">
<Toggle
on={config.offline === 'true'}
disabled={false}
onClick={(event) =>
this.onUpdateToggle('', 'offline', event, config)
}
/>
<div>
<span>{t('settings.overview.offline_mode')}</span>
<p>{t('settings.overview.description_offline_mode')}</p>
</div>
</div>
</BlockBody>
<BlockFooter>
<Button
@@ -1142,9 +1188,9 @@ class Settings extends React.Component {
</BlockHeader>
<BlockBody>
<p>{t('settings.conditions.description_regionofinterest')}</p>
{config.region && (
{config.region && images && images.length > 0 && (
<ImageCanvas
image={snapshotBase64 + snapshot}
image={snapshotBase64 + images[0]}
polygons={config.region.polygon}
rendered={false}
onAddRegion={this.onAddRegion}
@@ -1239,25 +1285,95 @@ class Settings extends React.Component {
{showOverviewSection && (
<Block>
<BlockHeader>
<h4>{t('settings.overview.advanced_configuration')}</h4>
<h4>{t('settings.overview.encryption')}</h4>
</BlockHeader>
<BlockBody>
<p>
{t('settings.overview.description_advanced_configuration')}
</p>
<p>{t('settings.overview.description_encryption')}</p>
<div className="toggle-wrapper">
<Toggle
on={config.offline === 'true'}
on={config.encryption.enabled === 'true'}
disabled={false}
onClick={(event) =>
this.onUpdateToggle('', 'offline', event, config)
this.onUpdateToggle(
'encryption',
'enabled',
event,
config.encryption
)
}
/>
<div>
<span>{t('settings.overview.offline_mode')}</span>
<p>{t('settings.overview.description_offline_mode')}</p>
<span>{t('settings.overview.encryption_enabled')}</span>
<p>
{t('settings.overview.description_encryption_enabled')}
</p>
</div>
</div>
<div className="toggle-wrapper">
<Toggle
on={config.encryption.recordings === 'true'}
disabled={false}
onClick={(event) =>
this.onUpdateToggle(
'encryption',
'recordings',
event,
config.encryption
)
}
/>
<div>
<span>
{t('settings.overview.encryption_recordings_enabled')}
</span>
<p>
{t(
'settings.overview.description_encryption_recordings_enabled'
)}
</p>
</div>
</div>
<Input
noPadding
label={t('settings.overview.encryption_fingerprint')}
value={config.encryption.fingerprint}
onChange={(value) =>
this.onUpdateField(
'encryption',
'fingerprint',
value,
config.encryption
)
}
/>
<Input
noPadding
label={t('settings.overview.encryption_privatekey')}
value={config.encryption.private_key}
onChange={(value) =>
this.onUpdateField(
'encryption',
'private_key',
value,
config.encryption
)
}
/>
<Input
noPadding
label={t('settings.overview.encryption_symmetrickey')}
value={config.encryption.symmetric_key}
onChange={(value) =>
this.onUpdateField(
'encryption',
'symmetric_key',
value,
config.encryption
)
}
/>
</BlockBody>
<BlockFooter>
<Button
@@ -2291,6 +2407,8 @@ class Settings extends React.Component {
const mapStateToProps = (state /* , ownProps */) => ({
config: state.agent.config,
connected: state.wss.connected,
images: state.wss.images,
});
const mapDispatchToProps = (dispatch /* , ownProps */) => ({
@@ -2309,11 +2427,14 @@ const mapDispatchToProps = (dispatch /* , ownProps */) => ({
dispatchAddRegion: (id, polygon) => dispatch(addRegion(id, polygon)),
dispatchRemoveRegion: (id, polygon) => dispatch(removeRegion(id, polygon)),
dispatchUpdateRegion: (id, polygon) => dispatch(updateRegion(id, polygon)),
dispatchSend: (message) => dispatch(send(message)),
});
Settings.propTypes = {
t: PropTypes.func.isRequired,
connected: PropTypes.bool.isRequired,
config: PropTypes.objectOf(PropTypes.object).isRequired,
images: PropTypes.array.isRequired,
dispatchVerifyHub: PropTypes.func.isRequired,
dispatchVerifyPersistence: PropTypes.func.isRequired,
dispatchGetConfig: PropTypes.func.isRequired,
@@ -2324,6 +2445,7 @@ Settings.propTypes = {
dispatchRemoveRegion: PropTypes.func.isRequired,
dispatchVerifyCamera: PropTypes.func.isRequired,
dispatchVerifyOnvif: PropTypes.func.isRequired,
dispatchSend: PropTypes.func.isRequired,
};
export default withTranslation()(