Compare commits

...

376 Commits

Author SHA1 Message Date
Cédric Verstraeten
396cfe5d8b Merge pull request #191 from kerberos-io/feature/migrate-to--mp4ff
feature/Add MP4 video handling and update IPCamera configuration
2025-06-24 13:39:56 +02:00
Cédric Verstraeten
39fe640ccf Refactor logging in AddSampleToTrack method to use structured logging 2025-06-23 10:21:02 +00:00
Cédric Verstraeten
d389c9b0b6 Add logging for sample addition in MP4 track 2025-06-23 10:07:30 +00:00
Cédric Verstraeten
b149686db8 Remove Bento4 build steps and clean up Dockerfile structure 2025-06-23 09:57:04 +00:00
Cédric Verstraeten
c4358cbfad Fix typo in IPCamera struct: update VPSNALUs field JSON tag from "pps_nalus" to "vps_nalus" 2025-06-23 09:03:00 +00:00
Cédric Verstraeten
cfc5bd3dfe Remove unused audio stream retrieval in HandleRecordStream function 2025-06-23 07:58:39 +00:00
Cédric Verstraeten
c29c1b6a92 Merge branch 'master' into feature/migrate-to--mp4ff 2025-06-23 09:55:31 +02:00
Cédric Verstraeten
0f45a2a4b4 Merge branch 'feature/migrate-to--mp4ff' of github.com:kerberos-io/agent into feature/migrate-to--mp4ff 2025-06-23 09:54:41 +02:00
Cédric Verstraeten
92edcc13c0 Refactor OpenTelemetry tracing integration in RTSP client and components for improved context handling 2025-06-23 07:54:34 +00:00
cedricve
5392e2ba90 Update Dockerfile to remove incorrect source path and add Bento4 build process 2025-06-22 19:46:03 +00:00
cedricve
79e1f659c7 Update mongo-driver dependency from v1.17.4 to v1.17.3 to maintain compatibility 2025-06-21 20:13:38 +00:00
cedricve
bf35e5efb6 Implement OpenTelemetry tracing in the agent
- Added OpenTelemetry tracing support in main.go, including a new function startTracing to initialize the tracer with a configurable endpoint.
- Updated the environment attribute from "testing" to "develop" for better clarity in tracing.
- Integrated tracing into the RTSP connection process in gortsplib.go by creating a span for the Connect method.
- Enhanced the Bootstrap function in Kerberos.go to include tracing, marking the start and end of the bootstrap process.
- Introduced a new span in RunAgent to trace the execution flow and ensure proper span management.
2025-06-20 09:35:13 +00:00
Cédric Verstraeten
c50137f255 Comment out OpenTelemetry tracing initialization in main.go to simplify the codebase and remove unused functionality. 2025-06-16 10:30:02 +00:00
Cédric Verstraeten
f12da749b2 Remove OpenTelemetry tracing code from main.go and Kerberos.go files to simplify the codebase and eliminate unused dependencies. 2025-06-16 10:08:55 +00:00
Cédric Verstraeten
a166083423 Update machinery/src/packets/stream.go
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-06-16 10:20:43 +02:00
Cédric Verstraeten
b400d4e773 Refactor Dockerfile build commands to streamline Go build process and improve clarity 2025-06-16 06:42:08 +00:00
Cédric Verstraeten
120054d3e5 Add SampleRate and Channels fields to IPCamera configuration and update audio stream handling 2025-06-16 06:37:19 +00:00
cedricve
620117c31b Refactor WriteToTrack to use updated PacketTimestamp for video and audio samples, improving synchronization accuracy. 2025-06-07 22:12:15 +00:00
cedricve
4e371488c1 Remove unnecessary copy of mp4fragment in Dockerfile, streamlining the agent setup process. 2025-06-07 21:22:49 +00:00
cedricve
b154b56308 Refactor Dockerfile to remove CGO_ENABLED=0 from build command, simplifying the build process for the agent. 2025-06-07 21:17:25 +00:00
cedricve
6d92817237 Refactor HandleRecordStream to adjust maxRecordingPeriod calculation for improved timing accuracy. Simplify mp4 segment encoding logic to ensure it always attempts to encode the last segment, enhancing error handling. 2025-06-07 12:30:42 +00:00
cedricve
b8c1855830 Refactor HandleRecordStream to use milliseconds for timing calculations, improving accuracy in recording periods and motion detection logic. Update mp4 encoding to ensure segment encoding only occurs if a segment exists, preventing potential panics. 2025-06-07 11:53:03 +00:00
cedricve
a9f7ff4b72 Refactor HandleRecordStream to remove unused mp4.Movmuxer and streamline video sample handling with mp4Video, enhancing recording process and error logging. 2025-06-07 06:26:23 +00:00
Cédric Verstraeten
b3cd080e14 Refactor Dockerfile and main.go to enhance build process and streamline video handling 2025-06-06 15:14:45 +00:00
Cédric Verstraeten
bfde87f888 Refactor WriteToTrack to improve sample handling by using last processed audio and video samples, enhancing buffer duration calculation and streamlining packet processing. 2025-06-06 14:36:19 +00:00
Cédric Verstraeten
c4453bb8b3 Fix packet handling in WriteToTrack to ensure proper processing of next packets on timeout and empty data 2025-06-06 13:36:30 +00:00
Cédric Verstraeten
40f65a30b3 Clarify audio transcoding process in WriteToTrack with detailed comments on AAC to PCM_MULAW conversion 2025-06-06 13:33:28 +00:00
Cédric Verstraeten
5361de63e0 Refactor packet handling in WriteToTrack to improve buffer duration calculation and streamline packet reading 2025-06-06 13:23:09 +00:00
Cédric Verstraeten
3a8552d362 Enhance MP4 handling by updating track IDs in fragment creation, improving H264 and H265 NAL unit conversion, and adding support for HVC1 compatible brands in the ftyp box 2025-06-05 14:48:19 +00:00
Cédric Verstraeten
d3840103fc Add VPS NALUs support in IPCamera configuration and MP4 handling for improved video processing 2025-06-05 13:28:10 +00:00
Cédric Verstraeten
d12a9f0612 Refactor MP4 handling by simplifying Close method and adding last sample DTS tracking for better audio and video sample management 2025-06-05 10:59:44 +00:00
cedricve
c0d74f7e09 Remove placeholder comments from AddSampleToTrack and Close methods for cleaner code 2025-06-04 19:23:48 +00:00
cedricve
8ebea9e4c5 Refactor MP4 struct by removing unused video and audio fragment fields, and enhance track handling in Close method for better audio and subtitle track management 2025-06-04 19:03:58 +00:00
cedricve
89269caf92 Refactor AddSampleToTrack and SplitAACFrame methods to enhance audio sample handling and improve error logging 2025-06-04 18:36:00 +00:00
Cédric Verstraeten
0c83170f51 Fix AAC descriptor index in Close method to ensure correct audio track setup 2025-06-04 13:15:08 +00:00
Cédric Verstraeten
6081cb4be9 Update mp4.go 2025-06-04 14:39:44 +02:00
Cédric Verstraeten
ea1dbb3087 Refactor AddSampleToTrack method to improve AAC frame handling by splitting frames and updating duration calculations for audio samples 2025-06-04 09:49:29 +00:00
Cédric Verstraeten
0523208d36 Update mp4.go 2025-06-04 11:28:16 +02:00
Cédric Verstraeten
919f21b48b Refactor AddSampleToTrack method to create separate video and audio fragments, enhancing sample handling and improving error logging for AAC frames 2025-06-04 08:45:54 +00:00
cedricve
2c1c10a2ac Refactor AddSampleToTrack and Close methods to improve sample handling and track management for video and audio 2025-06-03 20:33:00 +00:00
cedricve
7e3320b252 Refactor AddSampleToTrack method to remove duration parameter and enhance fragment handling for video and audio tracks 2025-06-03 19:18:16 +00:00
Cédric Verstraeten
35ccac8b65 Refactor MP4 fragment handling in AddSampleToTrack method to separate video and audio fragments for improved track management 2025-06-03 13:29:36 +00:00
Cédric Verstraeten
dad8165d11 Enhance sample handling in AddSampleToTrack method to support multiple packets and improve error logging 2025-06-03 12:30:03 +00:00
Cédric Verstraeten
ba54188de2 Refactor video and audio track handling in MP4 structure to store track names and return track IDs for better management 2025-06-03 10:23:14 +00:00
cedricve
3b440c9905 Add audio and video codec detection in HandleRecordStream function 2025-06-03 06:27:25 +00:00
cedricve
42b98b7f20 Update mp4.go 2025-06-03 08:25:51 +02:00
cedricve
ba3312b57c Refactor AddSampleToTrack method to return error instead of panicking for better error handling 2025-06-03 05:55:23 +00:00
cedricve
223ba255e9 Fix signature handling in MP4 closing logic to ensure valid signatures are used for fingerprint 2025-06-02 17:45:05 +00:00
Cédric Verstraeten
a1df2be207 Implement signing feature with default private key configuration and update MP4 closing logic to include fingerprint signing 2025-06-02 16:02:06 +00:00
Cédric Verstraeten
d7f225ca73 Add signing configuration placeholder to the agent's config 2025-06-02 14:08:47 +00:00
Cédric Verstraeten
b3cfabb5df Update signing configuration to use private key for recording validation 2025-06-02 14:06:16 +00:00
Cédric Verstraeten
5310dd4550 Add signing configuration options to the agent 2025-06-02 13:50:48 +00:00
Cédric Verstraeten
cde7dbb58a Add configuration options for signing recordings and public key usage 2025-06-02 13:41:15 +00:00
Cédric Verstraeten
65e68231c7 Refactor MP4 handling in capture and video modules
- Updated the HandleRecordStream function to use TimeLegacy for packet timestamps instead of the previous Time conversion method.
- Modified the MP4 struct to replace InitSegment with a list of MediaSegments, allowing for better management of segments.
- Introduced StartTime to the MP4 struct to track the creation time of the MP4 file.
- Enhanced the Close method in the MP4 struct to properly handle segment indexing (SIDX) and ensure accurate duration calculations.
- Implemented helper functions to fill SIDX boxes and find segment data, improving the overall structure and readability of the code.
2025-06-02 12:27:22 +00:00
Cédric Verstraeten
5502555869 Integrate OpenTelemetry tracing in main and components, enhancing observability 2025-06-02 07:30:49 +00:00
cedricve
ad6e7e752f Refactor MP4 handling to remove commented-out track additions and enhance moov box management 2025-06-02 07:15:24 +00:00
cedricve
63af4660ef Refactor MP4 initialization and closing logic to improve segment handling and add custom UUID support 2025-06-01 20:07:36 +00:00
cedricve
24fc340001 Refactor MP4 initialization and sample addition logic to enhance duration handling and segment management 2025-05-30 19:06:56 +00:00
cedricve
78d786b69d Add custom UUID box and enhance MP4 file closing logic 2025-05-29 10:14:43 +00:00
cedricve
756aeaa0eb Refactor MP4 handling to improve sample addition and duration calculation 2025-05-28 18:36:34 +00:00
cedricve
055fb67d7a Update mp4.go 2025-05-26 21:59:23 +02:00
cedricve
bee522a6bf Refactor MP4 handling to improve sample addition and segment management 2025-05-26 06:00:17 +00:00
Cédric Verstraeten
3fbf59c622 Merge pull request #192 from kerberos-io/fix/do-not-add-aac-track
fix/add audio codec handling in HandleRecordStream function
2025-05-22 21:07:28 +02:00
cedricve
abd8b8b605 Add audio codec handling in HandleRecordStream function 2025-05-22 18:33:13 +00:00
cedricve
abdad47bf3 Add MP4 video handling and update IPCamera configuration
- Introduced a new video package with MP4 struct for video file handling.
- Updated IPCamera struct to include SPS and PPS NALUs.
- Enhanced stream handling in the capture process to utilize the new MP4 library.
- Added stream index management for better tracking of video and audio streams.
2025-05-22 05:53:33 +00:00
Cédric Verstraeten
d2c24edf5d Merge pull request #190 from kerberos-io/feature/update-workflow-do-not-push-to-latest
Update Docker build workflow to use input tag for image naming
2025-05-20 16:05:04 +02:00
Cédric Verstraeten
22f4a7f119 Update Docker build workflow to use input tag for image naming 2025-05-20 14:03:44 +00:00
Cédric Verstraeten
a25d3d32e4 Merge pull request #189 from kerberos-io/feature/allow-release-workflow-to-triggered-manually
feature/Enhance release workflow to include tag input for Docker image
2025-05-20 14:46:26 +02:00
Cédric Verstraeten
ed68c32e04 Enhance release workflow to include tag input for Docker image 2025-05-20 12:45:52 +00:00
Cédric Verstraeten
4114b3839a Merge pull request #187 from kerberos-io/upgrade/base-image
Update base image version in Dockerfile
2025-05-19 15:22:36 +02:00
Cédric Verstraeten
3f73c009fd Update Dockerfile
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-05-19 15:15:33 +02:00
Cédric Verstraeten
02fb70c76e Update base image version in Dockerfile 2025-05-19 14:52:28 +02:00
Cédric Verstraeten
aaddcb854d Merge pull request #185 from kerberos-io/feature/retry-windows-secondary-vault
Feature/retry windows secondary vault
2025-05-17 21:40:58 +02:00
cedricve
e73c7a6ecc Remove kstorageRetryPolicy from configuration 2025-05-17 19:37:07 +00:00
cedricve
1dc2202f37 Enhance logging for secondary Kerberos Vault upload process 2025-05-17 19:29:35 +00:00
cedricve
ac710ae1f5 Fix typo in Kerberos Vault max retries translation key 2025-05-17 19:16:27 +00:00
cedricve
f5ea82ff03 Add Kerberos Vault settings for max retries and timeout configuration 2025-05-17 19:14:02 +00:00
cedricve
ef52325240 Update Kerberos Vault configuration for max retries and timeout; adjust upload delay 2025-05-17 08:37:40 +00:00
cedricve
354855feb1 Refactor Kerberos Vault configuration for retry policy consistency 2025-05-17 08:23:32 +00:00
cedricve
c4cd25b588 Add Kerberos Vault configuration options and retry policy support 2025-05-17 08:21:28 +00:00
cedricve
dbb870229e Update config.json 2025-05-16 19:00:33 +02:00
cedricve
a66fe8c054 Merge branch 'master' into feature/retry-windows-secondary-vault 2025-05-16 19:00:13 +02:00
Cédric Verstraeten
2352431c79 Merge pull request #184 from kerberos-io/upgrade/gortsplib
upgrade/dependencies
2025-05-16 18:54:45 +02:00
cedricve
49bc168812 Refactor code structure for improved readability and maintainability 2025-05-16 15:53:40 +00:00
cedricve
98f1ebf20a Add retry policy for Kerberos Vault uploads and update configuration model 2025-05-16 15:50:59 +00:00
cedricve
65feb6d182 Add initial configuration file for agent settings 2025-05-15 12:20:04 +00:00
cedricve
58555d352f Update .gitignore and launch.json to reference .env.local instead of .env 2025-05-15 10:42:01 +00:00
Cédric Verstraeten
839a177cf0 Merge branch 'master' into feature/retry-windows-secondary-vault 2025-05-14 14:57:53 +02:00
Cédric Verstraeten
404517ec40 Merge pull request #183 from kerberos-io/cedricve-patch-1
Create .env
2025-05-14 14:56:46 +02:00
Cédric Verstraeten
035bd18bc2 Create .env 2025-05-14 14:56:31 +02:00
Cédric Verstraeten
8bf7a0d244 Update devcontainer.json 2025-05-14 14:53:41 +02:00
Cédric Verstraeten
607d8fd0d1 Merge pull request #182 from kerberos-io/feature/retry-windows-secondary-vault
Remove .env + config file, we will manually add as these are part of the .gitignore
2025-05-14 14:52:15 +02:00
Cédric Verstraeten
12807e289c remove .env + config file, we will manually add as these are part of the .gitignore 2025-05-14 14:36:16 +02:00
Cédric Verstraeten
3a984f1c73 Merge pull request #180 from kerberos-io/fix/merge-secondary-kerberos-vault-settings
Add support for secondary Kerberos Vault settings in configuration
2025-04-27 21:23:55 +02:00
cedricve
b84e34da06 Add support for secondary Kerberos Vault settings in configuration 2025-04-27 21:21:00 +02:00
Cédric Verstraeten
541d151570 Merge pull request #179 from kerberos-io/fix/omit-blank-kstorage
Make KStorage fields optional in JSON and BSON serialization
2025-04-27 20:53:18 +02:00
cedricve
4ad97e1286 Make KStorage fields optional in JSON and BSON serialization 2025-04-27 18:46:14 +00:00
Kilian
a80b375e89 Update README.md 2025-04-25 13:04:28 +02:00
Cédric Verstraeten
91cb390f6e Merge pull request #178 from kerberos-io/fix/secondary-vault-initialization
Fix/Add kstorage_secondary configuration field and initialize in environment vars
2025-04-24 11:59:39 +02:00
Cédric Verstraeten
90780dae28 Add kstorage_secondary configuration field and initialize in environment variable overrides 2025-04-24 09:56:53 +00:00
Cédric Verstraeten
ddb08e90e1 Merge pull request #176 from kerberos-io/feature/add-secondary-kerberos-vault
Add secondary KStorage for fallback (hybrid scenario)
2025-04-24 11:23:14 +02:00
Cédric Verstraeten
0d95026819 Add loading state for secondary persistence verification in Settings UI 2025-04-24 08:58:33 +00:00
Cédric Verstraeten
79db3a9dfe Add support for secondary Kerberos Vault configuration in environment variable overrides 2025-04-24 07:20:12 +00:00
cedricve
9f63ffd540 Add secondary persistence verification and UI integration 2025-04-23 20:30:11 +00:00
cedricve
9c7116a462 Add secondary persistence verification and UI integration 2025-04-23 15:24:24 +00:00
cedricve
dd9b4d43ac Update development API URLs to use port 8080 2025-04-23 16:19:56 +02:00
Cédric Verstraeten
aa63eca24c Add persistence configuration inputs for Kerberos Vault in Settings 2025-04-23 12:46:43 +00:00
Cédric Verstraeten
6df97171d9 Merge pull request #177 from kerberos-io/fix/increase-channel-size-for-audio-motion-hdhandshake
Fix/ Increase channel buffer sizes for communication handling
2025-04-23 13:24:52 +02:00
Cédric Verstraeten
56f7d69b3d Increase channel buffer sizes for communication handling 2025-04-23 11:05:16 +00:00
Cédric Verstraeten
3e2b29284e Add secondary KStorage configuration to the Config struct 2025-04-18 12:11:37 +00:00
Cédric Verstraeten
18ceca7510 Merge pull request #175 from kerberos-io/fix/remote-region-not-properly-calculated
Fix/Remote config / region not properly calculated
2025-04-18 14:06:37 +02:00
Cédric Verstraeten
5a08d1f3de Update main.go 2025-04-18 14:03:23 +02:00
Cédric Verstraeten
18af6db00c Update base image in Dockerfile to version af04230 2025-04-15 13:17:07 +00:00
Cédric Verstraeten
6d170c8dc0 change to version 1.24 + change workflow name 2025-04-15 15:00:51 +02:00
Cédric Verstraeten
9c4c3c654d Update main.go 2025-04-15 14:23:56 +02:00
Cédric Verstraeten
6952e387f4 Merge pull request #172 from kerberos-io/improvement/cleanup-and-refactors
Enhancement / Cleanup and refactoring of documentation
2025-04-15 14:15:57 +02:00
Cédric Verstraeten
66c9ae5c27 Merge pull request #174 from kerberos-io/feature/webrtc-handle-nacks
Feature / enable interceptors for NACK and retransmission of packets
2025-04-15 14:15:19 +02:00
Cédric Verstraeten
0fb7601dcb Update main.go 2025-04-15 13:49:18 +02:00
Cédric Verstraeten
07c6e680d1 Create default .env 2025-04-14 13:15:18 +02:00
cedricve
b972bc3040 Fix: Convert audio type to mpeg4audio.ObjectType in WriteMPEG4Audio function
Updated the WriteMPEG4Audio function to convert the audio type from forma.Config.Type to mpeg4audio.ObjectType. This change ensures that the correct object type is used when creating ADTSPacket instances for MPEG-4 audio.
2025-04-13 17:28:41 +00:00
cedricve
969d42dbca Remove Travis CI configuration and build script 2025-04-13 07:54:20 +00:00
Cédric Verstraeten
6680df9382 Merge pull request #171 from kerberos-io/feature/improve-dev-container
Improvement / Refactor Dockerfile and devcontainer configuration;
2025-04-13 09:52:20 +02:00
cedricve
8877157db5 Refactor Dockerfile and devcontainer configuration; add FFmpeg and Node.js installation 2025-04-13 07:46:06 +00:00
Cédric Verstraeten
ac814dc357 Merge pull request #168 from thanhtantran/master
add Vietnamese locales
2025-04-11 21:24:14 +02:00
Orange Pi Vietnam
4fcb12c3a3 Update translation.json
remove duplicate
2025-04-08 22:31:25 +07:00
Tony Tran
7bcc30f4b7 add Vietnamese locales 2025-02-27 15:06:41 +00:00
Cédric Verstraeten
481f917fcf Merge pull request #166 from kerberos-io/fix/candidategather-extended-buffer
Fix ice candidate gather and extended candidate buffer
2025-02-11 21:56:49 +01:00
Cedric Verstraeten
700a32e4c8 Update main.go 2025-02-11 21:51:14 +01:00
Cedric Verstraeten
b5a72d904e do allow more candidates even after connection state, remove sdpmid + remove sleep and unused code. 2025-02-11 19:58:36 +01:00
Cedric Verstraeten
cf3e491462 upgrade bento4 2025-02-11 12:12:04 +01:00
Cedric Verstraeten
6068705c07 Merge branch 'master' of https://github.com/kerberos-io/agent 2025-02-09 19:28:03 +01:00
Cedric Verstraeten
37beaa64d7 delay sending response answer 2025-02-09 19:28:01 +01:00
Cédric Verstraeten
8c5b03487b Merge pull request #165 from kerberos-io/fix/iceconnection-state-event
Correct peerconnection states + proper cleanup peerconnection
2025-02-09 11:50:35 +01:00
Cedric Verstraeten
360ae0c0db correct peerconnection states 2025-02-09 11:01:54 +01:00
Cédric Verstraeten
6aad8b7b35 Merge pull request #164 from kerberos-io/fix/webrtc-packettimestamp
Fix / WebRTC skip AAC audio + introduce packet timestamps
2025-02-06 10:38:40 +01:00
Cedric Verstraeten
9ce037fdc0 Update main.go 2025-02-06 10:35:36 +01:00
Cedric Verstraeten
0eb77ccd16 Update main.go 2025-02-06 10:15:47 +01:00
Cedric Verstraeten
fb876bd216 Update main.go 2025-02-06 08:32:30 +01:00
Cédric Verstraeten
865aec88fc Merge pull request #163 from kerberos-io/fix/webrtc-sample-timing
Fix / WebRTC sample timing
2025-02-06 08:32:04 +01:00
Cedric Verstraeten
9792bdf494 Update main.go 2025-02-06 08:28:17 +01:00
Cedric Verstraeten
d836e89e7f upgrade to v3.3.3 2025-02-05 20:55:08 +01:00
Cédric Verstraeten
53a52b3594 Merge pull request #162 from kerberos-io/fix/webrtc-sample-duration
fix / revert to old webrtc sample format (duration instead of packettimestamp)
2025-02-05 20:54:18 +01:00
Cedric Verstraeten
ba6ce25b21 revert to old webrtc sample format (duration instead of packettimestamp) 2025-02-05 20:50:57 +01:00
Cedric Verstraeten
8c9e18475f Update README.md 2025-01-31 16:35:11 +01:00
Cédric Verstraeten
4548d5328b Merge pull request #158 from kerberos-io/fix/force-mqtt-tos-level-2
Enabled TOS 2 for MQTT to ensure higher quality
2025-01-26 20:27:32 +01:00
Cedric Verstraeten
da870fe890 undo file 2025-01-26 20:23:14 +01:00
Cedric Verstraeten
66b660e688 enabled TOS 2 2025-01-26 20:17:04 +01:00
Cédric Verstraeten
08f8ca78d6 Merge pull request #157 from kerberos-io/upgrade/3.3.1
Upgrade to 3.3.1
2025-01-24 13:43:53 +01:00
Cedric Verstraeten
1e61e99005 Update main.go 2025-01-23 16:56:00 +01:00
Cédric Verstraeten
c272e1ab5c Merge pull request #155 from kerberos-io/upgrade/onvif-stable
Stable release onvif v1.0.0
2025-01-19 11:03:30 +01:00
Cedric Verstraeten
5cff11c0af upgrade onvif v1.0.0 2025-01-19 10:50:37 +01:00
Cédric Verstraeten
28b213779f Merge pull request #154 from kerberos-io/fix/memory-leak-onvif
Memory leak on SendSoap ONVIF library
2025-01-16 21:55:16 +01:00
Cedric Verstraeten
666ff202ad update go.sum 2025-01-16 21:49:27 +01:00
Cédric Verstraeten
9cb3c9753a Merge pull request #153 from kerberos-io/feature/global-decoder
Initiate decoders globally
2025-01-16 21:47:26 +01:00
Cedric Verstraeten
c4577e94b1 improve closing of responses 2025-01-16 21:46:19 +01:00
Cedric Verstraeten
9756183d3b upgrade onvif dependency 2025-01-16 21:46:02 +01:00
Cedric Verstraeten
83c65fe3d8 Merge branch 'master' into feature/global-decoder 2025-01-16 08:05:55 +01:00
Cedric Verstraeten
e6717c87cd update secrets 2025-01-16 08:05:46 +01:00
Cedric Verstraeten
5a3c1d6c9d Create pr-description.yaml 2025-01-16 08:05:05 +01:00
Cedric Verstraeten
81045ea955 Update gortsplib.go 2025-01-15 21:51:23 +01:00
Cedric Verstraeten
9f9fe3bd37 Update gortsplib.go 2025-01-15 21:48:36 +01:00
Cedric Verstraeten
84f7f844c9 Update Server.go 2025-01-15 16:53:35 +01:00
Cédric Verstraeten
4fde419db9 Merge pull request #151 from kerberos-io/fix/pullpoint-crash
Verify if device is nil, if so do not proceed (avoic panic)
2025-01-06 08:50:00 +01:00
Cédric Verstraeten
78cad6cf06 Merge pull request #152 from kerberos-io/feature/upgrade-dependencies
Upgrade dependencies
2025-01-06 08:49:51 +01:00
Cedric Verstraeten
4763e5a92e Update Dockerfile 2025-01-04 19:53:18 +01:00
Cedric Verstraeten
50939ee4ce upgrade dependcies 2025-01-04 19:49:02 +01:00
Cédric Verstraeten
884bc2acc1 Update gortsplib.go 2025-01-04 13:21:26 +01:00
Cédric Verstraeten
11fd041fa9 Update gortsplib.go 2025-01-04 13:04:02 +01:00
Cedric Verstraeten
a6d5c2b614 Verify if device is nil, if so do not proceed (avoic panic) 2025-01-03 23:08:39 +01:00
Cédric Verstraeten
9e3d705c6f Merge pull request #150 from kerberos-io/fix/align-pts2-webrtc
Fix/align pts2 webrtc
2025-01-02 17:20:59 +01:00
Cedric Verstraeten
1004731903 increase gop size 2025-01-02 17:18:06 +01:00
Cedric Verstraeten
9f2ec91688 Update main.go 2025-01-02 16:56:58 +01:00
Cedric Verstraeten
185135ed94 add legacy timing for MP4 2025-01-02 16:55:58 +01:00
Cedric Verstraeten
27e7d98c68 align with PTS2 2025-01-02 16:40:24 +01:00
Cedric Verstraeten
79f56771e3 align with pts2 2025-01-02 16:34:22 +01:00
Cedric Verstraeten
a7839147d6 Update main.go 2024-10-23 22:28:42 +02:00
Cedric Verstraeten
834d82d532 upgrade to webrtc v4, keep writing to track 2024-10-23 22:26:34 +02:00
Cedric Verstraeten
989f2f5943 commit new dependencies 2024-10-23 20:38:18 +02:00
Cedric Verstraeten
3af1df5b19 set realtime processing to false 2024-10-23 16:08:29 +02:00
Cedric Verstraeten
acf06e6e63 fix database client #2 2024-10-21 20:44:31 +02:00
Cedric Verstraeten
3f43e15cc2 fix database client 2024-10-21 20:41:31 +02:00
Cedric Verstraeten
c14683ec0d update database client 2024-10-18 15:48:04 +02:00
Cédric Verstraeten
213aaa5c15 update agent to 3.2.3 2024-09-14 19:34:19 +02:00
Cedric Verstraeten
9fb00c32d5 hotfix: revert webrtc version (stream broken) 2024-08-27 23:47:58 +02:00
Cedric Verstraeten
57ec08066c upgrade dependencies 2024-08-27 12:43:30 +02:00
Cedric Verstraeten
e0c6375261 IO fix: workaround for ONVIF event system 2024-08-25 20:27:46 +02:00
Cedric Verstraeten
79205abe29 keep the release notes 2024-08-21 10:42:47 +02:00
Cedric Verstraeten
24326558d0 only run release build on creation 2024-08-21 10:30:45 +02:00
Cedric Verstraeten
3f981c0f2f Update docker.yml 2024-08-21 10:26:21 +02:00
Cedric Verstraeten
b6eb7b8317 do not create a tag as github will do it 2024-08-21 10:20:20 +02:00
Cedric Verstraeten
4267ae6305 Update docker.yml 2024-08-21 10:18:33 +02:00
Cedric Verstraeten
0cb40bd93a Update docker.yml 2024-08-21 10:18:14 +02:00
Cedric Verstraeten
d2a8890a43 refactory github actions 2024-08-21 10:13:18 +02:00
Cedric Verstraeten
e5a5a5326b Update docker.yml 2024-08-21 10:10:35 +02:00
Cedric Verstraeten
61febd55c8 Update docker.yml 2024-08-21 10:09:15 +02:00
Cedric Verstraeten
3eac752654 Update docker.yml 2024-08-21 10:08:12 +02:00
Cedric Verstraeten
df4f1863fc use different release approach 2024-08-21 10:06:03 +02:00
Cedric Verstraeten
acee2784d3 improvement for IO's: detection for avigilon and axis cameras 2024-08-21 10:03:01 +02:00
Cedric Verstraeten
8ecb2f94a9 reference deployment guide on top of readme 2024-08-17 07:46:06 +02:00
Cedric Verstraeten
8657baf641 add architecture and reference deployments repo 2024-08-17 07:41:01 +02:00
Cedric Verstraeten
13d1948c9f Revert "test: add pkttimestamp and timestamp to samples to improve WebRTC streaming"
This reverts commit b067758915.
2024-08-14 14:05:22 +02:00
Cédric Verstraeten
8e8d51b719 Update README.md - add slack link 2024-08-12 22:53:45 +02:00
Cedric Verstraeten
ca2413363e [release] v3.1.9 2024-08-04 10:19:43 +02:00
Cedric Verstraeten
b067758915 test: add pkttimestamp and timestamp to samples to improve WebRTC streaming 2024-08-04 10:15:01 +02:00
Cédric Verstraeten
b2b8485b28 add networks 2024-07-24 12:35:09 +02:00
Kilian
c69d635431 Update docker-compose.yaml
Update example docker compose file
2024-07-05 15:50:30 +02:00
Cedric Verstraeten
a305ca36ce move vslaunch to top level and add react launcher 2024-07-05 13:35:33 +02:00
Cedric Verstraeten
a6a97b09f0 update devcontainer 2024-07-05 12:58:53 +02:00
Cédric Verstraeten
4d17a15633 Merge pull request #143 from KilianBoute/patch-1
Update README.md
2024-07-04 12:33:06 +02:00
Cédric Verstraeten
5fdb4b712e Merge pull request #142 from ghosty2004/master
Add Romanian language
2024-07-04 12:21:41 +02:00
Kilian
3d39251ac6 Update README.md 2024-07-04 11:42:05 +02:00
Cedric Verstraeten
9e59cd1596 add support for opus + update dependencies ho mod 2024-06-30 19:44:13 +02:00
ghosty2004
0ada943699 Add Romanian language 2024-06-26 21:04:02 +03:00
Cedric Verstraeten
ecadf7a4db add realtime processing endpoint 2024-06-11 22:47:01 +02:00
Cedric Verstraeten
413ed12fe2 deprecate older version 2024-05-05 22:33:38 +02:00
Cedric Verstraeten
6195fa5b9c upgrade go version to 1.22.2 2024-05-05 22:31:35 +02:00
Cedric Verstraeten
d31524ae52 upgrade go1.22.2 + webrtc/sdp libraries 2024-05-05 22:24:09 +02:00
Cédric Verstraeten
472a40a5f6 add extra space to run command 2024-04-06 11:50:11 +02:00
Cedric Verstraeten
fb9de04002 update dependencies + retry for ONVIF authentication 2024-03-17 11:07:15 +01:00
Cédric Verstraeten
3f29d1c46f fix wrong docker command 2024-01-30 20:47:13 +01:00
Cedric Verstraeten
b67a72ba9a [release] v3.1.8 2024-01-30 13:26:44 +01:00
Cedric Verstraeten
8fc9bc264d feature: add camera friendly name to UI 2024-01-30 11:21:58 +01:00
Cedric Verstraeten
b2589f498d hot-fix: embed friendly name in recording when set 2024-01-30 10:56:31 +01:00
Cedric Verstraeten
b1ff5134f2 feature: add double encryption
we are now encrypting to Kerberos Hub by default, secondary encryption can be added through bring your own encryption keys.

