Add dual-stream support for Frigate with optional sub-stream selection

Features:
- Optional sub-stream selection from already discovered streams
- No additional scanning required - reuse existing results
- UI: "Add Sub Stream" button to select secondary stream
- UI: "Remove Sub Stream" button to clear selection
- Smart stream routing in Frigate configs
- Go2RTC: generates _main and _sub stream names
- Frigate: detect on sub (CPU efficient), record on main (quality)
- Frigate: auto-detection of stream resolution
- Object detection: person, car, cat, dog
- Motion-based recording by default
- Live view streams configuration
- Support for any resolution: HD, 4K, 8K+
- Comprehensive documentation with examples
This commit is contained in:
eduard256
2025-11-06 22:53:50 +03:00
parent 74fe12bcf1
commit 7fd1d78ffa
8 changed files with 874 additions and 129 deletions
+79 -10
View File
@@ -16,7 +16,9 @@ class StrixApp {
this.currentAddress = '';
this.currentStreams = [];
this.currentStream = null;
this.selectedMainStream = null;
this.selectedSubStream = null;
this.isSelectingSubStream = false;
this.init();
}
@@ -94,11 +96,16 @@ class StrixApp {
// Screen 4: Configuration output
document.getElementById('btn-back-to-streams').addEventListener('click', () => {
this.isSelectingSubStream = false;
this.showScreen('discovery');
});
document.getElementById('btn-copy-config').addEventListener('click', () => this.copyConfig());
document.getElementById('btn-download-config').addEventListener('click', () => this.downloadConfig());
document.getElementById('btn-add-sub-stream').addEventListener('click', () => this.addSubStream());
document.getElementById('btn-remove-sub').addEventListener('click', () => this.removeSubStream());
document.getElementById('btn-new-search').addEventListener('click', () => {
this.reset();
this.showScreen('address');
@@ -171,9 +178,16 @@ class StrixApp {
async searchCameraModels(query, limit = 10, append = false) {
const dropdown = document.getElementById('autocomplete-dropdown');
// Keep dropdown open and show loading state smoothly
if (!append) {
dropdown.innerHTML = '<div class="autocomplete-loading">Searching...</div>';
dropdown.classList.remove('hidden');
const isOpen = !dropdown.classList.contains('hidden');
if (!isOpen) {
dropdown.classList.remove('hidden');
}
// Show loading only if dropdown was empty or closed
if (!isOpen || dropdown.children.length === 0) {
dropdown.innerHTML = '<div class="autocomplete-loading">Searching...</div>';
}
}
try {
@@ -316,9 +330,52 @@ class StrixApp {
}
selectStream(stream, index) {
this.currentStream = stream;
this.configPanel.render(stream);
this.showScreen('output');
if (!this.isSelectingSubStream) {
// Selecting main stream
this.selectedMainStream = stream;
this.selectedSubStream = null;
this.configPanel.render(this.selectedMainStream, this.selectedSubStream);
this.updateSubStreamUI();
this.showScreen('output');
} else {
// Selecting sub stream
this.selectedSubStream = stream;
this.isSelectingSubStream = false;
this.configPanel.render(this.selectedMainStream, this.selectedSubStream);
this.updateSubStreamUI();
this.showScreen('output');
}
}
addSubStream() {
if (this.currentStreams.length === 0) {
showToast('No streams available to select');
return;
}
this.isSelectingSubStream = true;
showToast('Select a sub stream from available streams');
this.showScreen('discovery');
}
removeSubStream() {
this.selectedSubStream = null;
this.configPanel.render(this.selectedMainStream, this.selectedSubStream);
this.updateSubStreamUI();
showToast('Sub stream removed');
}
updateSubStreamUI() {
const subStreamInfo = document.getElementById('sub-stream-info');
const addSubStreamBtn = document.getElementById('btn-add-sub-stream');
if (this.selectedSubStream) {
subStreamInfo.classList.remove('hidden');
addSubStreamBtn.style.display = 'none';
} else {
subStreamInfo.classList.add('hidden');
addSubStreamBtn.style.display = 'inline-flex';
}
}
switchTab(tabName) {
@@ -336,12 +393,22 @@ class StrixApp {
const configElement = document.getElementById(`config-${activeTab}`);
const text = configElement.textContent;
navigator.clipboard.writeText(text).then(() => {
const textarea = document.createElement('textarea');
textarea.value = text;
textarea.style.position = 'fixed';
textarea.style.left = '-9999px';
document.body.appendChild(textarea);
textarea.select();
try {
document.execCommand('copy');
showToast('Copied to clipboard!');
}).catch(err => {
} catch (err) {
showToast('Failed to copy');
console.error('Copy error:', err);
});
} finally {
document.body.removeChild(textarea);
}
}
downloadConfig() {
@@ -364,7 +431,9 @@ class StrixApp {
reset() {
this.currentAddress = '';
this.currentStreams = [];
this.currentStream = null;
this.selectedMainStream = null;
this.selectedSubStream = null;
this.isSelectingSubStream = false;
document.getElementById('network-address').value = '';
document.getElementById('camera-model').value = '';