Compare commits

...

4 Commits

Author SHA1 Message Date
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
14 changed files with 386 additions and 15 deletions

View File

@@ -2,7 +2,7 @@ module github.com/kerberos-io/agent/machinery
go 1.19
//replace github.com/kerberos-io/joy4 v1.0.58 => ../../../../github.com/kerberos-io/joy4
//replace github.com/kerberos-io/joy4 v1.0.60 => ../../../../github.com/kerberos-io/joy4
// replace github.com/kerberos-io/onvif v0.0.6 => ../../../../github.com/kerberos-io/onvif
@@ -26,7 +26,7 @@ require (
github.com/golang-module/carbon/v2 v2.2.3
github.com/gorilla/websocket v1.5.0
github.com/kellydunn/golang-geo v0.7.0
github.com/kerberos-io/joy4 v1.0.60
github.com/kerberos-io/joy4 v1.0.62
github.com/kerberos-io/onvif v0.0.7
github.com/minio/minio-go/v6 v6.0.57
github.com/nsmith5/mjpeg v0.0.0-20200913181537-54b8ada0e53e
@@ -38,6 +38,7 @@ require (
github.com/swaggo/gin-swagger v1.5.3
github.com/swaggo/swag v1.8.9
github.com/tevino/abool v1.2.0
github.com/zaf/g711 v0.0.0-20220109202201-cf0017bf0359
go.mongodb.org/mongo-driver v1.7.5
gopkg.in/DataDog/dd-trace-go.v1 v1.46.0
gopkg.in/natefinch/lumberjack.v2 v2.0.0

View File

@@ -264,8 +264,8 @@ github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/kellydunn/golang-geo v0.7.0 h1:A5j0/BvNgGwY6Yb6inXQxzYwlPHc6WVZR+MrarZYNNg=
github.com/kellydunn/golang-geo v0.7.0/go.mod h1:YYlQPJ+DPEzrHx8kT3oPHC/NjyvCCXE+IuKGKdrjrcU=
github.com/kerberos-io/joy4 v1.0.60 h1:W9LMTHw+Lgz4J9/28xCvvVebhcAioup49NqxYVmrH38=
github.com/kerberos-io/joy4 v1.0.60/go.mod h1:nZp4AjvKvTOXRrmDyAIOw+Da+JA5OcSo/JundGfOlFU=
github.com/kerberos-io/joy4 v1.0.62 h1:LsjGrss5I2UGfTovAF0icTuEcxwOPptkSqGyxeIwa40=
github.com/kerberos-io/joy4 v1.0.62/go.mod h1:nZp4AjvKvTOXRrmDyAIOw+Da+JA5OcSo/JundGfOlFU=
github.com/kerberos-io/onvif v0.0.7 h1:LIrXjTH7G2W9DN69xZeJSB0uS3W1+C3huFO8kTqx7/A=
github.com/kerberos-io/onvif v0.0.7/go.mod h1:Hr2dJOH2LM5SpYKk17gYZ1CMjhGhUl+QlT5kwYogrW0=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
@@ -471,6 +471,8 @@ github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/zaf/g711 v0.0.0-20220109202201-cf0017bf0359 h1:P9yeMx2iNJxJqXEwLtMjSwWcD2a0AlFmFByeosMZhLM=
github.com/zaf/g711 v0.0.0-20220109202201-cf0017bf0359/go.mod h1:ySLGJD8AQluMQuu5JDvfJrwsBra+8iX1jFsKS8KfB2I=
github.com/ziutek/mymysql v1.5.4 h1:GB0qdRGsTwQSBVYuVShFBKaXSnSnYYC2d9knnE1LHFs=
github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0=
go.mongodb.org/mongo-driver v1.7.5 h1:ny3p0reEpgsR2cfA5cjgwFZg3Cv/ofFh/8jbhGtz9VI=

View File

@@ -278,6 +278,11 @@ loop:
cameraConnected = "false"
}
hasBackChannel := "false"
if communication.HasBackChannel {
hasBackChannel = "true"
}
// We will formated the uptime to a human readable format
// this will be used on Kerberos Hub: Uptime -> 1 day and 2 hours.
uptimeFormatted := uptimeStart.Format("2006-01-02 15:04:05")
@@ -382,13 +387,14 @@ loop:
"onvif_presets": "%s",
"onvif_presets_list": %s,
"cameraConnected": "%s",
"hasBackChannel": "%s",
"numberoffiles" : "33",
"timestamp" : 1564747908,
"cameratype" : "IPCamera",
"docker" : true,
"kios" : false,
"raspberrypi" : false
}`, config.Key, system.Version, system.CPUId, username, key, name, isEnterprise, system.Hostname, system.Architecture, system.TotalMemory, system.UsedMemory, system.FreeMemory, system.ProcessUsedMemory, macs, ips, "0", "0", "0", uptimeString, boottimeString, config.HubSite, onvifEnabled, onvifZoom, onvifPanTilt, onvifPresets, onvifPresetsList, cameraConnected)
}`, config.Key, system.Version, system.CPUId, username, key, name, isEnterprise, system.Hostname, system.Architecture, system.TotalMemory, system.UsedMemory, system.FreeMemory, system.ProcessUsedMemory, macs, ips, "0", "0", "0", uptimeString, boottimeString, config.HubSite, onvifEnabled, onvifZoom, onvifPanTilt, onvifPresets, onvifPresetsList, cameraConnected, hasBackChannel)
var jsonStr = []byte(object)
buffy := bytes.NewBuffer(jsonStr)