all encryption can be turned on/off if required
2024-01-17 20:53:42 +01:00
Cedric Verstraeten
3551d02d50 feature: add ability to force TURN server 2024-01-17 09:44:24 +01:00
Cedric Verstraeten
4c413012a4 [release] v3.1.7 2024-01-16 13:02:41 +01:00
Cedric Verstraeten
74ea2f6cdd hot-fix: make sure webrtc candidates are assigned to the correct session 2024-01-16 12:55:31 +01:00
Cedric Verstraeten
2a7d9b62d4 warning: printing the work sub url 2024-01-16 10:49:40 +01:00
Cedric Verstraeten
21d81b94dd [release] v3.1.6 2024-01-16 09:47:07 +01:00
Cedric Verstraeten
091662ff26 hot-fix: support avigilon backchannel 2024-01-16 09:39:34 +01:00
Cedric Verstraeten
803e8f55ef correct webrtc audio buffer duration 2024-01-14 21:37:58 +01:00
Cedric Verstraeten
14d38ecf08 [release] v3.1.5 2024-01-12 15:28:48 +01:00
Cedric Verstraeten
34d945055b Update main.go 2024-01-12 11:49:44 +01:00
Cedric Verstraeten
8c44da8233 hide passwords in ui + skip empty decode frames 2024-01-12 11:47:08 +01:00
Cédric Verstraeten
a8b79947ef Update README.md 2024-01-12 09:53:38 +01:00
Cedric Verstraeten
7c653f809d upgrqde dependencies + move file (decap) 2024-01-11 23:05:46 +01:00
Cedric Verstraeten
49f1603f40 align more blocking methods 2024-01-11 22:43:16 +01:00
Cedric Verstraeten
b4369ea932 improve non-blocking approve for agents tend to restart for some strange reason 2024-01-11 22:35:56 +01:00
Cedric Verstraeten
83ba7baa4b [release] v3.1.4
- hot-fix: preserve width and height of both main and sub stream
2024-01-10 17:06:49 +01:00
Cedric Verstraeten
9339ae30fd [release] v3.1.3 2024-01-10 16:30:20 +01:00
Cedric Verstraeten
c18f2bd445 remove file logger 2024-01-10 16:29:37 +01:00
Cedric Verstraeten
319876bbb0 hot-fix: onvif pull message might be empty 2024-01-10 16:28:40 +01:00
Cedric Verstraeten
442ba97c61 [release] v3.1.2
hot-fix: for missing SPS and PPS from opening codec.
2024-01-09 13:13:42 +01:00
Cedric Verstraeten
00e0b0b547 hot fix: capture SSP and PPS in a later decode, it might not be provided at the initialization, and keep it up to date. 2024-01-09 12:07:32 +01:00
Cedric Verstraeten
145f478249 go mod - upgrade dependencies 2024-01-08 13:08:33 +01:00
Cedric Verstraeten
aac2150a3a [release] v3.1.1 2024-01-07 22:14:44 +01:00
Cedric Verstraeten
9b713637b9 change version number of ui 2024-01-07 21:44:32 +01:00
Cedric Verstraeten
699660d472 only make release when putting [release] 2024-01-07 21:41:32 +01:00
Cedric Verstraeten
751aa17534 feature: make hub encryption configurable + only send heartbeat to vault when credentials are set 2024-01-07 21:30:57 +01:00
Cedric Verstraeten
2681bd2fe3 hot fix: keep track of main and sub stream separately (one of them might block) 2024-01-07 20:20:51 +01:00
Cedric Verstraeten
93adb3dabc different order in action 2024-01-07 08:29:53 +01:00
Cedric Verstraeten
0e15e58a88 try once more different format 2024-01-07 08:26:34 +01:00
Cedric Verstraeten
ef2ea999df only run release to docker when containing [release] 2024-01-07 08:22:24 +01:00
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
Chaitanya110703
6608018f86 doc(README): remove typo 2023-10-24 21:25:45 +05:30
Cedric Verstraeten
d2dd3dfa62 add outputconfiguration + change endpoint 2023-06-21 15:55:51 +02:00
109 changed files with 14396 additions and 3793 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,26 @@
FROM kerberos/devcontainer:b2bc659
LABEL AUTHOR=Kerberos.io
FROM mcr.microsoft.com/devcontainers/go:1.24-bookworm
# Install node environment
RUN apt-get update && \
apt-get install -y --no-install-recommends \
nodejs \
npm \
&& rm -rf /var/lib/apt/lists/*
# Install ffmpeg
RUN apt-get update && \
apt-get install -y --no-install-recommends \
ffmpeg \
libavcodec-extra \
libavutil-dev \
libavformat-dev \
libavfilter-dev \
libavdevice-dev \
libswscale-dev \
libswresample-dev \
&& rm -rf /var/lib/apt/lists/*
USER vscode
# Install go swagger
RUN go install github.com/swaggo/swag/cmd/swag@latest

View File

@@ -1,33 +1,24 @@
// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at:
// https://github.com/microsoft/vscode-dev-containers/tree/v0.245.2/containers/docker-existing-dockerfile
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
// README at: https://github.com/devcontainers/templates/tree/main/src/python
{
"name": "A Dockerfile containing FFmpeg, OpenCV, Go and Yarn",
// Sets the run context to one level up instead of the .devcontainer folder.
"context": "..",
// Update the 'dockerFile' property if you aren't using the standard 'Dockerfile' filename.
"dockerFile": "./Dockerfile",
// Use 'forwardPorts' to make a list of ports inside the container available locally.
"forwardPorts": [
3000,
80
"name": "go:1.24-bookworm",
"runArgs": [
"--name=agent",
"--network=host"
],
// Uncomment the next line to run commands after the container is created - for example installing curl.
"postCreateCommand": "cd ui && yarn install && yarn build && cd ../machinery && go mod download",
"features": {
"ghcr.io/devcontainers-contrib/features/ansible:1": {}
},
"dockerFile": "Dockerfile",
"customizations": {
"vscode": {
"extensions": [
"ms-kubernetes-tools.vscode-kubernetes-tools",
"GitHub.copilot"
"GitHub.copilot",
"ms-azuretools.vscode-docker",
"mongodb.mongodb-vscode"
]
}
},
// Uncomment when using a ptrace-based debugger like C++, Go, and Rust
// "runArgs": [ "--cap-add=SYS_PTRACE", "--security-opt", "seccomp=unconfined" ],
// Uncomment to use the Docker CLI from inside the container. See https://aka.ms/vscode-remote/samples/docker-from-docker.
// "mounts": [ "source=/var/run/docker.sock,target=/var/run/docker.sock,type=bind" ],
// Uncomment to connect as a non-root user if you've added one. See https://aka.ms/vscode-remote/containers/non-root.
// "remoteUser": "vscode"
"forwardPorts": [
3000,
8080
],
"postCreateCommand": "cd ui && yarn install && yarn build && cd ../machinery && go mod download"
}

View File

@@ -2,7 +2,7 @@ name: Docker development build
on:
push:
branches: [ develop ]
branches: [develop]
jobs:
build-amd64:
@@ -11,25 +11,25 @@ jobs:
matrix:
architecture: [amd64]
steps:
- name: Login to DockerHub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Checkout
uses: actions/checkout@v3
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Available platforms
run: echo ${{ steps.buildx.outputs.platforms }}
- name: Run Buildx
run: docker buildx build --platform linux/${{matrix.architecture}} -t kerberos/agent-dev: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-dev:$(echo $GITHUB_SHA | cut -c1-7) kerberos/agent-dev:arch-$(echo ${{matrix.architecture}} | tr / -)-$(echo $GITHUB_SHA | cut -c1-7)
- name: Create new and append to latest manifest
run: docker buildx imagetools create -t kerberos/agent-dev:latest kerberos/agent-dev:arch-$(echo ${{matrix.architecture}} | tr / -)-$(echo $GITHUB_SHA | cut -c1-7)
- name: Login to DockerHub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Checkout
uses: actions/checkout@v3
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Available platforms
run: echo ${{ steps.buildx.outputs.platforms }}
- name: Run Buildx
run: docker buildx build --platform linux/${{matrix.architecture}} -t kerberos/agent-dev: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-dev:$(echo $GITHUB_SHA | cut -c1-7) kerberos/agent-dev:arch-$(echo ${{matrix.architecture}} | tr / -)-$(echo $GITHUB_SHA | cut -c1-7)
- name: Create new and append to latest manifest
run: docker buildx imagetools create -t kerberos/agent-dev:latest kerberos/agent-dev:arch-$(echo ${{matrix.architecture}} | tr / -)-$(echo $GITHUB_SHA | cut -c1-7)
build-other:
runs-on: ubuntu-latest
strategy:
@@ -37,22 +37,22 @@ jobs:
#architecture: [arm64, arm/v7, arm/v6]
architecture: [arm64, arm/v7]
steps:
- name: Login to DockerHub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Checkout
uses: actions/checkout@v3
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Available platforms
run: echo ${{ steps.buildx.outputs.platforms }}
- name: Run Buildx
run: docker buildx build --platform linux/${{matrix.architecture}} -t kerberos/agent-dev: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-dev:$(echo $GITHUB_SHA | cut -c1-7) kerberos/agent-dev:arch-$(echo ${{matrix.architecture}} | tr / -)-$(echo $GITHUB_SHA | cut -c1-7)
- name: Create new and append to manifest latest
run: docker buildx imagetools create --append -t kerberos/agent-dev:latest kerberos/agent-dev:arch-$(echo ${{matrix.architecture}} | tr / -)-$(echo $GITHUB_SHA | cut -c1-7)
- name: Login to DockerHub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Checkout
uses: actions/checkout@v3
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Available platforms
run: echo ${{ steps.buildx.outputs.platforms }}
- name: Run Buildx
run: docker buildx build --platform linux/${{matrix.architecture}} -t kerberos/agent-dev: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-dev:$(echo $GITHUB_SHA | cut -c1-7) kerberos/agent-dev:arch-$(echo ${{matrix.architecture}} | tr / -)-$(echo $GITHUB_SHA | cut -c1-7)
- name: Create new and append to manifest latest
run: docker buildx imagetools create --append -t kerberos/agent-dev:latest kerberos/agent-dev:arch-$(echo ${{matrix.architecture}} | tr / -)-$(echo $GITHUB_SHA | cut -c1-7)

View File

@@ -7,18 +7,18 @@ on:
jobs:
build-amd64:
runs-on: ubuntu-latest
strategy:
matrix:
architecture: [amd64]
steps:
runs-on: ubuntu-latest
strategy:
matrix:
architecture: [amd64]
steps:
- name: Login to DockerHub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- 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,29 +26,29 @@ 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:
matrix:
architecture: [arm64, arm/v7, arm/v6]
steps:
- name: Login to DockerHub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Checkout
uses: actions/checkout@v3
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- 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 .
- 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)
- name: Login to DockerHub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Checkout
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
uses: docker/setup-buildx-action@v2
- name: Available platforms
run: echo ${{ steps.buildx.outputs.platforms }}
- name: Run Buildx
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: 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

@@ -1,12 +1,17 @@
name: Docker master build
name: Create a new release
on:
push:
branches: [ master ]
release:
types: [created]
workflow_dispatch:
inputs:
tag:
description: "Tag for the Docker image"
required: true
default: "test"
env:
REPO: kerberos/agent
jobs:
build-amd64:
runs-on: ubuntu-latest
@@ -19,8 +24,8 @@ jobs:
- name: Login to DockerHub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Checkout
uses: actions/checkout@v3
- uses: benjlevesque/short-sha@v2.1
@@ -34,26 +39,25 @@ jobs:
- name: Available platforms
run: echo ${{ steps.buildx.outputs.platforms }}
- name: Run Buildx
run: docker buildx build --platform linux/$(echo ${{matrix.architecture}} | tr - /) -t $REPO-arch:arch-${{matrix.architecture}}-${{steps.short-sha.outputs.sha}} --push .
run: docker buildx build --platform linux/$(echo ${{matrix.architecture}} | tr - /) -t $REPO-arch:arch-${{matrix.architecture}}-${{github.event.inputs.tag || github.ref_name}} --push .
- name: Create new and append to manifest
run: docker buildx imagetools create -t $REPO:${{ steps.short-sha.outputs.sha }} $REPO-arch:arch-${{matrix.architecture}}-${{steps.short-sha.outputs.sha}}
run: docker buildx imagetools create -t $REPO:${{ github.event.inputs.tag || github.ref_name }} $REPO-arch:arch-${{matrix.architecture}}-${{github.event.inputs.tag || github.ref_name}}
- name: Create new and append to manifest latest
run: docker buildx imagetools create -t $REPO:latest $REPO-arch:arch-${{matrix.architecture}}-${{steps.short-sha.outputs.sha}}
run: docker buildx imagetools create -t $REPO:latest $REPO-arch:arch-${{matrix.architecture}}-${{github.event.inputs.tag || github.ref_name}}
if: github.event.inputs.tag == 'test'
- name: Run Buildx with output
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 .
run: docker buildx build --platform linux/$(echo ${{matrix.architecture}} | tr - /) -t $REPO-arch:arch-$(echo ${{matrix.architecture}} | tr / -)-${{github.event.inputs.tag || github.ref_name}} --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 }}
message: "Release ${{ steps.short-sha.outputs.sha }}"
- name: Create a release
- name: Create a release
uses: ncipollo/release-action@v1
with:
latest: true
name: ${{ steps.short-sha.outputs.sha }}
tag: ${{ steps.short-sha.outputs.sha }}
allowUpdates: true
name: ${{ github.event.inputs.tag || github.ref_name }}
tag: ${{ github.event.inputs.tag || github.ref_name }}
generateReleaseNotes: false
omitBodyDuringUpdate: true
artifacts: "agent-${{matrix.architecture}}.tar"
# Taken from GoReleaser's own release workflow.
# The available Snapcraft Action has some bugs described in the issue below.
@@ -65,7 +69,7 @@ jobs:
# mkdir -p $HOME/.cache/snapcraft/download
# mkdir -p $HOME/.cache/snapcraft/stage-packages
#- name: Use Snapcraft
# run: tar -xf agent-${{matrix.architecture}}.tar && snapcraft
# run: tar -xf agent-${{matrix.architecture}}.tar && snapcraft
build-other:
runs-on: ubuntu-latest
permissions:
@@ -76,39 +80,41 @@ jobs:
architecture: [arm64, arm-v7, arm-v6]
#architecture: [arm64, arm-v7]
steps:
- name: Login to DockerHub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Checkout
uses: actions/checkout@v3
- uses: benjlevesque/short-sha@v2.1
id: short-sha
with:
length: 7
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Available platforms
run: echo ${{ steps.buildx.outputs.platforms }}
- name: Run Buildx
run: docker buildx build --platform linux/$(echo ${{matrix.architecture}} | tr - /) -t $REPO-arch:arch-${{matrix.architecture}}-${{steps.short-sha.outputs.sha}} --push .
- name: Create new and append to manifest
run: docker buildx imagetools create --append -t $REPO:${{ steps.short-sha.outputs.sha }} $REPO-arch:arch-${{matrix.architecture}}-${{steps.short-sha.outputs.sha}}
- name: Create new and append to manifest latest
run: docker buildx imagetools create --append -t $REPO:latest $REPO-arch:arch-${{matrix.architecture}}-${{steps.short-sha.outputs.sha}}
- name: Run Buildx with output
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
- name: Create a release
uses: ncipollo/release-action@v1
with:
latest: true
allowUpdates: true
name: ${{ steps.short-sha.outputs.sha }}
tag: ${{ steps.short-sha.outputs.sha }}
artifacts: "agent-${{matrix.architecture}}.tar"
- name: Login to DockerHub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Checkout
uses: actions/checkout@v3
- uses: benjlevesque/short-sha@v2.1
id: short-sha
with:
length: 7
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Available platforms
run: echo ${{ steps.buildx.outputs.platforms }}
- name: Run Buildx
run: docker buildx build --platform linux/$(echo ${{matrix.architecture}} | tr - /) -t $REPO-arch:arch-${{matrix.architecture}}-${{github.event.inputs.tag || github.ref_name}} --push .
- name: Create new and append to manifest
run: docker buildx imagetools create --append -t $REPO:${{ github.event.inputs.tag || github.ref_name }} $REPO-arch:arch-${{matrix.architecture}}-${{github.event.inputs.tag || github.ref_name}}
- name: Create new and append to manifest latest
run: docker buildx imagetools create --append -t $REPO:latest $REPO-arch:arch-${{matrix.architecture}}-${{github.event.inputs.tag || github.ref_name}}
if: github.event.inputs.tag == 'test'
- name: Run Buildx with output
run: docker buildx build --platform linux/$(echo ${{matrix.architecture}} | tr - /) -t $REPO-arch:arch-$(echo ${{matrix.architecture}} | tr / -)-${{github.event.inputs.tag || github.ref_name}} --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
- name: Create a release
uses: ncipollo/release-action@v1
with:
latest: true
allowUpdates: true
name: ${{ github.event.inputs.tag || github.ref_name }}
tag: ${{ github.event.inputs.tag || github.ref_name }}
generateReleaseNotes: false
omitBodyDuringUpdate: true
artifacts: "agent-${{matrix.architecture}}.tar"

View File

@@ -2,35 +2,37 @@ name: Go
on:
push:
branches: [ develop, master ]
branches: [develop, master]
pull_request:
branches: [ develop, master ]
branches: [develop, master]
jobs:
build:
name: Build
runs-on: ubuntu-latest
container:
image: kerberos/base:70d69dc
image: kerberos/base:eb6b088
strategy:
matrix:
go-version: [1.17, 1.18, 1.19]
#No longer supported Go versions.
#go-version: ['1.17', '1.18', '1.19', '1.20', '1.21']
go-version: ["1.24"]
steps:
- name: Set up Go ${{ matrix.go-version }}
uses: actions/setup-go@v2
with:
go-version: ${{ matrix.go-version }}
- name: Check out code into the Go module directory
uses: actions/checkout@v3
- name: Get dependencies
run: cd machinery && go mod download
- name: Build
run: cd machinery && go build -v ./...
- name: Vet
run: cd machinery && go vet -v ./...
- name: Test
run: cd machinery && go test -v ./...
- name: Set up Go ${{ matrix.go-version }}
uses: actions/setup-go@v2
with:
go-version: ${{ matrix.go-version }}
- name: Check out code into the Go module directory
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
run: cd machinery && go build -v ./...
- name: Vet
run: cd machinery && go vet -v ./...
- name: Test
run: cd machinery && go test -v ./...

19
.github/workflows/pr-description.yaml vendored Normal file
View File

@@ -0,0 +1,19 @@
name: Autofill PR description
on: pull_request
jobs:
openai-pr-description:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- name: Autofill PR description if empty using OpenAI
uses: cedricve/azureopenai-pr-description@master
with:
github_token: ${{ secrets.TOKEN }}
openai_api_key: ${{ secrets.OPENAI_API_KEY }}
azure_openai_api_key: ${{ secrets.AZURE_OPENAI_API_KEY }}
azure_openai_endpoint: ${{ secrets.AZURE_OPENAI_ENDPOINT }}
azure_openai_version: ${{ secrets.AZURE_OPENAI_VERSION }}
overwrite_description: true

3
.gitignore vendored
View File

@@ -10,5 +10,6 @@ machinery/data/recordings
machinery/data/snapshots
machinery/test*
machinery/init-dev.sh
machinery/.env
machinery/.env.local
machinery/vendor
deployments/docker/private-docker-compose.yaml

View File

@@ -1,19 +0,0 @@
language: go
go:
- 1.12.x
- 1.13.x
- 1.14.x
- 1.15.x
- tip
before_install:
- cd machinery
- go mod download
script:
- go vet
- go test -race -coverprofile=coverage.txt -covermode=atomic
after_success:
- bash <(curl -s https://codecov.io/bash)

33
.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,33 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Launch Golang",
"type": "go",
"request": "launch",
"mode": "auto",
"program": "${workspaceFolder}/machinery/main.go",
"args": [
"-action",
"run",
"-port",
"8080"
],
"envFile": "${workspaceFolder}/machinery/.env.local",
"buildFlags": "--tags dynamic",
},
{
"name": "Launch React",
"type": "node",
"request": "launch",
"cwd": "${workspaceFolder}/ui",
"runtimeExecutable": "yarn",
"runtimeArgs": [
"start"
],
}
]
}

View File

@@ -1,5 +1,6 @@
FROM kerberos/base:dc12d68 AS build-machinery
ARG BASE_IMAGE_VERSION=70ec57e
FROM kerberos/base:${BASE_IMAGE_VERSION} AS build-machinery
LABEL AUTHOR=Kerberos.io
ENV GOROOT=/usr/local/go
@@ -10,7 +11,7 @@ ENV GOSUMDB=off
##########################################
# Installing some additional dependencies.
RUN apt-get upgrade -y && 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 +21,7 @@ RUN apt-get upgrade -y && apt-get update && apt-get install -y --no-install-reco
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
@@ -42,8 +44,7 @@ RUN cd /go/src/github.com/kerberos-io/agent/machinery && \
mkdir -p /agent/data/log && \
mkdir -p /agent/data/recordings && \
mkdir -p /agent/data/capture-test && \
mkdir -p /agent/data/config && \
rm -rf /go/src/gitlab.com/
mkdir -p /agent/data/config
####################################
# Let's create a /dist folder containing just the files necessary for runtime.
@@ -57,18 +58,6 @@ RUN cp -r /agent ./
RUN /dist/agent/main version
###############################################
# Build Bento4 -> we want fragmented mp4 files
ENV BENTO4_VERSION 1.6.0-639
RUN cd /tmp && git clone https://github.com/axiomatic-systems/Bento4 && cd Bento4 && \
git checkout tags/v${BENTO4_VERSION} && \
cd Build && \
cmake -DCMAKE_BUILD_TYPE=Release .. && \
make && \
mv /tmp/Bento4/Build/mp4fragment /dist/agent/ && \
rm -rf /tmp/Bento4
FROM node:18.14.0-alpine3.16 AS build-ui
RUN apk update && apk upgrade --available && sync
@@ -110,7 +99,6 @@ RUN apk update && apk add ca-certificates curl libstdc++ libc6-compat --no-cache
# Try running agent
RUN mv /agent/* /home/agent/
RUN cp /home/agent/mp4fragment /usr/local/bin/
RUN /home/agent/main version
#######################
@@ -147,4 +135,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", "-action", "run", "-port", "80"]
CMD ["./main", "-action", "run", "-port", "80"]

288
README.md
View File

@@ -17,20 +17,23 @@
<a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="License: MIT"></a>
[![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)
[![Slack invite](https://img.shields.io/badge/join%20kerberos.io%20on%20slack-grey?style=for-the-badge&logo=slack)](https://joinslack.kerberos.io/)
[**Docker Hub**](https://hub.docker.com/r/kerberos/agent) | [**Documentation**](https://doc.kerberos.io) | [**Website**](https://kerberos.io) | [**View Demo**](https://demo.kerberos.io)
> Before you continue, this repository discusses one of the components of the Kerberos.io stack, the Kerberos Agent, in depth. If you are [looking for an end-to-end deployment guide have a look here](https://github.com/kerberos-io/deployment).
Kerberos Agent is an isolated and scalable video (surveillance) management agent made available as Open Source under the MIT License. This means that all the source code is available for you or your company, and you can use, transform and distribute the source code; as long you keep a reference of the original license. Kerberos Agent can be used for commercial usage (which was not the case for v2). Read more [about the license here](LICENSE).
![Kerberos Agent go through UI](./assets/img/kerberos-agent-overview.gif)
## :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).
- 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.
- 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, AMD64) 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?
@@ -46,41 +49,46 @@ There are a myriad of cameras out there (USB, IP and other cameras), and it migh
### 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
The easiest to get your Kerberos Agent up and running is to use our public image on [Docker hub](https://hub.docker.com/r/kerberos/agent). Once you have selected a specific tag, run below `docker` command, which will open the web interface of your Kerberos agent on port `80`, and off you go. For a more configurable and persistent deployment have a look at [Running and automating a Kerberos Agent](#running-and-automating-a-kerberos-agent).
The easiest way to get your Kerberos Agent up and running is to use our public image on [Docker hub](https://hub.docker.com/r/kerberos/agent). Once you have selected a specific tag, run `docker` command below, which will open the web interface of your Kerberos agent on port `80`, and off you go. For a more configurable and persistent deployment have a look at [Running and automating a Kerberos Agent](#running-and-automating-a-kerberos-agent).
docker run -p 80:80 --name mycamera -d --restart=always kerberos/agent:latest
If you want to connect to an USB or Raspberry Pi camera, [you'll need to run our side car container](https://github.com/kerberos-io/camera-to-rtsp) which proxy the camera to an RTSP stream. In that case you'll want to configure the Kerberos Agent container to run in the host network, so it can connect directly to the RTSP sidecar.
If you want to connect to a USB or Raspberry Pi camera, [you'll need to run our side car container](https://github.com/kerberos-io/camera-to-rtsp) which proxies the camera to an RTSP stream. In that case you'll want to configure the Kerberos Agent container to run in the host network, so it can connect directly to the RTSP sidecar.
docker run --network=host --name mycamera -d --restart=always kerberos/agent:latest
## Quickstart - Balena
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).
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 much 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).
[![deploy with balena](https://balena.io/deploy.svg)](https://dashboard.balena-cloud.com/deploy?repoUrl=https://github.com/kerberos-io/balena-agent)
@@ -96,33 +104,37 @@ Once installed you can find your Kerberos Agent configration at `/var/snap/kerbe
## A world of Kerberos Agents
The Kerberos Agent is an isolated and scalable video (surveillance) management agent with a strong focus on user experience, scalability, resilience, extension and integration. Next to the Kerberos Agent, Kerberos.io provides many other tools such as [Kerberos Factory](https://github.com/kerberos-io/factory), [Kerberos Vault](https://github.com/kerberos-io/vault) and [Kerberos Hub](https://github.com/kerberos-io/hub) to provide additional capabilities: bring your own cloud, bring your own storage, central overview, live streaming, machine learning etc.
The Kerberos Agent is an isolated and scalable video (surveillance) management agent with a strong focus on user experience, scalability, resilience, extension and integration. Next to the Kerberos Agent, Kerberos.io provides many other tools such as [Kerberos Factory](https://github.com/kerberos-io/factory), [Kerberos Vault](https://github.com/kerberos-io/vault), and [Kerberos Hub](https://github.com/kerberos-io/hub) to provide additional capabilities: bring your own cloud, bring your own storage, central overview, live streaming, machine learning, etc.
As mentioned above Kerberos.io applies the concept of agents. An agent is running next to (or on) your camera, and is processing a single camera feed. It applies motion based or continuous recording and make those recordings available through a user friendly web interface. A Kerberos Agent allows you to connect to other cloud services or integrates with custom applications. Kerberos Agent is used for personal usage and scales to enterprise production level deployments.
[![Deployment Agent](./assets/img/edge-deployment-agent.svg)](https://github.com/kerberos-io/deployment)
As mentioned above Kerberos.io applies the concept of agents. An agent is running next to (or on) your camera, and is processing a single camera feed. It applies motion based or continuous recording and makes those recordings available through a user friendly web interface. A Kerberos Agent allows you to connect to other cloud services or integrate with custom applications. Kerberos Agent is used for personal applications and scales to enterprise production level deployments. Learn more about the [deployment strategies here](<(https://github.com/kerberos-io/deployment)>).
This repository contains everything you'll need to know about our core product, Kerberos Agent. Below you'll find a brief list of features and functions.
- 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 (ARMv6, ARMv7, ARM64, AMD64)
- 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.
- End-to-end encryption through MQTT using RSA and AES.
- Ability to specifiy conditions: offline mode, motion region, time table, continuous recording, etc.
- Post- and pre-recording on motion detection.
- 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 though HLS fMP4.
- 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
## How to run and deploy a Kerberos Agent
As described before a Kerberos Agent is a container, which can be deployed through various ways and automation tools such as `docker`, `docker compose`, `kubernetes` and the list goes on. To simplify your life we have come with concrete and working examples of deployments to help you speed up your Kerberos.io journey.
A Kerberos Agent, as previously mentioned, is a container. You can deploy it using various methods and automation tools, including `docker`, `docker compose`, `kubernetes` and more. To streamline your Kerberos.io experience, we provide concrete deployment examples to speed up your Kerberos.io journey
We have documented the different deployment models [in the `deployments` directory](https://github.com/kerberos-io/agent/tree/master/deployments) of this repository. There you'll learn and find how to deploy using:
@@ -136,7 +148,7 @@ We have documented the different deployment models [in the `deployments` directo
- [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.
By default, your Kerberos Agents store all configuration and recordings within 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.
## Access the Kerberos Agent
@@ -149,39 +161,25 @@ The default username and password for the Kerberos Agent is:
**_Please note that you change the username and password for a final installation, see [Configure with environment variables](#configure-with-environment-variables) below._**
## 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
## Configure and persist with volume mounts
An example of how to mount a host directory is shown below using `docker`, but is applicable for [all the deployment models and tools described above](#running-and-automating-a-kerberos-agent).
An example of how to mount a host directory is shown below using `docker`, but is applicable for [all of the deployment models and tools described above](#running-and-automating-a-kerberos-agent).
You attach a volume to your container by leveraging the `-v` option. To mount your own configuration file and recordings folder, execute as following:
You attach a volume to your container by leveraging the `-v` option. To mount your own configuration file and recordings folder, run the following commands:
docker run -p 80:80 --name mycamera \
-v $(pwd)/agent/config:/home/agent/data/config \
-v $(pwd)/agent/recordings:/home/agent/data/recordings \
-d --restart=always kerberos/agent:latest
More example [can be found in the deployment section](https://github.com/kerberos-io/agent/tree/master/deployments) for each deployment and automation tool. Please note to verify the permissions of the directory/volume you are attaching. More information in [this issue](https://github.com/kerberos-io/agent/issues/80).
More examples for each deployment and automation tool [can be found in the deployment section](https://github.com/kerberos-io/agent/tree/master/deployments). Be sure to verify the permissions of the directory/volume you are attaching. More information in [this issue](https://github.com/kerberos-io/agent/issues/80).
chmod -R 755 kerberos-agent/
chown 100:101 kerberos-agent/ -R
## Configure with environment variables
Next to attaching the configuration file, it is also possible to override the configuration with environment variables. This makes deployments easier when leveraging `docker compose` or `kubernetes` deployments much easier and scalable. Using this approach we simplify automation through `ansible` and `terraform`.
Next to attaching the configuration file, it is also possible to override the configuration with environment variables. This makes deploying with `docker compose` or `kubernetes` much easier and more scalable. Using this approach, we simplify automation through `ansible` and `terraform`.
docker run -p 80:80 --name mycamera \
-e AGENT_NAME=mycamera \
@@ -192,68 +190,120 @@ Next to attaching the configuration file, it is also possible to override the co
| Name | Description | Default Value |
| --------------------------------------- | ----------------------------------------------------------------------------------------------- | ------------------------------ |
| `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. | "" |
| `AGENT_NAME` | The agent friendly-name. | "agent" |
| `AGENT_TIMEZONE` | Timezone which is used for converting time. | "Africa/Ceuta" |
| `AGENT_REMOVE_AFTER_UPLOAD` | When enabled, recordings uploaded successfully to a storage will be removed from disk. | "true" |
| `AGENT_OFFLINE` | Makes sure no external connection is made. | "false" |
| `AGENT_AUTO_CLEAN` | Cleans up the recordings directory. | "true" |
| `AGENT_AUTO_CLEAN_MAX_SIZE` | If `AUTO_CLEAN` enabled, set the max size of the recordings directory in (MB). | "100" |
| `AGENT_TIME` | Enable the timetable for Kerberos Agent | "false" |
| `AGENT_TIMETABLE` | A (weekly) time table to specify when to make recordings "start1,end1,start2,end2;start1.. | "" |
| `AGENT_REGION_POLYGON` | A single polygon set for motion detection: "x1,y1;x2,y2;x3,y3;... | "" |
| `AGENT_CAPTURE_IPCAMERA_RTSP` | Full-HD RTSP endpoint to the camera you're targetting. | "" |
| `AGENT_CAPTURE_IPCAMERA_SUB_RTSP` | Sub-stream RTSP endpoint used for livestreaming (WebRTC). | "" |
| `AGENT_CAPTURE_IPCAMERA_ONVIF` | Mark as a compliant ONVIF device. | "" |
| `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 "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" |
| `AGENT_CAPTURE_PIXEL_CHANGE` | If `CONTINUOUS` set to `false`, the number of pixel require to change before motion triggers. | "150" |
| `AGENT_CAPTURE_FRAGMENTED` | Set the format of the recorded MP4 to fragmented (suitable for HLS). | "false" |
| `AGENT_CAPTURE_FRAGMENTED_DURATION` | If `AGENT_CAPTURE_FRAGMENTED` set to `true`, define the duration (seconds) of a fragment. | "8" |
| `AGENT_MQTT_URI` | A MQTT broker endpoint that is used for bi-directional communication (live view, onvif, etc) | "tcp://mqtt.kerberos.io:1883" |
| `AGENT_MQTT_USERNAME` | Username of the MQTT broker. | "" |
| `AGENT_MQTT_PASSWORD` | Password of the MQTT broker. | "" |
| `AGENT_STUN_URI` | When using WebRTC, you'll need to provide a STUN server. | "stun:turn.kerberos.io:8443" |
| `AGENT_TURN_URI` | When using WebRTC, you'll need to provide a TURN server. | "turn:turn.kerberos.io:8443" |
| `AGENT_TURN_USERNAME` | TURN username used for WebRTC. | "username1" |
| `AGENT_TURN_PASSWORD` | TURN password used for WebRTC. | "password1" |
| `AGENT_CLOUD` | Store recordings in Kerberos Hub (s3), Kerberos Vault (kstorage) or Dropbox (dropbox). | "s3" |
| `AGENT_HUB_URI` | The Kerberos Hub API, defaults to our Kerberos Hub SAAS. | "https://api.hub.domain.com" |
| `AGENT_HUB_KEY` | The access key linked to your account in Kerberos Hub. | "" |
| `AGENT_HUB_PRIVATE_KEY` | The secret access key linked to your account in Kerberos Hub. | "" |
| `AGENT_HUB_REGION` | The Kerberos Hub region, to which you want to upload. | "" |
| `AGENT_HUB_SITE` | The site ID of a site you've created in your Kerberos Hub account. | "" |
| `AGENT_KERBEROSVAULT_URI` | The Kerberos Vault API url. | "https://vault.domain.com/api" |
| `AGENT_KERBEROSVAULT_ACCESS_KEY` | The access key of a Kerberos Vault account. | "" |
| `AGENT_KERBEROSVAULT_SECRET_KEY` | The secret key of a Kerberos Vault account. | "" |
| `AGENT_KERBEROSVAULT_PROVIDER` | A Kerberos Vault provider you have created (optional). | "" |
| `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. | "" |
| `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. | "" |
| `AGENT_NAME` | The agent friendly-name. | "agent" |
| `AGENT_TIMEZONE` | Timezone which is used for converting time. | "Africa/Ceuta" |
| `AGENT_REMOVE_AFTER_UPLOAD` | When enabled, recordings uploaded successfully to a storage will be removed from disk. | "true" |
| `AGENT_OFFLINE` | Makes sure no external connection is made. | "false" |
| `AGENT_AUTO_CLEAN` | Cleans up the recordings directory. | "true" |
| `AGENT_AUTO_CLEAN_MAX_SIZE` | If `AUTO_CLEAN` enabled, set the max size of the recordings directory (in MB). | "100" |
| `AGENT_TIME` | Enable the timetable for Kerberos Agent | "false" |
| `AGENT_TIMETABLE` | A (weekly) time table to specify when to make recordings "start1,end1,start2,end2;start1.. | "" |
| `AGENT_REGION_POLYGON` | A single polygon set for motion detection: "x1,y1;x2,y2;x3,y3;... | "" |
| `AGENT_CAPTURE_IPCAMERA_RTSP` | Full-HD RTSP endpoint to the camera you're targetting. | "" |
| `AGENT_CAPTURE_IPCAMERA_SUB_RTSP` | Sub-stream RTSP endpoint used for livestreaming (WebRTC). | "" |
| `AGENT_CAPTURE_IPCAMERA_ONVIF` | Mark as a compliant ONVIF device. | "" |
| `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 "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" |
| `AGENT_CAPTURE_PIXEL_CHANGE` | If `CONTINUOUS` set to `false`, the number of pixel require to change before motion triggers. | "150" |
| `AGENT_CAPTURE_FRAGMENTED` | Set the format of the recorded MP4 to fragmented (suitable for HLS). | "false" |
| `AGENT_CAPTURE_FRAGMENTED_DURATION` | If `AGENT_CAPTURE_FRAGMENTED` set to `true`, define the duration (seconds) of a fragment. | "8" |
| `AGENT_MQTT_URI` | An MQTT broker endpoint that is used for bi-directional communication (live view, onvif, etc) | "tcp://mqtt.kerberos.io:1883" |
| `AGENT_MQTT_USERNAME` | Username of the MQTT broker. | "" |
| `AGENT_MQTT_PASSWORD` | Password of the MQTT broker. | "" |
| `AGENT_REALTIME_PROCESSING` | If `AGENT_REALTIME_PROCESSING` set to `true`, the agent will send key frames to the topic | "" |
| `AGENT_REALTIME_PROCESSING_TOPIC` | The topic to which keyframes will be sent in base64 encoded format. | "" |
| `AGENT_STUN_URI` | When using WebRTC, you'll need to provide a STUN server. | "stun:turn.kerberos.io:8443" |
| `AGENT_FORCE_TURN` | Force using a TURN server, by generating relay candidates only. | "false" |
| `AGENT_TURN_URI` | When using WebRTC, you'll need to provide a TURN server. | "turn:turn.kerberos.io:8443" |
| `AGENT_TURN_USERNAME` | TURN username used for WebRTC. | "username1" |
| `AGENT_TURN_PASSWORD` | TURN password used for WebRTC. | "password1" |
| `AGENT_CLOUD` | Store recordings in Kerberos Hub (s3), Kerberos Vault (kstorage), or Dropbox (dropbox). | "s3" |
| `AGENT_HUB_ENCRYPTION` | Turning on/off encryption of traffic from your Kerberos Agent to Kerberos Hub. | "true" |
| `AGENT_HUB_URI` | The Kerberos Hub API, defaults to our Kerberos Hub SAAS. | "https://api.hub.domain.com" |
| `AGENT_HUB_KEY` | The access key linked to your account in Kerberos Hub. | "" |
| `AGENT_HUB_PRIVATE_KEY` | The secret access key linked to your account in Kerberos Hub. | "" |
| `AGENT_HUB_REGION` | The Kerberos Hub region, to which you want to upload. | "" |
| `AGENT_HUB_SITE` | The site ID of a site you've created in your Kerberos Hub account. | "" |
| `AGENT_KERBEROSVAULT_URI` | The Kerberos Vault API url. | "https://vault.domain.com/api" |
| `AGENT_KERBEROSVAULT_ACCESS_KEY` | The access key of a Kerberos Vault account. | "" |
| `AGENT_KERBEROSVAULT_SECRET_KEY` | The secret key of a Kerberos Vault account. | "" |
| `AGENT_KERBEROSVAULT_PROVIDER` | A Kerberos Vault provider you have created (optional). | "" |
| `AGENT_KERBEROSVAULT_DIRECTORY` | The directory, in the Kerberos vault, where the recordings will be stored. | "" |
| `AGENT_KERBEROSVAULT_SECONDARY_URI` | The Kerberos Vault API url. | "https://vault.domain.com/api" |
| `AGENT_KERBEROSVAULT_SECONDARY_ACCESS_KEY` | The access key of a secondary Kerberos Vault account. | "" |
| `AGENT_KERBEROSVAULT_SECONDARY_SECRET_KEY` | The secret key of a secondary Kerberos Vault account. | "" |
| `AGENT_KERBEROSVAULT_SECONDARY_PROVIDER` | A secondary Kerberos Vault provider you have created (optional). | "" |
| `AGENT_KERBEROSVAULT_SECONDARY_DIRECTORY` | The directory, in the secondary Kerberos vault, where the recordings will be stored. | "" |
| `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 Dropbox, where the recordings will be stored. | "" |
| `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 decrypt and sign requests send over MQTT. | "" |
| `AGENT_ENCRYPTION_SYMMETRIC_KEY` | The symmetric key (AES) to encrypt and decrypt requests sent over MQTT. | "" |
| `AGENT_SIGNING` | Enable 'true' or disable 'false' for signing recordings. | "true" |
| `AGENT_SIGNING_PRIVATE_KEY` | The private key (RSA) to sign the recordings fingerprint to validate origin. | "" - uses default one if empty |
## 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 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
Or 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
One of the major blockers for letting you contribute to an Open Source project is to setup your local development machine. Why? Because you might have already some tools and libraries installed that are used for other projects, and the libraries you would need for Kerberos Agent, for example FFmpeg, might require a different version. Welcome to the dependency hell..
One of the major blockers for letting you contribute to an Open Source project is to set up your local development machine. Why? Because you might already have some tools and libraries installed that are used for other projects, and the libraries you would need for Kerberos Agent, for example FFmpeg, might require a different version. Welcome to dependency hell...
By leveraging codespaces, which the Kerberos Agent repo supports, you will be able to setup the required development environment in a few minutes. By opening the `<> Code` tab on the top of the page, you will be able to create a codespace, [using the Kerberos Devcontainer](https://github.com/kerberos-io/devcontainer) base image. This image requires all the relevant dependencies: FFmpeg, OpenCV, Golang, Node, Yarn, etc.
By leveraging codespaces, which the Kerberos Agent repo supports, you will be able to set up the required development environment in a few minutes. By opening the `<> Code` tab on the top of the page, you will be able to create a codespace, [using the Kerberos Devcontainer](https://github.com/kerberos-io/devcontainer) base image. This image requires all the relevant dependencies: FFmpeg, OpenCV, Golang, Node, Yarn, etc.
![Kerberos Agent codespace](assets/img/codespace.png)
@@ -280,7 +330,7 @@ On opening of the GitHub Codespace, some dependencies will be installed. Once th
WS_URL: `${websocketprotocol}//${externalHost}/ws`,
};
Go and open two terminals one for the `ui` project and one for the `machinery` project.
Go and open two terminals: one for the `ui` project and one for the `machinery` project.
1. Terminal A:
@@ -296,11 +346,11 @@ Once executed, a popup will show up mentioning `portforwarding`. You should see
![Codespace make public](./assets/img/codespace-make-public.png)
As mentioned above, copy the hostname of the `machinery` DNS name, and past it in the `ui/src/config.json` file. Once done reload, the `ui` page in your browser, and you should be able to access the login page with the default credentials `root` and `root`.
As mentioned above, copy the hostname of the `machinery` DNS name, and paste it in the `ui/src/config.json` file. Once done, reload the `ui` page in your browser, and you should be able to access the login page with the default credentials `root` and `root`.
## Develop and build
Kerberos Agent is divided in two parts a `machinery` and `web`. Both parts live in this repository in their relative folders. For development or running the application on your local machine, you have to run both the `machinery` and the `web` as described below. When running in production everything is shipped as only one artifact, read more about this at [Building for production](#building-for-production).
The Kerberos Agent is divided in two parts: a `machinery` and `web` part. Both parts live in this repository in their relative folders. For development or running the application on your local machine, you have to run both the `machinery` and the `web` as described below. When running in production everything is shipped as only one artifact, read more about this at [Building for production](#building-for-production).
### UI
@@ -314,13 +364,13 @@ This will start a webserver and launches the web app on port `3000`.
![login-agent](./assets/img/agent-login.gif)
Once signed in you'll see the dashboard page showing up. After successfull configuration of your agent, you'll should see a live view and possible events recorded to disk.
Once signed in you'll see the dashboard page. After successfull configuration of your agent, you'll should see a live view and possible events recorded to disk.
![dashboard-agent](./assets/img/agent-dashboard.png)
### Machinery
The `machinery` is a **Golang** project which delivers two functions: it acts as the Kerberos Agent which is doing all the heavy lifting with camera processing and other kinds of logic, on the other hand it acts as a webserver (Rest API) that allows communication from the web (React) or any other custom application. The API is documented using `swagger`.
The `machinery` is a **Golang** project which delivers two functions: it acts as the Kerberos Agent which is doing all the heavy lifting with camera processing and other kinds of logic and on the other hand it acts as a webserver (Rest API) that allows communication from the web (React) or any other custom application. The API is documented using `swagger`.
You can simply run the `machinery` using following commands.
@@ -328,13 +378,13 @@ You can simply run the `machinery` using following commands.
cd machinery
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.
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 built in.
![VSCode desktop](./assets/img/vscode-desktop.png)
## Building from source
Running Kerberos Agent in production only require a single binary to run. Nevertheless, we have two parts, the `machinery` and the `web`, we merge them during build time. So this is what happens.
Running Kerberos Agent in production only requires a single binary to run. Nevertheless, we have two parts: the `machinery` and the `web`, we merge them during build time. So this is what happens.
### UI
@@ -345,7 +395,7 @@ To build the Kerberos Agent web app, you simply have to run the `build` command
### Machinery
Building the `machinery` is also super easy 🚀, by using `go build` you can create a single binary which ships it all; thank you Golang. After building you will endup with a binary called `main`, this is what contains everything you need to run Kerberos Agent.
Building the `machinery` is also super easy 🚀, by using `go build` you can create a single binary which ships it all; thank you Golang. After building you will end up with a binary called `main`, this is what contains everything you need to run Kerberos Agent.
Remember the build step of the `web` part, during build time we move the build directory to the `machinery` directory. Inside the `machinery` web server [we reference the](https://github.com/kerberos-io/agent/blob/master/machinery/src/routers/http/Server.go#L44) `build` directory. This makes it possible to just a have single web server that runs it all.
@@ -354,8 +404,8 @@ Remember the build step of the `web` part, during build time we move the build d
## Building for Docker
Inside the root of this `agent` repository, you will find a `Dockerfile`. This file contains the instructions for building and shipping **Kerberos Agent**. Important to note is that start from a prebuild base image, `kerberos/base:xxx`.
This base image contains already a couple of tools, such as Golang, FFmpeg and OpenCV. We do this for faster compilation times.
Inside the root of this `agent` repository, you will find a `Dockerfile`. This file contains the instructions for building and shipping a **Kerberos Agent**. Important to note is that you start from a prebuilt base image, `kerberos/base:xxx`.
This base image already contains a couple of tools, such as Golang, FFmpeg and OpenCV. We do this for faster compilation times.
By running the `docker build` command, you will create the Kerberos Agent Docker image. After building you can simply run the image as a Docker container.
@@ -371,7 +421,7 @@ Read more about this [at the FAQ](#faq) below.
## Contributors
This project exists thanks to all the people who contribute.
This project exists thanks to all the people who contribute. Bravo!
<a href="https://github.com/kerberos-io/agent/graphs/contributors">
<img src="https://contrib.rocks/image?repo=kerberos-io/agent" />

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 696 KiB

View File

@@ -1,10 +0,0 @@
export version=0.0.1
export name=agent
docker build -t $name .
docker tag $name kerberos/$name:$version
docker push kerberos/$name:$version
docker tag $name kerberos/$name:latest
docker push kerberos/$name:latest

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

@@ -36,12 +36,12 @@ You attach a volume to your container by leveraging the `-v` option. To mount yo
docker run -p 80:80 --name mycamera \
-v $(pwd)/agent/config:/home/agent/data/config \
-v $(pwd)/agent/recordings:/home/agent/data/recordings\
-d --restart=alwayskerberos/agent:latest
-v $(pwd)/agent/recordings:/home/agent/data/recordings \
-d --restart=always kerberos/agent:latest
### Override with environment variables
Next to attaching the configuration file, it is also possible to override the configuration with environment variables. This makes deployments easier when leveraging `docker compose` or `kubernetes` deployments much easier and scalable. Using this approach we simplify automation through `ansible` and `terraform`. You'll find [the full list of environment variables on the main README.md file](https://github.com/kerberos-io/agent#override-with-environment-variables).
Next to attaching the configuration file, it is also possible to override the configuration with environment variables. This makes deployments when leveraging `docker compose` or `kubernetes` much easier and more scalable. Using this approach we simplify automation through `ansible` and `terraform`. You'll find [the full list of environment variables on the main README.md file](https://github.com/kerberos-io/agent#override-with-environment-variables).
### 2. Running multiple containers with Docker compose

View File

@@ -1,35 +1,38 @@
version: "3.9"
x-common-variables: &common-variables
# Add variables here to add them to all agents
AGENT_HUB_KEY: "xxxxx" # The access key linked to your account in Kerberos Hub.
AGENT_HUB_PRIVATE_KEY: "xxxxx" # The secret access key linked to your account in Kerberos Hub.
# find full list of environment variables here: https://github.com/kerberos-io/agent#override-with-environment-variables
services:
kerberos-agent1:
image: "kerberos/agent:latest"
ports:
- "8081:80"
environment:
- AGENT_NAME=agent1
- AGENT_CAPTURE_IPCAMERA_RTSP=rtsp://x.x.x.x:554/Streaming/Channels/101
- AGENT_HUB_KEY=xxx
- AGENT_HUB_PRIVATE_KEY=xxx
- AGENT_CAPTURE_CONTINUOUS=true
- AGENT_CAPTURE_PRERECORDING=10
- AGENT_CAPTURE_POSTRECORDING=10
- AGENT_CAPTURE_MAXLENGTH=60
- AGENT_CAPTURE_PIXEL_CHANGE=150
# find full list of environment variables here: https://github.com/kerberos-io/agent#override-with-environment-variables
<<: *common-variables
AGENT_NAME: agent1
AGENT_CAPTURE_IPCAMERA_RTSP: rtsp://username:password@x.x.x.x/Streaming/Channels/101 # Hikvision camera RTSP url example
AGENT_KEY: "1"
kerberos-agent2:
image: "kerberos/agent:latest"
ports:
- "8082:80"
environment:
- AGENT_NAME=agent2
- AGENT_CAPTURE_IPCAMERA_RTSP=rtsp://x.x.x.x:554/Streaming/Channels/101
- AGENT_HUB_KEY=yyy
- AGENT_HUB_PRIVATE_KEY=yyy
<<: *common-variables
AGENT_NAME: agent2
AGENT_CAPTURE_IPCAMERA_RTSP: rtsp://username:password@x.x.x.x/channel1 # Linksys camera RTSP url example
AGENT_KEY: "2"
kerberos-agent3:
image: "kerberos/agent:latest"
ports:
- "8083:80"
environment:
- AGENT_NAME=agent3
- AGENT_CAPTURE_IPCAMERA_RTSP=rtsp://x.x.x.x:554/Streaming/Channels/101
- AGENT_HUB_KEY=zzz
- AGENT_HUB_PRIVATE_KEY=zzz
<<: *common-variables
AGENT_NAME: agent3
AGENT_CAPTURE_IPCAMERA_RTSP: rtsp://username:password@x.x.x.x/cam/realmonitor?channel=1&subtype=1 # Dahua camera RTSP url example
AGENT_KEY: "3"
networks:
default:
name: cluster-net
external: true

View File

@@ -16,7 +16,7 @@ spec:
spec:
containers:
- name: agent
image: kerberos/agent:latest
image: kerberos/agent:3.2.3
ports:
- containerPort: 80
protocol: TCP
@@ -50,4 +50,4 @@ spec:
- port: 80
targetPort: 80
selector:
app: agent
app: agent

BIN
machinery/.DS_Store vendored Normal file

Binary file not shown.

31
machinery/.env Normal file
View File

@@ -0,0 +1,31 @@
AGENT_NAME=camera-name
AGENT_KEY=uniq-camera-id
AGENT_TIMEZONE=Europe/Brussels
#AGENT_CAPTURE_CONTINUOUS=true
#AGENT_CAPTURE_IPCAMERA_RTSP=rtsp://fake.kerberos.io/stream
#AGENT_CAPTURE_IPCAMERA_SUB_RTSP=rtsp://fake.kerberos.io/stream
AGENT_CAPTURE_IPCAMERA_ONVIF_XADDR=x.x.x.x
AGENT_CAPTURE_IPCAMERA_ONVIF_USERNAME=xxx
AGENT_CAPTURE_IPCAMERA_ONVIF_PASSWORD=xxx
AGENT_HUB_URI=https://api.cloud.kerberos.io
AGENT_HUB_KEY=AKIXxxx4JBEI
AGENT_HUB_PRIVATE_KEY=DIOXxxxAlYpaxxxxXioL0txxx
AGENT_HUB_SITE=681xxxxxxx9bcda5
# By default will send to Hub (=S3), if you wish to send to Kerberos Vault, set to "kstorage"
AGENT_CLOUD=s3
AGENT_KERBEROSVAULT_URI=
AGENT_KERBEROSVAULT_PROVIDER=
AGENT_KERBEROSVAULT_DIRECTORY=
AGENT_KERBEROSVAULT_ACCESS_KEY=
AGENT_KERBEROSVAULT_SECRET_KEY=
AGENT_KERBEROSVAULT_MAX_RETRIES=10
AGENT_KERBEROSVAULT_TIMEOUT=120
AGENT_KERBEROSVAULT_SECONDARY_URI=
AGENT_KERBEROSVAULT_SECONDARY_PROVIDER=
AGENT_KERBEROSVAULT_SECONDARY_DIRECTORY=
AGENT_KERBEROSVAULT_SECONDARY_ACCESS_KEY=
AGENT_KERBEROSVAULT_SECONDARY_SECRET_KEY=
# Open telemetry tracing endpoint
OTEL_EXPORTER_OTLP_ENDPOINT=

View File

@@ -1,18 +0,0 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Launch Package",
"type": "go",
"request": "launch",
"mode": "auto",
"program": "main.go",
"args": ["-action", "run"],
"envFile": "${workspaceFolder}/.env",
"buildFlags": "--tags dynamic",
},
]
}

View File

@@ -98,19 +98,25 @@
"region": "eu-west-1"
},
"kstorage": {},
"kstorage_secondary": {},
"dropbox": {},
"mqtturi": "tcp://mqtt.kerberos.io:1883",
"mqtt_username": "",
"mqtt_password": "",
"stunuri": "stun:turn.kerberos.io:8443",
"turn_force": "false",
"turnuri": "turn:turn.kerberos.io:8443",
"turn_username": "username1",
"turn_password": "password1",
"heartbeaturi": "",
"hub_encryption": "true",
"hub_uri": "https://api.cloud.kerberos.io",
"hub_key": "",
"hub_private_key": "",
"hub_site": "",
"condition_uri": "",
"encryption": {}
}
"encryption": {},
"signing": {},
"realtimeprocessing": "false",
"realtimeprocessing_topic": ""
}

View File

@@ -1,5 +1,4 @@
// Package docs GENERATED BY SWAG; DO NOT EDIT
// This file was generated by swaggo/swag
// Package docs Code generated by swaggo/swag. DO NOT EDIT
package docs
import "github.com/swaggo/swag"
@@ -29,7 +28,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",
@@ -58,7 +57,7 @@ const docTemplate = `{
"post": {
"description": "Will activate the desired ONVIF preset.",
"tags": [
"camera"
"onvif"
],
"summary": "Will activate the desired ONVIF preset.",
"operationId": "camera-onvif-gotopreset",
@@ -83,11 +82,45 @@ const docTemplate = `{
}
}
},
"/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",
@@ -112,11 +145,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",
@@ -145,7 +253,7 @@ const docTemplate = `{
"post": {
"description": "Will return the ONVIF presets for the specific camera.",
"tags": [
"camera"
"onvif"
],
"summary": "Will return the ONVIF presets for the specific camera.",
"operationId": "camera-onvif-presets",
@@ -170,11 +278,45 @@ const docTemplate = `{
}
}
},
"/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",
@@ -199,6 +341,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": "OK"
}
}
}
},
"/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": "OK"
}
}
}
},
"/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.",
@@ -239,6 +465,75 @@ const docTemplate = `{
}
}
},
"/api/config": {
"get": {
"description": "Get the current configuration.",
"tags": [
"config"
],
"summary": "Get the current configuration.",
"operationId": "config",
"responses": {
"200": {
"description": "OK"
}
}
},
"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": "OK"
}
}
}
},
"/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": "OK"
}
}
}
},
"/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": "OK"
}
}
}
},
"/api/hub/verify": {
"post": {
"security": [
@@ -248,7 +543,7 @@ const docTemplate = `{
],
"description": "Will verify the hub connectivity.",
"tags": [
"config"
"persistence"
],
"summary": "Will verify the hub connectivity.",
"operationId": "verify-hub",
@@ -273,6 +568,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": "OK"
}
}
}
},
"/api/login": {
"post": {
"description": "Get Authorization token.",
@@ -302,40 +623,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": [
@@ -345,7 +632,7 @@ const docTemplate = `{
],
"description": "Will verify the persistence.",
"tags": [
"config"
"persistence"
],
"summary": "Will verify the persistence.",
"operationId": "verify-persistence",
@@ -505,6 +792,9 @@ const docTemplate = `{
"dropbox": {
"$ref": "#/definitions/models.Dropbox"
},
"encryption": {
"$ref": "#/definitions/models.Encryption"
},
"friendly_name": {
"type": "string"
},
@@ -512,6 +802,9 @@ const docTemplate = `{
"description": "obsolete",
"type": "string"
},
"hub_encryption": {
"type": "string"
},
"hub_key": {
"type": "string"
},
@@ -548,6 +841,12 @@ const docTemplate = `{
"offline": {
"type": "string"
},
"realtimeprocessing": {
"type": "string"
},
"realtimeprocessing_topic": {
"type": "string"
},
"region": {
"$ref": "#/definitions/models.Region"
},
@@ -572,6 +871,9 @@ const docTemplate = `{
"timezone": {
"type": "string"
},
"turn_force": {
"type": "string"
},
"turn_password": {
"type": "string"
},
@@ -608,12 +910,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"
},
@@ -629,8 +968,20 @@ const docTemplate = `{
"rtsp": {
"type": "string"
},
"sub_fps": {
"type": "string"
},
"sub_height": {
"type": "integer"
},
"sub_rtsp": {
"type": "string"
},
"sub_width": {
"type": "integer"
},
"width": {
"type": "integer"
}
}
},
@@ -835,6 +1186,8 @@ var SwaggerInfo = &swag.Spec{
Description: "This is the API for using and configure Kerberos Agent.",
InfoInstanceName: "swagger",
SwaggerTemplate: docTemplate,
LeftDelim: "{{",
RightDelim: "}}",
}
func init() {

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",
@@ -50,7 +50,7 @@
"post": {
"description": "Will activate the desired ONVIF preset.",
"tags": [
"camera"
"onvif"
],
"summary": "Will activate the desired ONVIF preset.",
"operationId": "camera-onvif-gotopreset",
@@ -75,11 +75,45 @@
}
}
},
"/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",
@@ -104,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",
@@ -137,7 +246,7 @@
"post": {
"description": "Will return the ONVIF presets for the specific camera.",
"tags": [
"camera"
"onvif"
],
"summary": "Will return the ONVIF presets for the specific camera.",
"operationId": "camera-onvif-presets",
@@ -162,11 +271,45 @@
}
}
},
"/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",
@@ -191,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": "OK"
}
}
}
},
"/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": "OK"
}
}
}
},
"/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.",
@@ -231,6 +458,75 @@
}
}
},
"/api/config": {
"get": {
"description": "Get the current configuration.",
"tags": [
"config"
],
"summary": "Get the current configuration.",
"operationId": "config",
"responses": {
"200": {
"description": "OK"
}
}
},
"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": "OK"
}
}
}
},
"/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": "OK"
}
}
}
},
"/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": "OK"
}
}
}
},
"/api/hub/verify": {
"post": {
"security": [
@@ -240,7 +536,7 @@
],
"description": "Will verify the hub connectivity.",
"tags": [
"config"
"persistence"
],
"summary": "Will verify the hub connectivity.",
"operationId": "verify-hub",
@@ -265,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": "OK"
}
}
}
},
"/api/login": {
"post": {
"description": "Get Authorization token.",
@@ -294,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": [
@@ -337,7 +625,7 @@
],
"description": "Will verify the persistence.",
"tags": [
"config"
"persistence"
],
"summary": "Will verify the persistence.",
"operationId": "verify-persistence",
@@ -497,6 +785,9 @@
"dropbox": {
"$ref": "#/definitions/models.Dropbox"
},
"encryption": {
"$ref": "#/definitions/models.Encryption"
},
"friendly_name": {
"type": "string"
},
@@ -504,6 +795,9 @@
"description": "obsolete",
"type": "string"
},
"hub_encryption": {
"type": "string"
},
"hub_key": {
"type": "string"
},
@@ -540,6 +834,12 @@
"offline": {
"type": "string"
},
"realtimeprocessing": {
"type": "string"
},
"realtimeprocessing_topic": {
"type": "string"
},
"region": {
"$ref": "#/definitions/models.Region"
},
@@ -564,6 +864,9 @@
"timezone": {
"type": "string"
},
"turn_force": {
"type": "string"
},
"turn_password": {
"type": "string"
},
@@ -600,12 +903,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"
},
@@ -621,8 +961,20 @@
"rtsp": {
"type": "string"
},
"sub_fps": {
"type": "string"
},
"sub_height": {
"type": "integer"
},
"sub_rtsp": {
"type": "string"
},
"sub_width": {
"type": "integer"
},
"width": {
"type": "integer"
}
}
},

View File

@@ -88,11 +88,15 @@ definitions:
type: string
dropbox:
$ref: '#/definitions/models.Dropbox'
encryption:
$ref: '#/definitions/models.Encryption'
friendly_name:
type: string
heartbeaturi:
description: obsolete
type: string
hub_encryption:
type: string
hub_key:
type: string
hub_private_key:
@@ -117,6 +121,10 @@ definitions:
type: string
offline:
type: string
realtimeprocessing:
type: string
realtimeprocessing_topic:
type: string
region:
$ref: '#/definitions/models.Region'
remove_after_upload:
@@ -133,6 +141,8 @@ definitions:
type: array
timezone:
type: string
turn_force:
type: string
turn_password:
type: string
turn_username:
@@ -156,10 +166,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:
@@ -170,8 +204,16 @@ definitions:
type: string
rtsp:
type: string
sub_fps:
type: string
sub_height:
type: integer
sub_rtsp:
type: string
sub_width:
type: integer
width:
type: integer
type: object
models.KStorage:
properties:
@@ -321,7 +363,7 @@ 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.
@@ -340,7 +382,28 @@ paths:
$ref: '#/definitions/models.APIResponse'
summary: Will activate the desired ONVIF preset.
tags:
- camera
- 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.
@@ -359,7 +422,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).
@@ -378,7 +488,7 @@ 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.
@@ -397,7 +507,28 @@ paths:
$ref: '#/definitions/models.APIResponse'
summary: Will return the ONVIF presets for the specific camera.
tags:
- camera
- 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.
@@ -416,6 +547,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: OK
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: OK
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:
@@ -445,6 +632,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: OK
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: OK
summary: Update the current configuration.
tags:
- config
/api/dashboard:
get:
description: Get all information showed on the dashboard.
operationId: dashboard
responses:
"200":
description: OK
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: OK
summary: Get all days stored in the recordings directory.
tags:
- general
/api/hub/verify:
post:
description: Will verify the hub connectivity.
@@ -465,7 +698,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: OK
summary: Get the latest recordings (events) from the recordings directory.
tags:
- general
/api/login:
post:
description: Get Authorization token.
@@ -485,27 +735,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.
@@ -526,7 +755,7 @@ paths:
- Bearer: []
summary: Will verify the persistence.
tags:
- config
- persistence
securityDefinitions:
Bearer:
in: header

View File

@@ -1,146 +1,139 @@
module github.com/kerberos-io/agent/machinery
go 1.19
go 1.24.2
//replace github.com/kerberos-io/joy4 v1.0.58 => ../../../../github.com/kerberos-io/joy4
// replace github.com/kerberos-io/onvif v0.0.6 => ../../../../github.com/kerberos-io/onvif
replace google.golang.org/genproto => google.golang.org/genproto v0.0.0-20250519155744-55703ea1f237
require (
github.com/Eyevinn/mp4ff v0.48.0
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/appleboy/gin-jwt/v2 v2.10.3
github.com/bluenviron/gortsplib/v4 v4.14.1
github.com/bluenviron/mediacommon v1.14.0
github.com/cedricve/go-onvif v0.0.0-20200222191200-567e8ce298f6
github.com/deepch/vdk v0.0.19
github.com/dromara/carbon/v2 v2.6.8
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/gofrs/uuid v3.2.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/eclipse/paho.mqtt.golang v1.5.0
github.com/elastic/go-sysinfo v1.15.3
github.com/gin-contrib/cors v1.7.5
github.com/gin-contrib/pprof v1.5.3
github.com/gin-gonic/contrib v0.0.0-20250521004450-2b1292699c15
github.com/gin-gonic/gin v1.10.1
github.com/gofrs/uuid v4.4.0+incompatible
github.com/golang-jwt/jwt/v4 v4.5.2
github.com/gorilla/websocket v1.5.3
github.com/kellydunn/golang-geo v0.7.0
github.com/kerberos-io/joy4 v1.0.60
github.com/kerberos-io/onvif v0.0.7
github.com/kerberos-io/joy4 v1.0.64
github.com/kerberos-io/onvif v1.0.0
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/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/pion/interceptor v0.1.40
github.com/pion/rtp v1.8.19
github.com/pion/webrtc/v4 v4.1.2
github.com/sirupsen/logrus v1.9.3
github.com/swaggo/files v1.0.1
github.com/swaggo/gin-swagger v1.6.0
github.com/swaggo/swag v1.16.4
github.com/tevino/abool v1.2.0
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
github.com/zaf/g711 v1.4.0
go.mongodb.org/mongo-driver v1.17.3
go.opentelemetry.io/otel v1.36.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.36.0
go.opentelemetry.io/otel/sdk v1.36.0
gopkg.in/natefinch/lumberjack.v2 v2.2.1
)
require (
github.com/DataDog/datadog-agent/pkg/obfuscate v0.0.0-20211129110424-6491aa3bf583 // indirect
github.com/DataDog/datadog-agent/pkg/remoteconfig/state v0.42.0-rc.1 // indirect
github.com/DataDog/datadog-go v4.8.2+incompatible // indirect
github.com/DataDog/datadog-go/v5 v5.0.2 // indirect
github.com/DataDog/go-tuf v0.3.0--fix-localmeta-fork // indirect
github.com/DataDog/gostackparse v0.5.0 // indirect
github.com/DataDog/sketches-go v1.2.1 // indirect
github.com/KyleBanks/depth v1.2.1 // indirect
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/cespare/xxhash/v2 v2.1.2 // indirect
github.com/beevik/etree v1.2.0 // indirect
github.com/bluenviron/mediacommon/v2 v2.2.0 // indirect
github.com/bytedance/sonic v1.13.2 // indirect
github.com/bytedance/sonic/loader v0.2.4 // indirect
github.com/cenkalti/backoff/v5 v5.0.2 // indirect
github.com/clbanning/mxj v1.8.4 // 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/clbanning/mxj/v2 v2.7.0 // indirect
github.com/cloudwego/base64x v0.1.5 // indirect
github.com/elastic/go-windows v1.0.2 // indirect
github.com/elgs/gostrgen v0.0.0-20161222160715-9d61ae07eeae // indirect
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
github.com/gin-contrib/sse v1.0.0 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // 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-stack/stack v1.8.0 // indirect
github.com/goccy/go-json v0.10.0 // indirect
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b // indirect
github.com/golang/protobuf v1.5.3 // 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.26.0 // indirect
github.com/goccy/go-json v0.10.5 // 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/joeshaw/multierror v0.0.0-20140124173710-69b34d4ec901 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect
github.com/icholy/digest v0.1.23 // 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/juju/errors v1.0.0 // indirect
github.com/klauspost/compress v1.16.7 // indirect
github.com/klauspost/cpuid v1.2.3 // indirect
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
github.com/kylelemons/go-gypsy v1.0.0 // indirect
github.com/leodido/go-urn v1.2.1 // 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/leodido/go-urn v1.4.0 // indirect
github.com/lib/pq v1.10.9 // indirect
github.com/mailru/easyjson v0.7.6 // indirect
github.com/mattn/go-isatty v0.0.20 // 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/philhofer/fwd v1.1.1 // indirect
github.com/pion/datachannel v1.5.5 // indirect
github.com/pion/dtls/v2 v2.1.5 // indirect
github.com/pion/ice/v2 v2.2.12 // indirect
github.com/pion/interceptor v0.1.11 // indirect
github.com/pion/logging v0.2.2 // indirect
github.com/pion/mdns v0.0.5 // indirect
github.com/montanaflynn/stats v0.7.1 // indirect
github.com/nxadm/tail v1.4.11 // indirect
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
github.com/pion/datachannel v1.5.10 // indirect
github.com/pion/dtls/v3 v3.0.6 // indirect
github.com/pion/ice/v4 v4.0.10 // indirect
github.com/pion/logging v0.2.3 // indirect
github.com/pion/mdns/v2 v2.0.7 // indirect
github.com/pion/randutil v0.1.0 // indirect
github.com/pion/rtcp v1.2.10 // 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
github.com/pion/stun v0.3.5 // indirect
github.com/pion/transport v0.14.1 // indirect
github.com/pion/turn/v2 v2.0.8 // indirect
github.com/pion/udp v0.1.1 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/prometheus/procfs v0.8.0 // indirect
github.com/richardartoul/molecule v1.0.1-0.20221107223329-32cfee06a052 // indirect
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/pion/rtcp v1.2.15 // indirect
github.com/pion/sctp v1.8.39 // indirect
github.com/pion/sdp/v3 v3.0.13 // indirect
github.com/pion/srtp/v3 v3.0.5 // indirect
github.com/pion/stun/v3 v3.0.0 // indirect
github.com/pion/transport/v3 v3.0.7 // indirect
github.com/pion/turn/v4 v4.0.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
github.com/wlynxg/anet v0.0.5 // 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
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect
github.com/xdg-go/scram v1.1.2 // indirect
github.com/xdg-go/stringprep v1.0.4 // indirect
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
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/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/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
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/otel/metric v1.36.0 // indirect
go.opentelemetry.io/otel/trace v1.36.0 // indirect
go.opentelemetry.io/proto/otlp v1.6.0 // indirect
golang.org/x/arch v0.16.0 // indirect
golang.org/x/crypto v0.38.0 // indirect
golang.org/x/net v0.40.0 // indirect
golang.org/x/oauth2 v0.30.0 // indirect
golang.org/x/sync v0.14.0 // indirect
golang.org/x/sys v0.33.0 // indirect
golang.org/x/text v0.25.0 // indirect
golang.org/x/tools v0.30.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237 // indirect
google.golang.org/grpc v1.72.1 // indirect
google.golang.org/protobuf v1.36.6 // 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
)

File diff suppressed because it is too large Load Diff

View File

@@ -3,53 +3,70 @@ package main
import (
"context"
"flag"
"fmt"
"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"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
"go.opentelemetry.io/otel/sdk/resource"
"go.opentelemetry.io/otel/sdk/trace"
semconv "go.opentelemetry.io/otel/semconv/v1.4.0"
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"
"gopkg.in/DataDog/dd-trace-go.v1/profiler"
)
var VERSION = "3.0.0"
var VERSION = utils.VERSION
func startTracing(agentKey string, otelEndpoint string) (*trace.TracerProvider, error) {
serviceName := "agent-" + agentKey
headers := map[string]string{
"content-type": "application/json",
}
exporter, err := otlptrace.New(
context.Background(),
otlptracehttp.NewClient(
otlptracehttp.WithEndpoint("74.241.203.114:4318"),
otlptracehttp.WithHeaders(headers),
otlptracehttp.WithInsecure(),
),
)
if err != nil {
return nil, fmt.Errorf("creating new exporter: %w", err)
}
tracerprovider := trace.NewTracerProvider(
trace.WithBatcher(
exporter,
trace.WithMaxExportBatchSize(trace.DefaultMaxExportBatchSize),
trace.WithBatchTimeout(trace.DefaultScheduleDelay*time.Millisecond),
trace.WithMaxExportBatchSize(trace.DefaultMaxExportBatchSize),
),
trace.WithResource(
resource.NewWithAttributes(
semconv.SchemaURL,
semconv.ServiceNameKey.String(serviceName),
attribute.String("environment", "develop"),
),
),
)
otel.SetTracerProvider(tracerprovider)
return tracerprovider, nil
}
func main() {
// You might be interested in debugging the agent.
if os.Getenv("DATADOG_AGENT_ENABLED") == "true" {
if os.Getenv("DATADOG_AGENT_K8S_ENABLED") == "true" {
tracer.Start()
defer tracer.Stop()
} else {
service := os.Getenv("DATADOG_AGENT_SERVICE")
environment := os.Getenv("DATADOG_AGENT_ENVIRONMENT")
log.Log.Info("Starting Datadog Agent with service: " + service + " and environment: " + environment)
rules := []tracer.SamplingRule{tracer.RateRule(1)}
tracer.Start(
tracer.WithSamplingRules(rules),
tracer.WithService(service),
tracer.WithEnv(environment),
)
defer tracer.Stop()
err := profiler.Start(
profiler.WithService(service),
profiler.WithEnv(environment),
profiler.WithProfileTypes(
profiler.CPUProfile,
profiler.HeapProfile,
),
)
if err != nil {
log.Log.Fatal(err.Error())
}
defer profiler.Stop()
}
}
// Start the show ;)
// We'll parse the flags (named variables), and start the agent.
@@ -67,27 +84,44 @@ func main() {
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(configDirectory, 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":
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("Decrypting: " + flag.Arg(0) + " with key: " + flag.Arg(1))
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: symmetric key should not be empty")
log.Log.Fatal("main.Main(): symmetric key should not be empty")
return
}
if len(symmetricKey) != 32 {
log.Log.Fatal("Main: symmetric key should be 32 bytes")
log.Log.Fatal("main.Main(): symmetric key should be 32 bytes")
return
}
@@ -95,7 +129,7 @@ func main() {
case "run":
{
// Print Kerberos.io ASCII art
// Print Agent ASCII art
utils.PrintASCIIArt()
// Print the environment variables which include "AGENT_" as prefix.
@@ -108,12 +142,29 @@ func main() {
configuration.Name = name
configuration.Port = port
// Open this configuration either from Kerberos Agent or Kerberos Factory.
// Open this configuration either from Agent or Factory.
configService.OpenConfig(configDirectory, &configuration)
// We will override the configuration with the environment variables
configService.OverrideWithEnvironmentVariables(&configuration)
// Start OpenTelemetry tracing
if otelEndpoint := os.Getenv("OTEL_EXPORTER_OTLP_ENDPOINT"); otelEndpoint == "" {
log.Log.Info("main.Main(): No OpenTelemetry endpoint provided, skipping tracing")
} else {
log.Log.Info("main.Main(): Starting OpenTelemetry tracing with endpoint: " + otelEndpoint)
agentKey := configuration.Config.Key
traceProvider, err := startTracing(agentKey, otelEndpoint)
if err != nil {
log.Log.Error("traceprovider: " + err.Error())
}
defer func() {
if err := traceProvider.Shutdown(context.Background()); err != nil {
log.Log.Error("traceprovider: " + err.Error())
}
}()
}
// Printing final configuration
utils.PrintConfiguration(&configuration)
@@ -123,7 +174,7 @@ func main() {
// Set timezone
timezone, _ := time.LoadLocation(configuration.Config.Timezone)
log.Log.Init(configDirectory, timezone)
log.Log.Init(logLevel, logOutput, configDirectory, timezone)
// Check if we have a device Key or not, if not
// we will generate one.
@@ -132,9 +183,9 @@ func main() {
configuration.Config.Key = key
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)
}
}
@@ -142,18 +193,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(configDirectory, &configuration, &communication)
go components.Bootstrap(ctx, configDirectory, &configuration, &communication, &capture)
// Start the REST API.
routers.StartWebserver(configDirectory, &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

@@ -1 +0,0 @@
package api

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, otelContext context.Context) error
// Connect to a backchannel RTSP server.
ConnectBackChannel(ctx context.Context, otelContext context.Context) error
// Start the RTSP client, and start reading packets.
Start(ctx context.Context, streamType string, queue *packets.Queue, configuration *models.Configuration, communication *models.Communication) error
// Start the RTSP client, and start reading packets.
StartBackChannel(ctx context.Context, otelContext context.Context) 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(ctx context.Context) 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)
}

File diff suppressed because it is too large Load Diff

View File

@@ -3,19 +3,21 @@ 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/kerberos-io/agent/machinery/src/video"
"go.opentelemetry.io/otel/trace"
)
func CleanupRecordingDirectory(configDirectory string, configuration *models.Configuration) {
@@ -52,48 +54,70 @@ func CleanupRecordingDirectory(configDirectory string, configuration *models.Con
}
}
func HandleRecordStream(queue *pubsub.Queue, configDirectory string, 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.
preRecording := config.Capture.PreRecording * 1000
postRecording := config.Capture.PostRecording * 1000 // number of seconds to record.
maxRecordingPeriod := config.Capture.MaxLengthRecording * 1000 // maximum number of seconds to record.
// Synchronise the last synced time
now := time.Now().Unix()
now := time.Now().UnixMilli()
startRecording := now
timestamp := now
if config.FriendlyName != "" {
config.Name = config.FriendlyName
}
// Get the audio and video codec from the camera.
// We only expect one audio and one video codec.
// If there are multiple audio or video streams, we will use the first one.
audioCodec := ""
videoCodec := ""
audioStreams, _ := rtspClient.GetAudioStreams()
videoStreams, _ := rtspClient.GetVideoStreams()
if len(audioStreams) > 0 {
audioCodec = audioStreams[0].Name
config.Capture.IPCamera.SampleRate = audioStreams[0].SampleRate
config.Capture.IPCamera.Channels = audioStreams[0].Channels
}
if len(videoStreams) > 0 {
videoCodec = videoStreams[0].Name
}
// Check if continuous recording.
if config.Capture.Continuous == "true" {
// Do not do anything!
log.Log.Info("HandleRecordStream: Start continuous recording ")
//var cws *cacheWriterSeeker
var mp4Video *video.MP4
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
postRecording = maxRecordingPeriod
// Recording file name
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()
@@ -105,33 +129,55 @@ func HandleRecordStream(queue *pubsub.Queue, configDirectory string, configurati
nextPkt, cursorError = recordingCursor.ReadPacket()
now := time.Now().Unix()
now := time.Now().UnixMilli()
if start && // If already recording and current frame is a keyframe and we should stop recording
nextPkt.IsKeyFrame && (timestamp+recordingPeriod-now <= 0 || now-startRecording >= maxRecordingPeriod) {
nextPkt.IsKeyFrame && (timestamp+postRecording-now <= 0 || now-startRecording >= maxRecordingPeriod) {
// Write the last packet
if err := myMuxer.WritePacket(pkt); err != nil {
log.Log.Error(err.Error())
pts := convertPTS(pkt.TimeLegacy)
if pkt.IsVideo {
// Write the last packet
if err := mp4Video.AddSampleToTrack(videoTrack, pkt.IsKeyFrame, pkt.Data, pts); err != nil {
log.Log.Error("capture.main.HandleRecordStream(continuous): " + err.Error())
}
} else if pkt.IsAudio {
// Write the last packet
if pkt.Codec == "AAC" {
if err := mp4Video.AddSampleToTrack(audioTrack, pkt.IsKeyFrame, pkt.Data, pts); 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())
}
log.Log.Info("HandleRecordStream: Recording finished: file save: " + name)
// Close mp4
mp4Video.Close(&config)
log.Log.Info("capture.main.HandleRecordStream(continuous): recording finished: file save: " + name)
// Cleanup muxer
start = false
myMuxer.Close()
myMuxer = nil
file.Close()
file = nil
// Check if need to convert to fragmented using bento
if config.Capture.Fragmented == "true" && config.Capture.FragmentedDuration > 0 {
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.
@@ -147,29 +193,13 @@ func HandleRecordStream(queue *pubsub.Queue, configDirectory string, configurati
// 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
@@ -184,8 +214,9 @@ func HandleRecordStream(queue *pubsub.Queue, configDirectory string, configurati
// - Number of changes
// - Token
startRecording = time.Now().Unix() // we mark the current time when the record started.ss
s := strconv.FormatInt(startRecording, 10) + "_" +
startRecording = time.Now().UnixMilli()
startRecordingSeconds := startRecording / 1000 // convert to seconds
s := strconv.FormatInt(startRecordingSeconds, 10) + "_" +
"6" + "-" +
"967003" + "_" +
config.Name + "_" +
@@ -196,43 +227,71 @@ func HandleRecordStream(queue *pubsub.Queue, configDirectory string, configurati
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)
// Get width and height from the camera.
width := configuration.Config.Capture.IPCamera.Width
height := configuration.Config.Capture.IPCamera.Height
// Get SPS and PPS NALUs from the camera.
spsNALUS := configuration.Config.Capture.IPCamera.SPSNALUs
ppsNALUS := configuration.Config.Capture.IPCamera.PPSNALUs
vpsNALUS := configuration.Config.Capture.IPCamera.VPSNALUs
// Create a video file, and set the dimensions.
mp4Video = video.NewMP4(fullName, spsNALUS, ppsNALUS, vpsNALUS)
mp4Video.SetWidth(width)
mp4Video.SetHeight(height)
if videoCodec == "H264" {
videoTrack = mp4Video.AddVideoTrack("H264")
} else if videoCodec == "H265" {
videoTrack = mp4Video.AddVideoTrack("H265")
}
if audioCodec == "AAC" {
audioTrack = mp4Video.AddAudioTrack("AAC")
} else if audioCodec == "PCM_MULAW" {
log.Log.Debug("capture.main.HandleRecordStream(continuous): no AAC audio codec detected, skipping audio track.")
}
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())
pts := convertPTS(pkt.TimeLegacy)
if pkt.IsVideo {
if err := mp4Video.AddSampleToTrack(videoTrack, pkt.IsKeyFrame, pkt.Data, pts); err != nil {
log.Log.Error("capture.main.HandleRecordStream(continuous): " + err.Error())
}
} else if pkt.IsAudio {
if pkt.Codec == "AAC" {
if err := mp4Video.AddSampleToTrack(audioTrack, pkt.IsKeyFrame, pkt.Data, pts); 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..
// We might need to use ffmpeg to transcode the audio to AAC.
// For now we will skip the audio track.
log.Log.Debug("capture.main.HandleRecordStream(continuous): no AAC audio codec detected, skipping audio track.")
}
}
if err := myMuxer.WritePacket(pkt); err != nil {
log.Log.Error(err.Error())
}
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)
pts := convertPTS(pkt.TimeLegacy)
if pkt.IsVideo {
// New method using new mp4 library
if err := mp4Video.AddSampleToTrack(videoTrack, pkt.IsKeyFrame, pkt.Data, pts); err != nil {
log.Log.Error("capture.main.HandleRecordStream(continuous): " + err.Error())
}
} else if pkt.IsAudio {
if pkt.Codec == "AAC" {
if err := mp4Video.AddSampleToTrack(audioTrack, pkt.IsKeyFrame, pkt.Data, pts); 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.")
}
}
}
pkt = nextPkt
}
@@ -241,22 +300,30 @@ func HandleRecordStream(queue *pubsub.Queue, configDirectory string, configurati
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("capture.main.HandleRecordStream(continuous): Recording finished: file save: " + name)
log.Log.Info("HandleRecordStream: Recording finished: file save: " + name)
// Cleanup muxer
start = false
myMuxer.Close()
myMuxer = nil
file.Close()
file = nil
// Check if need to convert to fragmented using bento
if config.Capture.Fragmented == "true" && config.Capture.FragmentedDuration > 0 {
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.
@@ -264,35 +331,37 @@ func HandleRecordStream(queue *pubsub.Queue, configDirectory string, configurati
fc.Close()
recordingStatus = "idle"
// Clean up the recording directory if necessary.
CleanupRecordingDirectory(configDirectory, configuration)
}
}
} else {
log.Log.Info("HandleRecordStream: Start motion based recording ")
log.Log.Info("capture.main.HandleRecordStream(motiondetection): Start motion based recording ")
var myMuxer *mp4.Muxer
var file *os.File
var err error
var lastDuration int64 = 0 // last duration in milliseconds
var lastRecordingTime int64 = 0 // last recording time in milliseconds
var lastDuration time.Duration
var lastRecordingTime int64
var videoTrack uint32
var audioTrack uint32
for motion := range communication.HandleMotion {
timestamp = time.Now().Unix()
startRecording = time.Now().Unix() // we mark the current time when the record started.
timestamp = time.Now().UnixMilli()
startRecording = time.Now().UnixMilli() // we mark the current time when the record started.
numberOfChanges := motion.NumberOfChanges
// If we have prerecording we will substract the number of seconds.
// Taking into account FPS = GOP size (Keyfram interval)
if config.Capture.PreRecording > 0 {
if preRecording > 0 && lastRecordingTime > 0 {
// Might be that recordings are coming short after each other.
// Therefore we do some math with the current time and the last recording time.
timeBetweenNowAndLastRecording := startRecording - lastRecordingTime
if timeBetweenNowAndLastRecording > int64(config.Capture.PreRecording) {
startRecording = startRecording - int64(config.Capture.PreRecording) + 1
if timeBetweenNowAndLastRecording > preRecording {
startRecording = startRecording - preRecording + 1000 // we add 1000 milliseconds to make sure we have a full second of pre-recording.
} else {
startRecording = startRecording - timeBetweenNowAndLastRecording
}
@@ -306,8 +375,8 @@ func HandleRecordStream(queue *pubsub.Queue, configDirectory string, configurati
// - Region
// - Number of changes
// - Token
s := strconv.FormatInt(startRecording, 10) + "_" +
startRecordingSeconds := startRecording / 1000 // convert to seconds
s := strconv.FormatInt(startRecordingSeconds, 10) + "_" +
"6" + "-" +
"967003" + "_" +
config.Name + "_" +
@@ -319,26 +388,40 @@ func HandleRecordStream(queue *pubsub.Queue, configDirectory string, configurati
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")
// Get width and height from the camera.
width := configuration.Config.Capture.IPCamera.Width
height := configuration.Config.Capture.IPCamera.Height
// Get SPS and PPS NALUs from the camera.
spsNALUS := configuration.Config.Capture.IPCamera.SPSNALUs
ppsNALUS := configuration.Config.Capture.IPCamera.PPSNALUs
vpsNALUS := configuration.Config.Capture.IPCamera.VPSNALUs
// Create a video file, and set the dimensions.
mp4Video := video.NewMP4(fullName, spsNALUS, ppsNALUS, vpsNALUS)
mp4Video.SetWidth(width)
mp4Video.SetHeight(height)
if videoCodec == "H264" {
videoTrack = mp4Video.AddVideoTrack("H264")
} else if videoCodec == "H265" {
videoTrack = mp4Video.AddVideoTrack("H265")
}
if audioCodec == "AAC" {
audioTrack = mp4Video.AddAudioTrack("AAC")
} else if audioCodec == "PCM_MULAW" {
log.Log.Debug("capture.main.HandleRecordStream(continuous): no AAC audio codec detected, skipping audio track.")
}
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()
@@ -348,39 +431,47 @@ func HandleRecordStream(queue *pubsub.Queue, configDirectory string, configurati
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()
now := time.Now().UnixMilli()
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))
if (timestamp+postRecording-now < 0 || now-startRecording > maxRecordingPeriod-1000) && nextPkt.IsKeyFrame {
log.Log.Info("capture.main.HandleRecordStream(motiondetection): closing recording (timestamp: " + strconv.FormatInt(timestamp, 10) + ", postRecording: " + strconv.FormatInt(postRecording, 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")
if pkt.IsKeyFrame && !start && (pkt.Time >= lastDuration || pkt.Time == 0) {
// We start the recording if we have a keyframe and the last duration is 0 or less than the current packet time.
// It could be start we start from the beginning of the recording.
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())
}
// 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)
pts := convertPTS(pkt.TimeLegacy)
if pkt.IsVideo {
log.Log.Debug("capture.main.HandleRecordStream(motiondetection): add video sample")
if err := mp4Video.AddSampleToTrack(videoTrack, pkt.IsKeyFrame, pkt.Data, pts); err != nil {
log.Log.Error("capture.main.HandleRecordStream(motiondetection): " + err.Error())
}
} else if pkt.IsAudio {
log.Log.Debug("capture.main.HandleRecordStream(motiondetection): add audio sample")
if pkt.Codec == "AAC" {
if err := mp4Video.AddSampleToTrack(audioTrack, pkt.IsKeyFrame, pkt.Data, pts); 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..
// We might need to use ffmpeg to transcode the audio to AAC.
// For now we will skip the audio track.
log.Log.Debug("capture.main.HandleRecordStream(motiondetection): no AAC audio codec detected, skipping audio track.")
}
}
}
@@ -388,23 +479,12 @@ func HandleRecordStream(queue *pubsub.Queue, configDirectory string, configurati
pkt = nextPkt
}
// This will write the trailer as well.
myMuxer.WriteTrailerWithPacket(nextPkt)
log.Log.Info("HandleRecordStream: file save: " + name)
// This will close the recording and write the last packet.
mp4Video.Close(&config)
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
// Check if need to convert to fragmented using bento
if config.Capture.Fragmented == "true" && config.Capture.FragmentedDuration > 0 {
utils.CreateFragmentedMP4(fullName, config.Capture.FragmentedDuration)
}
lastRecordingTime = time.Now().UnixMilli()
// Check if we need to encrypt the recording.
if config.Encryption != nil && config.Encryption.Enabled == "true" && config.Encryption.Recordings == "true" && config.Encryption.SymmetricKey != "" {
@@ -417,13 +497,13 @@ func HandleRecordStream(queue *pubsub.Queue, configDirectory string, configurati
// write back to file
err := os.WriteFile(fullName, []byte(encryptedContents), 0644)
if err != nil {
log.Log.Error("HandleRecordStream: error writing file: " + err.Error())
log.Log.Error("capture.main.HandleRecordStream(motiondetection): error writing file: " + err.Error())
}
} else {
log.Log.Error("HandleRecordStream: error encrypting file: " + err.Error())
log.Log.Error("capture.main.HandleRecordStream(motiondetection): error encrypting file: " + err.Error())
}
} else {
log.Log.Error("HandleRecordStream: error reading file: " + err.Error())
log.Log.Error("capture.main.HandleRecordStream(motiondetection): error reading file: " + err.Error())
}
}
@@ -436,7 +516,7 @@ func HandleRecordStream(queue *pubsub.Queue, configDirectory string, configurati
}
}
log.Log.Debug("HandleRecordStream: finished")
log.Log.Debug("capture.main.HandleRecordStream(): finished")
}
}
@@ -451,6 +531,10 @@ func HandleRecordStream(queue *pubsub.Queue, configDirectory string, configurati
// @Success 200 {object} models.APIResponse
func VerifyCamera(c *gin.Context) {
// Start OpenTelemetry tracing
ctxVerifyCamera, span := tracer.Start(context.Background(), "VerifyCamera", trace.WithSpanKind(trace.SpanKindServer))
defer span.End()
var cameraStreams models.CameraStreams
err := c.BindJSON(&cameraStreams)
@@ -469,30 +553,45 @@ 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, ctxVerifyCamera)
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(ctxVerifyCamera)
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(),
@@ -504,3 +603,98 @@ 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
// Try for 3 times in a row.
count := 0
for count < 3 {
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
} else {
count++
continue
}
}
} 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
// Try for 3 times in a row.
count := 0
for count < 3 {
if queue != nil && cursor != nil && rtspClient != nil {
pkt, err := cursor.ReadPacket()
if err == nil {
if !pkt.IsKeyFrame {
continue
}
image, err = (*rtspClient).DecodePacket(pkt)
if err != nil {
count++
continue
} else {
break
}
}
} else {
break
}
}
return image
}
func convertPTS(v time.Duration) uint64 {
return uint64(v.Milliseconds())
}
/*func convertPTS2(v int64) uint64 {
return uint64(v) / 100
}*/

