mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-06-29 16:41:16 +03:00
Compare commits
18 Commits
0d1d13d025
...
ecccc39389
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ecccc39389 | ||
|
|
3961b9412f | ||
|
|
a925a33f0f | ||
|
|
f31f6efa71 | ||
|
|
7bd89f8071 | ||
|
|
6da8519479 | ||
|
|
4a8376249b | ||
|
|
e3eede7cf6 | ||
|
|
455466989a | ||
|
|
894dfa029c | ||
|
|
c3360cd365 | ||
|
|
eaea7666e2 | ||
|
|
61b88ccaa0 | ||
|
|
6e8fcefb83 | ||
|
|
bf44dc6148 | ||
|
|
ed4e60d5b7 | ||
|
|
a500559576 | ||
|
|
a43ca9adcf |
@ -88,18 +88,8 @@ Configure a "friendly name" for your stream followed by the go2rtc stream name.
|
||||
<ConfigTabs>
|
||||
<TabItem value="ui">
|
||||
|
||||
1. Navigate to <NavPath path="Settings > Camera configuration > Live playback" /> and select your camera.
|
||||
2. Under **Live stream names**, click **Add stream** to add a new entry.
|
||||
3. In the **Stream name** field, enter a friendly name that will appear in the Live UI's stream dropdown (e.g., `Main Stream`).
|
||||
4. In the **go2rtc stream** field, open the dropdown and select the go2rtc stream this name should map to (e.g., `test_cam`). The dropdown lists every stream configured under `go2rtc.streams`. If the go2rtc stream hasn't been created yet, you can type the name and choose **Use "..."** to save a custom value.
|
||||
5. Repeat for each additional stream you want to expose (e.g., `Sub Stream` → `test_cam_sub`).
|
||||
6. Use the trash icon on a row to remove a stream, then **Save** the section.
|
||||
|
||||
:::tip
|
||||
|
||||
Configure your go2rtc streams first under <NavPath path="Settings > System > go2rtc streams" /> so the dropdown is populated with valid options.
|
||||
|
||||
:::
|
||||
1. Navigate to <NavPath path="Settings > Camera configuration > Live playback" />, then select your camera.
|
||||
- Under **Live stream names**, add entries mapping a friendly name to each go2rtc stream name (e.g., `Main Stream` mapped to `test_cam`, `Sub Stream` mapped to `test_cam_sub`).
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="yaml">
|
||||
|
||||
@ -23,7 +23,7 @@
|
||||
"singing": "غناء",
|
||||
"choir": "فرقة غناء",
|
||||
"chant": "تَرْنِيم",
|
||||
"mantra": "تعويذة",
|
||||
"mantra": "تَرْنِيمَة",
|
||||
"child_singing": "غِنَاء طِفْل",
|
||||
"synthetic_singing": "غِنَاء اِصْطِنَاعِيّ",
|
||||
"rapping": "رَاب",
|
||||
@ -50,7 +50,7 @@
|
||||
"hands": "أَيْدِي",
|
||||
"finger_snapping": "طَقْطَقَة الأَصَابِع",
|
||||
"clapping": "تَصْفِيق",
|
||||
"heart_murmur": "نفخة القَلْب",
|
||||
"heart_murmur": "لَغَط القَلْب",
|
||||
"cheering": "صِيَاح",
|
||||
"applause": "تَصْفِيق",
|
||||
"chatter": "حَدِيث",
|
||||
@ -74,80 +74,5 @@
|
||||
"bus": "حافلة",
|
||||
"train": "قطار",
|
||||
"boat": "زورق",
|
||||
"bird": "طائر",
|
||||
"sine_wave": "موجة الإشارة",
|
||||
"harmonic": "أوزة",
|
||||
"caw": "نُعَاقُ الغراب",
|
||||
"owl": "بومة",
|
||||
"hoot": "صاح",
|
||||
"flapping_wings": "أجنحة ترفرف",
|
||||
"dogs": "كلاب",
|
||||
"rats": "فئران",
|
||||
"mouse": "فأر",
|
||||
"patter": "طقطق",
|
||||
"insect": "حشرة",
|
||||
"cricket": "كريكيت",
|
||||
"mosquito": "بعوضة",
|
||||
"fly": "سافر",
|
||||
"buzz": "طنين",
|
||||
"frog": "ضفدع",
|
||||
"croak": "نق الضفدع",
|
||||
"snake": "ثعبان",
|
||||
"rattle": "جلجلية",
|
||||
"whale_vocalization": "أصوات الحيتان",
|
||||
"music": "موسيقى",
|
||||
"musical_instrument": "آلة موسيقية",
|
||||
"plucked_string_instrument": "آلة وترية",
|
||||
"guitar": "غيتار",
|
||||
"electric_guitar": "غيتار كهربائي",
|
||||
"bass_guitar": "غيتار البيس",
|
||||
"acoustic_guitar": "غيتار صوتي",
|
||||
"steel_guitar": "غيتار فولاذي",
|
||||
"tapping": "نقر",
|
||||
"strum": "داعب الأ وتار",
|
||||
"banjo": "البانجو",
|
||||
"sitar": "سيتار",
|
||||
"mandolin": "الماندولين",
|
||||
"zither": "زيثارة",
|
||||
"ukulele": "أوكوليلي",
|
||||
"keyboard": "لوحة المفاتيح",
|
||||
"piano": "بيانو",
|
||||
"electric_piano": "بيانو كهربائي",
|
||||
"organ": "أرغن",
|
||||
"electronic_organ": "الأورغن الإلكتروني",
|
||||
"hammond_organ": "أورغن هاموند",
|
||||
"synthesizer": "مُركِّب صوتي",
|
||||
"sampler": "عينة",
|
||||
"harpsichord": "بيان القيثاري",
|
||||
"percussion": "آلات الإيقاع",
|
||||
"drum_kit": "طقم طبول",
|
||||
"drum_machine": "آلة الطبول",
|
||||
"drum": "طبل",
|
||||
"snare_drum": "طبلة جانبية",
|
||||
"rimshot": "طقطة",
|
||||
"drum_roll": "قرع الطبول",
|
||||
"bass_drum": "طبلة الباس",
|
||||
"timpani": "الطبول",
|
||||
"tabla": "طبلة",
|
||||
"cymbal": "الصنج",
|
||||
"hi_hat": "هاي-هات",
|
||||
"wood_block": "كتلة خشبية",
|
||||
"tambourine": "دف",
|
||||
"maraca": "ماراكا",
|
||||
"gong": "غونغ",
|
||||
"tubular_bells": "أجراس أنبوبية",
|
||||
"cattle": "ماشية",
|
||||
"moo": "خوار",
|
||||
"cowbell": "جرس البقر",
|
||||
"pig": "خنزير",
|
||||
"oink": "أوينك",
|
||||
"goat": "معزة",
|
||||
"bleat": "ثغاء",
|
||||
"sheep": "غنم",
|
||||
"fowl": "الدواجن",
|
||||
"chicken": "دجاجة",
|
||||
"cluck": "قرقرة",
|
||||
"cock_a_doodle_doo": "كوكو-كو-كوووووو",
|
||||
"turkey": "ديك رومى",
|
||||
"gobble": "كركرة"
|
||||
"bird": "طائر"
|
||||
}
|
||||
|
||||
@ -18,9 +18,5 @@
|
||||
"train": "قطار",
|
||||
"boat": "زورق",
|
||||
"bench": "مقعدة",
|
||||
"bird": "طائر",
|
||||
"mouse": "فأر",
|
||||
"keyboard": "لوحة المفاتيح",
|
||||
"goat": "معزة",
|
||||
"sheep": "غنم"
|
||||
"bird": "طائر"
|
||||
}
|
||||
|
||||
@ -1729,17 +1729,6 @@
|
||||
},
|
||||
"semanticSearchModelSize": {
|
||||
"notApplicable": "No aplicable als proveïdors de GenAI"
|
||||
},
|
||||
"liveStreams": {
|
||||
"streamNameLabel": "Nom del flux",
|
||||
"streamNamePlaceholder": "p. ex., corrent HD principal",
|
||||
"go2rtcStreamLabel": "flux go2rtc",
|
||||
"go2rtcStreamPlaceholder": "Selecciona un flux go2rtc",
|
||||
"go2rtcStreamSearch": "Cerca o introdueix un nom de flux…",
|
||||
"noGo2rtcStreams": "No s'ha configurat cap flux go2rtc",
|
||||
"availableStreams": "Fluxos disponibles",
|
||||
"useCustom": "Utilitza \"{{value}}\"",
|
||||
"addStream": "Afegeix un flux"
|
||||
}
|
||||
},
|
||||
"globalConfig": {
|
||||
|
||||
@ -1405,17 +1405,6 @@
|
||||
"namePlaceholder": "e.g., Wife's Car",
|
||||
"platePlaceholder": "Plate number or regex"
|
||||
},
|
||||
"liveStreams": {
|
||||
"streamNameLabel": "Stream name",
|
||||
"streamNamePlaceholder": "e.g., Main HD Stream",
|
||||
"go2rtcStreamLabel": "go2rtc stream",
|
||||
"go2rtcStreamPlaceholder": "Select a go2rtc stream",
|
||||
"go2rtcStreamSearch": "Search or enter a stream name…",
|
||||
"noGo2rtcStreams": "No go2rtc streams configured",
|
||||
"availableStreams": "Available streams",
|
||||
"useCustom": "Use \"{{value}}\"",
|
||||
"addStream": "Add stream"
|
||||
},
|
||||
"timezone": {
|
||||
"defaultOption": "Use browser timezone"
|
||||
},
|
||||
|
||||
@ -141,8 +141,7 @@
|
||||
"id": "Bahasa Indonesia (indoneesia keel)",
|
||||
"ur": "اردو (urdu keel)",
|
||||
"hr": "Hrvatski (horvaadi keel)",
|
||||
"bs": "Bosanski (bosnia keel)",
|
||||
"zhHant": "繁體中文 (hiina keel traditsiooniliste hieroglüüfidega)"
|
||||
"bs": "Bosanski (bosnia keel)"
|
||||
},
|
||||
"system": "Süsteem",
|
||||
"systemMetrics": "Süsteemi meetrika",
|
||||
|
||||
@ -10,30 +10,7 @@
|
||||
},
|
||||
"copy": {
|
||||
"label": "Kopeeri lõikelauale",
|
||||
"success": "Logid on kopeeritud lõikelauale",
|
||||
"error": "Logide kopeerimine lõikelauale ei õnnestunud"
|
||||
},
|
||||
"websocket": {
|
||||
"filter": {
|
||||
"cameras_count_one": "{{count}} kaamera",
|
||||
"cameras_count_other": "{{count}} kaamerat"
|
||||
},
|
||||
"empty": "Ühtegi sõnumit pole veel hõivatud",
|
||||
"count_one": "{{count}} sõnum",
|
||||
"count_other": "{{count}} sõnumit"
|
||||
},
|
||||
"type": {
|
||||
"label": "Tüüp",
|
||||
"timestamp": "Ajatempel",
|
||||
"tag": "Silt",
|
||||
"message": "Sõnum"
|
||||
},
|
||||
"tips": "Logid on serverist voogedastamisel",
|
||||
"toast": {
|
||||
"error": {
|
||||
"fetchingLogsFailed": "Viga logide laadimisel: {{errorMessage}}",
|
||||
"whileStreamingLogs": "Viga logide voogedastamisel: {{errorMessage}}"
|
||||
}
|
||||
"success": "Logid on kopeeritud lõikelauale"
|
||||
}
|
||||
},
|
||||
"title": "Süsteem"
|
||||
|
||||
@ -222,8 +222,7 @@
|
||||
"id": "Bahasa Indonesia (Indonesiano)",
|
||||
"ur": "اردو (Urdu)",
|
||||
"hr": "Hrvatski (Croato)",
|
||||
"bs": "Bosanski (Bosniaco)",
|
||||
"zhHant": "繁體中文 (Cinese Tradizionale)"
|
||||
"bs": "Bosanski (Bosniaco)"
|
||||
},
|
||||
"darkMode": {
|
||||
"label": "Modalità scura",
|
||||
|
||||
@ -49,5 +49,5 @@
|
||||
}
|
||||
},
|
||||
"cameraDisabled": "La telecamera è disabilita",
|
||||
"cameraOff": "La telecamera è spenta"
|
||||
"cameraOff": "Camera spenta"
|
||||
}
|
||||
|
||||
@ -171,8 +171,7 @@
|
||||
"description": "Numero di fotogrammi senza cambio di posizione necessari per contrassegnare un oggetto come stazionario."
|
||||
},
|
||||
"max_frames": {
|
||||
"label": "Fotogrammi massimi",
|
||||
"description": "Limita la durata del tracciamento degli oggetti statici prima che vengano scartati."
|
||||
"label": "Max Fotogrammi"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@ -301,8 +301,7 @@
|
||||
"description": "Numero di fotogrammi senza cambio di posizione necessari per contrassegnare un oggetto come stazionario."
|
||||
},
|
||||
"max_frames": {
|
||||
"label": "Fotogrammi massimi",
|
||||
"description": "Limita la durata del tracciamento degli oggetti statici prima che vengano scartati."
|
||||
"label": "Max Fotogrammi"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@ -40,8 +40,7 @@
|
||||
"objectDetection": "Rilevamento oggetti",
|
||||
"recording": "Registrazione",
|
||||
"audioDetection": "Rilevamento audio",
|
||||
"transcription": "Trascrizione audio",
|
||||
"camera": "Telecamera"
|
||||
"transcription": "Trascrizione audio"
|
||||
},
|
||||
"history": {
|
||||
"label": "Mostra filmati storici"
|
||||
@ -99,9 +98,7 @@
|
||||
},
|
||||
"camera": {
|
||||
"enable": "Abilita telecamera",
|
||||
"disable": "Disabilita telecamera",
|
||||
"turnOn": "Attiva la telecamera",
|
||||
"turnOff": "Disattiva la telecamera"
|
||||
"disable": "Disabilita telecamera"
|
||||
},
|
||||
"muteCameras": {
|
||||
"enable": "Muta tutte le telecamere",
|
||||
|
||||
@ -449,7 +449,7 @@
|
||||
"enrichments": "Miglioramenti",
|
||||
"triggers": "Inneschi",
|
||||
"roles": "Ruoli",
|
||||
"cameraManagement": "Gestione della telecamera",
|
||||
"cameraManagement": "Gestione",
|
||||
"cameraReview": "Revisiona",
|
||||
"profiles": "Profili",
|
||||
"general": "Generale",
|
||||
@ -1404,13 +1404,13 @@
|
||||
"selectCamera": "Seleziona una telecamera",
|
||||
"backToSettings": "Torna alle impostazioni della telecamera",
|
||||
"streams": {
|
||||
"title": "Stato e dettagli della telecamera",
|
||||
"title": "Abilita/Disabilita telecamere",
|
||||
"desc": "Disattiva temporaneamente una telecamera fino al riavvio di Frigate. La disattivazione completa di una telecamera interrompe l'elaborazione dei flussi di questa telecamera da parte di Frigate. Rilevamento, registrazione e correzioni non saranno disponibili.<br /> <em>Nota: questa operazione non disattiva le ritrasmissioni di go2rtc.</em>",
|
||||
"enableLabel": "Telecamere abilitate",
|
||||
"enableDesc": "Disabilita temporaneamente una telecamera abilitata fino al riavvio di Frigate. La disabilitazione completa di una telecamera interrompe l'elaborazione dei flussi video di tale telecamera da parte di Frigate. Le funzioni di rilevamento, registrazione e correzioni non saranno disponibili.<br /> <em>Nota: questa operazione non disabilita le ritrasmissioni go2rtc.</em><br /><br />Trascina le schede per riordinare le telecamere nell'interfaccia utente. L'ordine delle telecamere abilitate verrà visualizzato in tutta l'interfaccia utente inclusa la schermata Dal vivo e i menu a tendina di selezione delle telecamere.",
|
||||
"disableLabel": "Telecamere disabilitate",
|
||||
"disableDesc": "Abilita una telecamera attualmente non visibile nell'interfaccia utente e disabilitata nella configurazione. Dopo l'abilitazione è necessario riavviare Frigate.",
|
||||
"enableSuccess": "{{cameraName}} abilitata. Riavvia Frigate per applicare le modifiche.",
|
||||
"enableSuccess": "{{cameraName}} abilitata nella configurazione. Riavvia Frigate per applicare le modifiche.",
|
||||
"friendlyName": {
|
||||
"edit": "Modifica il nome visualizzato della telecamera",
|
||||
"title": "Modifica il nome visualizzato",
|
||||
@ -1429,16 +1429,7 @@
|
||||
"webuiUrlLabel": "URL dell'interfaccia web della telecamera",
|
||||
"webuiUrlHelp": "URL per accedere direttamente all'interfaccia web della telecamera dalla vista Correzioni. Lasciare vuoto per disabilitare il collegamento.",
|
||||
"webuiUrlInvalid": "Deve essere un URL valido (ad esempio, https://esempio.com)."
|
||||
},
|
||||
"label": "Stato della telecamera",
|
||||
"description": "Imposta lo stato operativo per ciascuna telecamera.<br /><br /><strong>Accesa</strong>: i flussi vengono elaborati normalmente.<br /><strong>Spenta</strong>: mette temporaneamente in pausa l'elaborazione. Non viene mantenuta dopo il riavvio di Frigate.<br /><strong>Disabilitata</strong>: interrompe l'elaborazione e salva la modifica nella configurazione. È necessario riavviare Frigate per riattivare una telecamera disabilitata.<br /><br /><em>Nota: la disabilitazione non influisce sulle ritrasmissioni go2rtc.</em><br /><br />Trascina la maniglia per riordinare le telecamere attive nell'interfaccia utente, inclusi il pannello di controllo Dal vivo e i menu a tendina di selezione della telecamera.",
|
||||
"disabledSubheading": "Disabilitata nella configurazione",
|
||||
"status": {
|
||||
"on": "Accesa",
|
||||
"off": "Spenta",
|
||||
"disabled": "Disabilitata"
|
||||
},
|
||||
"disableSuccess": "{{cameraName}} disabilitata e salvata nella configurazione."
|
||||
}
|
||||
},
|
||||
"cameraConfig": {
|
||||
"add": "Aggiungi telecamera",
|
||||
@ -1473,13 +1464,11 @@
|
||||
"enabled": "Abilitato",
|
||||
"title": "Sovrascritture della telecamera del profilo",
|
||||
"selectLabel": "Seleziona il profilo",
|
||||
"description": "Configura quali telecamere vengono accese o spente all'attivazione di un profilo. Le telecamere impostate su \"Eredita\" mantengono il loro stato predefinito.",
|
||||
"description": "Configura quali telecamere vengono abilitate o disabilitate all'attivazione di un profilo. Le telecamere impostate su \"Eredita\" mantengono il loro stato di abilitazione predefinito.",
|
||||
"inherit": "Eredita",
|
||||
"disabled": "Disabilitato",
|
||||
"on": "Attivato",
|
||||
"off": "Disattivato"
|
||||
"disabled": "Disabilitato"
|
||||
},
|
||||
"description": "Aggiungi, modifica ed elimina le telecamere, controlla lo stato di ciascuna telecamera e configura le impostazioni personalizzate per profilo e tipo di telecamera. Per configurare flussi video, rilevamento, movimento e altre impostazioni specifiche per ciascuna telecamera, seleziona la sezione corrispondente in Configurazione telecamera.",
|
||||
"description": "Aggiungi, modifica ed elimina le telecamere, controlla quali telecamere sono abilitate e configura le impostazioni personalizzate per profilo e tipo di telecamera. Per configurare flussi video, rilevamento, movimento e altre impostazioni specifiche per ciascuna telecamera, seleziona la sezione corrispondente in Configurazione telecamera.",
|
||||
"deleteCamera": "Elimina telecamera",
|
||||
"deleteCameraDialog": {
|
||||
"title": "Elimina telecamera",
|
||||
@ -1781,17 +1770,6 @@
|
||||
},
|
||||
"semanticSearchModelSize": {
|
||||
"notApplicable": "Non applicabile ai fornitori GenAI"
|
||||
},
|
||||
"liveStreams": {
|
||||
"streamNameLabel": "Nome flusso",
|
||||
"streamNamePlaceholder": "p.es., flusso HD principale",
|
||||
"go2rtcStreamLabel": "flusso go2rtc",
|
||||
"go2rtcStreamPlaceholder": "Seleziona un flusso go2rtc",
|
||||
"go2rtcStreamSearch": "Cerca o inserisci il nome di un flusso…",
|
||||
"noGo2rtcStreams": "Nessun flusso go2rtc configurato",
|
||||
"availableStreams": "Flussi disponibili",
|
||||
"useCustom": "Utilizza \"{{value}}\"",
|
||||
"addStream": "Aggiungi flusso"
|
||||
}
|
||||
},
|
||||
"globalConfig": {
|
||||
|
||||
@ -222,8 +222,7 @@
|
||||
"id": "印度尼西亚语 (Bahasa Indonesia)",
|
||||
"ur": "乌尔都语 (اردو)",
|
||||
"hr": "克罗地亚语 (Hrvatski)",
|
||||
"bs": "波斯尼亚语(Bosanski)",
|
||||
"zhHant": "繁体中文 (Traditional Chinese)"
|
||||
"bs": "波斯尼亚语(Bosanski)"
|
||||
},
|
||||
"appearance": "外观",
|
||||
"darkMode": {
|
||||
|
||||
@ -1726,17 +1726,6 @@
|
||||
},
|
||||
"semanticSearchModelSize": {
|
||||
"notApplicable": "不适用于生成式 AI 提供者"
|
||||
},
|
||||
"liveStreams": {
|
||||
"streamNameLabel": "视频流名称",
|
||||
"streamNamePlaceholder": "例如:高清流",
|
||||
"go2rtcStreamLabel": "go2rtc 视频流",
|
||||
"go2rtcStreamPlaceholder": "选择 go2rtc 视频流",
|
||||
"go2rtcStreamSearch": "搜索或输入视频流名称…",
|
||||
"noGo2rtcStreams": "没有 go2rtc 视频流配置",
|
||||
"availableStreams": "可用的视频流",
|
||||
"useCustom": "使用“{{value}}”",
|
||||
"addStream": "添加视频流"
|
||||
}
|
||||
},
|
||||
"cameraConfig": {
|
||||
|
||||
@ -1 +0,0 @@
|
||||
{}
|
||||
@ -1 +0,0 @@
|
||||
{}
|
||||
@ -1 +0,0 @@
|
||||
{}
|
||||
@ -1 +0,0 @@
|
||||
{}
|
||||
@ -1 +0,0 @@
|
||||
{}
|
||||
@ -1 +0,0 @@
|
||||
{}
|
||||
@ -1 +0,0 @@
|
||||
{}
|
||||
@ -1 +0,0 @@
|
||||
{}
|
||||
@ -1 +0,0 @@
|
||||
{}
|
||||
@ -1 +0,0 @@
|
||||
{}
|
||||
@ -1 +0,0 @@
|
||||
{}
|
||||
@ -1 +0,0 @@
|
||||
{}
|
||||
@ -1 +0,0 @@
|
||||
{}
|
||||
@ -1 +0,0 @@
|
||||
{}
|
||||
@ -1 +0,0 @@
|
||||
{}
|
||||
@ -1 +0,0 @@
|
||||
{}
|
||||
@ -1 +0,0 @@
|
||||
{}
|
||||
@ -1 +0,0 @@
|
||||
{}
|
||||
@ -1 +0,0 @@
|
||||
{}
|
||||
@ -1 +0,0 @@
|
||||
{}
|
||||
@ -1 +0,0 @@
|
||||
{}
|
||||
@ -1 +0,0 @@
|
||||
{}
|
||||
@ -1 +0,0 @@
|
||||
{}
|
||||
@ -1 +0,0 @@
|
||||
{}
|
||||
@ -1 +0,0 @@
|
||||
{}
|
||||
@ -1 +0,0 @@
|
||||
{}
|
||||
@ -1 +0,0 @@
|
||||
{}
|
||||
@ -1 +0,0 @@
|
||||
{}
|
||||
@ -4,26 +4,17 @@ const live: SectionConfigOverrides = {
|
||||
base: {
|
||||
sectionDocs: "/configuration/live",
|
||||
restartRequired: [],
|
||||
fieldOrder: ["streams", "height", "quality"],
|
||||
fieldOrder: ["stream_name", "height", "quality"],
|
||||
fieldGroups: {},
|
||||
hiddenFields: ["enabled_in_config"],
|
||||
advancedFields: ["height", "quality"],
|
||||
},
|
||||
global: {
|
||||
restartRequired: ["streams", "height", "quality"],
|
||||
restartRequired: ["stream_name", "height", "quality"],
|
||||
hiddenFields: ["streams"],
|
||||
},
|
||||
camera: {
|
||||
restartRequired: ["height", "quality"],
|
||||
uiSchema: {
|
||||
streams: {
|
||||
"ui:field": "LiveStreamsField",
|
||||
"ui:options": {
|
||||
label: false,
|
||||
suppressDescription: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@ -1,346 +0,0 @@
|
||||
import type { FieldPathList, FieldProps, RJSFSchema } from "@rjsf/utils";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
Command,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from "@/components/ui/command";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Check, ChevronsUpDown, Plus } from "lucide-react";
|
||||
import { LuPlus, LuTrash2 } from "react-icons/lu";
|
||||
import type { ConfigFormContext } from "@/types/configForm";
|
||||
import get from "lodash/get";
|
||||
import { isSubtreeModified } from "../utils";
|
||||
|
||||
type LiveStreamsData = Record<string, string>;
|
||||
|
||||
type StreamValueComboboxProps = {
|
||||
id: string;
|
||||
value: string;
|
||||
options: string[];
|
||||
disabled?: boolean;
|
||||
readonly?: boolean;
|
||||
onChange: (next: string) => void;
|
||||
};
|
||||
|
||||
function StreamValueCombobox({
|
||||
id,
|
||||
value,
|
||||
options,
|
||||
disabled,
|
||||
readonly,
|
||||
onChange,
|
||||
}: StreamValueComboboxProps) {
|
||||
const { t } = useTranslation(["views/settings", "common"]);
|
||||
const [open, setOpen] = useState(false);
|
||||
const [searchValue, setSearchValue] = useState("");
|
||||
|
||||
const trimmedSearch = searchValue.trim();
|
||||
const matchesOption = useMemo(
|
||||
() => options.some((o) => o.toLowerCase() === trimmedSearch.toLowerCase()),
|
||||
[options, trimmedSearch],
|
||||
);
|
||||
const showCustomOption = trimmedSearch.length > 0 && !matchesOption;
|
||||
|
||||
const commit = (next: string) => {
|
||||
onChange(next);
|
||||
setSearchValue("");
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
const placeholder = t("configForm.liveStreams.go2rtcStreamPlaceholder", {
|
||||
ns: "views/settings",
|
||||
});
|
||||
const searchPlaceholder = t("configForm.liveStreams.go2rtcStreamSearch", {
|
||||
ns: "views/settings",
|
||||
});
|
||||
const noStreams = t("configForm.liveStreams.noGo2rtcStreams", {
|
||||
ns: "views/settings",
|
||||
});
|
||||
const availableHeading = t("configForm.liveStreams.availableStreams", {
|
||||
ns: "views/settings",
|
||||
});
|
||||
|
||||
return (
|
||||
<Popover
|
||||
open={open}
|
||||
onOpenChange={(next) => {
|
||||
setOpen(next);
|
||||
if (!next) setSearchValue("");
|
||||
}}
|
||||
>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
id={id}
|
||||
type="button"
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={open}
|
||||
disabled={disabled || readonly}
|
||||
className={cn(
|
||||
"w-full justify-between font-normal",
|
||||
!value && "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
<span className="truncate">{value || placeholder}</span>
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[--radix-popover-trigger-width] p-0">
|
||||
<Command>
|
||||
<CommandInput
|
||||
placeholder={searchPlaceholder}
|
||||
value={searchValue}
|
||||
onValueChange={setSearchValue}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" && showCustomOption) {
|
||||
e.preventDefault();
|
||||
commit(trimmedSearch);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<CommandList>
|
||||
{showCustomOption && (
|
||||
<CommandGroup>
|
||||
<CommandItem
|
||||
value={trimmedSearch}
|
||||
onSelect={() => commit(trimmedSearch)}
|
||||
>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
{t("configForm.liveStreams.useCustom", {
|
||||
ns: "views/settings",
|
||||
value: trimmedSearch,
|
||||
})}
|
||||
</CommandItem>
|
||||
</CommandGroup>
|
||||
)}
|
||||
{options.length > 0 ? (
|
||||
<CommandGroup heading={availableHeading}>
|
||||
{options.map((option) => (
|
||||
<CommandItem
|
||||
key={option}
|
||||
value={option}
|
||||
onSelect={() => commit(option)}
|
||||
>
|
||||
<Check
|
||||
className={cn(
|
||||
"mr-2 h-4 w-4",
|
||||
value === option ? "opacity-100" : "opacity-0",
|
||||
)}
|
||||
/>
|
||||
{option}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
) : !showCustomOption ? (
|
||||
<div className="p-4 text-center text-sm text-muted-foreground">
|
||||
{noStreams}
|
||||
</div>
|
||||
) : null}
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
||||
export function LiveStreamsField(props: FieldProps) {
|
||||
const { schema, formData, onChange, idSchema, disabled, readonly } = props;
|
||||
const formContext = props.registry?.formContext as
|
||||
| ConfigFormContext
|
||||
| undefined;
|
||||
|
||||
const configNamespace =
|
||||
formContext?.i18nNamespace ??
|
||||
(formContext?.level === "camera" ? "config/cameras" : "config/global");
|
||||
const { t: fallbackT } = useTranslation(["common", configNamespace]);
|
||||
const t = formContext?.t ?? fallbackT;
|
||||
|
||||
const data: LiveStreamsData = useMemo(() => {
|
||||
if (!formData || typeof formData !== "object" || Array.isArray(formData)) {
|
||||
return {};
|
||||
}
|
||||
return formData as LiveStreamsData;
|
||||
}, [formData]);
|
||||
|
||||
const entries = useMemo(() => Object.entries(data), [data]);
|
||||
|
||||
const id = idSchema?.$id ?? props.name;
|
||||
const sectionPrefix = formContext?.sectionI18nPrefix;
|
||||
|
||||
const title =
|
||||
t(`${sectionPrefix}.${id}.label`) ?? (schema as RJSFSchema).title;
|
||||
const description =
|
||||
t(`${sectionPrefix}.${id}.description`) ??
|
||||
(schema as RJSFSchema).description;
|
||||
|
||||
const go2rtcStreamNames = useMemo<string[]>(() => {
|
||||
const streams = formContext?.fullConfig?.go2rtc?.streams;
|
||||
if (!streams || typeof streams !== "object") return [];
|
||||
return Object.keys(streams).sort();
|
||||
}, [formContext?.fullConfig?.go2rtc?.streams]);
|
||||
|
||||
const emptyPath = useMemo(() => [] as FieldPathList, []);
|
||||
const fieldPath =
|
||||
(props as { fieldPathId?: { path?: FieldPathList } }).fieldPathId?.path ??
|
||||
emptyPath;
|
||||
|
||||
const isModified = useMemo(() => {
|
||||
const baselineRoot = formContext?.baselineFormData;
|
||||
const baselineValue = baselineRoot
|
||||
? get(baselineRoot, fieldPath)
|
||||
: undefined;
|
||||
return isSubtreeModified(
|
||||
data,
|
||||
baselineValue,
|
||||
formContext?.overrides,
|
||||
fieldPath,
|
||||
formContext?.formData,
|
||||
);
|
||||
}, [fieldPath, formContext, data]);
|
||||
|
||||
const handleAddEntry = useCallback(() => {
|
||||
const next = { ...data, "": "" };
|
||||
onChange(next, fieldPath);
|
||||
}, [data, fieldPath, onChange]);
|
||||
|
||||
const handleRemoveEntry = useCallback(
|
||||
(key: string) => {
|
||||
const next = { ...data };
|
||||
delete next[key];
|
||||
onChange(next, fieldPath);
|
||||
},
|
||||
[data, fieldPath, onChange],
|
||||
);
|
||||
|
||||
const handleRenameKey = useCallback(
|
||||
(oldKey: string, newKey: string) => {
|
||||
if (oldKey === newKey) return;
|
||||
const next: LiveStreamsData = {};
|
||||
for (const [k, v] of Object.entries(data)) {
|
||||
if (k === oldKey) {
|
||||
next[newKey] = v;
|
||||
} else {
|
||||
next[k] = v;
|
||||
}
|
||||
}
|
||||
onChange(next, fieldPath);
|
||||
},
|
||||
[data, fieldPath, onChange],
|
||||
);
|
||||
|
||||
const handleUpdateValue = useCallback(
|
||||
(key: string, value: string) => {
|
||||
const next = { ...data, [key]: value };
|
||||
onChange(next, fieldPath);
|
||||
},
|
||||
[data, fieldPath, onChange],
|
||||
);
|
||||
|
||||
const baseId = idSchema?.$id || "live_streams";
|
||||
const deleteLabel = t("button.delete", {
|
||||
ns: "common",
|
||||
defaultValue: "Delete",
|
||||
});
|
||||
const streamNameLabel = t("configForm.liveStreams.streamNameLabel", {
|
||||
ns: "views/settings",
|
||||
});
|
||||
const streamNamePlaceholder = t(
|
||||
"configForm.liveStreams.streamNamePlaceholder",
|
||||
{ ns: "views/settings" },
|
||||
);
|
||||
const go2rtcStreamLabel = t("configForm.liveStreams.go2rtcStreamLabel", {
|
||||
ns: "views/settings",
|
||||
});
|
||||
const addStreamLabel = t("configForm.liveStreams.addStream", {
|
||||
ns: "views/settings",
|
||||
});
|
||||
|
||||
return (
|
||||
<Card className="w-full">
|
||||
<CardHeader className="p-4">
|
||||
<CardTitle className={cn("text-sm", isModified && "text-unsaved")}>
|
||||
{title}
|
||||
</CardTitle>
|
||||
{description && (
|
||||
<p className="mt-1 text-xs text-muted-foreground">{description}</p>
|
||||
)}
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3 p-4 pt-0">
|
||||
{entries.map(([key, value], entryIndex) => {
|
||||
const entryId = `${baseId}-${entryIndex}`;
|
||||
return (
|
||||
<div
|
||||
key={entryIndex}
|
||||
className="grid grid-cols-12 items-end gap-2 rounded-md border p-3"
|
||||
>
|
||||
<div className="col-span-12 space-y-2 md:col-span-5">
|
||||
<Label htmlFor={`${entryId}-key`}>{streamNameLabel}</Label>
|
||||
<Input
|
||||
id={`${entryId}-key`}
|
||||
defaultValue={key}
|
||||
placeholder={streamNamePlaceholder}
|
||||
disabled={disabled || readonly}
|
||||
onBlur={(e) => handleRenameKey(key, e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-10 space-y-2 md:col-span-6">
|
||||
<Label htmlFor={`${entryId}-value`}>{go2rtcStreamLabel}</Label>
|
||||
<StreamValueCombobox
|
||||
id={`${entryId}-value`}
|
||||
value={value}
|
||||
options={go2rtcStreamNames}
|
||||
disabled={disabled}
|
||||
readonly={readonly}
|
||||
onChange={(next) => handleUpdateValue(key, next)}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-2 flex justify-end md:col-span-1">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => handleRemoveEntry(key)}
|
||||
disabled={disabled || readonly}
|
||||
aria-label={deleteLabel}
|
||||
title={deleteLabel}
|
||||
className="shrink-0"
|
||||
>
|
||||
<LuTrash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
<div>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleAddEntry}
|
||||
disabled={disabled || readonly}
|
||||
className="gap-2"
|
||||
>
|
||||
<LuPlus className="h-4 w-4" />
|
||||
{addStreamLabel}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default LiveStreamsField;
|
||||
@ -2,4 +2,3 @@
|
||||
export { LayoutGridField } from "./LayoutGridField";
|
||||
export { DetectorHardwareField } from "./DetectorHardwareField";
|
||||
export { ReplaceRulesField } from "./ReplaceRulesField";
|
||||
export { LiveStreamsField } from "./LiveStreamsField";
|
||||
|
||||
@ -51,7 +51,6 @@ import { ReplaceRulesField } from "./fields/ReplaceRulesField";
|
||||
import { CameraInputsField } from "./fields/CameraInputsField";
|
||||
import { DictAsYamlField } from "./fields/DictAsYamlField";
|
||||
import { KnownPlatesField } from "./fields/KnownPlatesField";
|
||||
import { LiveStreamsField } from "./fields/LiveStreamsField";
|
||||
|
||||
export interface FrigateTheme {
|
||||
widgets: RegistryWidgetsType;
|
||||
@ -110,6 +109,5 @@ export const frigateTheme: FrigateTheme = {
|
||||
CameraInputsField: CameraInputsField,
|
||||
DictAsYamlField: DictAsYamlField,
|
||||
KnownPlatesField: KnownPlatesField,
|
||||
LiveStreamsField: LiveStreamsField,
|
||||
},
|
||||
};
|
||||
|
||||
Loading…
Reference in New Issue
Block a user