View File

@@ -0,0 +1,80 @@
package components
import (
"bufio"
"fmt"
"os"
"time"
"github.com/kerberos-io/agent/machinery/src/log"
"github.com/kerberos-io/agent/machinery/src/models"
"github.com/kerberos-io/joy4/av"
"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(infile av.DemuxCloser, streams []av.CodecData, communication *models.Communication) {
log.Log.Info("WriteAudioToBackchannel: looking for backchannel audio codec")
pcmuCodec := GetBackChannelAudioCodec(streams, communication)
if pcmuCodec != nil {
log.Log.Info("WriteAudioToBackchannel: found backchannel audio codec")
length := 0
channel := pcmuCodec.GetIndex() * 2 // This is the same calculation as Interleaved property in the SDP file.
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)
}
infile.Write(bufferUlaw, channel, uint32(length))
length = (length + len(bufferUlaw)) % 65536
time.Sleep(128 * time.Millisecond)
}
}
log.Log.Info("WriteAudioToBackchannel: finished")
}
func WriteFileToBackChannel(infile av.DemuxCloser) {
// Do the warmup!
file, err := os.Open("./audiofile.bye")
if err != nil {
fmt.Println("WriteFileToBackChannel: error opening audiofile.bye file")
}
defer file.Close()
// Read file into buffer
reader := bufio.NewReader(file)
buffer := make([]byte, 1024)
count := 0
for {
_, err := reader.Read(buffer)
if err != nil {
break
}
// Send to backchannel
fmt.Println(buffer)
infile.Write(buffer, 2, uint32(count))
count = count + 1024
time.Sleep(128 * time.Millisecond)
}
}

View File