File diff suppressed because it is too large Load Diff

View File

@@ -1,105 +0,0 @@
package cloud
import (
"crypto/tls"
"errors"
"io/ioutil"
"net/http"
"os"
"github.com/kerberos-io/agent/machinery/src/log"
"github.com/kerberos-io/agent/machinery/src/models"
)
func UploadKerberosVault(configuration *models.Configuration, fileName string) (bool, bool, error) {
config := configuration.Config
if config.KStorage.AccessKey == "" ||
config.KStorage.SecretAccessKey == "" ||
config.KStorage.Directory == "" ||
config.KStorage.URI == "" {
err := "UploadKerberosVault: Kerberos Vault not properly configured."
log.Log.Info(err)
return false, false, errors.New(err)
}
// timestamp_microseconds_instanceName_regionCoordinates_numberOfChanges_token
// 1564859471_6-474162_oprit_577-283-727-375_1153_27.mp4
// - Timestamp
// - Size + - + microseconds
// - device
// - Region
// - Number of changes
// - Token
// KerberosCloud, this means storage is disabled and proxy enabled.
log.Log.Info("UploadKerberosVault: Uploading to Kerberos Vault (" + config.KStorage.URI + ")")
log.Log.Info("UploadKerberosVault: Upload started for " + fileName)
fullname := "data/recordings/" + fileName
file, err := os.OpenFile(fullname, os.O_RDWR, 0755)
if file != nil {
defer file.Close()
}
if err != nil {
err := "UploadKerberosVault: Upload Failed, file doesn't exists anymore."
log.Log.Info(err)
return false, false, errors.New(err)
}
publicKey := config.KStorage.CloudKey
// This is the new way ;)
if config.HubKey != "" {
publicKey = config.HubKey
}
req, err := http.NewRequest("POST", config.KStorage.URI+"/storage", file)
if err != nil {
errorMessage := "UploadKerberosVault: error reading request, " + config.KStorage.URI + "/storage: " + err.Error()
log.Log.Error(errorMessage)
return false, true, errors.New(errorMessage)
}
req.Header.Set("Content-Type", "video/mp4")
req.Header.Set("X-Kerberos-Storage-CloudKey", publicKey)
req.Header.Set("X-Kerberos-Storage-AccessKey", config.KStorage.AccessKey)
req.Header.Set("X-Kerberos-Storage-SecretAccessKey", config.KStorage.SecretAccessKey)
req.Header.Set("X-Kerberos-Storage-Provider", config.KStorage.Provider)
req.Header.Set("X-Kerberos-Storage-FileName", fileName)
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)
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 {
defer resp.Body.Close()
}
if err == nil {
if resp != nil {
body, err := ioutil.ReadAll(resp.Body)
if err == nil {
if resp.StatusCode == 200 {
log.Log.Info("UploadKerberosVault: Upload Finished, " + resp.Status + ", " + string(body))
return true, true, nil
} else {
log.Log.Info("UploadKerberosVault: Upload Failed, " + resp.Status + ", " + string(body))
return false, true, nil
}
}
}
}
errorMessage := "UploadKerberosVault: Upload Failed, " + err.Error()
log.Log.Info(errorMessage)
return false, true, errors.New(errorMessage)
}

View File

@@ -0,0 +1,194 @@
package cloud
import (
"crypto/tls"
"errors"
"io"
"net/http"
"os"
"time"
"github.com/kerberos-io/agent/machinery/src/log"
"github.com/kerberos-io/agent/machinery/src/models"
)
// We will count the number of retries we have done.
// If we have done more than "kstorageRetryPolicy" retries, we will stop, and start sending to the secondary storage.
var kstorageRetryCount = 0
var kstorageRetryTimeout = time.Now().Unix()
func UploadKerberosVault(configuration *models.Configuration, fileName string) (bool, bool, error) {
config := configuration.Config
if config.KStorage.AccessKey == "" ||
config.KStorage.SecretAccessKey == "" ||
config.KStorage.Directory == "" ||
config.KStorage.URI == "" {
err := "UploadKerberosVault: Kerberos Vault not properly configured"
log.Log.Info(err)
return false, false, errors.New(err)
}
// timestamp_microseconds_instanceName_regionCoordinates_numberOfChanges_token
// 1564859471_6-474162_oprit_577-283-727-375_1153_27.mp4
// - Timestamp
// - Size + - + microseconds
// - device
// - Region
// - Number of changes
// - Token
// KerberosCloud, this means storage is disabled and proxy enabled.
log.Log.Info("UploadKerberosVault: Uploading to Kerberos Vault (" + config.KStorage.URI + ")")
log.Log.Info("UploadKerberosVault: Upload started for " + fileName)
fullname := "data/recordings/" + fileName
file, err := os.OpenFile(fullname, os.O_RDWR, 0755)
if file != nil {
defer file.Close()
}
if err != nil {
err := "UploadKerberosVault: Upload Failed, file doesn't exists anymore"
log.Log.Info(err)
return false, false, errors.New(err)
}
publicKey := config.KStorage.CloudKey
if config.HubKey != "" {
publicKey = config.HubKey
}
// We need to check if we are in a retry timeout.
if kstorageRetryTimeout <= time.Now().Unix() {
req, err := http.NewRequest("POST", config.KStorage.URI+"/storage", file)
if err != nil {
errorMessage := "UploadKerberosVault: error reading request, " + config.KStorage.URI + "/storage: " + err.Error()
log.Log.Error(errorMessage)
return false, true, errors.New(errorMessage)
}
req.Header.Set("Content-Type", "video/mp4")
req.Header.Set("X-Kerberos-Storage-CloudKey", publicKey)
req.Header.Set("X-Kerberos-Storage-AccessKey", config.KStorage.AccessKey)
req.Header.Set("X-Kerberos-Storage-SecretAccessKey", config.KStorage.SecretAccessKey)
req.Header.Set("X-Kerberos-Storage-Provider", config.KStorage.Provider)
req.Header.Set("X-Kerberos-Storage-FileName", fileName)
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)
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 {
defer resp.Body.Close()
}
if err == nil {
if resp != nil {
body, err := io.ReadAll(resp.Body)
if err == nil {
if resp.StatusCode == 200 {
kstorageRetryCount = 0
log.Log.Info("UploadKerberosVault: Upload Finished, " + resp.Status + ", " + string(body))
return true, true, nil
} else {
// We increase the retry count, and set the timeout.
// If we have reached the retry policy, we set the timeout.
// This means we will not retry for the next 5 minutes.
if kstorageRetryCount < config.KStorage.MaxRetries {
kstorageRetryCount = (kstorageRetryCount + 1)
}
if kstorageRetryCount == config.KStorage.MaxRetries {
kstorageRetryTimeout = time.Now().Add(time.Duration(config.KStorage.Timeout) * time.Second).Unix()
}
log.Log.Info("UploadKerberosVault: Upload Failed, " + resp.Status + ", " + string(body))
}
}
}
} else {
log.Log.Info("UploadKerberosVault: Upload Failed, " + err.Error())
}
}
// We might need to check if we can upload to our secondary storage.
if config.KStorageSecondary.AccessKey == "" ||
config.KStorageSecondary.SecretAccessKey == "" ||
config.KStorageSecondary.Directory == "" ||
config.KStorageSecondary.URI == "" {
log.Log.Info("UploadKerberosVault (Secondary): Secondary Kerberos Vault not properly configured.")
} else {
if kstorageRetryCount < config.KStorage.MaxRetries {
log.Log.Info("UploadKerberosVault (Secondary): Do not upload to secondary storage, we are still in retry policy.")
return false, true, nil
}
log.Log.Info("UploadKerberosVault (Secondary): Uploading to Secondary Kerberos Vault (" + config.KStorageSecondary.URI + ")")
file, err = os.OpenFile(fullname, os.O_RDWR, 0755)
if file != nil {
defer file.Close()
}
if err != nil {
err := "UploadKerberosVault (Secondary): Upload Failed, file doesn't exists anymore"
log.Log.Info(err)
return false, false, errors.New(err)
}
req, err := http.NewRequest("POST", config.KStorageSecondary.URI+"/storage", file)
if err != nil {
errorMessage := "UploadKerberosVault (Secondary): error reading request, " + config.KStorageSecondary.URI + "/storage: " + err.Error()
log.Log.Error(errorMessage)
return false, true, errors.New(errorMessage)
}
req.Header.Set("Content-Type", "video/mp4")
req.Header.Set("X-Kerberos-Storage-CloudKey", publicKey)
req.Header.Set("X-Kerberos-Storage-AccessKey", config.KStorageSecondary.AccessKey)
req.Header.Set("X-Kerberos-Storage-SecretAccessKey", config.KStorageSecondary.SecretAccessKey)
req.Header.Set("X-Kerberos-Storage-Provider", config.KStorageSecondary.Provider)
req.Header.Set("X-Kerberos-Storage-FileName", fileName)
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.KStorageSecondary.Directory)
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 {
defer resp.Body.Close()
}
if err == nil {
if resp != nil {
body, err := io.ReadAll(resp.Body)
if err == nil {
if resp.StatusCode == 200 {
log.Log.Info("UploadKerberosVault (Secondary): Upload Finished to secondary, " + resp.Status + ", " + string(body))
return true, true, nil
} else {
log.Log.Info("UploadKerberosVault (Secondary): Upload Failed to secondary, " + resp.Status + ", " + string(body))
}
}
}
}
}
return false, true, nil
}

View File