@@ -11,6 +11,8 @@ import (
mqtt "github.com/eclipse/paho.mqtt.golang"
"github.com/kerberos-io/joy4/cgo/ffmpeg"
//"github.com/youpy/go-wav"
"github.com/kerberos-io/agent/machinery/src/capture"
"github.com/kerberos-io/agent/machinery/src/cloud"
"github.com/kerberos-io/agent/machinery/src/computervision"
@@ -244,6 +246,9 @@ func RunAgent(configDirectory string, configuration *models.Configuration, commu
go capture.HandleSubStream(subInfile, subQueue, communication)
}
// Handle processing of audio
communication.HandleAudio = make(chan models.AudioDataPartial)
// Handle processing of motion
communication.HandleMotion = make(chan models.MotionDataPartial, 1)
if subStreamEnabled {
@@ -285,6 +290,10 @@ func RunAgent(configDirectory string, configuration *models.Configuration, commu
// If we reach this point, we have a working RTSP connection.
communication.CameraConnected = true
// We might have a camera with audio backchannel enabled.
// Check if we have a stream with a backchannel and is PCMU encoded.
go WriteAudioToBackchannel(infile, streams, communication)
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
// This will go into a blocking state, once this channel is triggered
// the agent will cleanup and restart.
@@ -328,6 +337,8 @@ func RunAgent(configDirectory string, configuration *models.Configuration, commu
}
close(communication.HandleMotion)
communication.HandleMotion = nil
close(communication.HandleAudio)
communication.HandleAudio = 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.")

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

@@ -22,6 +22,7 @@ type Communication struct {
HandleStream chan string
HandleSubStream chan string
HandleMotion chan MotionDataPartial
HandleAudio chan AudioDataPartial
HandleUpload chan string
HandleHeartBeat chan string
HandleLiveSD chan int64
@@ -38,4 +39,5 @@ type Communication struct {
SubDecoder *ffmpeg.VideoDecoder
Image string
CameraConnected bool
HasBackChannel bool
}

View File

@@ -107,6 +107,12 @@ type Payload struct {
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.

View File

@@ -229,6 +229,8 @@ func MQTTListenerHandler(mqttClient mqtt.Client, hubKey string, configDirectory
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":
@@ -268,6 +270,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
@@ -457,14 +476,9 @@ 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
webrtc.RegisterCandidates(key, receiveHDCandidatesPayload)
} else {
log.Log.Info("HandleReceiveHDCandidates: received candidate, but camera is not connected.")
}

View File

@@ -87,6 +87,21 @@ func (w WebRTC) CreateOffer(sd []byte) pionWebRTC.SessionDescription {
return offer
}
func RegisterCandidates(key string, candidate models.ReceiveHDCandidatesPayload) {
// Set lock
CandidatesMutex.Lock()
defer CandidatesMutex.Unlock()
channel := CandidateArrays[key]
if channel == nil {
channel = make(chan string)
CandidateArrays[key] = channel
}
log.Log.Info("HandleReceiveHDCandidates: " + candidate.Candidate)
channel <- candidate.Candidate
}
func InitializeWebRTCConnection(configuration *models.Configuration, communication *models.Communication, mqttClient mqtt.Client, videoTrack *pionWebRTC.TrackLocalStaticSample, audioTrack *pionWebRTC.TrackLocalStaticSample, handshake models.RequestHDStreamPayload, candidates chan string) {
config := configuration.Config
@@ -145,6 +160,9 @@ func InitializeWebRTCConnection(configuration *models.Configuration, communicati
peerConnection.OnICEConnectionStateChange(func(connectionState pionWebRTC.ICEConnectionState) {
if connectionState == pionWebRTC.ICEConnectionStateDisconnected {
CandidatesMutex.Lock()
defer CandidatesMutex.Unlock()
atomic.AddInt64(&peerConnectionCount, -1)
peerConnections[handshake.SessionID] = nil
close(candidates)

View File

@@ -23,7 +23,7 @@
},
"dashboard": {
"title": "Dashboard",
"heading": "Overview of your video surveilance",
"heading": "Overview of your video surveillance",
"number_of_days": "Number of days",
"total_recordings": "Total recordings",
"connected": "Connected",
@@ -151,7 +151,7 @@
"stun_turn_description_webrtc": "Forward h264 stream through MQTT",
"stun_turn_transcode": "Transcode stream",
"stun_turn_description_transcode": "Convert stream to a lower resolution",
"stun_turn_downscale": "Downscale resolution (in % or original resolution)",
"stun_turn_downscale": "Downscale resolution (in % of original resolution)",
"mqtt": "MQTT",
"description_mqtt": "A MQTT broker is used to communicate from",
"description2_mqtt": "to the Kerberos Agent, to achieve for example livestreaming or ONVIF (PTZ) capabilities.",

View File

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

View File

@@ -25,6 +25,7 @@ const LanguageSelect = () => {
es: { label: 'Español', dir: 'ltr', active: false },
ja: { label: '日本', dir: 'rlt', active: false },
hi: { label: 'हिंदी', dir: 'ltr', active: false },
ru: { label: 'Русский', dir: 'ltr', active: false },
};
if (!languageMap[selected]) {

View File

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