@@ -2,14 +2,14 @@ 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"
"go.opentelemetry.io/otel"
"github.com/kerberos-io/agent/machinery/src/capture"
"github.com/kerberos-io/agent/machinery/src/cloud"
@@ -18,14 +18,20 @@ import (
"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(configDirectory string, configuration *models.Configuration, communication *models.Communication) {
log.Log.Debug("Bootstrap: started")
var tracer = otel.Tracer("github.com/kerberos-io/agent/machinery/src/components")
func Bootstrap(ctx context.Context, configDirectory string, configuration *models.Configuration, communication *models.Communication, captureDevice *capture.Capture) {
log.Log.Debug("components.Kerberos.Bootstrap(): bootstrapping the kerberos agent.")
bootstrapContext := context.Background()
_, span := tracer.Start(bootstrapContext, "Bootstrap")
// We will keep track of the Kerberos Agent up time
// This is send to Kerberos Hub in a heartbeat.
@@ -37,12 +43,20 @@ func Bootstrap(configDirectory string, configuration *models.Configuration, comm
packageCounter.Store(int64(0))
communication.PackageCounter = &packageCounter
var packageCounterSub atomic.Value
packageCounterSub.Store(int64(0))
communication.PackageCounterSub = &packageCounterSub
// This is used when the last packet was received (timestamp),
// this metric is used to determine if the camera is still online/connected.
var lastPacketTimer atomic.Value
packageCounter.Store(int64(0))
communication.LastPacketTimer = &lastPacketTimer
var lastPacketTimerSub atomic.Value
packageCounterSub.Store(int64(0))
communication.LastPacketTimerSub = &lastPacketTimerSub
// This is used to understand if we have a working Kerberos Hub connection
// cloudTimestamp will be updated when successfully sending heartbeats.
var cloudTimestamp atomic.Value
@@ -56,18 +70,14 @@ func Bootstrap(configDirectory string, configuration *models.Configuration, comm
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)
@@ -75,15 +85,19 @@ func Bootstrap(configDirectory string, configuration *models.Configuration, comm
// Configure a MQTT client which helps for a bi-directional communication
mqttClient := routers.ConfigureMQTT(configDirectory, configuration, communication)
span.End()
// 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(configDirectory, 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" {
@@ -105,258 +119,320 @@ func Bootstrap(configDirectory string, configuration *models.Configuration, comm
communication.Context = &ctx
communication.CancelContext = &cancel
}
log.Log.Debug("Bootstrap: finished")
}
func RunAgent(configDirectory string, 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")
ctx := context.Background()
ctxRunAgent, span := tracer.Start(ctx, "RunAgent")
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(ctx, ctxRunAgent)
if err != nil {
log.Log.Error("components.Kerberos.RunAgent(): error connecting to RTSP stream: " + err.Error())
rtspClient.Close(ctxRunAgent)
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()
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(ctxRunAgent)
time.Sleep(time.Second * 3)
return status
}
// Get the video stream from the RTSP server.
videoStream := videoStreams[0]
// 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 *pubsub.Queue
var subQueue *pubsub.Queue
// Set the SPS and PPS values in the configuration.
configuration.Config.Capture.IPCamera.SPSNALUs = [][]byte{videoStream.SPS}
configuration.Config.Capture.IPCamera.PPSNALUs = [][]byte{videoStream.PPS}
configuration.Config.Capture.IPCamera.VPSNALUs = [][]byte{videoStream.VPS}
var decoderMutex sync.Mutex
var subDecoderMutex sync.Mutex
// Define queues for the main and sub stream.
var queue *packets.Queue
var subQueue *packets.Queue
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
}
width := videoStream.(av.VideoCodecData).Width()
height := videoStream.(av.VideoCodecData).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 || cameraSettings.Num != num || cameraSettings.Denum != denum || cameraSettings.Codec != videoStream.(av.VideoCodecData).Type() {
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("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.RequestHDStreamPayload, 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, configDirectory, configuration, communication, streams)
// Handle Upload to cloud provider (Kerberos Hub, Kerberos Vault and others)
go cloud.HandleUpload(configDirectory, 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!
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"
if subStreamEnabled {
communication.HandleSubStream <- "stop"
}
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)
// 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))
}
log.Log.Debug("RunAgent: finished")
// 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
// Clean up, force garbage collection
runtime.GC()
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(ctx, ctxRunAgent)
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: " + subRtspUrl)
// 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(ctxRunAgent)
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.SubWidth = width
configuration.Config.Capture.IPCamera.SubHeight = height
}
// 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(ctx, "main", queue, configuration, communication)
// Main stream is connected and ready to go.
communication.MainStreamConnected = true
// Try to create backchannel
rtspBackChannelClient := captureDevice.SetBackChannelClient(rtspUrl)
err = rtspBackChannelClient.ConnectBackChannel(ctx, ctxRunAgent)
if err == nil {
log.Log.Info("components.Kerberos.RunAgent(): opened RTSP backchannel stream: " + rtspUrl)
go rtspBackChannelClient.StartBackChannel(ctx, ctxRunAgent)
}
rtspSubClient := captureDevice.RTSPSubClient
if subStreamEnabled && rtspSubClient != nil {
subQueue = packets.NewQueue()
communication.SubQueue = subQueue
subQueue.SetMaxGopCount(3) // GOP time frame is set to prerecording (we'll add 2 gops to leave some room).
subQueue.WriteHeader(videoSubStreams)
go rtspSubClient.Start(ctx, "sub", subQueue, configuration, communication)
// Sub stream is connected and ready to go.
communication.SubStreamConnected = true
}
// 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, 10)
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, 10)
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 realtime processing if enabled.
if subStreamEnabled {
realtimeProcessingCursor := subQueue.Latest()
go cloud.HandleRealtimeProcessing(realtimeProcessingCursor, configuration, communication, mqttClient, rtspClient)
} else {
realtimeProcessingCursor := queue.Latest()
go cloud.HandleRealtimeProcessing(realtimeProcessingCursor, 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, 10)
go onvif.HandleONVIFActions(configuration, communication)
communication.HandleAudio = make(chan models.AudioDataPartial, 10)
if rtspBackChannelClient.HasBackChannel {
communication.HasBackChannel = true
go WriteAudioToBackchannel(communication, rtspBackChannelClient)
}
// If we reach this point, we have a working RTSP connection.
communication.CameraConnected = true
// Otel end span
span.End()
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
// 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
communication.MainStreamConnected = false
communication.SubStreamConnected = 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" {
select {
case communication.HandleUpload <- "stop":
log.Log.Info("components.Kerberos.RunAgent(): stopping upload")
case <-time.After(1 * time.Second):
log.Log.Info("components.Kerberos.RunAgent(): stopping upload timed out")
}
}
select {
case communication.HandleStream <- "stop":
log.Log.Info("components.Kerberos.RunAgent(): stopping stream")
case <-time.After(1 * time.Second):
log.Log.Info("components.Kerberos.RunAgent(): stopping stream timed out")
}
// 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(ctxRunAgent)
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(ctxRunAgent)
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(ctxRunAgent)
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
packageSubCounter := communication.PackageCounterSub
go func() {
// A channel to check the camera activity
var previousPacket int64 = 0
var previousPacketSub int64 = 0
var occurence = 0
var occurenceSub = 0
for {
// If camera is connected, we'll check if we are still receiving packets.
if communication.CameraConnected {
// First we'll check the main stream.
packetsR := packageCounter.Load().(int64)
if packetsR == previousPacket {
// If we are already reconfiguring,
@@ -368,20 +444,313 @@ 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 from mainstream: " + strconv.FormatInt(packetsR, 10))
// After 15 seconds without activity this is thrown..
if occurence == 3 {
log.Log.Info("Main: Restarting machinery.")
communication.HandleBootstrap <- "restart"
time.Sleep(2 * time.Second)
log.Log.Info("components.Kerberos.ControlAgent(): Restarting machinery because of blocking mainstream.")
select {
case communication.HandleBootstrap <- "restart":
log.Log.Info("components.Kerberos.ControlAgent(): Restarting machinery because of blocking substream.")
case <-time.After(1 * time.Second):
log.Log.Info("components.Kerberos.ControlAgent(): Restarting machinery because of blocking substream timed out")
}
occurence = 0
}
// Now we'll check the sub stream.
packetsSubR := packageSubCounter.Load().(int64)
if communication.SubStreamConnected {
if packetsSubR == previousPacketSub {
// If we are already reconfiguring,
// we dont need to check if the stream is blocking.
if !communication.IsConfiguring.IsSet() {
occurenceSub = occurenceSub + 1
}
} else {
occurenceSub = 0
}
log.Log.Info("components.Kerberos.ControlAgent(): Number of packets read from substream: " + strconv.FormatInt(packetsSubR, 10))
// After 15 seconds without activity this is thrown..
if occurenceSub == 3 {
select {
case communication.HandleBootstrap <- "restart":
log.Log.Info("components.Kerberos.ControlAgent(): Restarting machinery because of blocking substream.")
case <-time.After(1 * time.Second):
log.Log.Info("components.Kerberos.ControlAgent(): Restarting machinery because of blocking substream timed out")
}
occurenceSub = 0
}
}
previousPacket = packageCounter.Load().(int64)
previousPacketSub = packageSubCounter.Load().(int64)
}
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).")
select {
case communication.HandleBootstrap <- "stop":
log.Log.Info("components.Kerberos.StopAgent(): Stopping machinery.")
case <-time.After(1 * time.Second):
log.Log.Info("components.Kerberos.StopAgent(): Stopping machinery timed out")
}
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.")
select {
case communication.HandleBootstrap <- "restart":
log.Log.Info("components.Kerberos.RestartAgent(): Restarting machinery.")
case <-time.After(1 * time.Second):
log.Log.Info("components.Kerberos.RestartAgent(): Restarting machinery timed out")
}
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,95 @@
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
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,33 +30,30 @@ 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.")
hubKey := config.HubKey
deviceKey := config.Key
// Allocate a VideoFrame
frame := ffmpeg.AllocVideoFrame()
// Initialise first 2 elements
var imageArray [3]*image.Gray
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++
}
}
@@ -70,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 {
@@ -108,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()
@@ -121,81 +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 != "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(),
// 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)
}
payload, err := models.PackageMQTTMessage(configuration, message)
if err == nil {
mqttClient.Publish("kerberos/hub/"+hubKey, 2, false, payload)
} else {
log.Log.Info("computervision.main.ProcessMotion(): failed to package MQTT message: " + err.Error())
}
} else {
log.Log.Info("ProcessMotion: failed to package MQTT message: " + err.Error())
mqttClient.Publish("kerberos/agent/"+deviceKey, 2, false, "motion")
}
} 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
}
}
@@ -209,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) {
@@ -225,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++ {
@@ -260,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

@@ -4,11 +4,9 @@ import (
"context"
"encoding/json"
"errors"
"image"
"io/ioutil"
"os"
"reflect"
"sort"
"strconv"
"strings"
"time"
@@ -20,25 +18,6 @@ import (
"go.mongodb.org/mongo-driver/bson"
)
func GetImageFromFilePath(configDirectory string) (image.Image, error) {
snapshotDirectory := configDirectory + "/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 := configDirectory + "/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.
@@ -80,7 +59,7 @@ func OpenConfig(configDirectory string, configuration *models.Configuration) {
// Write to mongodb
client := database.New()
db := client.Database(database.DatabaseName)
db := client.Client.Database(database.DatabaseName)
collection := db.Collection("configuration")
var globalConfig models.Config
@@ -156,6 +135,12 @@ func OpenConfig(configDirectory string, configuration *models.Configuration) {
conjungo.Merge(&kerberosvault, configuration.CustomConfig.KStorage, opts)
configuration.Config.KStorage = &kerberosvault
// Merge Secondary Kerberos Vault settings
var kerberosvaultSecondary models.KStorage
conjungo.Merge(&kerberosvaultSecondary, configuration.GlobalConfig.KStorageSecondary, opts)
conjungo.Merge(&kerberosvaultSecondary, configuration.CustomConfig.KStorageSecondary, opts)
configuration.Config.KStorageSecondary = &kerberosvaultSecondary
// Merge Kerberos S3 settings
var s3 models.S3
conjungo.Merge(&s3, configuration.GlobalConfig.S3, opts)
@@ -204,15 +189,19 @@ func OpenConfig(configDirectory string, configuration *models.Configuration) {
}
jsonFile.Close()
}
}
return
}
// This function will override the configuration with environment variables.
func OverrideWithEnvironmentVariables(configuration *models.Configuration) {
environmentVariables := os.Environ()
// Initialize the configuration for some new fields.
if configuration.Config.KStorageSecondary == nil {
configuration.Config.KStorageSecondary = &models.KStorage{}
}
for _, env := range environmentVariables {
if strings.Contains(env, "AGENT_") {
key := strings.Split(env, "=")[0]
@@ -403,10 +392,21 @@ func OverrideWithEnvironmentVariables(configuration *models.Configuration) {
configuration.Config.MQTTPassword = value
break
/* Real-time streaming of keyframes to a MQTT topic */
case "AGENT_REALTIME_PROCESSING":
configuration.Config.RealtimeProcessing = value
break
case "AGENT_REALTIME_PROCESSING_TOPIC":
configuration.Config.RealtimeProcessingTopic = value
break
/* WebRTC settings for live-streaming (remote) */
case "AGENT_STUN_URI":
configuration.Config.STUNURI = value
break
case "AGENT_FORCE_TURN":
configuration.Config.ForceTurn = value
break
case "AGENT_TURN_URI":
configuration.Config.TURNURI = value
break
@@ -427,6 +427,9 @@ func OverrideWithEnvironmentVariables(configuration *models.Configuration) {
break
/* When connected and storing in Kerberos Hub (SAAS) */
case "AGENT_HUB_ENCRYPTION":
configuration.Config.HubEncryption = value
break
case "AGENT_HUB_URI":
configuration.Config.HubURI = value
break
@@ -443,7 +446,7 @@ func OverrideWithEnvironmentVariables(configuration *models.Configuration) {
configuration.Config.S3.Region = value
break
/* When storing in a Kerberos Vault */
/* When storing in a Vault */
case "AGENT_KERBEROSVAULT_URI":
configuration.Config.KStorage.URI = value
break
@@ -460,6 +463,37 @@ func OverrideWithEnvironmentVariables(configuration *models.Configuration) {
configuration.Config.KStorage.Directory = value
break
/* Retry policy and timeout */
case "AGENT_KERBEROSVAULT_MAX_RETRIES":
maxRetries, err := strconv.Atoi(value)
if err == nil {
configuration.Config.KStorage.MaxRetries = maxRetries
}
break
case "AGENT_KERBEROSVAULT_TIMEOUT":
timeout, err := strconv.Atoi(value)
if err == nil {
configuration.Config.KStorage.Timeout = timeout
}
break
/* When storing in a secondary Vault */
case "AGENT_KERBEROSVAULT_SECONDARY_URI":
configuration.Config.KStorageSecondary.URI = value
break
case "AGENT_KERBEROSVAULT_SECONDARY_ACCESS_KEY":
configuration.Config.KStorageSecondary.AccessKey = value
break
case "AGENT_KERBEROSVAULT_SECONDARY_SECRET_KEY":
configuration.Config.KStorageSecondary.SecretAccessKey = value
break
case "AGENT_KERBEROSVAULT_SECONDARY_PROVIDER":
configuration.Config.KStorageSecondary.Provider = value
break
case "AGENT_KERBEROSVAULT_SECONDARY_DIRECTORY":
configuration.Config.KStorageSecondary.Directory = value
break
/* When storing in dropbox */
case "AGENT_DROPBOX_ACCESS_TOKEN":
configuration.Config.Dropbox.AccessToken = value
@@ -479,14 +513,32 @@ func OverrideWithEnvironmentVariables(configuration *models.Configuration) {
configuration.Config.Encryption.Fingerprint = value
break
case "AGENT_ENCRYPTION_PRIVATE_KEY":
configuration.Config.Encryption.PrivateKey = value
encryptionPrivateKey := strings.ReplaceAll(value, "\\n", "\n")
configuration.Config.Encryption.PrivateKey = encryptionPrivateKey
break
case "AGENT_ENCRYPTION_SYMMETRIC_KEY":
configuration.Config.Encryption.SymmetricKey = value
break
/* When signing is enabled */
case "AGENT_SIGNING":
configuration.Config.Signing.Enabled = value
break
case "AGENT_SIGNING_PRIVATE_KEY":
signingPrivateKey := strings.ReplaceAll(value, "\\n", "\n")
configuration.Config.Signing.PrivateKey = signingPrivateKey
break
}
}
}
// Signing is a new feature, so if empty we set default values.
if configuration.Config.Signing == nil || configuration.Config.Signing.PrivateKey == "" {
configuration.Config.Signing = &models.Signing{
Enabled: "true",
PrivateKey: "-----BEGIN PRIVATE KEY-----\nMIIJQgIBADANBgkqhkiG9w0BAQEFAASCCSwwggkoAgEAAoICAQDoSxjyw08lRxF4Yoqmcaewjq3XjB55dMy4tlN5MGLdr8aAPuNR9Mwh3jlh1bDpwQXNgZkHDV/q9bpdPGGi7SQo2xw+rDuo5Y1f3wdzz+iuCTPbzoGFalE+1PZlU5TEtUtlbt7MRc4pxTaLP3u0P3EtW3KnzcUarcJWZJYxzv7gqVNCA/47BN+1ptqjwz3LAlah5yaftEvVjkaANOsafUswbS4VT44XfSlbKgebORCKDuNgQiyhuV5gU+J0TOaqRWwwMAWV0UoScyJLfhHRBCrUwrCUTwqH9jfkB7pgRFsYoZJd4MKMeHJjFSum+QXCBqInSnwu8c2kJChiLMWqJ+mhpTdfUAmSkeUSStfbbcavIPbDABvMgzOcmYMIVXXe57twU0xdu3AqWLtc9kw1BkUgZblM9pSSpYrIDheEyMs2/hiLgXsIaM0nVQtqwrA7rbeEGuPblzA6hvHgwN9K6HaBqdlGSlpYZ0v3SWIMwmxRB+kIojlyuggm8Qa4mqL97GFDGl6gOBGlNUFTBUVEa3EaJ7NJpGobRGsh/9dXzcW4aYmT9WxlzTlIKksI1ro6KdRfuVWfEs4AnG8bVEJmofK8EUrueB9IdXlcJZB49xolnOZPFohtMe/0U7evQOQP3sZnX+KotCsE7OXJvL09oF58JKoqmK9lPp0+pFBU4g6NjQIDAQABAoICAA+RSWph1t+q5R3nxUxFTYMrhv5IjQe2mDxJpF3B409zolC9OHxgGUisobTY3pBqs0DtKbxUeH2A0ehUH/axEosWHcz3cmIbgxHE9kdlJ9B3Lmss6j/uw+PWutu1sgm5phaIFIvuNNRWhPB6yXUwU4sLRat1+Z9vTmIQiKdtLIrtJz/n2VDvrJxn1N+yAsE20fnrksFKyZuxVsJaZPiX/t5Yv1/z0LjFjVoL7GUA5/Si7csN4ftqEhUrkNr2BvcZlTyffrF4lZCXrtl76RNUaxhqIu3H0gFbV2UfBpuckkfAhNRpXJ4iFSxm4nQbk4ojV8+l21RFOBeDN2Z7Ocu6auP5MnzpopR66vmDCmPoid498VGgDzFQEVkOar8WAa4v9h85QgLKrth6FunmaWJUT6OggQD3yY58GSwp5+ARMETMBP2x6Eld+PGgqoJvPT1+l/e9gOw7/SJ+Wz6hRXZAm/eiXMppHtB7sfea5rscNanPjJkK9NvPM0MX9cq/iA6QjXuETkMbubjo+Cxk3ydZiIQmWQDAx/OgxTyHbeRCVhLPcAphX0clykCuHZpI9Mvvj643/LoE0mjTByWJXf/WuGJA8ElHkjSdokVJ7jumz8OZZHfq0+V7+la2opsObeQANHW5MLWrnHlRVzTGV0IRZDXh7h1ptUJ4ubdvw/GJ2NeTAoIBAQD0lXXdjYKWC4uZ4YlgydP8b1CGda9cBV5RcPt7q9Ya1R2E4ieYyohmzltopvdaOXdsTZzhtdzOzKF+2qNcbBKhBTleYZ8GN5RKbo7HwXWpzfCTjseKHOD/QPwvBKXzLVWNtXn1NrLR79Rv0wbkYF6DtoqpEPf5kMs4bx79yW+mz8FUgdEeMjKphx6Jd5RYlTUxS64K6bnK7gjHNCF2cwdxsh4B6EB649GKeNz4JXi+oQBmOcX5ncXnkJrbju+IjtCkQ40HINVNdX7XeEaaw6KGaImVjw61toPUuDaioYUojufayoyXaUJnDbHQ2tNekEpq5iwnenZCbUKWmSeRe7dLAoIBAQDzIscYujsrmPxiTj2prhG0v36NRNP99mShnnJGowiIs+UBS0EMdOmBFa2sC9uFs/VnreQNYPDJdfr7O5VK9kfbH/PSiiKJ+wVebfdAlWkJYH27JN2Kl2l/OsvRVelNvF3BWIYF46qzGxIM0axaz3T2ZAJ9SrUgeAYhak6uyM4fbexEWXxDgPGu6C0jB6IAzmHJnnh+j5+4ZXqjVyUxBYtUsWXF/TXomVcT9jxj7aUmS2/Us0XTVOVNpALqqYcekrzsX/wX0OEi5HkivYXHcNaDHx3NuUf6KdYof5DwPUM76qe+5/kWlSIHP3M6rIFK3pYFUnkHn2E8jNWcO97Aio+HAoIBAA+bcff/TbPxbKkXIUMR3fsfx02tONFwbkJYKVQM9Q6lRsrx+4Dee7HDvUWCUgpp3FsG4NnuVvbDTBLiNMZzBwVLZgvFwvYMmePeBjJs/+sj/xQLamQ/z4O6S91cOJK589mlGPEy2lpXKYExQCFWnPFetp5vPMOqH62sOZgMQJmubDHOTt/UaDM1Mhenj8nPS6OnpqV/oKF4awr7Ip+CW5k/unZ4sZSl8PsbF06mZXwUngfn6+Av1y8dpSQZjONz6ZBx1w/7YmEc/EkXnbnGfhqBlTX7+P5TdTofvyzFjc+2vsjRYANRbjFRSGWBcTd5kaYcpfim8eDvQ+6EO2gnMt0CggEAH2ln1Y8B5AEQ4lZ/avOdP//ZhsDUrqPtnl/NHckkahzrwj4JumVEYbP+SxMBGoYEd4+kvgG/OhfvBBRPlm65G9tF8fZ8vdzbdba5UfO7rUV1GP+LS8OCErjy6imySaPDbR5Vul8Oh7NAor1YCidxUf/bvnovanF3QUvtvHEfCDp4YuA4yLPZBaLjaforePUw9w5tPNSravRZYs74dBvmQ1vj7S9ojpN5B5AxfyuNwaPPX+iFZec69MvywISEe3Ozysof1Kfc3lgsOkvIA9tVK32SqSh93xkWnQbWH+OaUxxe7bAko0FDMzKEXZk53wVg1nEwR8bUljEPy+6EOdXs8wKCAQEAsEOWYMY5m7HkeG2XTTvX7ECmmdGl/c4ZDVwzB4IPxqUG7XfLmtsON8YoKOEUpJoc4ANafLXzmU+esUGbH4Ph22IWgP9jzws7jxaN/Zoku64qrSjgEZFTRIpKyhFk/ImWbS9laBW4l+m0tqTTRqoE0QEJf/2uv/04q65zrA70X9z2+KTrAtqOiRQPWl/IxRe9U4OEeGL+oD+YlXKCDsnJ3rwUIOZgJx0HWZg7K35DKwqs1nVi56FBdljiTRKAjVLRedjgDCSfGS1yUZ3krHzpaPt1qgnT3rdtYcIdbYDr66V2/gEEaz6XMGHuTk/ewjzUJxq9UTVeXOCbkRPXgVJg1w==\n-----END PRIVATE KEY-----",
}
}
}
func SaveConfig(configDirectory string, config models.Config, configuration *models.Configuration, communication *models.Communication) error {
@@ -502,7 +554,9 @@ func SaveConfig(configDirectory string, config models.Config, configuration *mod
if communication.CameraConnected {
select {
case communication.HandleBootstrap <- "restart":
default:
log.Log.Info("config.main.SaveConfig(): update config, restart agent.")
case <-time.After(1 * time.Second):
log.Log.Info("config.main.SaveConfig(): update config, restart agent.")
}
}
@@ -529,7 +583,7 @@ func StoreConfig(configDirectory string, config models.Config) error {
// Write to mongodb
client := database.New()
db := client.Database(database.DatabaseName)
db := client.Client.Database(database.DatabaseName)
collection := db.Collection("configuration")
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)

View File

@@ -15,12 +15,19 @@ type DB struct {
Client *mongo.Client
}
var TIMEOUT = 10 * time.Second
var _init_ctx sync.Once
var _instance *DB
var DatabaseName = "KerberosFactory"
func New() *mongo.Client {
var DatabaseName = os.Getenv("MONGODB_DATABASE_FACTORY")
func New() *DB {
if DatabaseName == "" {
DatabaseName = "KerberosFactory"
}
mongodbURI := os.Getenv("MONGODB_URI")
host := os.Getenv("MONGODB_HOST")
databaseCredentials := os.Getenv("MONGODB_DATABASE_CREDENTIALS")
replicaset := os.Getenv("MONGODB_REPLICASET")
@@ -28,28 +35,46 @@ func New() *mongo.Client {
password := os.Getenv("MONGODB_PASSWORD")
authentication := "SCRAM-SHA-256"
ctx, cancel := context.WithTimeout(context.Background(), TIMEOUT)
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 != "" {
mongodbURI = fmt.Sprintf("%s/?replicaSet=%s", mongodbURI, replicaset)
}
client, err := mongo.Connect(ctx, options.Client().ApplyURI(mongodbURI).SetAuth(options.Credential{
AuthMechanism: authentication,
AuthSource: databaseCredentials,
Username: username,
Password: password,
}))
if err != nil {
fmt.Printf("Error setting up mongodb connection: %+v\n", err)
os.Exit(1)
// We can also apply the complete URI
// e.g. "mongodb+srv://<username>:<password>@kerberos-hub.shhng.mongodb.net/?retryWrites=true&w=majority&appName=kerberos-hub"
if mongodbURI != "" {
serverAPI := options.ServerAPI(options.ServerAPIVersion1)
opts := options.Client().ApplyURI(mongodbURI).SetServerAPIOptions(serverAPI)
// Create a new client and connect to the server
client, err := mongo.Connect(ctx, opts)
if err != nil {
fmt.Printf("Error setting up mongodb connection: %+v\n", err)
os.Exit(1)
}
_instance.Client = client
} else {
// New MongoDB driver
mongodbURI := fmt.Sprintf("mongodb://%s:%s@%s", username, password, host)
if replicaset != "" {
mongodbURI = fmt.Sprintf("%s/?replicaSet=%s", mongodbURI, replicaset)
}
client, err := mongo.Connect(ctx, options.Client().ApplyURI(mongodbURI).SetAuth(options.Credential{
AuthMechanism: authentication,
AuthSource: databaseCredentials,
Username: username,
Password: password,
}))
if err != nil {
fmt.Printf("Error setting up mongodb connection: %+v\n", err)
os.Exit(1)
}
_instance.Client = client
}
_instance.Client = client
})
return _instance.Client
return _instance
}

View File

@@ -14,27 +14,15 @@ import (
"hash"
)
// DecryptWithPrivateKey decrypts data with private key
func DecryptWithPrivateKey(ciphertext string, privateKey *rsa.PrivateKey) ([]byte, error) {
// decode our encrypted string into cipher bytes
cipheredValue, _ := base64.StdEncoding.DecodeString(ciphertext)
// decrypt the data
out, err := rsa.DecryptPKCS1v15(nil, privateKey, cipheredValue)
return out, err
}
// SignWithPrivateKey signs data with private key
func SignWithPrivateKey(data []byte, privateKey *rsa.PrivateKey) ([]byte, error) {
// hash the data with sha256
hashed := sha256.Sum256(data)
// sign the data
signature, err := rsa.SignPKCS1v15(nil, privateKey, crypto.SHA256, hashed[:])
return signature, err
}
@@ -59,16 +47,10 @@ func AesEncrypt(content []byte, password string) ([]byte, error) {
copy(cipherText[:8], []byte("Salted__"))
copy(cipherText[8:16], salt)
copy(cipherText[16:], cipherBytes)
//cipherText := base64.StdEncoding.EncodeToString(data)
return cipherText, nil
}
func AesDecrypt(cipherText []byte, password string) ([]byte, error) {
//data, err := base64.StdEncoding.DecodeString(cipherText)
//if err != nil {
// return nil, err
//}
if string(cipherText[:8]) != "Salted__" {
return nil, errors.New("invalid crypto js aes encryption")
}
@@ -92,8 +74,6 @@ func AesDecrypt(cipherText []byte, password string) ([]byte, error) {
return result, nil
}
// https://stackoverflow.com/questions/27677236/encryption-in-javascript-and-decryption-with-php/27678978#27678978
// https://github.com/brix/crypto-js/blob/8e6d15bf2e26d6ff0af5277df2604ca12b60a718/src/evpkdf.js#L55
func EvpKDF(password []byte, salt []byte, keySize int, iterations int, hashAlgorithm string) ([]byte, error) {
var block []byte
var hasher hash.Hash
@@ -124,7 +104,6 @@ func EvpKDF(password []byte, salt []byte, keySize int, iterations int, hashAlgor
}
func DefaultEvpKDF(password []byte, salt []byte) (key []byte, iv []byte, err error) {
// https://github.com/brix/crypto-js/blob/8e6d15bf2e26d6ff0af5277df2604ca12b60a718/src/cipher-core.js#L775
keySize := 256 / 32
ivSize := 128 / 32
derivedKeyBytes, err := EvpKDF(password, salt, keySize+ivSize, 1, "md5")
@@ -134,7 +113,6 @@ func DefaultEvpKDF(password []byte, salt []byte) (key []byte, iv []byte, err err
return derivedKeyBytes[:keySize*4], derivedKeyBytes[keySize*4:], nil
}
// https://stackoverflow.com/questions/41579325/golang-how-do-i-decrypt-with-des-cbc-and-pkcs7
func PKCS5UnPadding(src []byte) []byte {
length := len(src)
unpadding := int(src[length-1])

View File

@@ -12,7 +12,6 @@ import (
// The logging library being used everywhere.
var Log = Logging{
Logger: "logrus",
Level: "debug",
}
// -----------------
@@ -45,19 +44,44 @@ func ConfigureGoLogging(configDirectory string, 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
} 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 +96,14 @@ func (u LocalTimeZoneFormatter) Format(e *logrus.Entry) ([]byte, error) {
type Logging struct {
Logger string
Level string
}
func (self *Logging) Init(configDirectory string, timezone *time.Location) {
func (self *Logging) Init(level string, logoutput string, configDirectory string, timezone *time.Location) {
switch self.Logger {
case "go-logging":
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"
)
@@ -17,11 +15,14 @@ type Communication struct {
CancelContext *context.CancelFunc
PackageCounter *atomic.Value
LastPacketTimer *atomic.Value
PackageCounterSub *atomic.Value
LastPacketTimerSub *atomic.Value
CloudTimestamp *atomic.Value
HandleBootstrap chan string
HandleStream chan string
HandleSubStream chan string
HandleMotion chan MotionDataPartial
HandleAudio chan AudioDataPartial
HandleUpload chan string
HandleHeartBeat chan string
HandleLiveSD chan int64
@@ -30,12 +31,11 @@ type Communication struct {
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
MainStreamConnected bool
SubStreamConnected bool
HasBackChannel bool
}

View File

@@ -12,37 +12,43 @@ type Configuration struct {
// Config is the highlevel struct which contains all the configuration of
// your Kerberos Open Source instance.
type Config struct {
Type string `json:"type"`
Key string `json:"key"`
Name string `json:"name"`
FriendlyName string `json:"friendly_name"`
Time string `json:"time" bson:"time"`
Offline string `json:"offline"`
AutoClean string `json:"auto_clean"`
RemoveAfterUpload string `json:"remove_after_upload"`
MaxDirectorySize int64 `json:"max_directory_size"`
Timezone string `json:"timezone"`
Capture Capture `json:"capture"`
Timetable []*Timetable `json:"timetable"`
Region *Region `json:"region"`
Cloud string `json:"cloud" bson:"cloud"`
S3 *S3 `json:"s3,omitempty" bson:"s3,omitempty"`
KStorage *KStorage `json:"kstorage,omitempty" bson:"kstorage,omitempty"`
Dropbox *Dropbox `json:"dropbox,omitempty" bson:"dropbox,omitempty"`
MQTTURI string `json:"mqtturi" bson:"mqtturi,omitempty"`
MQTTUsername string `json:"mqtt_username" bson:"mqtt_username"`
MQTTPassword string `json:"mqtt_password" bson:"mqtt_password"`
STUNURI string `json:"stunuri" bson:"stunuri"`
TURNURI string `json:"turnuri" bson:"turnuri"`
TURNUsername string `json:"turn_username" bson:"turn_username"`
TURNPassword string `json:"turn_password" bson:"turn_password"`
HeartbeatURI string `json:"heartbeaturi" bson:"heartbeaturi"` /*obsolete*/
HubURI string `json:"hub_uri" bson:"hub_uri"`
HubKey string `json:"hub_key" bson:"hub_key"`
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"`
Type string `json:"type"`
Key string `json:"key"`
Name string `json:"name"`
FriendlyName string `json:"friendly_name"`
Time string `json:"time" bson:"time"`
Offline string `json:"offline"`
AutoClean string `json:"auto_clean"`
RemoveAfterUpload string `json:"remove_after_upload"`
MaxDirectorySize int64 `json:"max_directory_size"`
Timezone string `json:"timezone"`
Capture Capture `json:"capture"`
Timetable []*Timetable `json:"timetable"`
Region *Region `json:"region"`
Cloud string `json:"cloud" bson:"cloud"`
S3 *S3 `json:"s3,omitempty" bson:"s3,omitempty"`
KStorage *KStorage `json:"kstorage,omitempty" bson:"kstorage,omitempty"`
KStorageSecondary *KStorage `json:"kstorage_secondary,omitempty" bson:"kstorage_secondary,omitempty"`
Dropbox *Dropbox `json:"dropbox,omitempty" bson:"dropbox,omitempty"`
MQTTURI string `json:"mqtturi" bson:"mqtturi,omitempty"`
MQTTUsername string `json:"mqtt_username" bson:"mqtt_username"`
MQTTPassword string `json:"mqtt_password" bson:"mqtt_password"`
STUNURI string `json:"stunuri" bson:"stunuri"`
ForceTurn string `json:"turn_force" bson:"turn_force"`
TURNURI string `json:"turnuri" bson:"turnuri"`
TURNUsername string `json:"turn_username" bson:"turn_username"`
TURNPassword string `json:"turn_password" bson:"turn_password"`
HeartbeatURI string `json:"heartbeaturi" bson:"heartbeaturi"` /*obsolete*/
HubEncryption string `json:"hub_encryption" bson:"hub_encryption"`
HubURI string `json:"hub_uri" bson:"hub_uri"`
HubKey string `json:"hub_key" bson:"hub_key"`
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"`
Signing *Signing `json:"signing,omitempty" bson:"signing,omitempty"`
RealtimeProcessing string `json:"realtimeprocessing,omitempty" bson:"realtimeprocessing,omitempty"`
RealtimeProcessingTopic string `json:"realtimeprocessing_topic" bson:"realtimeprocessing_topic"`
}
// Capture defines which camera type (Id) you are using (IP, USB or Raspberry Pi camera),
@@ -71,15 +77,23 @@ 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"`
ONVIF string `json:"onvif,omitempty" bson:"onvif"`
ONVIFXAddr string `json:"onvif_xaddr" bson:"onvif_xaddr"`
ONVIFUsername string `json:"onvif_username" bson:"onvif_username"`
ONVIFPassword string `json:"onvif_password" bson:"onvif_password"`
RTSP string `json:"rtsp"`
Width int `json:"width"`
Height int `json:"height"`
FPS string `json:"fps"`
SubRTSP string `json:"sub_rtsp"`
SubWidth int `json:"sub_width"`
SubHeight int `json:"sub_height"`
SubFPS string `json:"sub_fps"`
ONVIF string `json:"onvif,omitempty" bson:"onvif"`
ONVIFXAddr string `json:"onvif_xaddr" bson:"onvif_xaddr"`
ONVIFUsername string `json:"onvif_username" bson:"onvif_username"`
ONVIFPassword string `json:"onvif_password" bson:"onvif_password"`
SPSNALUs [][]byte `json:"sps_nalus,omitempty" bson:"sps_nalus,omitempty"`
PPSNALUs [][]byte `json:"pps_nalus,omitempty" bson:"pps_nalus,omitempty"`
VPSNALUs [][]byte `json:"vps_nalus,omitempty" bson:"vps_nalus,omitempty"`
SampleRate int `json:"sample_rate,omitempty" bson:"sample_rate,omitempty"`
Channels int `json:"channels,omitempty" bson:"channels,omitempty"`
}
// USBCamera configuration, such as the device path (/dev/video*)
@@ -151,6 +165,8 @@ type KStorage struct {
SecretAccessKey string `json:"secret_access_key,omitempty" bson:"secret_access_key,omitempty"`
Provider string `json:"provider,omitempty" bson:"provider,omitempty"`
Directory string `json:"directory,omitempty" bson:"directory,omitempty"`
MaxRetries int `json:"max_retries,omitempty" bson:"max_retries,omitempty"`
Timeout int `json:"timeout,omitempty" bson:"timeout,omitempty"`
}
// Dropbox integration
@@ -167,3 +183,9 @@ type Encryption struct {
PrivateKey string `json:"private_key" bson:"private_key"`
SymmetricKey string `json:"symmetric_key" bson:"symmetric_key"`
}
// Signing
type Signing struct {
Enabled string `json:"enabled" bson:"enabled"`
PrivateKey string `json:"private_key" bson:"private_key"`
}

View File

@@ -6,7 +6,7 @@ import (
"encoding/base64"
"encoding/json"
"encoding/pem"
"io/ioutil"
"io"
"strings"
"time"
@@ -27,10 +27,13 @@ func PackageMQTTMessage(configuration *Configuration, msg Message) ([]byte, erro
msg.DeviceId = msg.Payload.DeviceId
msg.Timestamp = time.Now().Unix()
// At the moment we don't do the encryption part, but we'll implement it
// once the legacy methods (subscriptions are moved).
// Configuration
config := configuration.Config
// 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" {
if config.Encryption != nil && config.Encryption.Enabled == "true" {
msg.Encrypted = true
}
msg.PublicKey = ""
@@ -42,41 +45,69 @@ func PackageMQTTMessage(configuration *Configuration, msg Message) ([]byte, erro
// Pload to base64
data, err := json.Marshal(pload)
if err != nil {
log.Log.Error("failed to marshal payload: " + err.Error())
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, _ := ioutil.ReadAll(r)
pemBytes, _ := io.ReadAll(r)
block, _ := pem.Decode(pemBytes)
if block == nil {
log.Log.Error("MQTTListenerHandler: error decoding PEM block containing private key")
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("MQTTListenerHandler: error parsing private key: " + err.Error())
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
if config.Encryption != nil && config.Encryption.SymmetricKey != "" {
k := 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{})
}
}
}
}
}
// 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 config.HubEncryption == "true" && config.HubPrivateKey != "" {
msg.Hidden = true
}
if msg.Hidden {
pload := msg.Payload
// Pload to base64
data, err := json.Marshal(pload)
if err != nil {
msg.Hidden = false
} else {
k := config.HubPrivateKey
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{})
}
msg.Payload.HiddenValue = data
msg.Payload.EncryptedValue = ""
msg.Payload.Signature = ""
msg.Payload.Value = make(map[string]interface{})
}
}
}
@@ -92,6 +123,7 @@ type Message struct {
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"`
@@ -104,9 +136,16 @@ type Payload struct {
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.
@@ -153,3 +192,9 @@ type NavigatePTZPayload struct {
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

@@ -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,21 @@
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 int64 // packet presentation time minus decode time for H264 B-Frame
Time int64 // packet decode time
TimeLegacy time.Duration
Data []byte // packet data
}

View File

@@ -0,0 +1,224 @@
// Packege pubsub implements publisher-subscribers model used in multi-channel streaming.
package packets
import (
"io"
"sync"
)
// 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 int64) *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,51 @@
package packets
type Stream struct {
// The ID of the stream.
Index int `json:"index" bson:"index"`
// 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
// SampleRate is the sample rate of the audio stream.
SampleRate int
// Channels is the number of audio channels.
Channels int
}

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,243 +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"
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/utils"
)
func AddRoutes(r *gin.Engine, authMiddleware *jwt.GinJWTMiddleware, configDirectory string, 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 := configService.SaveConfig(configDirectory, 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 := 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,
})
})
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 := 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(),
})
}
})
api.GET("/days", func(c *gin.Context) {
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(),
})
}
})
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 := 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(),
})
}
})
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, configDirectory)
})
// 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 := configService.GetImageFromFilePath(configDirectory)
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/presets", GetOnvifPresets)
api.POST("/camera/onvif/gotopreset", GoToOnvifPreset)
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

@@ -14,6 +14,7 @@ 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"
@@ -38,12 +39,15 @@ import (
// @in header
// @name Authorization
func StartServer(configDirectory string, 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()
// Profileerggerg
// Profiler
pprof.Register(r)
// Setup CORS
@@ -60,7 +64,7 @@ func StartServer(configDirectory string, configuration *models.Configuration, co
}
// Add all routes
AddRoutes(r, authMiddleware, configDirectory, configuration, communication)
AddRoutes(r, authMiddleware, configDirectory, configuration, communication, captureDevice)
// Update environment variables
environmentVariables := configDirectory + "/www/env.js"
@@ -105,8 +109,9 @@ func Files(c *gin.Context, configDirectory string, configuration *models.Configu
// Get symmetric key
symmetricKey := configuration.Config.Encryption.SymmetricKey
encryptedRecordings := configuration.Config.Encryption.Recordings
// Decrypt file
if symmetricKey != "" {
if encryptedRecordings == "true" && symmetricKey != "" {
// Read file
if err != nil {

View File

@@ -2,6 +2,7 @@ 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"
)
@@ -19,7 +20,7 @@ func Login() {}
// LoginToOnvif godoc
// @Router /api/camera/onvif/login [post]
// @ID camera-onvif-login
// @Tags camera
// @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.
@@ -43,11 +44,21 @@ func LoginToOnvif(c *gin.Context) {
}
cameraConfiguration := configuration.Config.Capture.IPCamera
device, err := onvif.ConnectToOnvifDevice(&cameraConfiguration)
device, capabilities, err := onvif.ConnectToOnvifDevice(&cameraConfiguration)
if err == nil {
c.JSON(200, gin.H{
"device": device,
})
// 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(),
@@ -63,7 +74,7 @@ func LoginToOnvif(c *gin.Context) {
// GetOnvifCapabilities godoc
// @Router /api/camera/onvif/capabilities [post]
// @ID camera-onvif-capabilities
// @Tags camera
// @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.
@@ -87,10 +98,10 @@ func GetOnvifCapabilities(c *gin.Context) {
}
cameraConfiguration := configuration.Config.Capture.IPCamera
device, err := onvif.ConnectToOnvifDevice(&cameraConfiguration)
_, capabilities, err := onvif.ConnectToOnvifDevice(&cameraConfiguration)
if err == nil {
c.JSON(200, gin.H{
"capabilities": onvif.GetCapabilitiesFromDevice(device),
"capabilities": capabilities,
})
} else {
c.JSON(400, gin.H{
@@ -107,7 +118,7 @@ func GetOnvifCapabilities(c *gin.Context) {
// DoOnvifPanTilt godoc
// @Router /api/camera/onvif/pantilt [post]
// @ID camera-onvif-pantilt
// @Tags camera
// @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).
@@ -131,7 +142,7 @@ func DoOnvifPanTilt(c *gin.Context) {
}
cameraConfiguration := configuration.Config.Capture.IPCamera
device, err := onvif.ConnectToOnvifDevice(&cameraConfiguration)
device, _, err := onvif.ConnectToOnvifDevice(&cameraConfiguration)
if err == nil {
// Get token from the first profile
@@ -181,7 +192,7 @@ func DoOnvifPanTilt(c *gin.Context) {
// DoOnvifZoom godoc
// @Router /api/camera/onvif/zoom [post]
// @ID camera-onvif-zoom
// @Tags camera
// @Tags onvif
// @Param zoom body models.OnvifZoom true "OnvifZoom"
// @Summary Zooming in or out the camera.
// @Description Zooming in or out the camera.
@@ -205,7 +216,7 @@ func DoOnvifZoom(c *gin.Context) {
}
cameraConfiguration := configuration.Config.Capture.IPCamera
device, err := onvif.ConnectToOnvifDevice(&cameraConfiguration)
device, _, err := onvif.ConnectToOnvifDevice(&cameraConfiguration)
if err == nil {
// Get token from the first profile
@@ -254,7 +265,7 @@ func DoOnvifZoom(c *gin.Context) {
// GetOnvifPresets godoc
// @Router /api/camera/onvif/presets [post]
// @ID camera-onvif-presets
// @Tags camera
// @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.
@@ -278,7 +289,7 @@ func GetOnvifPresets(c *gin.Context) {
}
cameraConfiguration := configuration.Config.Capture.IPCamera
device, err := onvif.ConnectToOnvifDevice(&cameraConfiguration)
device, _, err := onvif.ConnectToOnvifDevice(&cameraConfiguration)
if err == nil {
presets, err := onvif.GetPresetsFromDevice(device)
if err == nil {
@@ -305,7 +316,7 @@ func GetOnvifPresets(c *gin.Context) {
// GoToOnvifPReset godoc
// @Router /api/camera/onvif/gotopreset [post]
// @ID camera-onvif-gotopreset
// @Tags camera
// @Tags onvif
// @Param config body models.OnvifPreset true "OnvifPreset"
// @Summary Will activate the desired ONVIF preset.
// @Description Will activate the desired ONVIF preset.
@@ -329,7 +340,7 @@ func GoToOnvifPreset(c *gin.Context) {
}
cameraConfiguration := configuration.Config.Capture.IPCamera
device, err := onvif.ConnectToOnvifDevice(&cameraConfiguration)
device, _, err := onvif.ConnectToOnvifDevice(&cameraConfiguration)
if err == nil {
err := onvif.GoToPresetFromDevice(device, onvifPreset.Preset)
if err == nil {
@@ -352,3 +363,228 @@ func GoToOnvifPreset(c *gin.Context) {
})
}
}
// 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
device, _, err := onvif.ConnectToOnvifDevice(&cameraConfiguration)
onvifInputs, _ := onvif.GetDigitalInputs(device)
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)
}
}
// Iterate over inputs from onvif and compare
for _, input := range onvifInputs.DigitalInputs {
find := false
for _, event := range inputs {
key := string(input.Token)
if key == event.Key {
find = true
}
}
if !find {
key := string(input.Token)
inputs = append(inputs, onvif.ONVIFEvents{
Key: key,
Type: "input",
})
}
}
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,116 @@
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 hub settings.
api.POST("/hub/verify", func(c *gin.Context) {
cloud.VerifyHub(c)
})
// Will verify the persistence settings.
api.POST("/persistence/verify", func(c *gin.Context) {
cloud.VerifyPersistence(c, configDirectory)
})
// Will verify the secondary persistence settings.
api.POST("/persistence/secondary/verify", func(c *gin.Context) {
cloud.VerifySecondaryPersistence(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(configDirectory string, configuration *models.Configuration, communication *models.Communication) {
http.StartServer(configDirectory, configuration, communication)
func StartWebserver(configDirectory string, configuration *models.Configuration, communication *models.Communication, captureDevice *capture.Capture) {
http.StartServer(configDirectory, configuration, communication, captureDevice)
}

View File

@@ -66,7 +66,7 @@ func ConfigureMQTT(configDirectory string, configuration *models.Configuration,
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()
@@ -75,7 +75,7 @@ func ConfigureMQTT(configDirectory string, configuration *models.Configuration,
// 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.
@@ -84,8 +84,8 @@ func ConfigureMQTT(configDirectory string, configuration *models.Configuration,
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
@@ -121,13 +121,13 @@ func ConfigureMQTT(configDirectory string, configuration *models.Configuration,
}
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 susbcription for listen and reply
MQTTListenerHandler(c, hubKey, configDirectory, configuration, communication)
@@ -136,7 +136,7 @@ func ConfigureMQTT(configDirectory string, configuration *models.Configuration,
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
@@ -147,10 +147,10 @@ func ConfigureMQTT(configDirectory string, configuration *models.Configuration,
func MQTTListenerHandler(mqttClient mqtt.Client, hubKey string, configDirectory string, configuration *models.Configuration, communication *models.Communication) {
if hubKey == "" {
log.Log.Info("MQTTListenerHandler: no hub key provided, not subscribing to kerberos/hub/{hubkey}")
log.Log.Info("routers.mqtt.main.MQTTListenerHandler(): no hub key provided, not subscribing to kerberos/hub/{hubkey}")
} else {
topicOnvif := fmt.Sprintf("kerberos/agent/%s", hubKey)
mqttClient.Subscribe(topicOnvif, 1, func(c mqtt.Client, msg mqtt.Message) {
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.
// {
@@ -166,9 +166,33 @@ func MQTTListenerHandler(mqttClient mqtt.Client, hubKey string, configDirectory
// 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
// Messages might be hidden, if so we'll need to decrypt them using the Kerberos Hub private key.
if message.Hidden && configuration.Config.HubEncryption == "true" {
hiddenValue := message.Payload.HiddenValue
if len(hiddenValue) > 0 {
privateKey := configuration.Config.HubPrivateKey
if privateKey != "" {
data, err := base64.StdEncoding.DecodeString(hiddenValue)
if err != nil {
return
}
visibleValue, err := encryption.AesDecrypt(data, privateKey)
if err != nil {
log.Log.Error("routers.mqtt.main.MQTTListenerHandler(): error decrypting message: " + err.Error())
return
}
json.Unmarshal(visibleValue, &payload)
message.Payload = payload
} else {
log.Log.Error("routers.mqtt.main.MQTTListenerHandler(): error decrypting message, no private key provided.")
}
}
}
// Messages might be end-to-end encrypted, if so we'll need to decrypt them,
// using our own keys.
if message.Encrypted && configuration.Config.Encryption != nil && configuration.Config.Encryption.Enabled == "true" {
encryptedValue := message.Payload.EncryptedValue
if len(encryptedValue) > 0 {
@@ -178,14 +202,14 @@ func MQTTListenerHandler(mqttClient mqtt.Client, hubKey string, configDirectory
pemBytes, _ := ioutil.ReadAll(r)
block, _ := pem.Decode(pemBytes)
if block == nil {
log.Log.Error("MQTTListenerHandler: error decoding PEM block containing private key")
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("MQTTListenerHandler: error parsing private key: " + err.Error())
log.Log.Error("routers.mqtt.main.MQTTListenerHandler(): error parsing private key: " + err.Error())
return
} else {
// Conver key to *rsa.PrivateKey
@@ -205,16 +229,16 @@ func MQTTListenerHandler(mqttClient mqtt.Client, hubKey string, configDirectory
}
decryptedValue, err := encryption.AesDecrypt(data, string(decryptedKey))
if err != nil {
log.Log.Error("MQTTListenerHandler: error decrypting message: " + err.Error())
log.Log.Error("routers.mqtt.main.MQTTListenerHandler(): error decrypting message: " + err.Error())
return
}
json.Unmarshal(decryptedValue, &payload)
} else {
log.Log.Error("MQTTListenerHandler: error decrypting message, assymetric keys do not match.")
log.Log.Error("routers.mqtt.main.MQTTListenerHandler(): error decrypting message, assymetric keys do not match.")
return
}
} else if err != nil {
log.Log.Error("MQTTListenerHandler: error decrypting message: " + err.Error())
log.Log.Error("routers.mqtt.main.MQTTListenerHandler(): error decrypting message: " + err.Error())
return
}
}
@@ -225,10 +249,12 @@ func MQTTListenerHandler(mqttClient mqtt.Client, hubKey string, configDirectory
}
// We'll find out which message we received, and act accordingly.
log.Log.Info("MQTTListenerHandler: received message with action: " + payload.Action)
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":
@@ -245,6 +271,8 @@ func MQTTListenerHandler(mqttClient mqtt.Client, hubKey string, configDirectory
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)
}
}
@@ -268,6 +296,23 @@ func HandleRecording(mqttClient mqtt.Client, hubKey string, payload models.Paylo
}
}
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
@@ -280,7 +325,7 @@ func HandleGetPTZPosition(mqttClient mqtt.Client, hubKey string, payload models.
// Get Position from device
pos, err := onvif.GetPositionFromDevice(*configuration)
if err != nil {
log.Log.Error("HandlePTZPosition: error getting position from device: " + err.Error())
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)
@@ -296,9 +341,9 @@ func HandleGetPTZPosition(mqttClient mqtt.Client, hubKey string, payload models.
}
payload, err := models.PackageMQTTMessage(configuration, message)
if err == nil {
mqttClient.Publish("kerberos/hub/"+hubKey, 0, false, payload)
mqttClient.Publish("kerberos/hub/"+hubKey, 2, false, payload)
} else {
log.Log.Info("HandlePTZPosition: something went wrong while sending position to hub: " + string(payload))
log.Log.Info("routers.mqtt.main.HandlePTZPosition(): something went wrong while sending position to hub: " + string(payload))
}
}
}
@@ -315,9 +360,9 @@ func HandleUpdatePTZPosition(mqttClient mqtt.Client, hubKey string, payload mode
if onvifAction.Action != "" {
if communication.CameraConnected {
communication.HandleONVIF <- onvifAction
log.Log.Info("MQTTListenerHandleONVIF: Received an action - " + onvifAction.Action)
log.Log.Info("routers.mqtt.main.MQTTListenerHandleONVIF(): Received an action - " + onvifAction.Action)
} else {
log.Log.Info("MQTTListenerHandleONVIF: received action, but camera is not connected.")
log.Log.Info("routers.mqtt.main.MQTTListenerHandleONVIF(): received action, but camera is not connected.")
}
}
}
@@ -331,16 +376,27 @@ func HandleRequestConfig(mqttClient mqtt.Client, hubKey string, payload models.P
json.Unmarshal(jsonData, &configPayload)
if configPayload.Timestamp != 0 {
// Get Config from the device
// Get Config from the device
key := configuration.Config.Key
name := configuration.Config.Name
if configuration.Config.FriendlyName != "" {
name = configuration.Config.FriendlyName
}
if key != "" && name != "" {
// Copy the config, as we don't want to share the encryption part.
deepCopy := configuration.Config
// We need a fix for the width and height if a substream.
// The ROI requires the width and height of the sub stream.
if configuration.Config.Capture.IPCamera.SubRTSP != "" &&
configuration.Config.Capture.IPCamera.SubRTSP != configuration.Config.Capture.IPCamera.RTSP {
deepCopy.Capture.IPCamera.Width = configuration.Config.Capture.IPCamera.SubWidth
deepCopy.Capture.IPCamera.Height = configuration.Config.Capture.IPCamera.SubHeight
}
var configMap map[string]interface{}
inrec, _ := json.Marshal(deepCopy)
json.Unmarshal(inrec, &configMap)
@@ -357,16 +413,16 @@ func HandleRequestConfig(mqttClient mqtt.Client, hubKey string, payload models.P
}
payload, err := models.PackageMQTTMessage(configuration, message)
if err == nil {
mqttClient.Publish("kerberos/hub/"+hubKey, 0, false, payload)
mqttClient.Publish("kerberos/hub/"+hubKey, 2, false, payload)
} else {
log.Log.Info("HandleRequestConfig: something went wrong while sending config to hub: " + string(payload))
log.Log.Info("routers.mqtt.main.HandleRequestConfig(): something went wrong while sending config to hub: " + string(payload))
}
} else {
log.Log.Info("HandleRequestConfig: no config available")
log.Log.Info("routers.mqtt.main.HandleRequestConfig(): no config available")
}
log.Log.Info("HandleRequestConfig: Received a request for the config")
log.Log.Info("routers.mqtt.main.HandleRequestConfig(): Received a request for the config")
}
}
@@ -387,7 +443,7 @@ func HandleUpdateConfig(mqttClient mqtt.Client, hubKey string, payload models.Pa
err := configService.SaveConfig(configDirectory, config, configuration, communication)
if err == nil {
log.Log.Info("HandleUpdateConfig: Config updated")
log.Log.Info("routers.mqtt.main.HandleUpdateConfig(): Config updated")
message := models.Message{
Payload: models.Payload{
Action: "acknowledge-update-config",
@@ -396,12 +452,12 @@ func HandleUpdateConfig(mqttClient mqtt.Client, hubKey string, payload models.Pa
}
payload, err := models.PackageMQTTMessage(configuration, message)
if err == nil {
mqttClient.Publish("kerberos/hub/"+hubKey, 0, false, payload)
mqttClient.Publish("kerberos/hub/"+hubKey, 2, false, payload)
} else {
log.Log.Info("HandleRequestConfig: something went wrong while sending acknowledge config to hub: " + string(payload))
log.Log.Info("routers.mqtt.main.HandleUpdateConfig(): something went wrong while sending acknowledge config to hub: " + string(payload))
}
} else {
log.Log.Info("HandleUpdateConfig: Config update failed")
log.Log.Info("routers.mqtt.main.HandleUpdateConfig(): Config update failed")
}
}
}
@@ -419,9 +475,9 @@ func HandleRequestSDStream(mqttClient mqtt.Client, hubKey string, payload models
case communication.HandleLiveSD <- time.Now().Unix():
default:
}
log.Log.Info("HandleRequestSDStream: received request to livestream.")
log.Log.Info("routers.mqtt.main.HandleRequestSDStream(): received request to livestream.")
} else {
log.Log.Info("HandleRequestSDStream: 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.")
}
}
}
@@ -441,9 +497,9 @@ func HandleRequestHDStream(mqttClient mqtt.Client, hubKey string, payload models
case communication.HandleLiveHDHandshake <- requestHDStreamPayload:
default:
}
log.Log.Info("HandleRequestHDStream: received request to setup webrtc.")
log.Log.Info("routers.mqtt.main.HandleRequestHDStream(): received request to setup webrtc.")
} else {
log.Log.Info("HandleRequestHDStream: 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.")
}
}
}
@@ -457,16 +513,11 @@ func HandleReceiveHDCandidates(mqttClient mqtt.Client, hubKey string, payload mo
if receiveHDCandidatesPayload.Timestamp != 0 {
if communication.CameraConnected {
// Register candidate channel
key := configuration.Config.Key + "/" + receiveHDCandidatesPayload.SessionID
channel := webrtc.CandidateArrays[key]
if channel == nil {
channel = make(chan string)
webrtc.CandidateArrays[key] = channel
}
log.Log.Info("HandleReceiveHDCandidates: " + receiveHDCandidatesPayload.Candidate)
channel <- receiveHDCandidatesPayload.Candidate
go webrtc.RegisterCandidates(key, receiveHDCandidatesPayload)
} else {
log.Log.Info("HandleReceiveHDCandidates: received candidate, but camera is not connected.")
log.Log.Info("routers.mqtt.main.HandleReceiveHDCandidates(): received candidate, but camera is not connected.")
}
}
}
@@ -483,10 +534,40 @@ func HandleNavigatePTZ(mqttClient mqtt.Client, hubKey string, payload models.Pay
var onvifAction models.OnvifAction
json.Unmarshal([]byte(action), &onvifAction)
communication.HandleONVIF <- onvifAction
log.Log.Info("HandleNavigatePTZ: Received an action - " + onvifAction.Action)
log.Log.Info("routers.mqtt.main.HandleNavigatePTZ(): Received an action - " + onvifAction.Action)
} else {
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("HandleNavigatePTZ: received action, but camera is not connected.")
log.Log.Info("routers.mqtt.main.HandleTriggerRelay(): received trigger, but camera is not connected.")
}
}
}
@@ -498,6 +579,6 @@ func DisconnectMQTT(mqttClient mqtt.Client, config *models.Config) {
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,46 @@ 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 {
continue
}
} else {
log.Log.Error("ForwardSDStream:" + err.Error())
log.Log.Error("routers.websocket.main.ForwardSDStream():" + err.Error())
break logreader
}
}
@@ -163,7 +179,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 +189,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"
@@ -20,6 +23,8 @@ import (
"github.com/kerberos-io/agent/machinery/src/models"
)
const VERSION = "3.3.5"
const letterBytes = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
// MaxUint8 - maximum value which can be held in an uint8
@@ -395,3 +400,10 @@ func Decrypt(directoryOrFile string, symmetricKey []byte) {
}
}
}
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
}

986
machinery/src/video/mp4.go Normal file
View File

@@ -0,0 +1,986 @@
package video
import (
"bufio"
"bytes"
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"errors"
"fmt"
"io/ioutil"
"os"
"strings"
"time"
mp4ff "github.com/Eyevinn/mp4ff/mp4"
"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/utils"
)
var LastPTS uint64 = 0 // Last PTS for the current segment
type MP4 struct {
// FileName is the name of the file
FileName string
width int
height int
Segments []*mp4ff.MediaSegment // List of media segments
Segment *mp4ff.MediaSegment
MultiTrackFragment *mp4ff.Fragment
TrackIDs []uint32
FileWriter *os.File
Writer *bufio.Writer
SegmentCount int
SampleCount int
StartPTS uint64
VideoTotalDuration uint64
AudioTotalDuration uint64
AudioPTS uint64
Start bool
SPSNALUs [][]byte // SPS NALUs for H264
PPSNALUs [][]byte // PPS NALUs for H264
VPSNALUs [][]byte // VPS NALUs for H264
FreeBoxSize int64
MoofBoxes int64 // Number of moof boxes in the file
MoofBoxSizes []int64 // Sizes of each moof box
StartTime uint64 // Start time of the MP4 file
VideoTrackName string // Name of the video track
VideoTrack int // Track ID for the video track
AudioTrackName string // Name of the audio track
AudioTrack int // Track ID for the audio track
VideoFullSample *mp4ff.FullSample // Full sample for video track
AudioFullSample *mp4ff.FullSample // Full sample for audio track
LastAudioSampleDTS uint64 // Last PTS for audio sample
LastVideoSampleDTS uint64 // Last PTS for video sample
SampleType string // Type of the sample (e.g., "video", "audio", "subtitle")
}
// NewMP4 creates a new MP4 object
func NewMP4(fileName string, spsNALUs [][]byte, ppsNALUs [][]byte, vpsNALUs [][]byte) *MP4 {
init := mp4ff.NewMP4Init()
// Add a free box to the init segment
// Prepend a free box to the init segment with a size of 1000
freeBoxSize := 2048
free := mp4ff.NewFreeBox(make([]byte, freeBoxSize))
init.AddChild(free)
// Create a writer
ofd, err := os.Create(fileName)
if err != nil {
panic(err)
}
// Create a buffered writer
bufferedWriter := bufio.NewWriterSize(ofd, 64*1024) // 64KB buffer
// We will write the empty init segment to the file
// so we can overwrite it later with the actual init segment.
err = init.Encode(bufferedWriter)
if err != nil {
panic(err)
}
return &MP4{
FileName: fileName,
StartTime: uint64(time.Now().Unix()),
FreeBoxSize: int64(freeBoxSize),
FileWriter: ofd,
Writer: bufferedWriter,
SPSNALUs: spsNALUs,
PPSNALUs: ppsNALUs,
VPSNALUs: vpsNALUs,
}
}
// SetWidth sets the width of the video
func (mp4 *MP4) SetWidth(width int) {
// Set the width of the video
mp4.width = width
}
// SetHeight sets the height of the video
func (mp4 *MP4) SetHeight(height int) {
// Set the height of the video
mp4.height = height
}
// AddVideoTrack
// Add a video track to the MP4 file
func (mp4 *MP4) AddVideoTrack(codec string) uint32 {
nextTrack := uint32(len(mp4.TrackIDs) + 1)
mp4.VideoTrack = int(nextTrack)
mp4.TrackIDs = append(mp4.TrackIDs, nextTrack)
mp4.VideoTrackName = codec
return nextTrack
}
// AddAudioTrack
// Add an audio track to the MP4 file
func (mp4 *MP4) AddAudioTrack(codec string) uint32 {
nextTrack := uint32(len(mp4.TrackIDs) + 1)
mp4.AudioTrack = int(nextTrack)
mp4.TrackIDs = append(mp4.TrackIDs, nextTrack)
mp4.AudioTrackName = codec
return nextTrack
}
func (mp4 *MP4) AddMediaSegment(segNr int) {
}
func (mp4 *MP4) AddSampleToTrack(trackID uint32, isKeyframe bool, data []byte, pts uint64) error {
if isKeyframe {
// Write the segment to the file
if mp4.Start {
mp4.MoofBoxes = mp4.MoofBoxes + 1
mp4.MoofBoxSizes = append(mp4.MoofBoxSizes, int64(mp4.Segment.Size()))
err := mp4.Segment.Encode(mp4.Writer)
if err != nil {
return err
}
mp4.Segments = append(mp4.Segments, mp4.Segment)
}
mp4.Start = true
// Increment the segment count
mp4.SegmentCount = mp4.SegmentCount + 1
// Create a new media segment
seg := mp4ff.NewMediaSegment()
// Create a video fragment
multiTrackFragment, err := mp4ff.CreateMultiTrackFragment(uint32(mp4.SegmentCount), mp4.TrackIDs) // Assuming 1 for video track and 2 for audio track
if err != nil {
}
mp4.MultiTrackFragment = multiTrackFragment
seg.AddFragment(multiTrackFragment)
// Set to MP4 struct
mp4.Segment = seg
// Set the start PTS for the next segment
mp4.StartPTS = pts
}
if mp4.Start {
if trackID == uint32(mp4.VideoTrack) {
var lengthPrefixed []byte
var err error
if mp4.VideoTrackName == "H264" || mp4.VideoTrackName == "AVC1" { // Convert Annex B to length-prefixed NAL units if H264
lengthPrefixed, err = annexBToLengthPrefixed(data)
} else if mp4.VideoTrackName == "H265" || mp4.VideoTrackName == "HVC1" { // Convert H265 Annex B to length-prefixed NAL units
lengthPrefixed, err = annexBToLengthPrefixed(data)
}
if err == nil {
if mp4.VideoFullSample != nil {
duration := pts - mp4.VideoFullSample.DecodeTime
log.Log.Info("Adding sample to track " + fmt.Sprintf("%d, PTS: %d, Duration: %d, size: %d, Keyframe: %t", trackID, pts, duration, len(lengthPrefixed), isKeyframe))
mp4.LastVideoSampleDTS = duration
//fmt.Printf("Adding sample to track %d, PTS: %d, Duration: %d, size: %d, Keyframe: %t\n", trackID, pts, duration, len(mp4.VideoFullSample.Data), isKeyframe)
mp4.VideoTotalDuration += duration
mp4.VideoFullSample.DecodeTime = mp4.VideoTotalDuration - duration
mp4.VideoFullSample.Sample.Dur = uint32(duration)
err := mp4.MultiTrackFragment.AddFullSampleToTrack(*mp4.VideoFullSample, trackID)
if err != nil {
//log.Printf("Error adding sample to track %d: %v", trackID, err)
return err
}
}
// Set the sample data
var fullSample mp4ff.FullSample
flags := uint32(33554432)
if !isKeyframe {
flags = uint32(16842752)
}
fullSample.DecodeTime = pts
fullSample.Data = lengthPrefixed
fullSample.Sample = mp4ff.Sample{
Size: uint32(len(fullSample.Data)),
Flags: flags,
CompositionTimeOffset: 0, // No composition time offset for video
}
mp4.VideoFullSample = &fullSample
mp4.SampleType = "video"
}
} else if trackID == uint32(mp4.AudioTrack) {
if mp4.AudioFullSample != nil {
SplitAACFrame(mp4.AudioFullSample.Data, func(started bool, aac []byte) {
sampleToAdd := *mp4.AudioFullSample
dts := pts - mp4.AudioFullSample.DecodeTime
if pts < mp4.AudioFullSample.DecodeTime {
//log.Printf("Warning: PTS %d is less than previous sample's DecodeTime %d, resetting AudioFullSample", pts, mp4.AudioFullSample.DecodeTime)
dts = 1
}
if started {
dts = 1
}
mp4.LastAudioSampleDTS = dts
//fmt.Printf("Adding sample to track %d, PTS: %d, Duration: %d, size: %d\n", trackID, pts, dts, len(aac[7:]))
mp4.AudioTotalDuration += dts
mp4.AudioPTS += dts
sampleToAdd.Data = aac[7:] // Remove the ADTS header (first 7 bytes)
sampleToAdd.DecodeTime = mp4.AudioPTS - dts
sampleToAdd.Sample.Dur = uint32(dts)
sampleToAdd.Sample.Size = uint32(len(aac[7:]))
err := mp4.MultiTrackFragment.AddFullSampleToTrack(sampleToAdd, trackID)
if err != nil {
log.Log.Error("mp4.AddSampleToTrack(): error adding sample to track " + fmt.Sprintf("%d: %v", trackID, err))
}
})
}
// Set the sample data
//flags := uint32(33554432)
var fullSample mp4ff.FullSample
fullSample.DecodeTime = pts
fullSample.Data = data
fullSample.Sample = mp4ff.Sample{
Size: uint32(len(fullSample.Data)),
Flags: 0,
CompositionTimeOffset: 0, // No composition time offset for audio
}
mp4.AudioFullSample = &fullSample
mp4.SampleType = "audio"
}
}
return nil
}
func (mp4 *MP4) Close(config *models.Config) {
// Add the last sample to the track, we will predict the duration based on the last sample
// We are not insert the last sample as we might corrupt playback (as we do not know accurately the next PTS).
// In theory it means we will lose the last sample, so there is millisecond dataloss, but it is better than corrupting playback.
// We could this by using a delayed packet reader, and look for the next PTS (closest one), but that would require a lot of memory and CPU.
/*duration := uint64(0)
trackID := uint32(1)
if mp4.SampleType == "video" {
duration = mp4.LastVideoSampleDTS
trackID = uint32(mp4.VideoTrack)
} else if mp4.SampleType == "audio" {
duration = 21 //mp4.LastAudioSampleDTS
} else {
log.Println("mp4.Close(): unknown sample type, cannot calculate duration")
}
if duration > 0 {
mp4.VideoTotalDuration += duration
mp4.VideoFullSample.DecodeTime = mp4.VideoTotalDuration - duration
mp4.VideoFullSample.Sample.Dur = uint32(duration)
err := mp4.MultiTrackFragment.AddFullSampleToTrack(*mp4.VideoFullSample, trackID)
if err != nil {
}
mp4.Segments = append(mp4.Segments, mp4.Segment)
}*/
// Encode the last segment
err := mp4.Segment.Encode(mp4.Writer)
if err != nil {
panic(err)
}
mp4.Writer.Flush()
defer mp4.FileWriter.Close()
// Now we have all the moof and mdat boxes written to the file.
// We can now generate the ftyp and moov boxes, and replace it with the free box we added earlier (size of 10008 bytes).
init := mp4ff.NewMP4Init()
// Create a new ftyp box
majorBrand := "isom"
minorVersion := uint32(512)
compatibleBrands := []string{"iso2", "avc1", "hvc1", "mp41"}
ftyp := mp4ff.NewFtyp(majorBrand, minorVersion, compatibleBrands)
init.AddChild(ftyp)
// Create a new moov box
moov := mp4ff.NewMoovBox()
init.AddChild(moov)
// Set the creation time and modification time for the moov box
videoTimescale := uint32(1000)
audioTimescale := uint32(1000)
mvhd := &mp4ff.MvhdBox{
Version: 0,
Flags: 0,
CreationTime: mp4.StartTime,
ModificationTime: mp4.StartTime,
Timescale: videoTimescale,
Duration: mp4.VideoTotalDuration,
}
init.Moov.AddChild(mvhd)
// Set the total duration in the moov box
mvex := mp4ff.NewMvexBox()
mvex.AddChild(&mp4ff.MehdBox{FragmentDuration: int64(mp4.VideoTotalDuration)})
init.Moov.AddChild(mvex)
// Add a track for the video
if mp4.VideoTrackName == "H264" || mp4.VideoTrackName == "AVC1" {
init.AddEmptyTrack(videoTimescale, "video", "und")
includePS := true
err := init.Moov.Traks[0].SetAVCDescriptor("avc1", mp4.SPSNALUs, mp4.PPSNALUs, includePS)
if err != nil {
//panic(err)
}
init.Moov.Traks[0].Tkhd.Duration = mp4.VideoTotalDuration
init.Moov.Traks[0].Mdia.Hdlr.Name = "agent " + utils.VERSION
//init.Moov.Traks[0].Mdia.Mdhd.Duration = mp4.VideoTotalDuration
} else if mp4.VideoTrackName == "H265" || mp4.VideoTrackName == "HVC1" {
init.AddEmptyTrack(videoTimescale, "video", "und")
includePS := true
err := init.Moov.Traks[0].SetHEVCDescriptor("hvc1", mp4.VPSNALUs, mp4.SPSNALUs, mp4.PPSNALUs, [][]byte{}, includePS)
if err != nil {
//panic(err)
}
init.Moov.Traks[0].Tkhd.Duration = mp4.VideoTotalDuration
init.Moov.Traks[0].Mdia.Hdlr.Name = "agent " + utils.VERSION
//init.Moov.Traks[0].Mdia.Mdhd.Duration = mp4.VideoTotalDuration
}
// Try adding audio track if available
if mp4.AudioTrackName == "AAC" || mp4.AudioTrackName == "MP4A" {
// Add an audio track to the moov box
init.AddEmptyTrack(audioTimescale, "audio", "und")
// Check if the same sample rate is set, otherwise we default to 48000
audioSampleRate := 48000
if config.Capture.IPCamera.SampleRate > 0 {
audioSampleRate = config.Capture.IPCamera.SampleRate
}
// Set the audio descriptor
err := init.Moov.Traks[1].SetAACDescriptor(29, audioSampleRate)
if err != nil {
//panic(err)
}
init.Moov.Traks[1].Tkhd.Duration = mp4.AudioTotalDuration
init.Moov.Traks[1].Mdia.Hdlr.Name = "agent " + utils.VERSION
//init.Moov.Traks[1].Mdia.Mdhd.Duration = mp4.AudioTotalDuration
}
// Try adding subtitle track if available
if mp4.VideoTrackName == "VTT" || mp4.VideoTrackName == "WebVTT" {
// Add a subtitle track to the moov box
init.AddEmptyTrack(videoTimescale, "subtitle", "und")
// Set the subtitle descriptor
err := init.Moov.Traks[2].SetWvttDescriptor("")
if err != nil {
//log.Log.Error("mp4.Close(): error setting VTT descriptor: " + err.Error())
//return
}
init.Moov.Traks[2].Mdia.Hdlr.Name = "agent " + utils.VERSION
}
// We will create a fingerprint that's be encrypted with the public key, so we can verify the integrity of the file later.
// The fingerprint will be a UUID box, which is a custom box that we can use to store the fingerprint.
// Following fields are included in the fingerprint (UUID):
// - Moov.Mvhd.CreationTime (the time the file was created)
// - Moov.Mvhd.Duration (the total duration of the video)
// - Moov.Trak.Hdlr.Name // (the name of the handler, which is the agent and version)
// - len(Moof) // (the number of moof boxes in the file)
// - size(Moof1) // (the size of the first moof box)
// - size(Moof2) // (the size of the second moof box)
// ..
//
// All attributes of the fingerprint are concatenated into a single string, which is then hashed using SHA-256
// and encrypted with the public key.
fingerprint := fmt.Sprintf("%d", init.Moov.Mvhd.CreationTime) + "_" +
fmt.Sprintf("%d", init.Moov.Mvhd.Duration) + "_" +
init.Moov.Trak.Mdia.Hdlr.Name + "_" +
fmt.Sprintf("%d", mp4.MoofBoxes) + "_" // Number of moof boxes
for i, size := range mp4.MoofBoxSizes {
fingerprint += fmt.Sprintf("%d", size)
if i < len(mp4.MoofBoxSizes)-1 {
fingerprint += "_"
}
}
// Remove trailing underscore if present
if len(fingerprint) > 0 && fingerprint[len(fingerprint)-1] == '_' {
fingerprint = fingerprint[:len(fingerprint)-1]
}
// Load the private key from the configuration
privateKey := config.Signing.PrivateKey
r := strings.NewReader(privateKey)
pemBytes, _ := ioutil.ReadAll(r)
block, _ := pem.Decode(pemBytes)
if block == nil {
//log.Log.Error("mp4.Close(): 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("mp4.Close(): error parsing private key: " + err.Error())
//return
} else {
// Conver key to *rsa.PrivateKey
rsaKey, _ := key.(*rsa.PrivateKey)
fingerprintBytes := []byte(fingerprint)
signature, err := encryption.SignWithPrivateKey(fingerprintBytes, rsaKey)
if err == nil && len(signature) > 0 {
uuid := &mp4ff.UUIDBox{}
uuid.SetUUID("6b0c1f8e-3d2a-4f5b-9c7d-8f1e2b3c4d5e")
uuid.UnknownPayload = signature
init.Moov.AddChild(uuid)
} else {
//log.Log.Error("mp4.Close(): error signing fingerprint: " + err.Error())
}
}
}
// We will also calculate the SIDX box, which is a segment index box that contains information about the segments in the file.
// This is useful for seeking in the file, and for streaming the file.
/*sidx := &mp4ff.SidxBox{
Version: 0,
Flags: 0,
ReferenceID: 0,
Timescale: videoTimescale,
EarliestPresentationTime: 0,
FirstOffset: 0,
SidxRefs: make([]mp4ff.SidxRef, 0),
}
referenceTrak := init.Moov.Trak
trex, ok := init.Moov.Mvex.GetTrex(referenceTrak.Tkhd.TrackID)
if !ok {
// We have an issue.
}
segDatas, err := findSegmentData(mp4.Segments, referenceTrak, trex)
if err != nil {
// We have an issue.
}
fillSidx(sidx, referenceTrak, segDatas, true)
// Add the SIDX box to the moov box
init.AddChild(sidx)*/
// Get a bit slice writer for the init segment
// Get a byte buffer of 10008 bytes to write the init segment
buffer := bytes.NewBuffer(make([]byte, 0))
init.Encode(buffer)
// The first 10008 bytes of the file is a free box, so we can read it and replace it with the moov box.
// The init box might not be 10008 bytes, so we need to read the first 10008 bytes and then replace it with the moov box.
// while the remaining bytes are for a new free box.
// Write the init segment at the beginning of the file, replacing the free box
if _, err := mp4.FileWriter.WriteAt(buffer.Bytes(), 0); err != nil {
panic(err)
}
// Calculate the remaining size for the free box
remainingSize := mp4.FreeBoxSize - int64(buffer.Len())
if remainingSize > 0 {
newFreeBox := mp4ff.NewFreeBox(make([]byte, remainingSize))
var freeBuf bytes.Buffer
if err := newFreeBox.Encode(&freeBuf); err != nil {
panic(err)
}
if _, err := mp4.FileWriter.WriteAt(freeBuf.Bytes(), int64(buffer.Len())); err != nil {
panic(err)
}
}
}
type segData struct {
startPos uint64
presentationTime uint64
baseDecodeTime uint64
dur uint32
size uint32
}
func fillSidx(sidx *mp4ff.SidxBox, refTrak *mp4ff.TrakBox, segDatas []segData, nonZeroEPT bool) {
ept := uint64(0)
if nonZeroEPT {
ept = segDatas[0].presentationTime
}
sidx.Version = 1
sidx.Timescale = refTrak.Mdia.Mdhd.Timescale
sidx.ReferenceID = 1
sidx.EarliestPresentationTime = ept
sidx.FirstOffset = 0
sidx.SidxRefs = make([]mp4ff.SidxRef, 0, len(segDatas))
for _, segData := range segDatas {
size := segData.size
sidx.SidxRefs = append(sidx.SidxRefs, mp4ff.SidxRef{
ReferencedSize: size,
SubSegmentDuration: segData.dur,
StartsWithSAP: 1,
SAPType: 1,
})
}
}
// findSegmentData returns a slice of segment media data using a reference track.
func findSegmentData(segs []*mp4ff.MediaSegment, refTrak *mp4ff.TrakBox, trex *mp4ff.TrexBox) ([]segData, error) {
segDatas := make([]segData, 0, len(segs))
for _, seg := range segs {
var firstCompositionTimeOffest int64
dur := uint32(0)
var baseTime uint64
for fIdx, frag := range seg.Fragments {
for _, traf := range frag.Moof.Trafs {
tfhd := traf.Tfhd
if tfhd.TrackID == refTrak.Tkhd.TrackID { // Find track that gives sidx time values
if fIdx == 0 {
baseTime = traf.Tfdt.BaseMediaDecodeTime()
}
for i, trun := range traf.Truns {
trun.AddSampleDefaultValues(tfhd, trex)
samples := trun.GetSamples()
for j, sample := range samples {
if fIdx == 0 && i == 0 && j == 0 {
firstCompositionTimeOffest = int64(sample.CompositionTimeOffset)
}
dur += sample.Dur
}
}
}
}
}
sd := segData{
startPos: seg.StartPos,
presentationTime: uint64(int64(baseTime) + firstCompositionTimeOffest),
baseDecodeTime: baseTime,
dur: dur,
size: uint32(seg.Size()),
}
segDatas = append(segDatas, sd)
}
return segDatas, nil
}
// annexBToLengthPrefixed converts Annex B formatted H264 data (with start codes)
// into length-prefixed NAL units (4-byte length before each NAL unit).
func annexBToLengthPrefixed(data []byte) ([]byte, error) {
var out bytes.Buffer
// Find start codes and split NAL units
nalus := splitNALUs(data)
if len(nalus) == 0 {
return nil, fmt.Errorf("no NAL units found")
}
for _, nalu := range nalus {
// Remove Annex B start codes (0x000001 or 0x00000001) from the beginning of each NALU
nalu = removeAnnexBStartCode(nalu)
if len(nalu) == 0 {
continue
}
// Write 4-byte big-endian length
length := uint32(len(nalu))
lenBytes := []byte{
byte(length >> 24),
byte(length >> 16),
byte(length >> 8),
byte(length),
}
out.Write(lenBytes)
out.Write(nalu)
}
return out.Bytes(), nil
}
// removeAnnexBStartCode removes a leading Annex B start code from a NALU if present.
func removeAnnexBStartCode(nalu []byte) []byte {
if len(nalu) >= 4 && nalu[0] == 0x00 && nalu[1] == 0x00 {
if nalu[2] == 0x01 {
return nalu[3:]
}
if nalu[2] == 0x00 && nalu[3] == 0x01 {
return nalu[4:]
}
}
return nalu
}
// splitNALUs splits Annex B data into raw NAL units without start codes.
func splitNALUs(data []byte) [][]byte {
var nalus [][]byte
start := 0
for start < len(data) {
// Find next start code (0x000001 or 0x00000001)
i := findStartCode(data, start+3)
if i < 0 {
// Last NALU till end of data
nalus = append(nalus, data[start:])
break
}
// NAL unit is between start and i
nalus = append(nalus, data[start:i])
start = i
}
return nalus
}
// findStartCode returns the index of the next Annex B start code (0x000001 or 0x00000001) after pos, or -1 if none.
func findStartCode(data []byte, pos int) int {
for i := pos; i+3 < len(data); i++ {
if data[i] == 0x00 && data[i+1] == 0x00 {
if data[i+2] == 0x01 {
return i
}
if i+3 < len(data) && data[i+2] == 0x00 && data[i+3] == 0x01 {
return i
}
}
}
return -1
}
// FindSyncword searches for the AAC syncword (0xFFF0) in the given byte slice starting from the specified offset.
func FindSyncword(aac []byte, offset int) int {
for i := offset; i < len(aac)-1; i++ {
if aac[i] == 0xFF && aac[i+1]&0xF0 == 0xF0 {
return i
}
}
return -1
}
// Table 31 Profiles
// index profile
// 0 Main profile
// 1 Low Complexity profile (LC)
// 2 Scalable Sampling Rate profile (SSR)
// 3 (reserved)
type START_CODE_TYPE int
const (
START_CODE_3 START_CODE_TYPE = 3
START_CODE_4 START_CODE_TYPE = 4
)
func FindStartCode(nalu []byte, offset int) (int, START_CODE_TYPE) {
idx := bytes.Index(nalu[offset:], []byte{0x00, 0x00, 0x01})
switch {
case idx > 0:
if nalu[offset+idx-1] == 0x00 {
return offset + idx - 1, START_CODE_4
}
fallthrough
case idx == 0:
return offset + idx, START_CODE_3
}
return -1, START_CODE_3
}
func SplitFrame(frames []byte, onFrame func(nalu []byte) bool) {
beg, sc := FindStartCode(frames, 0)
for beg >= 0 {
end, sc2 := FindStartCode(frames, beg+int(sc))
if end == -1 {
if onFrame != nil {
onFrame(frames[beg+int(sc):])
}
break
}
if onFrame != nil && onFrame(frames[beg+int(sc):end]) == false {
break
}
beg = end
sc = sc2
}
}
func SplitFrameWithStartCode(frames []byte, onFrame func(nalu []byte) bool) {
beg, sc := FindStartCode(frames, 0)
for beg >= 0 {
end, sc2 := FindStartCode(frames, beg+int(sc))
if end == -1 {
if onFrame != nil && (beg+int(sc)) < len(frames) {
onFrame(frames[beg:])
}
break
}
if onFrame != nil && (beg+int(sc)) < end && onFrame(frames[beg:end]) == false {
break
}
beg = end
sc = sc2
}
}
func SplitAACFrame(frames []byte, onFrame func(started bool, aac []byte)) {
var adts ADTS_Frame_Header
start := FindSyncword(frames, 0)
started := false
for start >= 0 {
adts.Decode(frames[start:])
onFrame(started, frames[start:start+int(adts.Variable_Header.Frame_length)])
start = FindSyncword(frames, start+int(adts.Variable_Header.Frame_length))
started = true
}
}
type AAC_PROFILE int
const (
MAIN AAC_PROFILE = iota
LC
SSR
)
type AAC_SAMPLING_FREQUENCY int
const (
AAC_SAMPLE_96000 AAC_SAMPLING_FREQUENCY = iota
AAC_SAMPLE_88200
AAC_SAMPLE_64000
AAC_SAMPLE_48000
AAC_SAMPLE_44100
AAC_SAMPLE_32000
AAC_SAMPLE_24000
AAC_SAMPLE_22050
AAC_SAMPLE_16000
AAC_SAMPLE_12000
AAC_SAMPLE_11025
AAC_SAMPLE_8000
AAC_SAMPLE_7350
)
var AAC_Sampling_Idx [13]int = [13]int{96000, 88200, 64000, 48000, 44100, 32000, 24000, 22050, 16000, 12000, 11025, 8000, 7350}
// Table 4 Syntax of adts_sequence()
// adts_sequence() {
// while (nextbits() == syncword) {
// adts_frame();
// }
// }
// Table 5 Syntax of adts_frame()
// adts_frame() {
// adts_fixed_header();
// adts_variable_header();
// if (number_of_raw_data_blocks_in_frame == 0) {
// adts_error_check();
// raw_data_block();
// }
// else {
// adts_header_error_check();
// for (i = 0; i <= number_of_raw_data_blocks_in_frame;i++ {
// raw_data_block();
// adts_raw_data_block_error_check();
// }
// }
// }
// adts_fixed_header()
// {
// syncword; 12 bslbf
// ID; 1 bslbf
// layer; 2 uimsbf
// protection_absent; 1 bslbf
// profile; 2 uimsbf
// sampling_frequency_index; 4 uimsbf
// private_bit; 1 bslbf
// channel_configuration; 3 uimsbf
// original/copy; 1 bslbf
// home; 1 bslbf
// }
type ADTS_Fix_Header struct {
ID uint8
Layer uint8
Protection_absent uint8
Profile uint8
Sampling_frequency_index uint8
Private_bit uint8
Channel_configuration uint8
Originalorcopy uint8
Home uint8
}
// adts_variable_header() {
// copyright_identification_bit; 1 bslbf
// copyright_identification_start; 1 bslbf
// frame_length; 13 bslbf
// adts_buffer_fullness; 11 bslbf
// number_of_raw_data_blocks_in_frame; 2 uimsfb
// }
type ADTS_Variable_Header struct {
Copyright_identification_bit uint8
copyright_identification_start uint8
Frame_length uint16
Adts_buffer_fullness uint16
Number_of_raw_data_blocks_in_frame uint8
}
type ADTS_Frame_Header struct {
Fix_Header ADTS_Fix_Header
Variable_Header ADTS_Variable_Header
}
func NewAdtsFrameHeader() *ADTS_Frame_Header {
return &ADTS_Frame_Header{
Fix_Header: ADTS_Fix_Header{
ID: 0,
Layer: 0,
Protection_absent: 1,
Profile: uint8(MAIN),
Sampling_frequency_index: uint8(AAC_SAMPLE_44100),
Private_bit: 0,
Channel_configuration: 0,
Originalorcopy: 0,
Home: 0,
},
Variable_Header: ADTS_Variable_Header{
copyright_identification_start: 0,
Copyright_identification_bit: 0,
Frame_length: 0,
Adts_buffer_fullness: 0,
Number_of_raw_data_blocks_in_frame: 0,
},
}
}
func (frame *ADTS_Frame_Header) Decode(aac []byte) {
_ = aac[6]
frame.Fix_Header.ID = aac[1] >> 3
frame.Fix_Header.Layer = aac[1] >> 1 & 0x03
frame.Fix_Header.Protection_absent = aac[1] & 0x01
frame.Fix_Header.Profile = aac[2] >> 6 & 0x03
frame.Fix_Header.Sampling_frequency_index = aac[2] >> 2 & 0x0F
frame.Fix_Header.Private_bit = aac[2] >> 1 & 0x01
frame.Fix_Header.Channel_configuration = (aac[2] & 0x01 << 2) | (aac[3] >> 6)
frame.Fix_Header.Originalorcopy = aac[3] >> 5 & 0x01
frame.Fix_Header.Home = aac[3] >> 4 & 0x01
frame.Variable_Header.Copyright_identification_bit = aac[3] >> 3 & 0x01
frame.Variable_Header.copyright_identification_start = aac[3] >> 2 & 0x01
frame.Variable_Header.Frame_length = (uint16(aac[3]&0x03) << 11) | (uint16(aac[4]) << 3) | (uint16(aac[5]>>5) & 0x07)
frame.Variable_Header.Adts_buffer_fullness = (uint16(aac[5]&0x1F) << 6) | uint16(aac[6]>>2)
frame.Variable_Header.Number_of_raw_data_blocks_in_frame = aac[6] & 0x03
}
func (frame *ADTS_Frame_Header) Encode() []byte {
var hdr []byte
if frame.Fix_Header.Protection_absent == 1 {
hdr = make([]byte, 7)
} else {
hdr = make([]byte, 9)
}
hdr[0] = 0xFF
hdr[1] = 0xF0
hdr[1] = hdr[1] | (frame.Fix_Header.ID << 3) | (frame.Fix_Header.Layer << 1) | frame.Fix_Header.Protection_absent
hdr[2] = frame.Fix_Header.Profile<<6 | frame.Fix_Header.Sampling_frequency_index<<2 | frame.Fix_Header.Private_bit<<1 | frame.Fix_Header.Channel_configuration>>2
hdr[3] = frame.Fix_Header.Channel_configuration<<6 | frame.Fix_Header.Originalorcopy<<5 | frame.Fix_Header.Home<<4
hdr[3] = hdr[3] | frame.Variable_Header.copyright_identification_start<<3 | frame.Variable_Header.Copyright_identification_bit<<2 | byte(frame.Variable_Header.Frame_length<<11)
hdr[4] = byte(frame.Variable_Header.Frame_length >> 3)
hdr[5] = byte((frame.Variable_Header.Frame_length&0x07)<<5) | byte(frame.Variable_Header.Adts_buffer_fullness>>3)
hdr[6] = byte(frame.Variable_Header.Adts_buffer_fullness&0x3F<<2) | frame.Variable_Header.Number_of_raw_data_blocks_in_frame
return hdr
}
func SampleToAACSampleIndex(sampling int) int {
for i, v := range AAC_Sampling_Idx {
if v == sampling {
return i
}
}
panic("not Found AAC Sample Index")
}
func AACSampleIdxToSample(idx int) int {
return AAC_Sampling_Idx[idx]
}
// +--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
// | audio object type(5 bits) | sampling frequency index(4 bits) | channel configuration(4 bits) | GA framelength flag(1 bits) | GA Depends on core coder(1 bits) | GA Extension Flag(1 bits) |
// +--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
type AudioSpecificConfiguration struct {
Audio_object_type uint8
Sample_freq_index uint8
Channel_configuration uint8
GA_framelength_flag uint8
GA_depends_on_core_coder uint8
GA_extension_flag uint8
}
func NewAudioSpecificConfiguration() *AudioSpecificConfiguration {
return &AudioSpecificConfiguration{
Audio_object_type: 0,
Sample_freq_index: 0,
Channel_configuration: 0,
GA_framelength_flag: 0,
GA_depends_on_core_coder: 0,
GA_extension_flag: 0,
}
}
func (asc *AudioSpecificConfiguration) Encode() []byte {
buf := make([]byte, 2)
buf[0] = (asc.Audio_object_type & 0x1f << 3) | (asc.Sample_freq_index & 0x0F >> 1)
buf[1] = (asc.Sample_freq_index & 0x0F << 7) | (asc.Channel_configuration & 0x0F << 3) | (asc.GA_framelength_flag & 0x01 << 2) | (asc.GA_depends_on_core_coder & 0x01 << 1) | (asc.GA_extension_flag & 0x01)
return buf
}
func (asc *AudioSpecificConfiguration) Decode(buf []byte) error {
if len(buf) < 2 {
return errors.New("len of buf < 2 ")
}
asc.Audio_object_type = buf[0] >> 3
asc.Sample_freq_index = (buf[0] & 0x07 << 1) | (buf[1] >> 7)
asc.Channel_configuration = buf[1] >> 3 & 0x0F
asc.GA_framelength_flag = buf[1] >> 2 & 0x01
asc.GA_depends_on_core_coder = buf[1] >> 1 & 0x01
asc.GA_extension_flag = buf[1] & 0x01
return nil
}
func ConvertADTSToASC(frame []byte) (*AudioSpecificConfiguration, error) {
if len(frame) < 7 {
return nil, errors.New("len of frame < 7")
}
adts := NewAdtsFrameHeader()
adts.Decode(frame)
asc := NewAudioSpecificConfiguration()
asc.Audio_object_type = adts.Fix_Header.Profile + 1
asc.Channel_configuration = adts.Fix_Header.Channel_configuration
asc.Sample_freq_index = adts.Fix_Header.Sampling_frequency_index
return asc, nil
}
func ConvertASCToADTS(asc []byte, aacbytes int) (*ADTS_Frame_Header, error) {
aac_asc := NewAudioSpecificConfiguration()
err := aac_asc.Decode(asc)
if err != nil {
return nil, err
}
aac_adts := NewAdtsFrameHeader()
aac_adts.Fix_Header.Profile = aac_asc.Audio_object_type - 1
aac_adts.Fix_Header.Channel_configuration = aac_asc.Channel_configuration
aac_adts.Fix_Header.Sampling_frequency_index = aac_asc.Sample_freq_index
aac_adts.Fix_Header.Protection_absent = 1
aac_adts.Variable_Header.Adts_buffer_fullness = 0x3F
aac_adts.Variable_Header.Frame_length = uint16(aacbytes)
return aac_adts, nil
}

View File

@@ -3,23 +3,23 @@ package webrtc
import (
"encoding/base64"
"encoding/json"
"fmt"
"io"
"strconv"
"sync"
"sync/atomic"
"time"
//"github.com/izern/go-fdkaac/fdkaac"
"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"
"github.com/pion/interceptor"
"github.com/pion/interceptor/pkg/intervalpli"
pionWebRTC "github.com/pion/webrtc/v4"
pionMedia "github.com/pion/webrtc/v4/pkg/media"
)
var (
@@ -27,7 +27,6 @@ var (
CandidateArrays map[string](chan string)
peerConnectionCount int64
peerConnections map[string]*pionWebRTC.PeerConnection
//encoder *ffmpeg.VideoEncoder
)
type WebRTC struct {
@@ -40,24 +39,6 @@ type WebRTC struct {
PacketsCount chan int
}
// No longer used, is for transcoding, might comeback on this!
/*func init() {
// Encoder is created for once and for all.
var err error
encoder, err = ffmpeg.NewVideoEncoderByCodecType(av.H264)
if err != nil {
return
}
if encoder == nil {
err = fmt.Errorf("Video encoder not found")
return
}
encoder.SetFramerate(30, 1)
encoder.SetPixelFormat(av.I420)
encoder.SetBitrate(1000000) // 1MB
encoder.SetGopSize(30 / 1) // 1s
}*/
func CreateWebRTC(name string, stunServers []string, turnServers []string, turnServersUsername string, turnServersCredential string) *WebRTC {
return &WebRTC{
Name: name,
@@ -66,14 +47,13 @@ func CreateWebRTC(name string, stunServers []string, turnServers []string, turnS
TurnServersUsername: turnServersUsername,
TurnServersCredential: turnServersCredential,
Timer: time.NewTimer(time.Second * 10),
PacketsCount: make(chan int),
}
}
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,7 +67,36 @@ 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.RequestHDStreamPayload, candidates chan string) {
func RegisterCandidates(key string, candidate models.ReceiveHDCandidatesPayload) {
// Set lock
CandidatesMutex.Lock()
_, ok := CandidateArrays[key]
if !ok {
CandidateArrays[key] = make(chan string, 100)
}
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 RegisterDefaultInterceptors(mediaEngine *pionWebRTC.MediaEngine, interceptorRegistry *interceptor.Registry) error {
if err := pionWebRTC.ConfigureNack(mediaEngine, interceptorRegistry); err != nil {
return err
}
if err := pionWebRTC.ConfigureRTCPReports(interceptorRegistry); err != nil {
return err
}
if err := pionWebRTC.ConfigureSimulcastExtensionHeaders(mediaEngine); err != nil {
return err
}
return nil
}
func InitializeWebRTCConnection(configuration *models.Configuration, communication *models.Communication, mqttClient mqtt.Client, videoTrack *pionWebRTC.TrackLocalStaticSample, audioTrack *pionWebRTC.TrackLocalStaticSample, handshake models.RequestHDStreamPayload) {
config := configuration.Config
deviceKey := config.Key
@@ -96,6 +105,15 @@ func InitializeWebRTCConnection(configuration *models.Configuration, communicati
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, 100)
}
CandidatesMutex.Unlock()
// Set variables
hubKey := handshake.HubKey
sessionDescription := handshake.SessionDescription
@@ -108,10 +126,39 @@ func InitializeWebRTCConnection(configuration *models.Configuration, communicati
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))
// Create a InterceptorRegistry. This is the user configurable RTP/RTCP Pipeline.
// This provides NACKs, RTCP Reports and other features. If you use `webrtc.NewPeerConnection`
// this is enabled by default. If you are manually managing You MUST create a InterceptorRegistry
// for each PeerConnection.
interceptorRegistry := &interceptor.Registry{}
// Use the default set of Interceptors
if err := pionWebRTC.RegisterDefaultInterceptors(mediaEngine, interceptorRegistry); err != nil {
panic(err)
}
// Register a intervalpli factory
// This interceptor sends a PLI every 3 seconds. A PLI causes a video keyframe to be generated by the sender.
// This makes our video seekable and more error resilent, but at a cost of lower picture quality and higher bitrates
// A real world application should process incoming RTCP packets from viewers and forward them to senders
intervalPliFactory, err := intervalpli.NewReceiverInterceptor()
if err != nil {
panic(err)
}
interceptorRegistry.Add(intervalPliFactory)
api := pionWebRTC.NewAPI(
pionWebRTC.WithMediaEngine(mediaEngine),
pionWebRTC.WithInterceptorRegistry(interceptorRegistry),
)
policy := pionWebRTC.ICETransportPolicyAll
if config.ForceTurn == "true" {
policy = pionWebRTC.ICETransportPolicyRelay
}
peerConnection, err := api.NewPeerConnection(
pionWebRTC.Configuration{
@@ -125,64 +172,102 @@ func InitializeWebRTCConnection(configuration *models.Configuration, communicati
Credential: w.TurnServersCredential,
},
},
//ICETransportPolicy: pionWebRTC.ICETransportPolicyRelay,
ICETransportPolicy: policy,
},
)
if err == nil && peerConnection != nil {
if _, err = peerConnection.AddTrack(videoTrack); err != nil {
panic(err)
var videoSender *pionWebRTC.RTPSender = nil
if videoSender, err = peerConnection.AddTrack(videoTrack); err != nil {
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)
}
peerConnection.OnICEConnectionStateChange(func(connectionState pionWebRTC.ICEConnectionState) {
if connectionState == pionWebRTC.ICEConnectionStateDisconnected {
atomic.AddInt64(&peerConnectionCount, -1)
peerConnections[handshake.SessionID] = nil
close(candidates)
close(w.PacketsCount)
if err := peerConnection.Close(); err != nil {
panic(err)
}
} else if connectionState == pionWebRTC.ICEConnectionStateConnected {
atomic.AddInt64(&peerConnectionCount, 1)
} else if connectionState == pionWebRTC.ICEConnectionStateChecking {
// Iterate over the candidates and send them to the remote client
// Non blocking channel
for candidate := range candidates {
log.Log.Info("InitializeWebRTCConnection: Received candidate.")
if candidateErr := peerConnection.AddICECandidate(pionWebRTC.ICECandidateInit{Candidate: string(candidate)}); candidateErr != nil {
log.Log.Error("InitializeWebRTCConnection: something went wrong while adding candidate: " + candidateErr.Error())
}
// Read incoming RTCP packets
// Before these packets are returned they are processed by interceptors. For things
// like NACK this needs to be called.
go func() {
rtcpBuf := make([]byte, 1500)
for {
if _, _, rtcpErr := videoSender.Read(rtcpBuf); rtcpErr != nil {
return
}
}
log.Log.Info("InitializeWebRTCConnection: connection state changed to: " + connectionState.String())
log.Log.Info("InitializeWebRTCConnection: Number of peers connected (" + strconv.FormatInt(peerConnectionCount, 10) + ")")
}()
var audioSender *pionWebRTC.RTPSender = nil
if audioSender, err = peerConnection.AddTrack(audioTrack); err != nil {
log.Log.Error("webrtc.main.InitializeWebRTCConnection(): something went wrong while adding audio track: " + err.Error())
} // Read incoming RTCP packets
// Before these packets are returned they are processed by interceptors. For things
// like NACK this needs to be called.
go func() {
rtcpBuf := make([]byte, 1500)
for {
if _, _, rtcpErr := audioSender.Read(rtcpBuf); rtcpErr != nil {
return
}
}
}()
peerConnection.OnConnectionStateChange(func(connectionState pionWebRTC.PeerConnectionState) {
if connectionState == pionWebRTC.PeerConnectionStateDisconnected || connectionState == pionWebRTC.PeerConnectionStateClosed {
// Set lock
CandidatesMutex.Lock()
atomic.AddInt64(&peerConnectionCount, -1)
_, ok := CandidateArrays[sessionKey]
if ok {
close(CandidateArrays[sessionKey])
delete(CandidateArrays, sessionKey)
}
// Not really needed.
//senders := peerConnection.GetSenders()
//for _, sender := range senders {
// if err := peerConnection.RemoveTrack(sender); err != nil {
// log.Log.Error("webrtc.main.InitializeWebRTCConnection(): something went wrong while removing track: " + err.Error())
// }
//}
if err := peerConnection.Close(); err != nil {
log.Log.Error("webrtc.main.InitializeWebRTCConnection(): something went wrong while closing peer connection: " + err.Error())
}
peerConnections[handshake.SessionID] = nil
delete(peerConnections, handshake.SessionID)
CandidatesMutex.Unlock()
} else if connectionState == pionWebRTC.PeerConnectionStateConnected {
CandidatesMutex.Lock()
atomic.AddInt64(&peerConnectionCount, 1)
CandidatesMutex.Unlock()
} else if connectionState == pionWebRTC.PeerConnectionStateFailed {
log.Log.Info("webrtc.main.InitializeWebRTCConnection(): ICEConnectionStateFailed")
}
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) + ")")
})
go func() {
// Iterate over the candidates and send them to the remote client
// Non blocking channe
for candidate := range CandidateArrays[sessionKey] {
CandidatesMutex.Lock()
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())
}
CandidatesMutex.Unlock()
}
}()
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())
}
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) {
@@ -190,19 +275,16 @@ func InitializeWebRTCConnection(configuration *models.Configuration, communicati
return
}
candidatesMux.Lock()
defer candidatesMux.Unlock()
// Create a config map
valueMap := make(map[string]interface{})
candateJSON := candidate.ToJSON()
sdpmid := "0"
candateJSON.SDPMid = &sdpmid
candateBinary, err := json.Marshal(candateJSON)
if err == nil {
valueMap["candidate"] = string(candateBinary)
valueMap["sdp"] = []byte(base64.StdEncoding.EncodeToString([]byte(answer.SDP)))
valueMap["session_id"] = handshake.SessionID
} else {
log.Log.Info("HandleRequestConfig: something went wrong while marshalling candidate: " + err.Error())
log.Log.Info("webrtc.main.InitializeWebRTCConnection(): something went wrong while marshalling candidate: " + err.Error())
}
// We'll send the candidate to the hub
@@ -215,11 +297,10 @@ func InitializeWebRTCConnection(configuration *models.Configuration, communicati
}
payload, err := models.PackageMQTTMessage(configuration, message)
if err == nil {
log.Log.Info("InitializeWebRTCConnection:" + string(candateBinary))
token := mqttClient.Publish("kerberos/hub/"+hubKey, 2, false, payload)
token.Wait()
} else {
log.Log.Info("HandleRequestConfig: something went wrong while sending acknowledge config to hub: " + string(payload))
log.Log.Info("webrtc.main.InitializeWebRTCConnection(): while packaging mqtt message: " + err.Error())
}
})
@@ -230,7 +311,8 @@ func InitializeWebRTCConnection(configuration *models.Configuration, communicati
// Create a config map
valueMap := make(map[string]interface{})
valueMap["sdp"] = []byte(base64.StdEncoding.EncodeToString([]byte(answer.SDP)))
log.Log.Info("InitializeWebRTCConnection: Send SDP answer")
valueMap["session_id"] = handshake.SessionID
log.Log.Info("webrtc.main.InitializeWebRTCConnection(): Send SDP answer")
// We'll send the candidate to the hub
message := models.Message{
@@ -245,30 +327,29 @@ func InitializeWebRTCConnection(configuration *models.Configuration, communicati
token := mqttClient.Publish("kerberos/hub/"+hubKey, 2, false, payload)
token.Wait()
} else {
log.Log.Info("HandleRequestConfig: something went wrong while sending acknowledge config to hub: " + string(payload))
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
}
}
@@ -276,7 +357,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
@@ -285,51 +366,51 @@ 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
hasAAC := false
hasOpus := false
streams, _ := rtspClient.GetStreams()
for _, stream := range streams {
if stream.Name == "H264" {
hasH264 = true
} else if stream.Name == "PCM_MULAW" {
hasPCM_MULAW = true
} else if stream.Name == "AAC" {
hasAAC = true
} else if stream.Name == "OPUS" {
hasOpus = true
}
}
if videoIdx == -1 {
log.Log.Error("WriteToTrack: no video codec found.")
if !hasH264 && !hasPCM_MULAW && !hasAAC && !hasOpus {
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 previousTime time.Duration
var pkt packets.Packet
var lastAudioSample *pionMedia.Sample = nil
var lastVideoSample *pionMedia.Sample = nil
start := false
receivedKeyFrame := false
codecData := codecs[videoIdx]
lastKeepAlive := "0"
peerCount := "0"
for cursorError == nil {
pkt, cursorError = livestreamCursor.ReadPacket()
bufferDuration := pkt.Time - previousTime
previousTime = pkt.Time
if config.Capture.ForwardWebRTC != "true" && peerConnectionCount == 0 {
start = false
receivedKeyFrame = false
continue
}
//if config.Capture.ForwardWebRTC != "true" && peerConnectionCount == 0 {
// start = false
// receivedKeyFrame = false
// continue
//}
select {
case lastKeepAlive = <-communication.HandleLiveHDKeepalive:
@@ -365,74 +446,68 @@ 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()
}
}*/
if pkt.IsVideo {
}
switch int(pkt.Idx) {
case videoIdx:
// For every key-frame pre-pend the SPS and PPS
pkt.Data = pkt.Data[4:]
// 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 start {
sample := pionMedia.Sample{Data: pkt.Data, Duration: bufferDuration}
sample := pionMedia.Sample{Data: pkt.Data, PacketTimestamp: uint32(pkt.Time)}
//sample = pionMedia.Sample{Data: pkt.Data, Duration: time.Second}
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())
if lastVideoSample != nil {
duration := sample.PacketTimestamp - lastVideoSample.PacketTimestamp
bufferDurationCasted := time.Duration(duration) * time.Millisecond
lastVideoSample.Duration = bufferDurationCasted
if err := videoTrack.WriteSample(*lastVideoSample); err != nil && err != io.ErrClosedPipe {
log.Log.Error("webrtc.main.WriteToTrack(): something went wrong while writing sample: " + err.Error())
}
}
lastVideoSample = &sample
}
}
case audioIdx:
// 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())
} else if pkt.IsAudio {
// @TODO: We need to check if the audio is PCM_MULAW or AAC
// If AAC we need to transcode it to PCM_MULAW
// If PCM_MULAW we can send it directly.
if hasAAC {
// We will transcode the audio from AAC to PCM_MULAW
// Not sure how to do this yet, but we need to use a decoder
// and then encode it to PCM_MULAW.
// TODO..
//d := fdkaac.NewAacDecoder()
continue
}
// We will send the audio
sample := pionMedia.Sample{Data: pkt.Data, PacketTimestamp: uint32(pkt.Time)}
if lastAudioSample != nil {
duration := sample.PacketTimestamp - lastAudioSample.PacketTimestamp
bufferDurationCasted := time.Duration(duration) * time.Millisecond
lastAudioSample.Duration = bufferDurationCasted
if err := audioTrack.WriteSample(*lastAudioSample); err != nil && err != io.ErrClosedPipe {
log.Log.Error("webrtc.main.WriteToTrack(): something went wrong while writing sample: " + err.Error())
}
}
lastAudioSample = &sample
}
}
}
for _, p := range peerConnections {
if p != nil {
p.Close()
}
}
peerConnectionCount = 0
log.Log.Info("WriteToTrack: stop writing to track.")
log.Log.Info("webrtc.main.WriteToTrack(): stop writing to track.")
}

4
machinery/update-mod.sh Executable file
View File

@@ -0,0 +1,4 @@
export GOSUMDB=off
rm -rf go.*
go mod init github.com/kerberos-io/agent/machinery
go mod tidy

View File

@@ -1,6 +0,0 @@
#!/bin/sh -e
cp -R $SNAP/data $SNAP_COMMON/
cp -R $SNAP/www $SNAP_COMMON/
cp -R $SNAP/version $SNAP_COMMON/
cp -R $SNAP/mp4fragment $SNAP_COMMON/

View File

@@ -1,23 +0,0 @@
name: kerberosio # you probably want to 'snapcraft register <name>'
base: core22 # the base snap is the execution environment for this snap
version: '3.0.0' # just for humans, typically '1.2+git' or '1.3.2'
summary: A stand-alone open source video surveillance system # 79 char long summary
description: |
Kerberos Agent is an isolated and scalable video (surveillance) management
agent made available as Open Source under the MIT License. This means that
all the source code is available for you or your company, and you can use,
transform and distribute the source code; as long you keep a reference of
the original license. Kerberos Agent can be used for commercial usage.
grade: stable # stable # must be 'stable' to release into candidate/stable channels
confinement: strict # use 'strict' once you have the right plugs and slots
environment:
GIN_MODE: release
apps:
agent:
command: main -config /var/snap/kerberosio/common
plugs: [ network, network-bind ]
parts:
agent:
source: . #https://github.com/kerberos-io/agent/releases/download/21c0e01/agent-amd64.tar
plugin: dump

View File

@@ -1,7 +1,6 @@
{
"name": "agent-ui",
"version": "0.1.0",
"private": false,
"dependencies": {
"@giantmachines/redux-websocket": "^1.5.1",
"@kerberos-io/ui": "^1.76.0",

View File

@@ -80,6 +80,7 @@
"description_general": "Allgemeine Einstellungen für den Kerberos Agent",
"key": "Schlüssel",
"camera_name": "Kamera Name",
"camera_friendly_name": "Kamera Anzeigename",
"timezone": "Zeitzone",
"select_timezone": "Zeitzone auswählen",
"advanced_configuration": "Erweiterte Konfiguration",
@@ -99,9 +100,9 @@
"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",
@@ -145,6 +146,8 @@
"turn_server": "TURN Server",
"turn_username": "Benutzername",
"turn_password": "Passwort",
"force_turn": "Erzwinge TURN",
"force_turn_description": "Erzwinge die Verwendung von TURN",
"stun_turn_forward": "Weiterleiten und transkodieren",
"stun_turn_description_forward": "Optiemierungen und Verbesserungen der TURN/STUN Kommunikation.",
"stun_turn_webrtc": "Weiterleiten an WebRTC Schnittstelle",
@@ -185,6 +188,8 @@
"description_persistence": "Die möglichkeit zur Speicherung der Daten an einem Zentralen Ort ist der Beginn einer effektiven Videoüberwachung. Es kann zwischen",
"description2_persistence": ", oder einem Drittanbieter gewählt werden.",
"select_persistence": "Speicherort auswählen",
"kerberoshub_encryption": "Encryption",
"kerberoshub_encryption_description": "All traffic from/to Kerberos Hub will encrypted using AES-256.",
"kerberoshub_proxyurl": "Kerberos Hub Proxy URL",
"kerberoshub_description_proxyurl": "Der Proxy Endpunkt zum hochladen der Aufnahmen.",
"kerberoshub_apiurl": "Kerberos Hub API URL",

View File

@@ -9,7 +9,7 @@
},
"navigation": {
"profile": "Profile",
"admin": "admin",
"admin": "Admin",
"management": "Management",
"dashboard": "Dashboard",
"recordings": "Recordings",
@@ -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",
@@ -32,11 +32,11 @@
"latest_events": "Latest events",
"configure_connection": "Configure connection",
"no_events": "No events",
"no_events_description": "No recordings where found, make sure your Kerberos Agent is properly configured.",
"no_events_description": "No recordings were found, make sure your Agent is properly configured.",
"motion_detected": "Motion was detected",
"live_view": "Live view",
"loading_live_view": "Loading live view",
"loading_live_view_description": "Hold on we are loading your live view here. If you didn't configure your camera connection, update it on the settings pages.",
"loading_live_view_description": "Hold on, we are loading your live view here. If you didn't configure your camera connection, update it on the settings pages.",
"time": "Time",
"description": "Description",
"name": "Name"
@@ -59,31 +59,32 @@
"persistence": "Persistence"
},
"info": {
"kerberos_hub_demo": "Have a look at our Kerberos Hub demo environment, to see Kerberos Hub in action!",
"configuration_updated_success": "Your configuration have been updated successfully.",
"kerberos_hub_demo": "Have a look at our Hub demo environment, to see Hub in action!",
"configuration_updated_success": "Your configuration has been updated successfully.",
"configuration_updated_error": "Something went wrong while saving.",
"verify_hub": "Verifying your Kerberos Hub settings.",
"verify_hub_success": "Kerberos Hub settings are successfully verified.",
"verify_hub_error": "Something went wrong while verifying Kerberos Hub",
"verify_hub": "Verifying your Hub settings.",
"verify_hub_success": "Hub settings are successfully verified.",
"verify_hub_error": "Something went wrong while verifying Hub.",
"verify_persistence": "Verifying your persistence settings.",
"verify_persistence_success": "Persistence settings are successfully verified.",
"verify_persistence_error": "Something went wrong while verifying the persistence",
"verify_persistence_error": "Something went wrong while verifying the persistence.",
"verify_camera": "Verifying your camera settings.",
"verify_camera_success": "Camera settings are successfully verified.",
"verify_camera_error": "Something went wrong while verifying the camera settings",
"verify_camera_error": "Something went wrong while verifying the camera settings.",
"verify_onvif": "Verifying your ONVIF settings.",
"verify_onvif_success": "ONVIF settings are successfully verified.",
"verify_onvif_error": "Something went wrong while verifying the ONVIF settings"
"verify_onvif_error": "Something went wrong while verifying the ONVIF settings."
},
"overview": {
"general": "General",
"description_general": "General settings for your Kerberos Agent",
"description_general": "General settings for your Agent",
"key": "Key",
"camera_name": "Camera name",
"camera_friendly_name": "Friendly name",
"timezone": "Timezone",
"select_timezone": "Select a timezone",
"advanced_configuration": "Advanced configuration",
"description_advanced_configuration": "Detailed configuration options to enable or disable specific parts of the Kerberos Agent",
"description_advanced_configuration": "Detailed configuration options to enable or disable specific parts of the Agent",
"offline_mode": "Offline mode",
"description_offline_mode": "Disable all outgoing traffic",
"encryption": "Encryption",
@@ -99,10 +100,10 @@
"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.",
"rtsp_url": "RTSP url",
"rtsp_h264": "A H264 RTSP connection to your camera.",
"sub_rtsp_url": "Sub RTSP url (used for livestreaming)",
"only_h264": "Currently only H264/H265 RTSP streams are supported.",
"rtsp_url": "RTSP URL",
"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",
"description_onvif": "Credentials to communicate with ONVIF capabilities. These are used for PTZ or other capabilities provided by the camera.",
@@ -114,28 +115,28 @@
},
"recording": {
"recording": "Recording",
"description_recording": "Specify how you would like to make recordings. Having a continuous 24/7 setup or a motion based recording.",
"description_recording": "Specify how you would like to make recordings. Having a continuous 24/7 setup or a motion-based recording.",
"continuous_recording": "Continuous recording",
"description_continuous_recording": "Make 24/7 or motion based recordings.",
"max_duration": "max video duration (seconds)",
"description_continuous_recording": "Make 24/7 or motion-based recordings.",
"max_duration": "Max video duration (seconds)",
"description_max_duration": "The maximum duration of a recording.",
"pre_recording": "pre recording (key frames buffered)",
"pre_recording": "Pre recording (key frames buffered)",
"description_pre_recording": "Seconds before an event occurred.",
"post_recording": "post recording (seconds)",
"post_recording": "Post recording (seconds)",
"description_post_recording": "Seconds after an event occurred.",
"threshold": "Recording threshold (pixels)",
"description_threshold": "The number of pixels changed to record",
"description_threshold": "The number of pixels changed to record.",
"autoclean": "Auto clean",
"description_autoclean": "Specify if the Kerberos Agent can cleanup recordings when a specific storage capacity (MB) is reached. This will remove the oldest recordings when the capacity is reached.",
"description_autoclean": "Specify if the Agent can clean up recordings when a specific storage capacity (MB) is reached. This will remove the oldest recordings when the capacity is reached.",
"autoclean_enable": "Enable auto clean",
"autoclean_description_enable": "Remove oldest recording when capacity reached.",
"autoclean_max_directory_size": "Maximum directory size (MB)",
"autoclean_description_max_directory_size": "The maximum MB's of recordings stored.",
"autoclean_description_max_directory_size": "The maximum MBs of recordings stored.",
"fragmentedrecordings": "Fragmented recordings",
"description_fragmentedrecordings": "When recordings are fragmented they are suitable for an HLS stream. When turned on the MP4 container will look a bit different.",
"description_fragmentedrecordings": "When recordings are fragmented they are suitable for an HLS stream. When turned on, the MP4 container will look a bit different.",
"fragmentedrecordings_enable": "Enable fragmentation",
"fragmentedrecordings_description_enable": "Fragmented recordings are required for HLS.",
"fragmentedrecordings_duration": "fragment duration",
"fragmentedrecordings_duration": "Fragment duration",
"fragmentedrecordings_description_duration": "Duration of a single fragment."
},
"streaming": {
@@ -145,19 +146,26 @@
"turn_server": "TURN server",
"turn_username": "Username",
"turn_password": "Password",
"force_turn": "Force TURN",
"force_turn_description": "Force TURN usage, even when STUN is available.",
"stun_turn_forward": "Forwarding and transcoding",
"stun_turn_description_forward": "Optimisations and enhancements for TURN/STUN communication.",
"stun_turn_description_forward": "Optimizations and enhancements for TURN/STUN communication.",
"stun_turn_webrtc": "Forwarding to WebRTC broker",
"stun_turn_description_webrtc": "Forward h264 stream through MQTT",
"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.",
"mqtt_brokeruri": "Broker Uri",
"description_mqtt": "An MQTT broker is used to communicate from",
"description2_mqtt": "to the Agent, to achieve for example livestreaming or ONVIF (PTZ) capabilities.",
"mqtt_brokeruri": "Broker URI",
"mqtt_username": "Username",
"mqtt_password": "Password"
"mqtt_password": "Password",
"realtimeprocessing": "Realtime Processing",
"description_realtimeprocessing": "By enabling realtime processing, you will receive realtime video keyframes through the MQTT connection specified above.",
"realtimeprocessing_topic": "Topic to publish",
"realtimeprocessing_enabled": "Enable realtime processing",
"description_realtimeprocessing_enabled": "Send realtime video keyframes through MQTT."
},
"conditions": {
"timeofinterest": "Time Of Interest",
@@ -172,53 +180,61 @@
"friday": "Friday",
"saturday": "Saturday",
"externalcondition": "External Condition",
"description_externalcondition": "Depending on an external webservice recording can be enabled or disabled.",
"description_externalcondition": "Depending on an external web service, recording can be enabled or disabled.",
"regionofinterest": "Region Of Interest",
"description_regionofinterest": "By defining one or more regions, motion will be tracked only in the regions you have defined."
},
"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.",
"kerberoshub": "Hub",
"description_kerberoshub": "Agents can send heartbeats to a central",
"description2_kerberoshub": "installation. Heartbeats and other relevant information are synced to Hub to show realtime information about your video landscape.",
"persistence": "Persistence",
"saasoffering": "Kerberos Hub (SAAS offering)",
"secondary_persistence": "Secondary Persistence",
"description_secondary_persistence": "Recordings will be sent to secondary persistence if the primary persistence is unavailable or fails. This can be useful for failover purposes.",
"saasoffering": "Hub (SaaS offering)",
"description_persistence": "Having the ability to store your recordings is the beginning of everything. You can choose between our",
"description2_persistence": ", or a 3rd party provider",
"select_persistence": "Select a persistence",
"kerberoshub_proxyurl": "Kerberos Hub Proxy URL",
"kerberoshub_encryption": "Encryption",
"kerberoshub_encryption_description": "All traffic from/to Hub will be encrypted using AES-256.",
"kerberoshub_proxyurl": "Hub Proxy URL",
"kerberoshub_description_proxyurl": "The Proxy endpoint for uploading your recordings.",
"kerberoshub_apiurl": "Kerberos Hub API URL",
"kerberoshub_apiurl": "Hub API URL",
"kerberoshub_description_apiurl": "The API endpoint for uploading your recordings.",
"kerberoshub_publickey": "Public key",
"kerberoshub_description_publickey": "The public key granted to your Kerberos Hub account.",
"kerberoshub_description_publickey": "The public key granted to your Hub account.",
"kerberoshub_privatekey": "Private key",
"kerberoshub_description_privatekey": "The private key granted to your Kerberos Hub account.",
"kerberoshub_description_privatekey": "The private key granted to your Hub account.",
"kerberoshub_site": "Site",
"kerberoshub_description_site": "The site ID the Kerberos Agents are belonging to in Kerberos Hub.",
"kerberoshub_description_site": "The site ID the Agents belong to in Hub.",
"kerberoshub_region": "Region",
"kerberoshub_description_region": "The region we are storing our recordings in.",
"kerberoshub_bucket": "Bucket",
"kerberoshub_description_bucket": "The bucket we are storing our recordings in.",
"kerberoshub_username": "Username/Directory (should match Kerberos Hub username)",
"kerberoshub_description_username": "The username of your Kerberos Hub account.",
"kerberosvault_apiurl": "Kerberos Vault API URL",
"kerberosvault_description_apiurl": "The Kerberos Vault API",
"kerberoshub_username": "Username/Directory (should match Hub username)",
"kerberoshub_description_username": "The username of your Hub account.",
"kerberosvault_apiurl": "Vault API URL",
"kerberosvault_description_apiurl": "The Vault API",
"kerberosvault_provider": "Provider",
"kerberosvault_description_provider": "The provider to which your recordings will be send.",
"kerberosvault_directory": "Directory (should match Kerberos Hub username)",
"kerberosvault_description_directory": "Sub directory the recordings will be stored in your provider.",
"kerberosvault_description_provider": "The provider to which your recordings will be sent.",
"kerberosvault_directory": "Directory (should match Hub username)",
"kerberosvault_description_directory": "Subdirectory the recordings will be stored in your provider.",
"kerberosvault_accesskey": "Access key",
"kerberosvault_description_accesskey": "The access key of your Kerberos Vault account.",
"kerberosvault_description_accesskey": "The access key of your Vault account.",
"kerberosvault_secretkey": "Secret key",
"kerberosvault_description_secretkey": "The secret key of your Kerberos Vault account.",
"kerberosvault_description_secretkey": "The secret key of your Vault account.",
"kerberosvault_maxretries": "Max retries",
"kerberosvault_description_maxretries": "The maximum number of retries to upload a recording.",
"kerberosvault_timeout": "Timeout",
"kerberosvault_description_timeout": "If a timeout occurs, recordings will be sent directly to the secondary Vault.",
"dropbox_directory": "Directory",
"dropbox_description_directory": "The sub directory where the recordings will be stored in your Dropbox account.",
"dropbox_description_directory": "The subdirectory where the recordings will be stored in your Dropbox account.",
"dropbox_accesstoken": "Access token",
"dropbox_description_accesstoken": "The access token of your Dropbox account/app.",
"verify_connection": "Verify Connection",
"remove_after_upload": "Once recordings are uploaded to some persistence, you might want to remove them from the local Kerberos Agent.",
"remove_after_upload": "Once recordings are uploaded to some persistence, you might want to remove them from the local Agent.",
"remove_after_upload_description": "Remove recordings after they are uploaded successfully.",
"remove_after_upload_enabled": "Enabled delete on upload"
"remove_after_upload_enabled": "Enable delete on upload"
}
}
}
}

View File

@@ -80,6 +80,7 @@
"description_general": "General settings for your Kerberos Agent",
"key": "Key",
"camera_name": "Camera name",
"camera_friendly_name": "Camera friendly name",
"timezone": "Timezone",
"select_timezone": "Select a timezone",
"advanced_configuration": "Advanced configuration",
@@ -99,9 +100,9 @@
"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",
@@ -145,6 +146,8 @@
"turn_server": "TURN server",
"turn_username": "Username",
"turn_password": "Password",
"force_turn": "Force TURN",
"force_turn_description": "Force TURN usage, even when STUN is available.",
"stun_turn_forward": "Forwarding and transcoding",
"stun_turn_description_forward": "Optimisations and enhancements for TURN/STUN communication.",
"stun_turn_webrtc": "Forwarding to WebRTC broker",
@@ -185,6 +188,8 @@
"description_persistence": "Having the ability to store your recordings is the beginning of everything. You can choose between our",
"description2_persistence": ", or a 3rd party provider",
"select_persistence": "Select a persistence",
"kerberoshub_encryption": "Encryption",
"kerberoshub_encryption_description": "All traffic from/to Kerberos Hub will encrypted using AES-256.",
"kerberoshub_proxyurl": "Kerberos Hub Proxy URL",
"kerberoshub_description_proxyurl": "The Proxy endpoint for uploading your recordings.",
"kerberoshub_apiurl": "Kerberos Hub API URL",

View File

@@ -79,6 +79,7 @@
"description_general": "Paramètres généraux pour votre Agent Kerberos",
"key": "Clé",
"camera_name": "Nom de la caméra",
"camera_friendly_name": "Nom convivial de la caméra",
"timezone": "Fuseau horaire",
"select_timezone": "Sélectionner un fuseau horaire",
"advanced_configuration": "Configuration avancée",
@@ -98,9 +99,9 @@
"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",
@@ -144,6 +145,8 @@
"turn_server": "Serveur TURN",
"turn_username": "Nom d'utilisateur",
"turn_password": "Mot de passe",
"force_turn": "Forcer l'utilisation de TURN",
"force_turn_description": "Forcer l'utilisation de TURN au lieu de STUN",
"stun_turn_forward": "Redirection et transcodage",
"stun_turn_description_forward": "Optimisations et améliorations pour la communication TURN/STUN.",
"stun_turn_webrtc": "Redirection pour l'agent WebRTC",
@@ -184,6 +187,8 @@
"description_persistence": "Avoir la possibilité de stocker vos enregistrements est le commencement de tout. Vous pouvez choisir entre notre",
"description2_persistence": " ou auprès d'un fournisseur tiers",
"select_persistence": "Sélectionner une persistance",
"kerberoshub_encryption": "Encryption",
"kerberoshub_encryption_description": "All traffic from/to Kerberos Hub will encrypted using AES-256.",
"kerberoshub_proxyurl": "URL du proxy Kerberos Hub",
"kerberoshub_description_proxyurl": "Le point de terminaison du proxy pour téléverser vos enregistrements.",
"kerberoshub_apiurl": "URL de l'API Kerberos Hub",

View File

@@ -80,6 +80,7 @@
"description_general": "आपके Kerberos एजेंट के लिए सामान्य सेटिंग्स",
"key": "की",
"camera_name": "कैमरे का नाम",
"camera_friendly_name": "कैमरे का नाम",
"timezone": "समय क्षेत्र",
"select_timezone": "समयक्षेत्र चुनें",
"advanced_configuration": "एडवांस कॉन्फ़िगरेशन",
@@ -99,9 +100,9 @@
"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": "दुसरी RTSP URL (लाइवस्ट्रीमिंग के लिए प्रयुक्त)",
"sub_rtsp_h264": "आपके कैमरे के कम रिज़ॉल्यूशन के लिए एक दुसरी RTSP कनेक्शन।",
"onvif": "ONVIF",
@@ -145,6 +146,8 @@
"turn_server": "TURN server",
"turn_username": "उपयोगकर्ता नाम",
"turn_password": "पासवर्ड",
"force_turn": "Force TURN",
"force_turn_description": "Force TURN usage, even when STUN is available.",
"stun_turn_forward": "फोरवर्डींग और ट्रांसकोडिंग",
"stun_turn_description_forward": "TURN/STUN संचार के लिए अनुकूलन और संवर्द्धन।",
"stun_turn_webrtc": "WebRTC ब्रोकर को फोरवर्डींग किया जा रहा है",
@@ -185,6 +188,8 @@
"description_persistence": "अपनी रिकॉर्डिंग संग्रहीत करने की क्षमता होना हर चीज़ की शुरुआत है। ",
"description2_persistence": ", या कोई तृतीय पक्ष प्रदाता",
"select_persistence": "एक दृढ़ता का चयन करें",
"kerberoshub_encryption": "Encryption",
"kerberoshub_encryption_description": "All traffic from/to Kerberos Hub will encrypted using AES-256.",
"kerberoshub_proxyurl": "Kerberos हब प्रॉक्सी URL",
"kerberoshub_description_proxyurl": "आपकी रिकॉर्डिंग अपलोड करने के लिए प्रॉक्सी एंडपॉइंट।",
"kerberoshub_apiurl": "Kerberos हब API URL",

View File

@@ -80,6 +80,7 @@
"description_general": "Impostazioni generali del Kerberos Agent",
"key": "Chiave",
"camera_name": "Nome videocamera",
"camera_friendly_name": "Nome amichevole videocamera",
"timezone": "Fuso orario",
"select_timezone": "Seleziona un fuso orario",
"advanced_configuration": "Configurazione avanzata",
@@ -99,9 +100,9 @@
"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.",
"only_h264": "Al momento sono supportati solo streams RTSP H264/H265.",
"rtsp_url": "Url RTSP",
"rtsp_h264": "Connessione RTSP H264 alla videocamera.",
"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",
@@ -145,6 +146,8 @@
"turn_server": "TURN server",
"turn_username": "Username",
"turn_password": "Password",
"force_turn": "Forza TURN",
"force_turn_description": "Forza l'uso di TURN per lo streaming in diretta.",
"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",
@@ -185,6 +188,8 @@
"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_encryption": "Encryption",
"kerberoshub_encryption_description": "All traffic from/to Kerberos Hub will encrypted using AES-256.",
"kerberoshub_proxyurl": "URL Proxy Kerberos Hub",
"kerberoshub_description_proxyurl": "Endpoint del Proxy per l'upload delle registrazioni.",
"kerberoshub_apiurl": "API URL Kerberos Hub",
@@ -221,4 +226,4 @@
"remove_after_upload_enabled": "Abilita cancellazione al caricamento"
}
}
}
}

View File

@@ -80,6 +80,7 @@
"description_general": "Kerberos エージェントの一般設定",
"key": "鍵",
"camera_name": "カメラ名",
"camera_friendly_name": "カメラのフレンドリー名",
"timezone": "タイムゾーン",
"select_timezone": "タイムゾーンを選択",
"advanced_configuration": "詳細設定",
@@ -99,9 +100,9 @@
"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",
@@ -145,6 +146,8 @@
"turn_server": "TURNサーバー",
"turn_username": "ユーザー名",
"turn_password": "パスワード",
"force_turn": "Force TURN",
"force_turn_description": "Force TURN usage, even when STUN is available.",
"stun_turn_forward": "転送とトランスコーディング",
"stun_turn_description_forward": "TURN/STUN 通信の最適化と機能強化。",
"stun_turn_webrtc": "WebRTC ブローカーへの転送",
@@ -185,6 +188,8 @@
"description_persistence": "録音を保存する機能を持つことは、すべての始まりです。",
"description2_persistence": "、またはサードパーティのプロバイダ",
"select_persistence": "永続性を選択",
"kerberoshub_encryption": "Encryption",
"kerberoshub_encryption_description": "All traffic from/to Kerberos Hub will encrypted using AES-256.",
"kerberoshub_proxyurl": "Kerberos ハブ プロキシ URL",
"kerberoshub_description_proxyurl": "記録をアップロードするためのプロキシ エンドポイント。",
"kerberoshub_apiurl": "ケルベロス ハブ API URL",
@@ -221,4 +226,4 @@
"remove_after_upload_enabled": "Enabled delete on upload"
}
}
}
}

View File

@@ -80,6 +80,7 @@
"description_general": "Algemene instellingen voor jouw Kerberos Agent",
"key": "Key",
"camera_name": "Camera naam",
"camera_friendly_name": "Camera vriendelijke naam",
"timezone": "Tijdzone",
"select_timezone": "Selecteer uw tijdzone",
"advanced_configuration": "Geavanceerde instellingen",
@@ -99,9 +100,9 @@
"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",
@@ -146,6 +147,8 @@
"turn_server": "TURN server",
"turn_username": "Gebruikersnaam",
"turn_password": "Wachtwoord",
"force_turn": "Verplicht TURN",
"force_turn_description": "Verplicht TURN connectie, ook al is er een STUN connectie mogelijk.",
"stun_turn_forward": "Doorsturen en transcoden",
"stun_turn_description_forward": "Optimalisatie en verbetering voor TURN/STUN communicatie.",
"stun_turn_webrtc": "Doorsturen naar een WebRTC broker",
@@ -186,6 +189,8 @@
"description_persistence": "De mogelijkheid om jouw opnames op te slaan is het begin van alles. Je kan kiezen tussen ons",
"description2_persistence": ", of een 3rd party provider",
"select_persistence": "Selecteer een opslagmethode",
"kerberoshub_encryption": "Encryption",
"kerberoshub_encryption_description": "All traffic from/to Kerberos Hub will encrypted using AES-256.",
"kerberoshub_proxyurl": "Kerberos Hub Proxy URL",
"kerberoshub_description_proxyurl": "De Proxy url voor het opladen van jouw opnames.",
"kerberoshub_apiurl": "Kerberos Hub API URL",

View File

@@ -80,6 +80,7 @@
"description_general": "General settings for your Kerberos Agent",
"key": "Key",
"camera_name": "Camera name",
"camera_friendly_name": "Camera friendly name",
"timezone": "Timezone",
"select_timezone": "Select a timezone",
"advanced_configuration": "Advanced configuration",
@@ -99,9 +100,9 @@
"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",
@@ -145,6 +146,8 @@
"turn_server": "TURN server",
"turn_username": "Username",
"turn_password": "Password",
"force_turn": "Force TURN",
"force_turn_description": "Force TURN usage, even when STUN is available.",
"stun_turn_forward": "Forwarding and transcoding",
"stun_turn_description_forward": "Optimisations and enhancements for TURN/STUN communication.",
"stun_turn_webrtc": "Forwarding to WebRTC broker",
@@ -185,6 +188,8 @@
"description_persistence": "Having the ability to store your recordings is the beginning of everything. You can choose between our",
"description2_persistence": ", or a 3rd party provider",
"select_persistence": "Select a persistence",
"kerberoshub_encryption": "Encryption",
"kerberoshub_encryption_description": "All traffic from/to Kerberos Hub will encrypted using AES-256.",
"kerberoshub_proxyurl": "Kerberos Hub Proxy URL",
"kerberoshub_description_proxyurl": "The Proxy endpoint for uploading your recordings.",
"kerberoshub_apiurl": "Kerberos Hub API URL",

View File

@@ -80,6 +80,7 @@
"description_general": "Configurações gerais para seu agente Kerberos",
"key": "Chave",
"camera_name": "Nome da câmera",
"camera_friendly_name": "Nome amigável da câmera",
"timezone": "Fuso horário",
"select_timezone": "Selecione a timezone",
"advanced_configuration": "Configurações avançadas",
@@ -99,9 +100,9 @@
"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",
@@ -145,6 +146,8 @@
"turn_server": "Servidor TURN",
"turn_username": "Usuario",
"turn_password": "Senha",
"force_turn": "Forçar TURN",
"force_turn_description": "Forçar o uso de TURN em vez de STUN.",
"stun_turn_forward": "Encaminhamento e transcodificação",
"stun_turn_description_forward": "Otimizações e melhorias para a comunicação TURN/STUN.",
"stun_turn_webrtc": "Encaminhamento para broker WebRTC",
@@ -185,6 +188,8 @@
"description_persistence": "Ter a capacidade de armazenar suas gravações é o começo de tudo. Você pode escolher entre nossos",
"description2_persistence": ", ou um provedor terceirizado",
"select_persistence": "Selecione um provedor de armazenamento",
"kerberoshub_encryption": "Encryption",
"kerberoshub_encryption_description": "All traffic from/to Kerberos Hub will encrypted using AES-256.",
"kerberoshub_proxyurl": "Url proxy para Kerberos Hub",
"kerberoshub_description_proxyurl": "O endpoint Proxy para enviar suas gravações.",
"kerberoshub_apiurl": "Url de API do Kerberos Hub",

View File

@@ -0,0 +1,234 @@
{
"breadcrumb": {
"watch_recordings": "Vizionează înregistrări",
"configure": "Configurează"
},
"buttons": {
"save": "Salvează",
"verify_connection": "Verifică conexiunea"
},
"navigation": {
"profile": "Profil",
"admin": "admin",
"management": "Management",
"dashboard": "Tablou de bord",
"recordings": "Înregistrări",
"settings": "Setări",
"help_support": "Ajutor & Suport",
"swagger": "Swagger API",
"documentation": "Documentație",
"ui_library": "Bibliotecă UI",
"layout": "Limbă și aspect",
"choose_language": "Alege limba"
},
"dashboard": {
"title": "Tablou de bord",
"heading": "Prezentare generală a supravegherii video",
"number_of_days": "Număr de zile",
"total_recordings": "Înregistrări totale",
"connected": "Conectat",
"not_connected": "Neconectat",
"offline_mode": "Mod offline",
"latest_events": "Evenimente recente",
"configure_connection": "Configurează conexiunea",
"no_events": "Niciun eveniment",
"no_events_description": "Nu au fost găsite înregistrări, asigurați-vă că agentul dvs. Kerberos este configurat corect.",
"motion_detected": "Mișcare detectată",
"live_view": "Vizualizare live",
"loading_live_view": "Se încarcă vizualizarea live",
"loading_live_view_description": "Așteptați, încărcăm vizualizarea dvs. live. Dacă nu ați configurat conexiunea camerei, actualizați-o în paginile de setări.",
"time": "Timp",
"description": "Descriere",
"name": "Nume"
},
"recordings": {
"title": "Înregistrări",
"heading": "Toate înregistrările tale într-un singur loc",
"search_media": "Caută media"
},
"settings": {
"title": "Setări",
"heading": "Prezentare generală a setărilor camerei și agentului",
"submenu": {
"all": "Toate",
"overview": "General",
"camera": "Camera",
"recording": "Înregistrare",
"streaming": "Streaming",
"conditions": "Condiții",
"persistence": "Persistență"
},
"info": {
"kerberos_hub_demo": "Aruncă o privire asupra mediului nostru de demonstrație Kerberos Hub, pentru a vedea Kerberos Hub în acțiune!",
"configuration_updated_success": "Configurarea ta a fost actualizată cu succes.",
"configuration_updated_error": "Ceva a mers prost în timpul salvării.",
"verify_hub": "Verificarea setărilor tale Kerberos Hub.",
"verify_hub_success": "Setările Kerberos Hub au fost verificate cu succes.",
"verify_hub_error": "Ceva a mers prost în timpul verificării Kerberos Hub.",
"verify_persistence": "Verificarea setărilor tale de persistență.",
"verify_persistence_success": "Setările de persistență au fost verificate cu succes.",
"verify_persistence_error": "Ceva a mers prost în timpul verificării persistenței.",
"verify_camera": "Verificarea setărilor tale pentru cameră.",
"verify_camera_success": "Setările pentru cameră au fost verificate cu succes.",
"verify_camera_error": "Ceva a mers prost în timpul verificării setărilor pentru cameră.",
"verify_onvif": "Verificarea setărilor tale ONVIF.",
"verify_onvif_success": "Setările ONVIF au fost verificate cu succes.",
"verify_onvif_error": "Ceva a mers prost în timpul verificării setărilor ONVIF."
},
"overview": {
"general": "General",
"description_general": "Setări generale pentru Agentul tău Kerberos",
"key": "Cheie",
"camera_name": "Numele camerei",
"camera_friendly_name": "Nume prietenos",
"timezone": "Fus orar",
"select_timezone": "Selectează un fus orar",
"advanced_configuration": "Configurare avansată",
"description_advanced_configuration": "Opțiuni detaliate de configurare pentru activarea sau dezactivarea anumitor părți ale Agentului Kerberos",
"offline_mode": "Mod offline",
"description_offline_mode": "Dezactivează tot traficul ieșit",
"encryption": "Criptare",
"description_encryption": "Activează criptarea pentru tot traficul ieșit. Mesajele MQTT și/sau înregistrările vor fi criptate folosind AES-256. O cheie privată este utilizată pentru semnare.",
"encryption_enabled": "Activează criptarea MQTT",
"description_encryption_enabled": "Activează criptarea pentru toate mesajele MQTT.",
"encryption_recordings_enabled": "Activează criptarea înregistrărilor",
"description_encryption_recordings_enabled": "Activează criptarea pentru toate înregistrările.",
"encryption_fingerprint": "Amprentă",
"encryption_privatekey": "Cheie privată",
"encryption_symmetrickey": "Cheie simetrică"
},
"camera": {
"camera": "Camera",
"description_camera": "Setările camerei sunt necesare pentru a face o conexiune cu camera aleasă de tine.",
"only_h264": "În prezent sunt suportate doar fluxurile RTSP H264/H265.",
"rtsp_url": "URL RTSP",
"rtsp_h264": "O conexiune RTSP H264/H265 la camera ta.",
"sub_rtsp_url": "URL RTSP secundar (folosit pentru transmisie live)",
"sub_rtsp_h264": "O conexiune RTSP secundară la rezoluția redusă a camerei tale.",
"onvif": "ONVIF",
"description_onvif": "Credențiale pentru comunicarea cu capabilitățile ONVIF. Acestea sunt folosite pentru funcții PTZ sau alte capabilități oferite de cameră.",
"onvif_xaddr": "Adresă ONVIF",
"onvif_username": "Nume utilizator ONVIF",
"onvif_password": "Parolă ONVIF",
"verify_connection": "Verifică conexiunea",
"verify_sub_connection": "Verifică conexiunea secundară"
},
"recording": {
"recording": "Înregistrare",
"description_recording": "Specificați cum doriți să realizați înregistrări. Puteți avea o configurație continuă 24/7 sau înregistrări bazate pe mișcare.",
"continuous_recording": "Înregistrare continuă",
"description_continuous_recording": "Realizați înregistrări 24/7 sau bazate pe mișcare.",
"max_duration": "durata maximă a videoclipului (secunde)",
"description_max_duration": "Durata maximă a unei înregistrări.",
"pre_recording": "pre-înregistrare (cadre cheie tamponate)",
"description_pre_recording": "Secunde înainte de producerea unui eveniment.",
"post_recording": "post-înregistrare (secunde)",
"description_post_recording": "Secunde după producerea unui eveniment.",
"threshold": "Prag de înregistrare (pixeli)",
"description_threshold": "Numărul de pixeli modificați pentru a înregistra.",
"autoclean": "Curățare automată",
"description_autoclean": "Specificați dacă Agentul Kerberos poate curăța automat înregistrările când se atinge o anumită capacitate de stocare (MB). Se vor șterge cele mai vechi înregistrări când se atinge capacitatea specificată.",
"autoclean_enable": "Activează curățarea automată",
"autoclean_description_enable": "Șterge cele mai vechi înregistrări când capacitatea este atinsă.",
"autoclean_max_directory_size": "Dimensiunea maximă a directorului (MB)",
"autoclean_description_max_directory_size": "Maximum de MB stocați în înregistrări.",
"fragmentedrecordings": "Înregistrări fragmentate",
"description_fragmentedrecordings": "Când înregistrările sunt fragmentate, sunt potrivite pentru un flux HLS. Când este activat, containerul MP4 va arăta puțin diferit.",
"fragmentedrecordings_enable": "Activează fragmentarea",
"fragmentedrecordings_description_enable": "Înregistrările fragmentate sunt necesare pentru HLS.",
"fragmentedrecordings_duration": "durata fragmentului",
"fragmentedrecordings_description_duration": "Durata unui singur fragment."
},
"streaming": {
"stun_turn": "STUN/TURN pentru WebRTC",
"description_stun_turn": "Pentru transmisii live la rezoluție completă folosim conceptul WebRTC. Una dintre capabilitățile cheie este funcționalitatea ICE-candidate, care permite traversarea NAT folosind conceptele STUN/TURN.",
"stun_server": "Server STUN",
"turn_server": "Server TURN",
"turn_username": "Nume utilizator",
"turn_password": "Parolă",
"force_turn": "Forțează TURN",
"force_turn_description": "Utilizează TURN în mod forțat, chiar și atunci când STUN este disponibil.",
"stun_turn_forward": "Redirecționare și transcodare",
"stun_turn_description_forward": "Optimizări și îmbunătățiri pentru comunicarea TURN/STUN.",
"stun_turn_webrtc": "Redirecționare către broker WebRTC",
"stun_turn_description_webrtc": "Redirecționare flux h264 prin MQTT",
"stun_turn_transcode": "Transcodare flux",
"stun_turn_description_transcode": "Convertire flux la o rezoluție mai mică",
"stun_turn_downscale": "Scădere rezoluție (în % din rezoluția originală)",
"mqtt": "MQTT",
"description_mqtt": "Un broker MQTT este utilizat pentru comunicare de la",
"description2_mqtt": "către Agentul Kerberos, pentru a realiza, de exemplu, transmisiuni live sau capabilități ONVIF (PTZ).",
"mqtt_brokeruri": "URI broker MQTT",
"mqtt_username": "Nume utilizator",
"mqtt_password": "Parolă",
"realtimeprocessing": "Procesare în timp real",
"description_realtimeprocessing": "Prin activarea procesării în timp real, veți primi cadre cheie video în timp real prin conexiunea MQTT specificată mai sus.",
"realtimeprocessing_topic": "Topic pentru publicare",
"realtimeprocessing_enabled": "Activează procesarea în timp real",
"description_realtimeprocessing_enabled": "Trimite cadre video în timp real prin MQTT."
},
"conditions": {
"timeofinterest": "Timpul de Interes",
"description_timeofinterest": "Realizează înregistrări doar între intervale de timp specifice (bazate pe fusul orar).",
"timeofinterest_enabled": "Activat",
"timeofinterest_description_enabled": "Dacă este activat, puteți specifica intervale de timp",
"sunday": "Duminică",
"monday": "Luni",
"tuesday": "Marți",
"wednesday": "Miercuri",
"thursday": "Joi",
"friday": "Vineri",
"saturday": "Sâmbătă",
"externalcondition": "Condiție Externă",
"description_externalcondition": "În funcție de un serviciu web extern, înregistrarea poate fi activată sau dezactivată.",
"regionofinterest": "Regiunea de Interes",
"description_regionofinterest": "Prin definirea unei sau mai multor regiuni, mișcarea va fi urmărită doar în regiunile pe care le-ați definit."
},
"persistence": {
"kerberoshub": "Kerberos Hub",
"description_kerberoshub": "Agenta Kerberos poate trimite semnale de puls către o",
"description2_kerberoshub": "instalație centrală. Semnalele de puls și alte informații relevante sunt sincronizate cu Kerberos Hub pentru a afișa informații în timp real despre peisajul video.",
"persistence": "Persistență",
"saasoffering": "Kerberos Hub (ofertă SAAS)",
"description_persistence": "Capacitatea de a stoca înregistrările este începutul fiecărei",
"description2_persistence": ", sau de la un furnizor terț",
"select_persistence": "Selectați o persistență",
"kerberoshub_encryption": "Criptare",
"kerberoshub_encryption_description": "Tot traficul de la/spre Kerberos Hub va fi criptat folosind AES-256.",
"kerberoshub_proxyurl": "URL Proxy Kerberos Hub",
"kerberoshub_description_proxyurl": "Punctul final Proxy pentru încărcarea înregistrărilor tale.",
"kerberoshub_apiurl": "URL API Kerberos Hub",
"kerberoshub_description_apiurl": "Punctul final API pentru încărcarea înregistrărilor tale.",
"kerberoshub_publickey": "Cheie publică",
"kerberoshub_description_publickey": "Cheia publică acordată contului tău Kerberos Hub.",
"kerberoshub_privatekey": "Cheie privată",
"kerberoshub_description_privatekey": "Cheia privată acordată contului tău Kerberos Hub.",
"kerberoshub_site": "Site",
"kerberoshub_description_site": "ID-ul site-ului la care aparțin Agenta Kerberos în Kerberos Hub.",
"kerberoshub_region": "Regiune",
"kerberoshub_description_region": "Regiunea în care sunt stocate înregistrările noastre.",
"kerberoshub_bucket": "Bucket",
"kerberoshub_description_bucket": "Bucket-ul în care sunt stocate înregistrările noastre.",
"kerberoshub_username": "Nume utilizator/Director (trebuie să se potrivească cu numele de utilizator Kerberos Hub)",
"kerberoshub_description_username": "Numele de utilizator al contului tău Kerberos Hub.",
"kerberosvault_apiurl": "URL API Kerberos Vault",
"kerberosvault_description_apiurl": "API-ul Kerberos Vault",
"kerberosvault_provider": "Furnizor",
"kerberosvault_description_provider": "Furnizorul către care vor fi trimise înregistrările tale.",
"kerberosvault_directory": "Director (trebuie să se potrivească cu numele de utilizator Kerberos Hub)",
"kerberosvault_description_directory": "Subdirectorul în care vor fi stocate înregistrările la furnizorul tău.",
"kerberosvault_accesskey": "Cheie de acces",
"kerberosvault_description_accesskey": "Cheia de acces a contului tău Kerberos Vault.",
"kerberosvault_secretkey": "Cheie secretă",
"kerberosvault_description_secretkey": "Cheia secretă a contului tău Kerberos Vault.",
"dropbox_directory": "Director",
"dropbox_description_directory": "Subdirectorul în care vor fi stocate înregistrările în contul tău Dropbox.",
"dropbox_accesstoken": "Token de acces",
"dropbox_description_accesstoken": "Tokenul de acces al contului/aplicației tale Dropbox.",
"verify_connection": "Verifică conexiunea",
"remove_after_upload": "Odată ce înregistrările sunt încărcate într-o persistență, este posibil să doriți să le ștergeți de pe Agenta Kerberos locală.",
"remove_after_upload_description": "Ștergeți înregistrările după ce sunt încărcate cu succes.",
"remove_after_upload_enabled": "Ștergere activată la încărcare"
}
}
}

View File

@@ -0,0 +1,229 @@
{
"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": "Название камеры",
"camera_friendly_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": "Пароль",
"force_turn": "Force TURN",
"force_turn_description": "Force TURN usage, even when STUN is available.",
"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_encryption": "Encryption",
"kerberoshub_encryption_description": "All traffic from/to Kerberos Hub will encrypted using AES-256.",
"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

@@ -0,0 +1,234 @@
{
"breadcrumb": {
"watch_recordings": "Xem bản ghi",
"configure": "Cấu hình"
},
"buttons": {
"save": "Lưu",
"verify_connection": "Xác minh kết nối"
},
"navigation": {
"profile": "Hồ sơ",
"admin": "Quản trị",
"management": "Quản lý",
"dashboard": "Bảng điều khiển",
"recordings": "Bản ghi",
"settings": "Cài đặt",
"help_support": "Trợ giúp & Hỗ trợ",
"swagger": "API Swagger",
"documentation": "Tài liệu",
"ui_library": "Thư viện UI",
"layout": "Ngôn ngữ & Bố cục",
"choose_language": "Chọn ngôn ngữ"
},
"dashboard": {
"title": "Bảng điều khiển",
"heading": "Tổng quan về giám sát video của bạn",
"number_of_days": "Số ngày",
"total_recordings": "Tổng số bản ghi",
"connected": "Đã kết nối",
"not_connected": "Chưa kết nối",
"offline_mode": "Chế độ ngoại tuyến",
"latest_events": "Sự kiện gần đây",
"configure_connection": "Cấu hình kết nối",
"no_events": "Không có sự kiện",
"no_events_description": "Không tìm thấy bản ghi nào, hãy đảm bảo Kerberos Agent của bạn được cấu hình đúng cách.",
"motion_detected": "Phát hiện chuyển động",
"live_view": "Xem trực tiếp",
"loading_live_view": "Đang tải xem trực tiếp",
"loading_live_view_description": "Vui lòng chờ trong khi chúng tôi tải xem trực tiếp của bạn. Nếu bạn chưa cấu hình kết nối camera, hãy cập nhật trong trang cài đặt.",
"time": "Thời gian",
"description": "Mô tả",
"name": "Tên"
},
"recordings": {
"title": "Bản ghi",
"heading": "Tất cả bản ghi của bạn ở một nơi",
"search_media": "Tìm kiếm phương tiện"
},
"settings": {
"title": "Cài đặt",
"heading": "Thiết lập camera của bạn",
"submenu": {
"all": "Tất cả",
"overview": "Tổng quan",
"camera": "Camera",
"recording": "Ghi hình",
"streaming": "Truyền phát",
"conditions": "Điều kiện",
"persistence": "Lưu trữ"
},
"info": {
"kerberos_hub_demo": "Xem thử môi trường demo của Kerberos Hub để thấy Kerberos Hub hoạt động như thế nào!",
"configuration_updated_success": "Cấu hình của bạn đã được cập nhật thành công.",
"configuration_updated_error": "Đã xảy ra lỗi khi lưu.",
"verify_hub": "Đang xác minh cài đặt Kerberos Hub của bạn.",
"verify_hub_success": "Cài đặt Kerberos Hub đã được xác minh thành công.",
"verify_hub_error": "Đã xảy ra lỗi khi xác minh Kerberos Hub",
"verify_persistence": "Đang xác minh cài đặt lưu trữ.",
"verify_persistence_success": "Cài đặt lưu trữ đã được xác minh thành công.",
"verify_persistence_error": "Đã xảy ra lỗi khi xác minh lưu trữ",
"verify_camera": "Đang xác minh cài đặt camera.",
"verify_camera_success": "Cài đặt camera đã được xác minh thành công.",
"verify_camera_error": "Đã xảy ra lỗi khi xác minh cài đặt camera",
"verify_onvif": "Đang xác minh cài đặt ONVIF.",
"verify_onvif_success": "Cài đặt ONVIF đã được xác minh thành công.",
"verify_onvif_error": "Đã xảy ra lỗi khi xác minh cài đặt ONVIF"
},
"overview": {
"general": "Chung",
"description_general": "Cài đặt chung cho Kerberos Agent của bạn",
"key": "Khóa",
"camera_name": "Tên camera",
"camera_friendly_name": "Tên thân thiện",
"timezone": "Múi giờ",
"select_timezone": "Chọn múi giờ",
"advanced_configuration": "Cấu hình nâng cao",
"description_advanced_configuration": "Tùy chọn cấu hình chi tiết để bật hoặc tắt các phần cụ thể của Kerberos Agent",
"offline_mode": "Chế độ ngoại tuyến",
"description_offline_mode": "Vô hiệu hóa toàn bộ lưu lượng đi",
"encryption": "Mã hóa",
"description_encryption": "Bật mã hóa cho toàn bộ lưu lượng đi. Các tin nhắn MQTT và/hoặc bản ghi sẽ được mã hóa bằng AES-256. Một khóa riêng tư được sử dụng để ký.",
"encryption_enabled": "Bật mã hóa MQTT",
"description_encryption_enabled": "Bật mã hóa cho toàn bộ tin nhắn MQTT.",
"encryption_recordings_enabled": "Bật mã hóa bản ghi",
"description_encryption_recordings_enabled": "Bật mã hóa cho tất cả các bản ghi.",
"encryption_fingerprint": "Dấu vân tay",
"encryption_privatekey": "Khóa riêng tư",
"encryption_symmetrickey": "Khóa đối xứng"
},
"camera": {
"camera": "Camera",
"description_camera": "Cài đặt camera là bắt buộc để kết nối với camera bạn chọn.",
"only_h264": "Hiện tại chỉ hỗ trợ luồng RTSP H264/H265.",
"rtsp_url": "URL RTSP",
"rtsp_h264": "Kết nối RTSP H264/H265 với camera của bạn.",
"sub_rtsp_url": "URL RTSP phụ (dùng để phát trực tiếp)",
"sub_rtsp_h264": "Kết nối RTSP phụ với độ phân giải thấp của camera.",
"onvif": "ONVIF",
"description_onvif": "Thông tin xác thực để giao tiếp với các chức năng ONVIF. Chúng được sử dụng cho PTZ hoặc các khả năng khác do camera cung cấp.",
"onvif_xaddr": "Địa chỉ ONVIF",
"onvif_username": "Tên người dùng ONVIF",
"onvif_password": "Mật khẩu ONVIF",
"verify_connection": "Xác minh kết nối",
"verify_sub_connection": "Xác minh kết nối phụ"
},
"recording": {
"recording": "Ghi hình",
"description_recording": "Chỉ định cách bạn muốn thực hiện ghi hình. Có thể ghi liên tục 24/7 hoặc dựa trên chuyển động.",
"continuous_recording": "Ghi hình liên tục",
"description_continuous_recording": "Ghi hình liên tục 24/7 hoặc dựa trên chuyển động.",
"max_duration": "Thời lượng video tối đa (giây)",
"description_max_duration": "Thời lượng tối đa của một bản ghi.",
"pre_recording": "Ghi trước (khung hình chính được lưu vào bộ đệm)",
"description_pre_recording": "Số giây trước khi sự kiện xảy ra.",
"post_recording": "Ghi sau (giây)",
"description_post_recording": "Số giây sau khi sự kiện xảy ra.",
"threshold": "Ngưỡng ghi hình (pixel)",
"description_threshold": "Số pixel thay đổi cần đạt để bắt đầu ghi hình.",
"autoclean": "Tự động dọn dẹp",
"description_autoclean": "Chỉ định xem Kerberos Agent có thể dọn dẹp các bản ghi khi dung lượng lưu trữ đạt giới hạn nhất định (MB) hay không. Hệ thống sẽ xóa bản ghi cũ nhất khi đạt giới hạn.",
"autoclean_enable": "Bật tự động dọn dẹp",
"autoclean_description_enable": "Xóa bản ghi cũ nhất khi đạt giới hạn dung lượng.",
"autoclean_max_directory_size": "Dung lượng thư mục tối đa (MB)",
"autoclean_description_max_directory_size": "Dung lượng tối đa (MB) của các bản ghi được lưu trữ.",
"fragmentedrecordings": "Ghi hình phân đoạn",
"description_fragmentedrecordings": "Khi các bản ghi được phân đoạn, chúng phù hợp để phát trực tuyến HLS. Khi bật, định dạng MP4 sẽ có một số khác biệt.",
"fragmentedrecordings_enable": "Bật ghi hình phân đoạn",
"fragmentedrecordings_description_enable": "Ghi hình phân đoạn là bắt buộc đối với HLS.",
"fragmentedrecordings_duration": "Thời lượng phân đoạn",
"fragmentedrecordings_description_duration": "Thời lượng của một phân đoạn duy nhất."
},
"streaming": {
"stun_turn": "STUN/TURN cho WebRTC",
"description_stun_turn": "Để phát trực tiếp độ phân giải đầy đủ, chúng tôi sử dụng khái niệm WebRTC. Một trong những tính năng chính là ICE-candidate, cho phép vượt qua NAT bằng STUN/TURN.",
"stun_server": "Máy chủ STUN",
"turn_server": "Máy chủ TURN",
"turn_username": "Tên người dùng",
"turn_password": "Mật khẩu",
"force_turn": "Buộc sử dụng TURN",
"force_turn_description": "Buộc sử dụng TURN ngay cả khi STUN có sẵn.",
"stun_turn_forward": "Chuyển tiếp và mã hóa",
"stun_turn_description_forward": "Tối ưu hóa và cải thiện giao tiếp TURN/STUN.",
"stun_turn_webrtc": "Chuyển tiếp đến WebRTC broker",
"stun_turn_description_webrtc": "Chuyển tiếp luồng H264 qua MQTT",
"stun_turn_transcode": "Chuyển mã luồng",
"stun_turn_description_transcode": "Chuyển đổi luồng sang độ phân giải thấp hơn",
"stun_turn_downscale": "Giảm độ phân giải (theo % của độ phân giải gốc)",
"mqtt": "MQTT",
"description_mqtt": "Một MQTT broker được sử dụng để giao tiếp từ",
"description2_mqtt": "đến Kerberos Agent, nhằm hỗ trợ phát trực tiếp hoặc chức năng ONVIF (PTZ).",
"mqtt_brokeruri": "Broker Uri",
"mqtt_username": "Tên người dùng",
"mqtt_password": "Mật khẩu",
"realtimeprocessing": "Xử lý thời gian thực",
"description_realtimeprocessing": "Bằng cách bật xử lý thời gian thực, bạn sẽ nhận được các khung hình video thời gian thực qua kết nối MQTT đã chỉ định.",
"realtimeprocessing_topic": "Chủ đề để xuất bản",
"realtimeprocessing_enabled": "Bật xử lý thời gian thực",
"description_realtimeprocessing_enabled": "Gửi khung hình video thời gian thực qua MQTT."
},
"conditions": {
"timeofinterest": "Thời gian quan tâm",
"description_timeofinterest": "Chỉ ghi hình trong các khoảng thời gian cụ thể (dựa trên múi giờ).",
"timeofinterest_enabled": "Đã bật",
"timeofinterest_description_enabled": "Nếu bật, bạn có thể chỉ định các khoảng thời gian ghi hình.",
"sunday": "Chủ nhật",
"monday": "Thứ hai",
"tuesday": "Thứ ba",
"wednesday": "Thứ tư",
"thursday": "Thứ năm",
"friday": "Thứ sáu",
"saturday": "Thứ bảy",
"externalcondition": "Điều kiện bên ngoài",
"description_externalcondition": "Tùy thuộc vào một dịch vụ web bên ngoài, việc ghi hình có thể được bật hoặc tắt.",
"regionofinterest": "Khu vực quan tâm",
"description_regionofinterest": "Bằng cách xác định một hoặc nhiều khu vực, hệ thống sẽ chỉ theo dõi chuyển động trong các khu vực bạn đã chọn."
},
"persistence": {
"kerberoshub": "Kerberos Hub",
"description_kerberoshub": "Các Kerberos Agent có thể gửi tín hiệu nhịp tim đến một hệ thống trung tâm",
"description2_kerberoshub": "để đồng bộ hóa thông tin quan trọng với Kerberos Hub, giúp hiển thị trạng thái giám sát video theo thời gian thực.",
"persistence": "Lưu trữ",
"saasoffering": "Kerberos Hub (dịch vụ SAAS)",
"description_persistence": "Khả năng lưu trữ bản ghi là bước khởi đầu của mọi thứ. Bạn có thể chọn giữa dịch vụ của chúng tôi",
"description2_persistence": "hoặc một nhà cung cấp bên thứ ba.",
"select_persistence": "Chọn phương thức lưu trữ",
"kerberoshub_encryption": "Mã hóa",
"kerberoshub_encryption_description": "Tất cả lưu lượng đến/từ Kerberos Hub sẽ được mã hóa bằng AES-256.",
"kerberoshub_proxyurl": "URL Proxy Kerberos Hub",
"kerberoshub_description_proxyurl": "Điểm cuối Proxy để tải bản ghi lên.",
"kerberoshub_apiurl": "URL API Kerberos Hub",
"kerberoshub_description_apiurl": "Điểm cuối API để tải bản ghi lên.",
"kerberoshub_publickey": "Khóa công khai",
"kerberoshub_description_publickey": "Khóa công khai được cấp cho tài khoản Kerberos Hub của bạn.",
"kerberoshub_privatekey": "Khóa riêng tư",
"kerberoshub_description_privatekey": "Khóa riêng tư được cấp cho tài khoản Kerberos Hub của bạn.",
"kerberoshub_site": "Trang web",
"kerberoshub_description_site": "ID trang web mà các Kerberos Agent thuộc về trong Kerberos Hub.",
"kerberoshub_region": "Khu vực",
"kerberoshub_description_region": "Khu vực nơi chúng tôi lưu trữ bản ghi.",
"kerberoshub_bucket": "Kho lưu trữ",
"kerberoshub_description_bucket": "Kho lưu trữ nơi chúng tôi lưu trữ bản ghi.",
"kerberoshub_username": "Tên người dùng / Thư mục (phải khớp với tên người dùng Kerberos Hub)",
"kerberoshub_description_username": "Tên người dùng tài khoản Kerberos Hub của bạn.",
"kerberosvault_apiurl": "URL API Kerberos Vault",
"kerberosvault_description_apiurl": "API của Kerberos Vault",
"kerberosvault_provider": "Nhà cung cấp",
"kerberosvault_description_provider": "Nhà cung cấp nơi bản ghi của bạn sẽ được gửi đến.",
"kerberosvault_directory": "Thư mục (phải khớp với tên người dùng Kerberos Hub)",
"kerberosvault_description_directory": "Thư mục con nơi các bản ghi sẽ được lưu trữ trong nhà cung cấp của bạn.",
"kerberosvault_accesskey": "Khóa truy cập",
"kerberosvault_description_accesskey": "Khóa truy cập của tài khoản Kerberos Vault của bạn.",
"kerberosvault_secretkey": "Khóa bí mật",
"kerberosvault_description_secretkey": "Khóa bí mật của tài khoản Kerberos Vault của bạn.",
"dropbox_directory": "Thư mục",
"dropbox_description_directory": "Thư mục con nơi bản ghi sẽ được lưu trữ trong tài khoản Dropbox của bạn.",
"dropbox_accesstoken": "Mã truy cập",
"dropbox_description_accesstoken": "Mã truy cập của tài khoản / ứng dụng Dropbox của bạn.",
"verify_connection": "Xác minh kết nối",
"remove_after_upload": "Sau khi bản ghi được tải lên một hệ thống lưu trữ, bạn có thể muốn xóa chúng khỏi Kerberos Agent cục bộ.",
"remove_after_upload_description": "Xóa bản ghi sau khi chúng được tải lên thành công.",
"remove_after_upload_enabled": "Bật xóa sau khi tải lên"
}
}
}

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