29 Commits

Author SHA1 Message Date
eduard256 031e494787 Merge develop into main for v1.0.7 release 2025-11-23 22:54:20 +03:00
eduard256 de389588ce Release v1.0.7: Fix Hikvision channel numbering and improve database
- Fixed channel numbering for Hikvision-style cameras (reported by @sergbond_com)
- Added universal [CHANNEL+1] placeholder support
- Supports both 0-based and 1-based channel numbering
- Updated 14 camera brands with universal patterns
- Fixed brand+model search matching
- Removed invalid test data from database
2025-11-23 22:54:03 +03:00
eduard256 4c03ad8d3c Add [CHANNEL+1] placeholder support for Hikvision-style channel numbering
- Added [CHANNEL+1], [channel+1], {CHANNEL+1}, {channel+1} placeholders to builder.go
- Updated 14 camera brands with universal channel patterns
- Hikvision: replaced 10 hardcoded patterns with 6 universal patterns
- Hiwatch: replaced 4 hardcoded patterns with 8 universal patterns (including ISAPI)
- Other brands: Annke, Swann, Abus, 7links, LevelOne, AlienDVR, Oswoo, AV102IP-40, Acvil, TBKVision, Deltaco, Night Owl
- Universal patterns placed first for faster discovery, hardcoded patterns kept as fallback
- Supports both 0-based (channel=0 -> 101) and 1-based (channel=1 -> 101) numbering
- Added 6 high-priority patterns to popular_stream_patterns.json
2025-11-23 22:39:20 +03:00
eduard256 d569a76700 Use intelligent brand+model search in stream discovery 2025-11-23 21:33:44 +03:00
eduard256 a405d6198f Merge main into develop: Add dynamic channel support for HiWatch cameras 2025-11-22 22:22:41 +03:00
eduard256 4143c267cd Remove invalid URL entry from Hikvision database
- Removed entry with embedded credentials and IP address
- Entry contained: rtsp://huntertech:Superuser01!@10.0.55.11:554
- This was likely test data that accidentally got committed
- Model "Bullet-4K" entry removed from database
2025-11-22 21:52:53 +03:00
eduard256 19e58db70f Add dynamic channel support for HiWatch cameras
- Added 5 new URL patterns with [CHANNEL] placeholder
- Supports channels 0-255 for multi-camera DVR/NVR systems
- Patterns include /Streaming/Channels/[CHANNEL]01, [CHANNEL]02
- ISAPI format support with dynamic channels
- All existing hardcoded patterns preserved for compatibility
2025-11-22 21:45:56 +03:00
eduard256 11e6ba9902 Merge develop: Fix SSE timeout issues 2025-11-22 20:35:48 +03:00
eduard256 a6e9cc2c5e Fix SSE timeout issues with long-running stream discovery
Problem:
- WriteTimeout was 30 seconds
- Progress only sent when values changed
- Long ffprobe tests (7-8s each) could cause 30+ seconds without writes
- Result: "curl: (18) transfer closed with outstanding read data remaining"

Solution:
- Increase WriteTimeout from 30s to 5 minutes
- Send progress every 1 second (instead of 3 seconds)
- Always send progress, even if values unchanged
- Guarantees write every second, preventing timeout

Changes:
- internal/config/config.go: WriteTimeout 30s → 5min
- internal/camera/discovery/scanner.go:
  - Progress ticker 3s → 1s
  - Remove "only if changed" check
  - Always send progress to keep connection alive

Testing:
- HiWatch camera with 591 streams: Previously timed out at ~338/591
- Should now complete all 591 streams without timeout
2025-11-22 19:48:03 +03:00
eduard256 12770ed5b9 Merge pull request #1 from eduard256/develop
WebUI Improvements - Mock Mode, Tooltips, and UX Enhancements
2025-11-22 00:38:56 +03:00
eduard256 90c4416709 Add informational tooltips for stream types and update mock data
- Add tooltips for all 7 stream types: FFMPEG, ONVIF, MJPEG, HLS, BUBBLE, JPEG, HTTP_VIDEO
- Each tooltip explains protocol features, use cases, and compatibility
- Add BUBBLE protocol icon and detailed description (XMEye/DVRIP cameras)
- Update mock streams to show one example of each type
- Remove unused mock-data.js file to reduce confusion
- Add CSS styles for stream type info icons
2025-11-22 00:29:07 +03:00
eduard256 d602c8dfca Improve WebUI UX with tooltips, auto-fill and button visibility
- Add informational tooltips to all configuration fields
- Reorder tabs: Frigate first, then Go2RTC, then URL
- Hide Copy/Download buttons on Frigate tab until config is generated
- Auto-fill username field with "admin" as default value
- Smart pre-fill network address based on server IP (first 3 octets)
- Add tooltips for Main Stream, Sub Stream, and all buttons
- Improve user guidance throughout the configuration flow
2025-11-22 00:03:54 +03:00
eduard256 596cf1ccdc Add interactive tooltips to camera configuration form
Добавлены информационные тултипы для всех полей формы настройки камеры с подробными описаниями, примерами использования и рекомендациями. Улучшает пользовательский опыт и помогает пользователям правильно заполнить форму.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-21 23:37:02 +03:00
eduard256 779ae33bac Redesign stream discovery UI with vertical list layout
- Replace carousel navigation with scrollable vertical list
- Remove statistics counters (Tested/Found/Remaining)
- Add collapsible stream details with expand/collapse toggle
- Show stream URL preview in header, full URL in details
- Position URL below stream type badge for better readability
- Add new StreamList component replacing StreamCarousel
- Update CSS with improved layout and hover effects

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-21 23:25:30 +03:00
eduard256 71d6f2aac8 Fix missing statistics elements in stream discovery UI
Restored stats block (Tested, Found, Remaining) that was accidentally
removed when adding mock mode functionality. This fixes JavaScript
errors where main.js tried to update non-existent DOM elements.
2025-11-21 23:02:39 +03:00
eduard256 56c06dfa98 Add mock mode for WebUI development
- Add mock API classes for camera search and stream discovery
- Add mock mode toggle via ?mock=true URL parameter
- Add visual mock mode indicator badge
- Add dev-server.sh script for local development
- Mock data includes 10 diverse streams (FFMPEG, ONVIF, JPEG, MJPEG, HLS, HTTP_VIDEO)
2025-11-21 22:57:37 +03:00
eduard256 8bf92e6598 Add mock mode for web UI development and testing
- Add mock data module with simulated camera search and stream discovery
- Enable mock mode via ?mock=true URL parameter
- Show MOCK MODE indicator when enabled
- Remove statistics cards from discovery screen, keep only progress bar
- Mock mode works independently from Go backend for easier UI testing
2025-11-21 22:40:38 +03:00
eduard256 522d274dd4 Fix CPU usage percentage in README 2025-11-20 19:19:59 +03:00
eduard256 8036d3e9be Add GitHub Stars badge to README 2025-11-18 17:03:54 +03:00
eduard256 5b2f80f057 Add Docker and Docker Compose auto-installation to compose command 2025-11-18 16:05:47 +03:00
eduard256 e2b9802fd8 Fix duplicate badges in README
Removed duplicate license and Docker pulls badges from README.
2025-11-18 16:04:23 +03:00
eduard256 65a198d119 Simplify RTSP URL description in README
Updated README to simplify the RTSP URL description and remove redundant lines.
2025-11-18 16:03:24 +03:00
eduard256 722c629c01 Move demo gif after badges for better visual flow 2025-11-18 16:01:30 +03:00
eduard256 c81d9a1e63 Complete README.md rewrite with improved structure and documentation 2025-11-18 15:59:59 +03:00
eduard256 06de1c198b Version bump to 1.0.4 2025-11-18 14:16:11 +03:00
eduard256 ded9b507d6 Fix stream discovery URL for Home Assistant Ingress
- Remove new URL() construction with window.location.origin
- Use simple relative path like camera-search API
- Fixes stream discovery in HA Ingress environment
- Maintains compatibility with direct Docker installations
- Version bump to 1.0.3
2025-11-18 13:59:54 +03:00
eduard256 90063c3f3a Fix API paths for Home Assistant Ingress compatibility
- Change absolute API paths to relative paths in camera-search.js
- Change absolute API paths to relative paths in stream-discovery.js
- Fixes resource loading in HA Ingress environment
- Maintains compatibility with direct Docker installations
- Version bump to 1.0.2
2025-11-18 13:35:29 +03:00
eduard256 1bab993a13 Fix CSS and JS paths for Home Assistant Ingress compatibility
- Change absolute paths (/css/, /js/) to relative paths (css/, js/)
- Maintains compatibility with direct Docker installations
- Fixes resource loading in HA Ingress environment
- API calls remain unchanged and work correctly
2025-11-18 00:58:11 +03:00
eduard256 1163af6fac Remove Home Assistant addon integration
- Remove homeassistant-addon directory
- Remove addon workflow
- Remove repository.yaml
- Update README to focus on Docker and source installation
- Keep ARM/Raspberry Pi support (arm64, armv7)
2025-11-18 00:09:00 +03:00
48 changed files with 4039 additions and 3602 deletions
-155
View File
@@ -1,155 +0,0 @@
name: Home Assistant Add-on
on:
push:
branches:
- main
paths:
- 'homeassistant-addon/**'
- 'cmd/**'
- 'internal/**'
- 'pkg/**'
- 'data/**'
- 'webui/**'
- 'go.mod'
- 'go.sum'
- 'Makefile'
- '.github/workflows/addon.yml'
tags:
- 'v*'
workflow_dispatch:
env:
BUILD_NAME: strix
jobs:
build:
name: Build Add-on
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
strategy:
matrix:
arch: [aarch64, amd64, armv7]
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.24'
- name: Get version
id: version
run: |
if [[ $GITHUB_REF == refs/tags/* ]]; then
VERSION=${GITHUB_REF#refs/tags/v}
else
VERSION="dev-$(git rev-parse --short HEAD)"
fi
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "Version: $VERSION"
- name: Build binary for ${{ matrix.arch }}
run: |
case "${{ matrix.arch }}" in
aarch64)
GOARCH=arm64
;;
amd64)
GOARCH=amd64
;;
armv7)
GOARCH=arm
GOARM=7
;;
esac
CGO_ENABLED=0 GOOS=linux GOARCH=$GOARCH GOARM=${GOARM:-} go build \
-ldflags="-s -w -X main.Version=${{ steps.version.outputs.version }}" \
-o homeassistant-addon/strix \
cmd/strix/main.go
- name: Copy required files
run: |
cp -r data homeassistant-addon/
cp -r webui homeassistant-addon/
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v5
with:
context: ./homeassistant-addon
file: ./homeassistant-addon/Dockerfile
platforms: linux/${{ matrix.arch == 'aarch64' && 'arm64' || matrix.arch == 'amd64' && 'amd64' || 'arm/v7' }}
push: ${{ github.event_name != 'pull_request' }}
tags: |
ghcr.io/${{ github.repository_owner }}/strix-addon-${{ matrix.arch }}:latest
ghcr.io/${{ github.repository_owner }}/strix-addon-${{ matrix.arch }}:${{ steps.version.outputs.version }}
build-args: |
BUILD_FROM=ghcr.io/home-assistant/${{ matrix.arch }}-base:3.20
STRIX_VERSION=${{ steps.version.outputs.version }}
cache-from: type=gha,scope=${{ matrix.arch }}
cache-to: type=gha,mode=max,scope=${{ matrix.arch }}
update-repository:
name: Update Repository File
needs: build
if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/')
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Get version
id: version
run: |
if [[ $GITHUB_REF == refs/tags/* ]]; then
VERSION=${GITHUB_REF#refs/tags/v}
else
VERSION="dev-$(git rev-parse --short HEAD)"
fi
echo "version=$VERSION" >> $GITHUB_OUTPUT
- name: Update config.yaml version
run: |
sed -i "s/^version:.*/version: \"${{ steps.version.outputs.version }}\"/" homeassistant-addon/config.yaml
- name: Create/Update repository.yaml
run: |
cat > repository.yaml <<EOF
name: Strix Home Assistant Add-ons
url: https://github.com/${{ github.repository }}
maintainer: ${{ github.repository_owner }}
EOF
- name: Commit changes
if: startsWith(github.ref, 'refs/tags/')
run: |
git config --local user.email "github-actions[bot]@users.noreply.github.com"
git config --local user.name "github-actions[bot]"
git add homeassistant-addon/config.yaml repository.yaml
git diff --quiet && git diff --staged --quiet || git commit -m "Update add-on to version ${{ steps.version.outputs.version }}"
- name: Push changes
if: startsWith(github.ref, 'refs/tags/')
uses: ad-m/github-push-action@master
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
branch: main
+23
View File
@@ -5,6 +5,29 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.0.7] - 2025-11-23
### Fixed
- Fixed channel numbering for Hikvision-style cameras (reported by @sergbond_com)
- Removed invalid test data from Hikvision database
- Fixed brand+model search matching in stream discovery
### Added
- Universal `[CHANNEL+1]` placeholder support for flexible channel numbering
- Support for both 0-based (channel=0 → 101) and 1-based (channel=1 → 101) channel selection
- Added 6 high-priority Hikvision patterns to popular stream patterns database
### Changed
- Updated 14 camera brands with universal channel patterns (Hikvision, Hiwatch, Annke, Swann, Abus, 7links, LevelOne, AlienDVR, Oswoo, AV102IP-40, Acvil, TBKVision, Deltaco, Night Owl)
- Hikvision: replaced 10 hardcoded patterns with 6 universal patterns
- Hiwatch: replaced 4 hardcoded patterns with 8 universal patterns (including ISAPI variants)
- Universal patterns now tested first for faster discovery, hardcoded patterns kept as fallback
- Improved stream discovery performance with intelligent pattern ordering
### Technical
- Added support for `[CHANNEL+1]`, `[channel+1]`, `{CHANNEL+1}`, `{channel+1}` placeholders in URL builder
- Modified 16 files: +2448 additions, -1954 deletions
## [0.1.0] - 2025-11-06
### Added
-310
View File
@@ -1,310 +0,0 @@
# 🎉 Home Assistant Add-on для Strix - Готов!
## ✅ Что сделано
Создан полностью функциональный Home Assistant Add-on с автоматизацией через GitHub Actions.
### 📦 Созданные файлы
```
homeassistant-addon/
├── config.yaml # HA конфигурация аддона
├── Dockerfile # Multi-arch Docker сборка
├── build.yaml # Настройки сборки (aarch64/amd64/armv7)
├── run.sh # Стартовый скрипт с интеграцией HA
├── README.md # Документация для пользователей (EN)
├── README-RU.md # Документация для пользователей (RU)
├── DOCS.md # Подробная инструкция по использованию
├── CHANGELOG.md # История изменений
├── INSTALLATION.md # Гайд по установке и публикации
├── icon.png.todo # Заметка - нужна иконка 128x128px
└── logo.png.todo # Заметка - нужен логотип 256x256px
.github/workflows/
└── addon.yml # GitHub Actions для автосборки
repository.yaml # Файл репозитория для HA
README.md (обновлен) # Добавлена секция про HA Add-on
```
## 🚀 Как это работает
### Автоматизация через GitHub Actions
При пуше в `main` или создании тега `v*`:
1. **Сборка для каждой архитектуры** (aarch64, amd64, armv7):
- Компиляция Go бинарника
- Копирование data и webui
- Создание Docker образа
- Публикация в GitHub Container Registry (ghcr.io)
2. **Обновление репозитория**:
- Автоматическое обновление версии в config.yaml
- Обновление repository.yaml
- Коммит изменений (только для тегов)
### Docker образы
Публикуются автоматически:
- `ghcr.io/eduard256/strix-addon-aarch64:latest`
- `ghcr.io/eduard256/strix-addon-amd64:latest`
- `ghcr.io/eduard256/strix-addon-armv7:latest`
С версионированием:
- `ghcr.io/eduard256/strix-addon-aarch64:1.0.0`
- и т.д.
## 🎯 Что дальше
### 1. Добавить иконки (опционально, но рекомендуется)
```bash
# Создать или добавить:
homeassistant-addon/icon.png # 128x128px
homeassistant-addon/logo.png # 256x256px
# Стиль: сова или камера, синий/белый (Home Assistant theme)
```
### 2. Первый релиз
```bash
# Закоммитить все изменения
git add .
git commit -m "Add Home Assistant Add-on v1.0.0"
git push origin main
# Создать тег релиза
git tag v1.0.0
git push origin v1.0.0
```
### 3. Дождаться сборки
GitHub Actions автоматически:
- Соберет все архитектуры
- Создаст Docker образы
- Опубликует в ghcr.io
Проверить статус: https://github.com/eduard256/Strix/actions
### 4. Установить в Home Assistant
#### Для пользователей:
1. **Supervisor****Add-on Store****⋮** (меню) → **Repositories**
2. Добавить репозиторий: `https://github.com/eduard256/Strix`
3. Найти **Strix Camera Discovery** в списке аддонов
4. Нажать **Install**
5. Настроить (если нужно)
6. Нажать **Start**
7. Нажать **Open Web UI**
## 🎨 Особенности реализации
### 1. Максимальное использование существующей инфраструктуры
- ✅ Использует существующий Dockerfile как основу
- ✅ Переиспользует Docker workflow
- ✅ Встраивается в существующий CI/CD пайплайн
- ✅ Не требует дублирования кода
### 2. Автоматизация
- ✅ Автоматическая сборка при пуше/теге
- ✅ Multi-arch сборка (3 архитектуры параллельно)
- ✅ Автоматическая публикация образов
- ✅ Автоматическое обновление версий
### 3. Интеграция с Home Assistant
- ✅ Ingress поддержка (встроенный iframe)
- ✅ Иконка в боковой панели
- ✅ Настройка через HA UI
- ✅ Логирование в HA
- ✅ Health check мониторинг
- ✅ Auto-start при загрузке
### 4. Безопасность
- ✅ Non-root пользователь (UID 1000)
- ✅ Минимальный Alpine образ
- ✅ Credentials не сохраняются
- ✅ Работа только в локальной сети
- ✅ Health check для мониторинга
## ⚙️ Конфигурация для пользователей
```yaml
log_level: info # debug, info, warn, error
port: 4567 # Порт веб-интерфейса (default: 4567)
strict_validation: true # Строгая валидация потоков
```
## 📊 Возможности аддона
- 🔍 **3,600+ камер** в базе данных
- 🌐 **ONVIF discovery** - автопоиск камер
-**Real-time SSE** - живые обновления
- 🎨 **WebUI** - красивый интерфейс
- 🔌 **RESTful API** - для автоматизации
- 🚀 **Fast concurrent testing** - параллельная проверка
- 📦 **All-in-one** - всё включено
## 🔄 Обновления
### Разработка (dev builds)
```bash
git add .
git commit -m "feat: new feature"
git push origin main
```
Создаст dev-сборку с тегом `dev-<git-hash>`.
### Релизы (production)
```bash
# 1. Обновить версию
sed -i 's/^version:.*/version: "1.1.0"/' homeassistant-addon/config.yaml
# 2. Обновить CHANGELOG.md
nano homeassistant-addon/CHANGELOG.md
# 3. Закоммитить и создать тег
git add .
git commit -m "release: v1.1.0"
git tag v1.1.0
git push origin main v1.1.0
```
Создаст релиз-сборку с тегом `1.1.0`.
## 📚 Документация
| Файл | Описание |
|------|----------|
| `README.md` | Краткое описание для пользователей (EN) |
| `README-RU.md` | Краткое описание для пользователей (RU) |
| `DOCS.md` | Полная документация по использованию |
| `CHANGELOG.md` | История версий и изменений |
| `INSTALLATION.md` | Инструкция для разработчиков/публикации |
## 🐛 Troubleshooting
### Сборка не прошла
1. Проверить GitHub Actions: https://github.com/eduard256/Strix/actions
2. Проверить логи ошибок
3. Частые проблемы:
- Ошибки компиляции Go → исправить в коде
- Ошибки Docker → проверить Dockerfile
- Права доступа → проверить GITHUB_TOKEN permissions
### Аддон не устанавливается
1. Проверить repository.yaml в корне репозитория
2. Убедиться что config.yaml валиден (YAML syntax)
3. Проверить что Docker образы опубликованы в ghcr.io
4. Проверить что URL репозитория правильный
### Аддон не запускается
1. Открыть логи в HA: **Addon page****Log** tab
2. Частые проблемы:
- Порт 4567 занят → изменить port в настройках
- Отсутствуют файлы data → проверить сборку
- Permission denied → проверить права в Dockerfile
## ✅ Чеклист перед первым релизом
- [x] Создана структура аддона
- [x] Настроен Dockerfile
- [x] Настроен GitHub Actions workflow
- [x] Создана документация (EN)
- [x] Создана документация (RU)
- [x] Обновлен основной README
- [x] Создан repository.yaml
- [ ] Добавлены иконки (icon.png, logo.png)
- [ ] Протестирована локальная сборка
- [ ] Создан git tag v1.0.0
- [ ] Проверена публикация в ghcr.io
- [ ] Протестирована установка в HA
## 🎁 Дополнительные возможности (будущее)
Можно добавить в будущих версиях:
- ✨ Автоматическое добавление камер как entities в HA
- 🔧 Генератор конфигов для go2rtc
- 📹 Генератор конфигов для Frigate
- 🔔 ONVIF события и уведомления
- 📸 Галерея снимков камер
- 🔍 Сетевой сканер для массового поиска
## 🤝 Распространение
### Вариант 1: Кастомный репозиторий (рекомендуется)
Пользователи добавляют репозиторий вручную.
**Преимущества:**
- Полный контроль
- Быстрые обновления
- Нет процесса одобрения
**Недостатки:**
- Нужно добавлять вручную
- Не в официальном store
### Вариант 2: Home Assistant Community Add-ons
Подать заявку в официальный репозиторий:
https://github.com/home-assistant/addons
**Преимущества:**
- Официальное признание
- Легче найти пользователям
- Автообновления
**Недостатки:**
- Строгие требования
- Процесс ревью
- Медленные обновления
## 🎉 Готово к использованию!
Всё готово для первого релиза. После добавления иконок и создания тега v1.0.0, аддон будет полностью готов к работе.
### Команды для быстрого старта:
```bash
# 1. Добавить иконки (опционально)
# Поместить icon.png и logo.png в homeassistant-addon/
# 2. Закоммитить
git add .
git commit -m "Add Home Assistant Add-on v1.0.0"
git push origin main
# 3. Создать релиз
git tag v1.0.0
git push origin v1.0.0
# 4. Дождаться окончания GitHub Actions
# https://github.com/eduard256/Strix/actions
# 5. Установить в Home Assistant
# Добавить репозиторий: https://github.com/eduard256/Strix
```
---
**Поделиться с сообществом:**
- 💬 Home Assistant Community Forum
- 🔴 Reddit r/homeassistant
- 💭 GitHub Discussions
- 💬 Discord серверы
+509 -187
View File
@@ -1,234 +1,556 @@
# 🦉 Strix - Smart IP Camera Stream Discovery System
![Strix Demo](assets/main.gif?v=2)
[![Go Version](https://img.shields.io/badge/Go-1.21+-00ADD8?style=flat&logo=go)](https://go.dev/)
# Strix
[![GitHub Stars](https://img.shields.io/github/stars/eduard256/strix?style=social)](https://github.com/eduard256/strix/stargazers)
[![License](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
[![API Version](https://img.shields.io/badge/API-v1-green.svg)](https://github.com/eduard256/Strix)
[![Docker Pulls](https://img.shields.io/docker/pulls/eduard256/strix)](https://hub.docker.com/r/eduard256/strix)
Strix is an intelligent IP camera stream discovery system that acts as a bridge between users and streaming servers like go2rtc. It automatically discovers and validates camera streams, eliminating the need for manual URL configuration.
## Spent 2 years googling URL for your Chinese camera?
## 🎯 Features
**Strix finds working streams automatically. In 30 seconds.**
- **Intelligent Camera Search**: Fuzzy search across 3,600+ camera models
- **Automatic Stream Discovery**: ONVIF, database patterns, and popular URL detection
- **Real-time Updates**: Server-Sent Events (SSE) for live discovery progress
- **Universal Protocol Support**: RTSP, HTTP, MJPEG, JPEG snapshots, and more
- **Smart URL Building**: Automatic placeholder replacement and authentication handling
- **Concurrent Testing**: Fast parallel stream validation with ffprobe
- **Memory Efficient**: Streaming JSON parsing for large camera databases
- **API-First Design**: RESTful API with comprehensive documentation
- **67,288** camera models
- **3,636** brands (from Hikvision to AliExpress no-name)
- **102,787** URL patterns (RTSP, HTTP, MJPEG, JPEG, BUBBLE)
## 🚀 Quick Start
![Demo](assets/main.gif)
### Home Assistant Add-on (Recommended)
---
The easiest way to use Strix is as a Home Assistant Add-on:
## Your Problem?
1. Add this repository to your Home Assistant:
- Go to **Supervisor****Add-on Store**
- Click **⋮** (menu) → **Repositories**
- Add: `https://github.com/eduard256/Strix`
- ❌ Bought ZOSI NVR, zero documentation
- ❌ Camera has no RTSP, only weird JPEG snapshots
- ❌ Frigate eating 70% CPU
- ❌ Config breaks after adding each camera
- ❌ Don't understand Frigate syntax
2. Install the **Strix Camera Discovery** add-on
## Solution
3. Start the add-on and open the Web UI
-**Auto-discovery** - tests 102,787 URL variations in parallel
-**Any protocol** - No RTSP? Finds HTTP MJPEG
-**Config generation** - ready Frigate.yml in 2 minutes
-**Sub/Main streams** - CPU from 30% → 8%
-**Smart merging** - adds camera to existing config with 500+ cameras
4. Start discovering your cameras!
---
For detailed installation instructions, see [Home Assistant Add-on Documentation](homeassistant-addon/DOCS.md).
## 🚀 Installation (One Command)
### Docker
### Ubuntu / Debian
```bash
# Using Docker Compose (recommended)
docker-compose up -d
# Or using Docker directly
docker run -d \
--name strix \
-p 4567:4567 \
eduard256/strix:latest
# Access at http://localhost:4567
sudo apt update && command -v docker >/dev/null 2>&1 || curl -fsSL https://get.docker.com | sudo sh && docker run -d --name strix -p 4567:4567 --restart unless-stopped eduard256/strix:latest
```
See [Docker documentation](DOCKER.md) for more options.
Open **http://YOUR_SERVER_IP:4567**
### Build from Source
Prerequisites:
- Go 1.21 or higher
- ffprobe (optional, for enhanced stream validation)
### Docker Compose
```bash
# Clone the repository
git clone https://github.com/eduard256/Strix
cd strix
# Install dependencies
make deps
# Build the application
make build
# Run the application
make run
# The server will start on http://localhost:4567
# Open your browser and navigate to http://localhost:4567
sudo apt update && command -v docker >/dev/null 2>&1 || curl -fsSL https://get.docker.com | sudo sh && command -v docker-compose >/dev/null 2>&1 || { sudo curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose && sudo chmod +x /usr/local/bin/docker-compose; } && curl -fsSL https://raw.githubusercontent.com/eduard256/Strix/main/docker-compose.yml -o docker-compose.yml && docker-compose up -d
```
## 📡 API Endpoints
### Home Assistant Add-on (Beta)
⚠️ **Status:** Experimental (SSE has bugs, Docker recommended)
**Installation:**
1. Go to **Settings****Add-ons****Add-on Store**
2. Click **⋮** (top right) → **Repositories**
3. Add: `https://github.com/eduard256/hassio-strix`
4. Find **"Strix"** in store
5. Click **Install**
6. Enable **"Start on boot"** and **"Show in sidebar"**
7. Click **Start**
**Known Issues:**
- Real-time progress may not display (Ingress SSE limitation)
- Use Docker installation for better experience
---
## How to Use
### Step 1: Open Web Interface
```
http://YOUR_SERVER_IP:4567
```
### Step 2: Enter Camera Details
- **IP Address**: `192.168.1.100`
- **Username**: `admin` (if required)
- **Password**: your camera password
- **Model**: optional, improves accuracy
### Step 3: Discover Streams
Click **"Discover Streams"**
Watch real-time progress:
- Which URL is being tested
- How many tested
- Found streams appear instantly
Wait 30-60 seconds.
### Step 4: Choose Stream
Strix shows details for each stream:
| Stream | Details |
|--------|---------|
| **Protocol** | RTSP, HTTP, MJPEG, JPEG |
| **Resolution** | 1920x1080, 640x480 |
| **FPS** | 25, 15, 10 |
| **Codec** | H264, H265, MJPEG |
| **Audio** | Yes / No |
### Step 5: Generate Frigate Config
Click **"Use Stream"** → **"Generate Frigate Config"**
You get ready config:
```yaml
go2rtc:
streams:
'192_168_1_100_main':
- http://admin:pass@192.168.1.100:8000/video.mjpg
'192_168_1_100_sub':
- http://admin:pass@192.168.1.100:8000/video2.mjpg
cameras:
camera_192_168_1_100:
ffmpeg:
inputs:
- path: rtsp://127.0.0.1:8554/192_168_1_100_sub
roles: [detect] # CPU 8% instead of 70%
- path: rtsp://127.0.0.1:8554/192_168_1_100_main
roles: [record] # HD recording
objects:
track: [person, car, cat, dog]
record:
enabled: true
```
**Smart Merging:**
- Paste your existing `frigate.yml` with 500 cameras
- Strix adds camera #501 correctly
- Doesn't break structure
- Preserves all settings
### Step 6: Add to Frigate
Copy config → Paste to `frigate.yml` → Restart Frigate
**Done!**
---
## Features
### Exotic Camera Support
90% of Chinese cameras don't have RTSP. Strix supports everything:
- **HTTP MJPEG** - most old cameras
- **JPEG snapshots** - auto-converted to stream via FFmpeg
- **RTSP** - if available
- **HTTP-FLV** - some Chinese brands
- **BUBBLE** - proprietary Chinese NVR/DVR protocol
- **ONVIF** - auto-discovery
### Camera Database
**67,288 models from 3,636 brands:**
- **Known brands**: Hikvision, Dahua, Axis, Foscam, TP-Link
- **Chinese no-names**: ZOSI, Escam, Sricam, Wanscam, Besder
- **AliExpress junk**: cameras without name, OEM models
- **Old systems**: NVR/DVR with proprietary protocols
### Discovery Methods
Strix tries all methods in parallel:
**1. ONVIF** (30% success rate)
- Asks camera directly for stream URLs
- Works for ONVIF-compatible cameras
**2. Database Lookup** (60% success rate)
- 67,288 models with known working URLs
- Brand and model-specific patterns
**3. Popular Patterns** (90% success rate)
- 206 most common URL paths
- Works even for unknown cameras
**Result: Finds stream for 95% of cameras**
### Frigate Config Generation
**What you get:**
**Main/Sub streams**
- Main (HD) for recording
- Sub (low res) for object detection
- CPU usage reduced 5-10x
**Ready go2rtc config**
- Stream multiplexing
- Protocol conversion
- JPEG → RTSP via FFmpeg
**Smart config merging**
- Add to existing config
- Preserve structure
- No manual YAML editing
**Pre-configured detection**
- person, car, cat, dog
- Ready motion recording
- 7 days retention
### Speed
- Tests **20 URLs in parallel**
- Average discovery time: **30-60 seconds**
- Complex cameras: **2-3 minutes**
- Real-time progress updates via SSE
---
## Advanced Configuration
### Docker Environment Variables
```yaml
environment:
- STRIX_API_LISTEN=:8080 # Custom port
- STRIX_LOG_LEVEL=debug # Detailed logs
- STRIX_LOG_FORMAT=json # JSON logging
```
### Config File
Create `strix.yaml`:
```yaml
api:
listen: ":8080"
```
Example: [strix.yaml.example](strix.yaml.example)
### Discovery Parameters
In web UI under **Advanced**:
- **Channel** - for NVR systems (usually 0)
- **Timeout** - max discovery time (default: 240s)
- **Max Streams** - stop after N streams (default: 10)
---
## FAQ
### No streams found?
**Check network:**
```bash
ping 192.168.1.100
```
Camera must be reachable.
**Verify credentials:**
- Username/password correct?
- Try without credentials (some cameras are open)
**Try without model:**
- Strix will run ONVIF + 206 popular patterns
- Works for cameras not in database
### Camera not in database?
**No problem.**
Strix will still find stream via:
1. ONVIF (if supported)
2. 206 popular URL patterns
3. Common ports and paths
4. HTTP MJPEG on various ports
5. JPEG snapshot endpoints
**Help the project:**
- Found working stream? [Create Issue](https://github.com/eduard256/Strix/issues)
- Share model and URL
- We'll add to database
### Found only JPEG snapshots?
**Normal for old cameras.**
Strix auto-converts JPEG to stream via FFmpeg:
```yaml
go2rtc:
streams:
camera_main:
- exec:ffmpeg -loop 1 -framerate 10 -i http://192.168.1.100/snapshot.jpg -c:v libx264 -f rtsp {output}
```
Frigate gets normal 10 FPS stream.
### Stream found but doesn't work in Frigate?
**Try another stream:**
- Strix usually finds 3-10 variants
- Some may need special FFmpeg parameters
**Use sub stream:**
- For object detection
- Less CPU load
- Better performance
### How does config generation work?
**For new config:**
- Strix creates complete `frigate.yml` from scratch
- Includes go2rtc, camera, object detection
**For existing config:**
- Paste your current `frigate.yml`
- Strix adds new camera
- Preserves all existing cameras
- Doesn't break structure
**Main/Sub streams:**
- Main (HD) - for recording
- Sub (low res) - for detection
- CPU savings 5-10x
### Is it safe to enter passwords?
**Yes.**
- Strix runs locally on your network
- Nothing sent to external servers
- Passwords not saved
- Open source - check the code yourself
### Works offline?
**Yes.**
- Database embedded in Docker image
- Internet only needed to download image
- Runs offline after that
---
## API Reference
REST API available for automation:
### Health Check
```bash
GET /api/v1/health
```
### Camera Search
### Search Cameras
```bash
POST /api/v1/cameras/search
{
"query": "zosi zg23213m",
"query": "hikvision",
"limit": 10
}
```
### Stream Discovery (SSE)
### Discover Streams (SSE)
```bash
POST /api/v1/streams/discover
{
"target": "192.168.1.100", # IP or stream URL
"model": "zosi zg23213m", # Optional camera model
"username": "admin", # Optional
"password": "password", # Optional
"timeout": 240, # Seconds (default: 240)
"max_streams": 10, # Maximum streams to find
"channel": 0 # For NVR systems
"target": "192.168.1.100",
"username": "admin",
"password": "12345",
"model": "DS-2CD2xxx",
"timeout": 240,
"max_streams": 10
}
```
## 🔍 How It Works
Returns Server-Sent Events with real-time progress.
1. **Camera Search**: Intelligent fuzzy matching across brand and model database
2. **URL Collection**: Combines ONVIF discovery, model-specific patterns, and popular URLs
3. **Stream Validation**: Concurrent testing using ffprobe and HTTP requests
4. **Real-time Updates**: SSE streams provide instant feedback on discovered streams
5. **Smart Filtering**: Deduplicates URLs and prioritizes working streams
## 📁 Project Structure
```
strix/
├── cmd/strix/ # Application entry point
├── internal/ # Private application code
│ ├── api/ # HTTP handlers and routing
│ ├── camera/ # Camera database and discovery
│ │ ├── database/ # Database loading and search
│ │ ├── discovery/ # ONVIF and stream discovery
│ │ └── stream/ # URL building and validation
│ ├── config/ # Configuration management
│ └── models/ # Data structures
├── pkg/ # Public packages
│ └── sse/ # Server-Sent Events
├── data/ # Camera database (3,600+ models)
│ ├── brands/ # Brand-specific JSON files
│ ├── popular_stream_patterns.json
│ └── query_parameters.json
└── go.mod
```
## 🛠️ Configuration
Strix can be configured via `strix.yaml` file or environment variables.
### Configuration File (strix.yaml)
Create a `strix.yaml` file in the same directory as the binary:
```yaml
# API Server Configuration
api:
listen: ":4567" # Format: ":port" or "host:port"
```
Examples:
```yaml
api:
listen: ":4567" # All interfaces, port 4567 (default)
# listen: "127.0.0.1:4567" # Localhost only
# listen: ":8080" # Custom port
```
### Environment Variables
Environment variables override config file values:
```bash
STRIX_API_LISTEN=":4567" # Server listen address (overrides strix.yaml)
STRIX_LOG_LEVEL=info # Log level: debug, info, warn, error
STRIX_LOG_FORMAT=json # Log format: json, text
```
### Configuration Priority
1. **Environment variable** `STRIX_API_LISTEN` (highest priority)
2. **Config file** `strix.yaml`
3. **Default value** `:4567` (lowest priority)
### Quick Start with Custom Port
```bash
# Using environment variable
STRIX_API_LISTEN=":8080" ./strix
# Or using config file
cp strix.yaml.example strix.yaml
# Edit strix.yaml, then:
./strix
```
## 📊 Camera Database
The system includes a comprehensive database of camera models:
- **3,600+ camera brands**
- **150+ popular stream patterns**
- **258 query parameter variations**
- **Automatic placeholder replacement**
## 🔧 Development
```bash
# Run tests
make test
# Format code
make fmt
# Run linter
make lint
# Build for all platforms
make build-all
# Development mode with live reload
make dev
```
## 📄 License
This project is licensed under the MIT License - see the LICENSE file for details.
## 🙏 Acknowledgments
- Camera database sourced from ispyconnect.com
- Inspired by go2rtc project
- Built with Go and Chi router
**Full API documentation:** [DOCKER.md](DOCKER.md)
---
Made with ❤️ for the home automation community
## Technical Details
### Architecture
- **Language:** Go 1.24
- **Database:** 3,636 JSON files
- **Image size:** 80-90 MB (Alpine Linux)
- **Dependencies:** FFmpeg/FFprobe for validation
- **Concurrency:** Worker pool (20 parallel tests)
- **Real-time:** Server-Sent Events (SSE)
### Build from Source
```bash
git clone https://github.com/eduard256/Strix
cd Strix
make build
./bin/strix
```
**Requirements:**
- Go 1.21+
- FFprobe (optional, for stream validation)
### Docker Platforms
- linux/amd64
- linux/arm64
Auto-built and published to Docker Hub on every push to `main`.
---
## Use Cases
### Home Automation
- Add cheap cameras to Home Assistant
- Integrate with Frigate NVR
- Object detection with low CPU
- Motion recording
### Security Systems
- Discover streams in old NVR systems
- Find backup cameras without docs
- Migrate from proprietary DVR to Frigate
- Reduce hardware requirements
### IP Camera Testing
- Test cameras before deployment
- Verify stream quality
- Find optimal resolution/FPS
- Check codec compatibility
---
## Troubleshooting
### Frigate still eating CPU?
**Use sub stream:**
1. Find both main and sub streams with Strix
2. Generate config with both
3. Sub for detect, main for record
4. CPU drops 5-10x
**Example:**
```yaml
inputs:
- path: rtsp://127.0.0.1:8554/camera_sub # 640x480 for detect
roles: [detect]
- path: rtsp://127.0.0.1:8554/camera_main # 1920x1080 for record
roles: [record]
```
### Can't find specific stream quality?
**In web UI:**
- Strix shows all found streams
- Filter by resolution
- Choose optimal FPS
- Select codec (H264 recommended for Frigate)
### Stream works but no audio in Frigate?
**Check Strix stream details:**
- "Has Audio" field shows if audio present
- Some cameras have video-only streams
- Try different stream URL from Strix results
### Discovery takes too long?
**Reduce search scope:**
- Specify exact camera model (faster database lookup)
- Lower "Max Streams" (stops after N found)
- Reduce timeout (default 240s)
**In Advanced settings:**
```
Max Streams: 5 (instead of 10)
Timeout: 120 (instead of 240)
```
---
## Contributing
### Add Your Camera
Found working stream for camera not in database?
1. [Create Issue](https://github.com/eduard256/Strix/issues)
2. Provide:
- Camera brand and model
- Working URL pattern
- Protocol (RTSP/HTTP/etc)
3. We'll add to database
### Report Bugs
- [GitHub Issues](https://github.com/eduard256/Strix/issues)
- Include logs (set `STRIX_LOG_LEVEL=debug`)
- Camera model and IP (if possible)
### Feature Requests
- [GitHub Discussions](https://github.com/eduard256/Strix/discussions)
- Describe use case
- Explain expected behavior
---
## Credits
- **Camera database:** [ispyconnect.com](https://www.ispyconnect.com)
- **Inspiration:** [go2rtc](https://github.com/AlexxIT/go2rtc) by AlexxIT
- **Community:** Home Assistant, Frigate NVR users
---
## License
MIT License - use commercially, modify, distribute freely.
See [LICENSE](LICENSE) file for details.
---
## Support
- **Issues:** [GitHub Issues](https://github.com/eduard256/Strix/issues)
- **Discussions:** [GitHub Discussions](https://github.com/eduard256/Strix/discussions)
- **Docker:** [Docker Hub](https://hub.docker.com/r/eduard256/strix)
---
**Made for people tired of cameras without documentation**
*Tested on Chinese AliExpress junk that finally works now.*
+1 -1
View File
@@ -20,7 +20,7 @@ import (
const (
// Version is the application version
Version = "1.0.0"
Version = "1.0.7"
// Banner is the application banner
Banner = `
+64 -28
View File
@@ -4,6 +4,42 @@
"last_updated": "2025-10-17",
"source": "ispyconnect.com",
"entries": [
{
"models": [
"ALL"
],
"type": "FFMPEG",
"protocol": "rtsp",
"port": 8554,
"url": "/Streaming/Channels/[CHANNEL+1]01"
},
{
"models": [
"ALL"
],
"type": "FFMPEG",
"protocol": "rtsp",
"port": 8554,
"url": "/Streaming/Channels/[CHANNEL]01"
},
{
"models": [
"ALL"
],
"type": "FFMPEG",
"protocol": "rtsp",
"port": 8554,
"url": "/Streaming/Channels/[CHANNEL+1]02"
},
{
"models": [
"ALL"
],
"type": "FFMPEG",
"protocol": "rtsp",
"port": 8554,
"url": "/Streaming/Channels/[CHANNEL]02"
},
{
"models": [
"3628-675",
@@ -313,15 +349,6 @@
"port": 0,
"url": ""
},
{
"models": [
"IPC-300"
],
"type": "FFMPEG",
"protocol": "rtsp",
"port": 8554,
"url": "/Streaming/Channels/101"
},
{
"models": [
"IPC-340HD",
@@ -465,15 +492,6 @@
"port": 0,
"url": "snapshot.jpg"
},
{
"models": [
"IPC-740"
],
"type": "FFMPEG",
"protocol": "rtsp",
"port": 8554,
"url": "/Streaming/Channels/102"
},
{
"models": [
"IP-CAM",
@@ -631,16 +649,6 @@
"port": 80,
"url": "/videostream.asf?user=[USERNAME]&pwd=[PASSWORD]&resolution=320x240"
},
{
"models": [
"PX3615",
"SK7008-T1F1"
],
"type": "FFMPEG",
"protocol": "rtsp",
"port": 554,
"url": "/Streaming/channels/401"
},
{
"models": [
"PX-3615-675"
@@ -722,6 +730,34 @@
"protocol": "http",
"port": 82,
"url": "/cgi/mjpg/mjpg.cgi"
},
{
"models": [
"IPC-300"
],
"type": "FFMPEG",
"protocol": "rtsp",
"port": 8554,
"url": "/Streaming/Channels/101"
},
{
"models": [
"IPC-740"
],
"type": "FFMPEG",
"protocol": "rtsp",
"port": 8554,
"url": "/Streaming/Channels/102"
},
{
"models": [
"PX3615",
"SK7008-T1F1"
],
"type": "FFMPEG",
"protocol": "rtsp",
"port": 554,
"url": "/Streaming/channels/401"
}
]
}
+61 -25
View File
@@ -4,6 +4,42 @@
"last_updated": "2025-10-17",
"source": "ispyconnect.com",
"entries": [
{
"models": [
"ALL"
],
"type": "FFMPEG",
"protocol": "rtsp",
"port": 554,
"url": "/Streaming/Channels/[CHANNEL+1]01"
},
{
"models": [
"ALL"
],
"type": "FFMPEG",
"protocol": "rtsp",
"port": 554,
"url": "/Streaming/Channels/[CHANNEL]01"
},
{
"models": [
"ALL"
],
"type": "FFMPEG",
"protocol": "rtsp",
"port": 554,
"url": "/Streaming/Channels/[CHANNEL+1]02"
},
{
"models": [
"ALL"
],
"type": "FFMPEG",
"protocol": "rtsp",
"port": 554,
"url": "/Streaming/Channels/[CHANNEL]02"
},
{
"models": [
"10550",
@@ -320,31 +356,6 @@
"port": 554,
"url": "/s2"
},
{
"models": [
"IPCA53000",
"IPCB42510B",
"IPCB44510A",
"IPCB64515B"
],
"type": "FFMPEG",
"protocol": "rtsp",
"port": 554,
"url": "/Streaming/Channels/102"
},
{
"models": [
"IPCB42550",
"IPCB78520",
"NVR10030",
"TVIP41500",
"TVIP52500"
],
"type": "FFMPEG",
"protocol": "rtsp",
"port": 0,
"url": "/Streaming/Channels/101"
},
{
"models": [
"IPCB54611B",
@@ -635,6 +646,31 @@
"protocol": "rtsp",
"port": 554,
"url": "/mpeg4/media.amp?resolution=640x480"
},
{
"models": [
"IPCA53000",
"IPCB42510B",
"IPCB44510A",
"IPCB64515B"
],
"type": "FFMPEG",
"protocol": "rtsp",
"port": 554,
"url": "/Streaming/Channels/102"
},
{
"models": [
"IPCB42550",
"IPCB78520",
"NVR10030",
"TVIP41500",
"TVIP52500"
],
"type": "FFMPEG",
"protocol": "rtsp",
"port": 0,
"url": "/Streaming/Channels/101"
}
]
}
+18
View File
@@ -4,6 +4,24 @@
"last_updated": "2025-10-17",
"source": "ispyconnect.com",
"entries": [
{
"models": [
"ALL"
],
"type": "FFMPEG",
"protocol": "rtsp",
"port": 0,
"url": "/Streaming/Channels/[CHANNEL+1]02"
},
{
"models": [
"ALL"
],
"type": "FFMPEG",
"protocol": "rtsp",
"port": 0,
"url": "/Streaming/Channels/[CHANNEL]02"
},
{
"models": [
"WIFI-5MP-30"
+18
View File
@@ -4,6 +4,24 @@
"last_updated": "2025-10-17",
"source": "ispyconnect.com",
"entries": [
{
"models": [
"ALL"
],
"type": "FFMPEG",
"protocol": "rtsp",
"port": 0,
"url": "/Streaming/Channels/[CHANNEL+1]01"
},
{
"models": [
"ALL"
],
"type": "FFMPEG",
"protocol": "rtsp",
"port": 0,
"url": "/Streaming/Channels/[CHANNEL]01"
},
{
"models": [
"mega216"
+127 -91
View File
@@ -4,6 +4,42 @@
"last_updated": "2025-10-17",
"source": "ispyconnect.com",
"entries": [
{
"models": [
"ALL"
],
"type": "FFMPEG",
"protocol": "rtsp",
"port": 0,
"url": "/Streaming/Channels/[CHANNEL+1]01"
},
{
"models": [
"ALL"
],
"type": "FFMPEG",
"protocol": "rtsp",
"port": 0,
"url": "/Streaming/Channels/[CHANNEL]01"
},
{
"models": [
"ALL"
],
"type": "FFMPEG",
"protocol": "rtsp",
"port": 0,
"url": "/Streaming/Channels/[CHANNEL+1]02"
},
{
"models": [
"ALL"
],
"type": "FFMPEG",
"protocol": "rtsp",
"port": 0,
"url": "/Streaming/Channels/[CHANNEL]02"
},
{
"models": [
"NVR",
@@ -220,55 +256,6 @@
"port": 0,
"url": "snapshot.jpg?user=[USERNAME]&pwd=[PASSWORD]&strm=[CHANNEL]"
},
{
"models": [
"141CS",
"151DB",
"151de",
"151dj",
"151DM",
"191BS",
"2MP",
"4MP Bullet",
"4MP DOME",
"720P",
"AC500",
"AK-N48PIA0-68DT",
"c500",
"C800",
"DE81GB",
"DN41R",
"DN81R",
"DVR",
"DW81KD",
"i15dx",
"i51dm",
"I51DS",
"I51DX",
"I61BK",
"I61DR",
"I61FC",
"I61G",
"I91BD",
"I91BF",
"I91BM",
"I91F",
"l51DM",
"N481Y",
"N48PI",
"NC400",
"NC800",
"NCPT500",
"Other",
"P01",
"POE",
"VIEW"
],
"type": "FFMPEG",
"protocol": "rtsp",
"port": 0,
"url": "/Streaming/Channels/101"
},
{
"models": [
"141CS",
@@ -498,39 +485,6 @@
"port": 554,
"url": "/onvif2"
},
{
"models": [
"191BS",
"AC500",
"c800",
"C800-4k",
"I51DX",
"I91BF",
"NC800"
],
"type": "FFMPEG",
"protocol": "rtsp",
"port": 554,
"url": "/Streaming/Channels/102"
},
{
"models": [
"191df"
],
"type": "FFMPEG",
"protocol": "rtsp",
"port": 554,
"url": "/Streaming/channels/102"
},
{
"models": [
"191df"
],
"type": "FFMPEG",
"protocol": "rtsp",
"port": 554,
"url": "/Streaming/channels/101"
},
{
"models": [
"2MP",
@@ -659,15 +613,6 @@
"port": 80,
"url": "/cgi-bin/snapshot.cgi?chn=4&u=[USERNAME]&p=[PASSWORD]"
},
{
"models": [
"DVR"
],
"type": "FFMPEG",
"protocol": "rtsp",
"port": 0,
"url": "/Streaming/Channels/201"
},
{
"models": [
"h264",
@@ -851,6 +796,97 @@
"protocol": "rtsp",
"port": 0,
"url": "/h264/ch1/main/av_stream"
},
{
"models": [
"141CS",
"151DB",
"151de",
"151dj",
"151DM",
"191BS",
"2MP",
"4MP Bullet",
"4MP DOME",
"720P",
"AC500",
"AK-N48PIA0-68DT",
"c500",
"C800",
"DE81GB",
"DN41R",
"DN81R",
"DVR",
"DW81KD",
"i15dx",
"i51dm",
"I51DS",
"I51DX",
"I61BK",
"I61DR",
"I61FC",
"I61G",
"I91BD",
"I91BF",
"I91BM",
"I91F",
"l51DM",
"N481Y",
"N48PI",
"NC400",
"NC800",
"NCPT500",
"Other",
"P01",
"POE",
"VIEW"
],
"type": "FFMPEG",
"protocol": "rtsp",
"port": 0,
"url": "/Streaming/Channels/101"
},
{
"models": [
"191BS",
"AC500",
"c800",
"C800-4k",
"I51DX",
"I91BF",
"NC800"
],
"type": "FFMPEG",
"protocol": "rtsp",
"port": 554,
"url": "/Streaming/Channels/102"
},
{
"models": [
"191df"
],
"type": "FFMPEG",
"protocol": "rtsp",
"port": 554,
"url": "/Streaming/channels/102"
},
{
"models": [
"191df"
],
"type": "FFMPEG",
"protocol": "rtsp",
"port": 554,
"url": "/Streaming/channels/101"
},
{
"models": [
"DVR"
],
"type": "FFMPEG",
"protocol": "rtsp",
"port": 0,
"url": "/Streaming/Channels/201"
}
]
}
+18
View File
@@ -4,6 +4,24 @@
"last_updated": "2025-10-17",
"source": "ispyconnect.com",
"entries": [
{
"models": [
"ALL"
],
"type": "FFMPEG",
"protocol": "rtsp",
"port": 0,
"url": "/Streaming/Channels/[CHANNEL+1]01"
},
{
"models": [
"ALL"
],
"type": "FFMPEG",
"protocol": "rtsp",
"port": 0,
"url": "/Streaming/Channels/[CHANNEL]01"
},
{
"models": [
"Other"
+36
View File
@@ -4,6 +4,42 @@
"last_updated": "2025-10-17",
"source": "ispyconnect.com",
"entries": [
{
"models": [
"ALL"
],
"type": "FFMPEG",
"protocol": "rtsp",
"port": 8554,
"url": "/Streaming/Channels/[CHANNEL+1]01"
},
{
"models": [
"ALL"
],
"type": "FFMPEG",
"protocol": "rtsp",
"port": 8554,
"url": "/Streaming/Channels/[CHANNEL]01"
},
{
"models": [
"ALL"
],
"type": "FFMPEG",
"protocol": "rtsp",
"port": 8554,
"url": "/Streaming/Channels/[CHANNEL+1]02"
},
{
"models": [
"ALL"
],
"type": "FFMPEG",
"protocol": "rtsp",
"port": 8554,
"url": "/Streaming/Channels/[CHANNEL]02"
},
{
"models": [
"Outdoor Smart Home Camera",
+1097 -1052
View File
File diff suppressed because it is too large Load Diff
+200 -83
View File
@@ -4,6 +4,123 @@
"last_updated": "2025-10-17",
"source": "ispyconnect.com",
"entries": [
{
"models": [
"ALL"
],
"type": "FFMPEG",
"protocol": "rtsp",
"port": 554,
"url": "/Streaming/Channels/[CHANNEL+1]01"
},
{
"models": [
"ALL"
],
"type": "FFMPEG",
"protocol": "rtsp",
"port": 554,
"url": "/Streaming/Channels/[CHANNEL]01"
},
{
"models": [
"ALL"
],
"type": "FFMPEG",
"protocol": "rtsp",
"port": 554,
"url": "/Streaming/Channels/[CHANNEL+1]02"
},
{
"models": [
"ALL"
],
"type": "FFMPEG",
"protocol": "rtsp",
"port": 554,
"url": "/Streaming/Channels/[CHANNEL]02"
},
{
"models": [
"ALL"
],
"type": "FFMPEG",
"protocol": "rtsp",
"port": 554,
"url": "/ISAPI/Streaming/Channels/[CHANNEL+1]01"
},
{
"models": [
"ALL"
],
"type": "FFMPEG",
"protocol": "rtsp",
"port": 554,
"url": "/ISAPI/Streaming/Channels/[CHANNEL]01"
},
{
"models": [
"ALL"
],
"type": "FFMPEG",
"protocol": "rtsp",
"port": 554,
"url": "/ISAPI/Streaming/Channels/[CHANNEL+1]02"
},
{
"models": [
"ALL"
],
"type": "FFMPEG",
"protocol": "rtsp",
"port": 554,
"url": "/ISAPI/Streaming/Channels/[CHANNEL]02"
},
{
"models": [
"ALL"
],
"type": "FFMPEG",
"protocol": "rtsp",
"port": 554,
"url": "/Streaming/Channels/[CHANNEL]01"
},
{
"models": [
"ALL"
],
"type": "FFMPEG",
"protocol": "rtsp",
"port": 554,
"url": "/Streaming/Channels/[CHANNEL]02"
},
{
"models": [
"ALL"
],
"type": "FFMPEG",
"protocol": "rtsp",
"port": 554,
"url": "/ISAPI/Streaming/Channels/[CHANNEL]01"
},
{
"models": [
"ALL"
],
"type": "FFMPEG",
"protocol": "rtsp",
"port": 554,
"url": "/ISAPI/Streaming/Channels/[CHANNEL]02"
},
{
"models": [
"ALL"
],
"type": "FFMPEG",
"protocol": "rtsp",
"port": 554,
"url": "/Streaming/Channels/[CHANNEL]"
},
{
"models": [
"040",
@@ -47,69 +164,6 @@
"port": 554,
"url": "/Streaming/Channels/1"
},
{
"models": [
"ALL",
"B220",
"C6T",
"D110",
"DS-H216Q",
"DS-I102",
"DS-I113",
"DS-I114",
"DS-I114W",
"DS-i126",
"ds-i200",
"DS-I200(D)",
"ds-i203",
"DS-I213",
"ds-i214",
"DS-I214(B)",
"ds-i214w(b)",
"ds-i223",
"DS-I400(C)",
"ds-l122",
"ds-n241w",
"i100",
"i110",
"I114",
"i114w",
"I120",
"IPC-B120-I",
"IPC-B140",
"IPC-B622-G2/ZS",
"IPC-D082-G2/S",
"IPC-D120",
"IPC-T640-Z",
"l110",
"Other",
"VDP-D2201",
"VDP-D2211W(B)",
"watch"
],
"type": "FFMPEG",
"protocol": "rtsp",
"port": 554,
"url": "/Streaming/Channels/101"
},
{
"models": [
"ALL",
"DS-I102",
"ds-i200",
"Ds-i203",
"DS-I214(B)",
"DS-I214W(B)",
"DS-I253",
"ds-i458",
"HiWatch DS-N208(C)",
"i450s"
],
"type": "FFMPEG",
"protocol": "rtsp",
"port": 554,
"url": "/ISAPI/Streaming/Channels/101"
},
{
"models": [
"DC-I200",
@@ -221,16 +275,6 @@
"port": 554,
"url": "/h264_stream"
},
{
"models": [
"ds-i200",
"VDP-D2201"
],
"type": "FFMPEG",
"protocol": "rtsp",
"port": 555,
"url": "/Streaming/Channels/102"
},
{
"models": [
"Ds-i203"
@@ -240,16 +284,6 @@
"port": 8000,
"url": "/"
},
{
"models": [
"DS-I214(B)",
"DS-I405"
],
"type": "FFMPEG",
"protocol": "rtsp",
"port": 554,
"url": "/ISAPI/Streaming/Channels/102"
},
{
"models": [
"DS-I220",
@@ -310,6 +344,89 @@
"protocol": "rtsp",
"port": 554,
"url": "/onvif1"
},
{
"models": [
"ALL",
"B220",
"C6T",
"D110",
"DS-H216Q",
"DS-I102",
"DS-I113",
"DS-I114",
"DS-I114W",
"DS-i126",
"ds-i200",
"DS-I200(D)",
"ds-i203",
"DS-I213",
"ds-i214",
"DS-I214(B)",
"ds-i214w(b)",
"ds-i223",
"DS-I400(C)",
"ds-l122",
"ds-n241w",
"i100",
"i110",
"I114",
"i114w",
"I120",
"IPC-B120-I",
"IPC-B140",
"IPC-B622-G2/ZS",
"IPC-D082-G2/S",
"IPC-D120",
"IPC-T640-Z",
"l110",
"Other",
"VDP-D2201",
"VDP-D2211W(B)",
"watch"
],
"type": "FFMPEG",
"protocol": "rtsp",
"port": 554,
"url": "/Streaming/Channels/101"
},
{
"models": [
"ALL",
"DS-I102",
"ds-i200",
"Ds-i203",
"DS-I214(B)",
"DS-I214W(B)",
"DS-I253",
"ds-i458",
"HiWatch DS-N208(C)",
"i450s"
],
"type": "FFMPEG",
"protocol": "rtsp",
"port": 554,
"url": "/ISAPI/Streaming/Channels/101"
},
{
"models": [
"ds-i200",
"VDP-D2201"
],
"type": "FFMPEG",
"protocol": "rtsp",
"port": 555,
"url": "/Streaming/Channels/102"
},
{
"models": [
"DS-I214(B)",
"DS-I405"
],
"type": "FFMPEG",
"protocol": "rtsp",
"port": 554,
"url": "/ISAPI/Streaming/Channels/102"
}
]
}
+27 -9
View File
@@ -4,6 +4,24 @@
"last_updated": "2025-10-17",
"source": "ispyconnect.com",
"entries": [
{
"models": [
"ALL"
],
"type": "FFMPEG",
"protocol": "rtsp",
"port": 0,
"url": "/Streaming/Channels/[CHANNEL+1]02"
},
{
"models": [
"ALL"
],
"type": "FFMPEG",
"protocol": "rtsp",
"port": 0,
"url": "/Streaming/Channels/[CHANNEL]02"
},
{
"models": [
"0010/0020",
@@ -647,15 +665,6 @@
"port": 0,
"url": "cam[CHANNEL]/h264"
},
{
"models": [
"FCS-3084"
],
"type": "FFMPEG",
"protocol": "rtsp",
"port": 0,
"url": "/Streaming/Channels/102"
},
{
"models": [
"FCS-4051",
@@ -770,6 +779,15 @@
"protocol": "http",
"port": 80,
"url": "/cgi-bin/video.jpg"
},
{
"models": [
"FCS-3084"
],
"type": "FFMPEG",
"protocol": "rtsp",
"port": 0,
"url": "/Streaming/Channels/102"
}
]
}
+36 -18
View File
@@ -4,6 +4,24 @@
"last_updated": "2025-10-17",
"source": "ispyconnect.com",
"entries": [
{
"models": [
"ALL"
],
"type": "FFMPEG",
"protocol": "rtsp",
"port": 554,
"url": "/Streaming/Channels/[CHANNEL+1]01"
},
{
"models": [
"ALL"
],
"type": "FFMPEG",
"protocol": "rtsp",
"port": 554,
"url": "/Streaming/Channels/[CHANNEL]01"
},
{
"models": [
"0v600-365-kd",
@@ -153,24 +171,6 @@
"port": 0,
"url": "snapshot.jpg?account=[USERNAME]&password=[PASSWORD]"
},
{
"models": [
"BTD2",
"CAM2",
"DVR-FTD4-8",
"DVR-THD30B",
"FTD4",
"Other",
"WM-CAM-WAWNP2L",
"wmvr-wnip2",
"WNIP2-CM",
"WNIP-2lta-bs"
],
"type": "FFMPEG",
"protocol": "rtsp",
"port": 554,
"url": "/Streaming/channels/301"
},
{
"models": [
"CAM-1",
@@ -375,6 +375,24 @@
"protocol": "http",
"port": 80,
"url": "/cgi-bin/snapshot.cgi?chn=0&u=[USERNAME]&p=[PASSWORD]"
},
{
"models": [
"BTD2",
"CAM2",
"DVR-FTD4-8",
"DVR-THD30B",
"FTD4",
"Other",
"WM-CAM-WAWNP2L",
"wmvr-wnip2",
"WNIP2-CM",
"WNIP-2lta-bs"
],
"type": "FFMPEG",
"protocol": "rtsp",
"port": 554,
"url": "/Streaming/channels/301"
}
]
}
+18
View File
@@ -4,6 +4,24 @@
"last_updated": "2025-10-17",
"source": "ispyconnect.com",
"entries": [
{
"models": [
"ALL"
],
"type": "FFMPEG",
"protocol": "rtsp",
"port": 10554,
"url": "/Streaming/Channels/[CHANNEL+1]01"
},
{
"models": [
"ALL"
],
"type": "FFMPEG",
"protocol": "rtsp",
"port": 10554,
"url": "/Streaming/Channels/[CHANNEL]01"
},
{
"models": [
"801",
+128 -92
View File
@@ -4,6 +4,42 @@
"last_updated": "2025-10-17",
"source": "ispyconnect.com",
"entries": [
{
"models": [
"ALL"
],
"type": "FFMPEG",
"protocol": "rtsp",
"port": 0,
"url": "/Streaming/Channels/[CHANNEL+1]01"
},
{
"models": [
"ALL"
],
"type": "FFMPEG",
"protocol": "rtsp",
"port": 0,
"url": "/Streaming/Channels/[CHANNEL]01"
},
{
"models": [
"ALL"
],
"type": "FFMPEG",
"protocol": "rtsp",
"port": 0,
"url": "/Streaming/Channels/[CHANNEL+1]02"
},
{
"models": [
"ALL"
],
"type": "FFMPEG",
"protocol": "rtsp",
"port": 0,
"url": "/Streaming/Channels/[CHANNEL]02"
},
{
"models": [
"005FTCD",
@@ -588,58 +624,6 @@
"port": 554,
"url": "/ch05/1"
},
{
"models": [
"7-12",
"8ch 3MP NVR",
"dv8-3425",
"DVR w/ Web Port",
"DVR W/ WEB PORT",
"DVR4 4350",
"DVR8",
"DVR8-4900",
"DVR8-8050",
"DVR8-8075",
"HDR8050",
"lv-9808",
"NHD-850CAM",
"NHH-880CAM",
"nvr16-7090",
"NVR-7200",
"Other",
"SWIFI-FLOCAM2",
"swifi-spotcam",
"SWIFI-XTRCAM",
"SWWHD-OUTCAM",
"T855"
],
"type": "FFMPEG",
"protocol": "rtsp",
"port": 0,
"url": "/Streaming/Channels/101"
},
{
"models": [
"880",
"DVR4 4350",
"DVR8-1500",
"DVR8-1525",
"DVR8-4500",
"DVR8-4900",
"HDR8050",
"lv-9808",
"NHD-850CAM",
"nvr16-7090",
"NVR-7200",
"Other",
"SPOTCAM",
"WIFI-PT"
],
"type": "FFMPEG",
"protocol": "rtsp",
"port": 0,
"url": "/Streaming/Channels/102"
},
{
"models": [
"887"
@@ -874,37 +858,6 @@
"port": 0,
"url": "/Streaming/Unicast/channels/401"
},
{
"models": [
"DVR W/ WEB PORT",
"DVR4 4350",
"DVR8-8075",
"lv-9808",
"Other"
],
"type": "FFMPEG",
"protocol": "rtsp",
"port": 0,
"url": "/Streaming/channels/101"
},
{
"models": [
"DVR-1500"
],
"type": "FFMPEG",
"protocol": "rtsp",
"port": 554,
"url": "/Streaming/Channels/701"
},
{
"models": [
"DVR-1500"
],
"type": "FFMPEG",
"protocol": "rtsp",
"port": 554,
"url": "/Streaming/Channels/601"
},
{
"models": [
"DVR4",
@@ -942,15 +895,6 @@
"port": 80,
"url": "/?action=stream"
},
{
"models": [
"DVR8-4500"
],
"type": "FFMPEG",
"protocol": "rtsp",
"port": 554,
"url": "/Streaming/Channels/301"
},
{
"models": [
"DVR8-4500",
@@ -1314,6 +1258,98 @@
"protocol": "rtsp",
"port": 0,
"url": "/Streaming/Channels/2"
},
{
"models": [
"7-12",
"8ch 3MP NVR",
"dv8-3425",
"DVR w/ Web Port",
"DVR W/ WEB PORT",
"DVR4 4350",
"DVR8",
"DVR8-4900",
"DVR8-8050",
"DVR8-8075",
"HDR8050",
"lv-9808",
"NHD-850CAM",
"NHH-880CAM",
"nvr16-7090",
"NVR-7200",
"Other",
"SWIFI-FLOCAM2",
"swifi-spotcam",
"SWIFI-XTRCAM",
"SWWHD-OUTCAM",
"T855"
],
"type": "FFMPEG",
"protocol": "rtsp",
"port": 0,
"url": "/Streaming/Channels/101"
},
{
"models": [
"880",
"DVR4 4350",
"DVR8-1500",
"DVR8-1525",
"DVR8-4500",
"DVR8-4900",
"HDR8050",
"lv-9808",
"NHD-850CAM",
"nvr16-7090",
"NVR-7200",
"Other",
"SPOTCAM",
"WIFI-PT"
],
"type": "FFMPEG",
"protocol": "rtsp",
"port": 0,
"url": "/Streaming/Channels/102"
},
{
"models": [
"DVR W/ WEB PORT",
"DVR4 4350",
"DVR8-8075",
"lv-9808",
"Other"
],
"type": "FFMPEG",
"protocol": "rtsp",
"port": 0,
"url": "/Streaming/channels/101"
},
{
"models": [
"DVR-1500"
],
"type": "FFMPEG",
"protocol": "rtsp",
"port": 554,
"url": "/Streaming/Channels/701"
},
{
"models": [
"DVR-1500"
],
"type": "FFMPEG",
"protocol": "rtsp",
"port": 554,
"url": "/Streaming/Channels/601"
},
{
"models": [
"DVR8-4500"
],
"type": "FFMPEG",
"protocol": "rtsp",
"port": 554,
"url": "/Streaming/Channels/301"
}
]
}
+18
View File
@@ -4,6 +4,24 @@
"last_updated": "2025-10-17",
"source": "ispyconnect.com",
"entries": [
{
"models": [
"ALL"
],
"type": "FFMPEG",
"protocol": "rtsp",
"port": 554,
"url": "/Streaming/Channels/[CHANNEL+1]01"
},
{
"models": [
"ALL"
],
"type": "FFMPEG",
"protocol": "rtsp",
"port": 554,
"url": "/Streaming/Channels/[CHANNEL]01"
},
{
"models": [
"TBK-BUL8841Z"
+48
View File
@@ -31,6 +31,54 @@
"notes": "Common RTSP sub stream for ONVIF cameras",
"model_count": 9998
},
{
"url": "/Streaming/Channels/[CHANNEL+1]01",
"type": "FFMPEG",
"protocol": "rtsp",
"port": 554,
"notes": "Hikvision main stream - 0-based channel input (channel 0 -> 101, 1 -> 201)",
"model_count": 9500
},
{
"url": "/Streaming/Channels/[CHANNEL]01",
"type": "FFMPEG",
"protocol": "rtsp",
"port": 554,
"notes": "Hikvision main stream - 1-based channel input (channel 1 -> 101, 2 -> 201)",
"model_count": 9490
},
{
"url": "/Streaming/Channels/[CHANNEL+1]02",
"type": "FFMPEG",
"protocol": "rtsp",
"port": 554,
"notes": "Hikvision sub stream - 0-based channel input (channel 0 -> 102, 1 -> 202)",
"model_count": 9480
},
{
"url": "/Streaming/Channels/[CHANNEL]02",
"type": "FFMPEG",
"protocol": "rtsp",
"port": 554,
"notes": "Hikvision sub stream - 1-based channel input (channel 1 -> 102, 2 -> 202)",
"model_count": 9470
},
{
"url": "/Streaming/Channels/[CHANNEL+1]03",
"type": "FFMPEG",
"protocol": "rtsp",
"port": 554,
"notes": "Hikvision third stream - 0-based channel input (channel 0 -> 103, 1 -> 203)",
"model_count": 9460
},
{
"url": "/Streaming/Channels/[CHANNEL]03",
"type": "FFMPEG",
"protocol": "rtsp",
"port": 554,
"notes": "Hikvision third stream - 1-based channel input (channel 1 -> 103, 2 -> 203)",
"model_count": 9450
},
{
"url": "/ch2",
"type": "FFMPEG",
+1 -1
View File
@@ -9,6 +9,7 @@ require (
github.com/go-chi/chi/v5 v5.2.3
github.com/go-playground/validator/v10 v10.28.0
github.com/lithammer/fuzzysearch v1.1.8
gopkg.in/yaml.v3 v3.0.1
)
require (
@@ -23,5 +24,4 @@ require (
golang.org/x/crypto v0.42.0 // indirect
golang.org/x/sys v0.36.0 // indirect
golang.org/x/text v0.29.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
+1
View File
@@ -67,6 +67,7 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-69
View File
@@ -1,69 +0,0 @@
# Changelog
All notable changes to this Home Assistant add-on will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.0.1] - 2025-11-17
### Fixed
- GitHub Actions permissions for publishing Docker images to ghcr.io
- Added `packages: write` permission to build job
- Added `contents: write` permission to update-repository job
## [1.0.0] - 2025-01-15
### Added
- Initial release of Strix Home Assistant Add-on
- Support for aarch64, amd64, and armv7 architectures
- Web UI integration with Home Assistant panel
- Ingress support for seamless integration
- Configurable port and logging options
- Strict validation mode toggle
- Health check monitoring
- Comprehensive documentation
- Multi-arch Docker builds via GitHub Actions
- Automatic updates through Home Assistant Supervisor
### Features
- 3,600+ camera models in database
- ONVIF discovery support
- Real-time stream discovery via SSE
- RESTful API for automation
- Fuzzy search for camera models
- Multiple stream protocol support (RTSP, HTTP, MJPEG, JPEG)
- FFProbe integration for stream validation
- Concurrent stream testing with worker pool
- Camera database with popular stream patterns
### Security
- Runs as non-root user (UID 1000)
- Minimal Alpine-based container
- No credential storage
- Local network only operation
- Read-only filesystem where possible
### Documentation
- Complete installation guide
- Configuration reference
- API documentation
- Troubleshooting guide
- Integration examples for HA, go2rtc, and Frigate
## [Unreleased]
### Planned
- Auto-discovery integration with Home Assistant
- Automatic camera entity creation
- go2rtc configuration generator
- Frigate configuration generator
- ONVIF event monitoring
- Motion detection API
- Camera snapshot gallery
- Network scanner for bulk discovery
- Custom camera database additions
---
**Full Changelog**: https://github.com/eduard256/Strix/commits/main/homeassistant-addon
-391
View File
@@ -1,391 +0,0 @@
# Strix Camera Discovery - Documentation
## Installation
### Method 1: Add Repository (Recommended)
1. Navigate to **Supervisor****Add-on Store** in your Home Assistant
2. Click the **⋮** menu (top right) → **Repositories**
3. Add repository URL: `https://github.com/eduard256/Strix`
4. Find **Strix Camera Discovery** in the store
5. Click **Install**
6. Configure the add-on (optional)
7. Click **Start**
8. Click **Open Web UI**
### Method 2: Manual Installation
1. SSH into your Home Assistant server
2. Navigate to the addons directory:
```bash
cd /addons
```
3. Clone the repository:
```bash
git clone https://github.com/eduard256/Strix
cd Strix/homeassistant-addon
```
4. Restart Home Assistant Supervisor
5. Find the add-on in the **Local Add-ons** section
## Configuration
The add-on can be configured through the Home Assistant UI:
```yaml
log_level: info
port: 4567
strict_validation: true
```
### Configuration Options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `log_level` | string | `info` | Logging level: `debug`, `info`, `warn`, `error` |
| `port` | integer | `4567` | Port for web interface and API |
| `strict_validation` | boolean | `true` | Enable strict stream validation |
### Advanced Configuration
For advanced users, you can modify environment variables:
- `STRIX_LOG_LEVEL` - Log level (debug, info, warn, error)
- `STRIX_LOG_FORMAT` - Log format (json, text)
- `STRIX_API_LISTEN` - Server listen address (set via `port` option)
- `STRIX_DATA_PATH` - Camera database path (default: `/app/data`)
## Usage
### Quick Start Guide
1. **Open the Web UI**
- Click "Open Web UI" in the add-on panel
- Or navigate to: `http://homeassistant.local:4567`
2. **Find Your Camera Model**
- Use the search bar to find your camera
- Example: "Hikvision DS-2CD2032"
- Supports fuzzy search (typos are okay!)
3. **Discover Streams**
- Enter camera IP address (e.g., `192.168.1.100`)
- Enter credentials (username/password)
- Select discovered camera model
- Click "Discover Streams"
4. **Real-time Progress**
- Watch live updates as Strix tests different URLs
- See which streams are working
- Get detailed validation results
5. **Copy Stream URLs**
- Copy working URLs to use in Home Assistant
- Supports RTSP, HTTP, MJPEG, JPEG snapshots
### Camera Search
The search functionality includes:
- **3,600+ camera models** in database
- **Fuzzy matching** - handles typos and variations
- **Brand and model search** - search by manufacturer or model number
- **Popular cameras** - common models are prioritized
Example searches:
- "hikvision" - finds all Hikvision cameras
- "ds-2cd2032" - finds specific model
- "axis m1045" - finds AXIS camera
- "dahua ipc" - finds Dahua IP cameras
### Stream Discovery
Discovery process:
1. **ONVIF Discovery** - Attempts automatic detection via ONVIF protocol
2. **Model Patterns** - Tests URL patterns specific to camera model
3. **Popular Patterns** - Tests 150+ common stream URL patterns
4. **Validation** - Verifies each stream using ffprobe
Stream types discovered:
- RTSP streams (`rtsp://`)
- HTTP streams (`http://`)
- MJPEG streams (`http://.../video.cgi`)
- JPEG snapshots (`http://.../snapshot.jpg`)
### API Usage
#### Health Check
```bash
curl http://homeassistant.local:4567/api/v1/health
```
Response:
```json
{
"status": "ok",
"timestamp": "2025-01-15T10:30:00Z"
}
```
#### Camera Search
```bash
curl -X POST http://homeassistant.local:4567/api/v1/cameras/search \
-H "Content-Type: application/json" \
-d '{
"query": "hikvision",
"limit": 10
}'
```
Response:
```json
{
"cameras": [
{
"brand": "Hikvision",
"model": "DS-2CD2032-I",
"score": 0.95
}
],
"count": 1
}
```
#### Stream Discovery (Server-Sent Events)
```bash
curl -N -X POST http://homeassistant.local:4567/api/v1/streams/discover \
-H "Content-Type: application/json" \
-d '{
"target": "192.168.1.100",
"model": "hikvision ds-2cd2032",
"username": "admin",
"password": "password",
"timeout": 240,
"max_streams": 10
}'
```
SSE Events:
```
event: progress
data: {"message": "Testing RTSP stream...", "percent": 25}
event: stream_found
data: {"url": "rtsp://192.168.1.100:554/stream1", "type": "rtsp"}
event: complete
data: {"total_found": 3, "duration": 45.2}
```
## Integration with Home Assistant
### Generic Camera Platform
```yaml
camera:
- platform: generic
name: Front Door
still_image_url: http://192.168.1.100/snapshot.jpg
stream_source: rtsp://admin:password@192.168.1.100:554/stream1
verify_ssl: false
```
### go2rtc Integration
```yaml
go2rtc:
streams:
front_door:
- rtsp://admin:password@192.168.1.100:554/stream1
back_yard:
- rtsp://admin:password@192.168.1.101:554/stream1
```
### Frigate Integration
```yaml
cameras:
front_door:
ffmpeg:
inputs:
- path: rtsp://admin:password@192.168.1.100:554/stream1
roles:
- detect
- record
```
## Troubleshooting
### Add-on won't start
Check the logs:
1. Go to **Supervisor****Strix Camera Discovery** → **Log**
2. Look for error messages
3. Common issues:
- Port 4567 already in use
- Insufficient resources
- Database files missing
### Can't find camera model
- Try different search terms (brand name, model number)
- Use partial model numbers
- Check the full database at: `/app/data/brands/`
- If camera not in database, use "Generic" or similar brand camera
### Discovery finds no streams
Possible causes:
1. **Wrong IP address** - Verify camera is reachable: `ping 192.168.1.100`
2. **Wrong credentials** - Double-check username/password
3. **Firewall blocking** - Ensure RTSP port (554) is accessible
4. **ONVIF disabled** - Enable ONVIF in camera settings
5. **Network isolation** - Camera and HA must be on same network
Debug steps:
```bash
# Test if camera responds
curl -u admin:password http://192.168.1.100/
# Test RTSP stream manually
ffprobe rtsp://admin:password@192.168.1.100:554/stream1
```
### FFProbe warnings
If you see "ffprobe not found" warnings:
- This is normal if ffprobe isn't installed
- Stream validation will be limited to HTTP checks
- RTSP streams may not be validated properly
- The add-on includes ffprobe by default
### Slow discovery
Discovery can take 2-4 minutes because:
- Testing 150+ URL patterns
- Validating each stream with ffprobe
- Network latency to camera
- Camera response time
To speed up:
- Select specific camera model (reduces URLs to test)
- Reduce `timeout` value (default: 240 seconds)
- Reduce `max_streams` (stops after N streams found)
### Port conflicts
If port 4567 is in use:
1. Change the `port` option in add-on configuration
2. Restart the add-on
3. Access Web UI at new port
## Performance
### Resource Usage
Typical resource consumption:
- **Memory**: 50-100 MB
- **CPU**: Low (spikes during discovery)
- **Disk**: ~50 MB (including database)
- **Network**: Depends on discovery activity
### Concurrent Discoveries
The add-on can handle multiple concurrent discovery requests:
- Uses worker pool (20 concurrent workers)
- Queues excess requests
- No limit on simultaneous users
## Security
### Network Security
- Add-on runs in Home Assistant network
- No external internet access required
- All traffic is local to your network
### Credentials
- Camera credentials never stored
- Sent only during discovery session
- Not logged (even in debug mode)
- Transmitted over local network only
### Container Security
- Runs as non-root user (UID 1000)
- Minimal attack surface (Alpine base)
- No unnecessary packages
- Read-only filesystem where possible
## Database
### Camera Database
Location: `/app/data/brands/`
Contains:
- 3,600+ camera models
- Organized by brand (JSON files)
- Stream URL patterns
- Query parameter variations
Format example:
```json
{
"brand": "Hikvision",
"models": [
{
"model": "DS-2CD2032-I",
"patterns": [
"/Streaming/channels/101",
"/h264/ch1/main/av_stream"
]
}
]
}
```
### Updating Database
Database updates come with add-on updates:
- Check for updates in Add-on Store
- Updates include new camera models
- No manual database updates needed
## Support
### Getting Help
1. **Documentation**: Read this guide thoroughly
2. **Logs**: Check add-on logs for errors
3. **GitHub Issues**: https://github.com/eduard256/Strix/issues
4. **Community**: Home Assistant Community Forum
### Reporting Bugs
Include in bug reports:
1. Home Assistant version
2. Add-on version
3. Full logs from add-on
4. Camera brand/model
5. Steps to reproduce
### Feature Requests
Submit feature requests on GitHub with:
- Clear description of feature
- Use case / why it's needed
- Any relevant examples
## Changelog
See [CHANGELOG.md](CHANGELOG.md) for version history.
## License
MIT License - See [LICENSE](https://github.com/eduard256/Strix/blob/main/LICENSE)
-44
View File
@@ -1,44 +0,0 @@
ARG BUILD_FROM
FROM ${BUILD_FROM}
# Install runtime dependencies
RUN apk add --no-cache \
ffmpeg \
ca-certificates \
tzdata \
wget \
&& rm -rf /var/cache/apk/*
# Set working directory
WORKDIR /app
# Copy binary from build context
COPY strix /app/strix
# Copy camera database (CRITICAL)
COPY data /app/data
# Copy WebUI files
COPY webui /app/webui
# Copy run script
COPY run.sh /
RUN chmod a+x /run.sh
# Create non-root user for security
RUN addgroup -g 1000 strix && \
adduser -D -u 1000 -G strix strix && \
chown -R strix:strix /app
# Switch to non-root user
USER strix
# Expose default port
EXPOSE 4567
# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=10s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:4567/api/v1/health || exit 1
# Start application
CMD ["/run.sh"]
-274
View File
@@ -1,274 +0,0 @@
# Home Assistant Add-on Installation Guide
This guide explains how to set up and publish the Strix Home Assistant Add-on.
## 📋 Overview
The add-on structure is ready and includes:
- ✅ `config.yaml` - Add-on configuration
- ✅ `Dockerfile` - Multi-arch Docker build
- ✅ `build.yaml` - Build configuration for aarch64/amd64/armv7
- ✅ `run.sh` - Startup script with HA integration
- ✅ `README.md` - User-facing documentation
- ✅ `DOCS.md` - Comprehensive usage guide
- ✅ `CHANGELOG.md` - Version history
- ✅ GitHub Actions workflow for automated builds
## 🚀 Quick Deployment
### Step 1: Enable GitHub Actions
The `.github/workflows/addon.yml` workflow will automatically:
1. Build binaries for all architectures (aarch64, amd64, armv7)
2. Create multi-arch Docker images
3. Push to GitHub Container Registry (ghcr.io)
4. Update version numbers on tags
No manual setup needed - just push to GitHub!
### Step 2: Create First Release
```bash
# Make sure all changes are committed
git add .
git commit -m "Add Home Assistant Add-on"
# Push to main branch (this will trigger a dev build)
git push origin main
# Create and push a version tag (this will trigger a release build)
git tag v1.0.0
git push origin v1.0.0
```
The GitHub Action will automatically build and publish Docker images to:
- `ghcr.io/eduard256/strix-addon-aarch64:latest`
- `ghcr.io/eduard256/strix-addon-amd64:latest`
- `ghcr.io/eduard256/strix-addon-armv7:latest`
### Step 3: Add Icons (Optional but Recommended)
Add these files to `homeassistant-addon/`:
- `icon.png` - 128x128px icon for the add-on store
- `logo.png` - 256x256px logo for the add-on page
Recommended: Simple owl or camera icon in Home Assistant style (blue/white theme).
### Step 4: Test Installation
After the GitHub Action completes:
1. In Home Assistant, go to **Supervisor** → **Add-on Store**
2. Click **⋮** (menu) → **Repositories**
3. Add: `https://github.com/eduard256/Strix`
4. Find "Strix Camera Discovery" in the store
5. Click **Install**
6. Configure and start the add-on
7. Click **Open Web UI**
## 🔄 Updating the Add-on
### For New Features/Fixes
```bash
# Make your changes to the codebase
git add .
git commit -m "feat: add new feature"
git push origin main
```
The dev build will automatically trigger, creating images tagged with `dev-<git-hash>`.
### For New Releases
```bash
# Update version in homeassistant-addon/config.yaml
sed -i 's/^version:.*/version: "1.1.0"/' homeassistant-addon/config.yaml
# Update CHANGELOG.md
# Add new version section
# Commit and tag
git add homeassistant-addon/config.yaml homeassistant-addon/CHANGELOG.md
git commit -m "release: v1.1.0"
git tag v1.1.0
git push origin main
git push origin v1.1.0
```
The release build will automatically create versioned images.
## 📦 Manual Build (Optional)
If you need to build locally for testing:
```bash
# Build for your architecture
cd homeassistant-addon
# Build the Go binary
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \
-ldflags="-s -w" \
-o strix \
../cmd/strix/main.go
# Copy required files
cp -r ../data .
cp -r ../webui .
# Build Docker image
docker build \
--build-arg BUILD_FROM=ghcr.io/home-assistant/amd64-base:3.20 \
-t strix-addon:test .
# Test the image
docker run --rm -p 4567:4567 strix-addon:test
```
## 🔧 Configuration Options
Users can configure the add-on through the Home Assistant UI:
### Default Configuration
```yaml
log_level: info
port: 4567
strict_validation: true
```
### Advanced Options
Edit `homeassistant-addon/config.yaml` to add more options:
```yaml
options:
log_level: info
port: 4567
strict_validation: true
# Add new options here
schema:
log_level: list(debug|info|warn|error)
port: port
strict_validation: bool
# Add new option schemas here
```
Then update `run.sh` to use the new options:
```bash
NEW_OPTION=$(bashio::config 'new_option')
export STRIX_NEW_OPTION="${NEW_OPTION}"
```
## 🌐 Publishing to Community
### Option 1: Keep as Custom Repository (Recommended for Start)
Users add your repository manually:
```
https://github.com/eduard256/Strix
```
**Pros:**
- Full control
- Faster updates
- No approval process
**Cons:**
- Users must add repository manually
- Not in official add-on store
### Option 2: Submit to Home Assistant Community Add-ons
To get listed in the official community store:
1. Follow Home Assistant Add-on guidelines:
https://developers.home-assistant.io/docs/add-ons/
2. Submit to Community Add-ons repository:
https://github.com/home-assistant/addons
3. Wait for review and approval
**Pros:**
- Official recognition
- Easier for users to find
- Auto-update support
**Cons:**
- Strict guidelines
- Review process
- Slower updates
## 📊 Monitoring Builds
Check GitHub Actions status:
```
https://github.com/eduard256/Strix/actions
```
View published images:
```
https://github.com/eduard256/Strix/pkgs/container/strix-addon-amd64
https://github.com/eduard256/Strix/pkgs/container/strix-addon-aarch64
https://github.com/eduard256/Strix/pkgs/container/strix-addon-armv7
```
## 🐛 Troubleshooting
### Build Fails
Check GitHub Actions logs for errors. Common issues:
- Go build errors → Fix in main codebase
- Docker build errors → Check Dockerfile
- Permission errors → Ensure GITHUB_TOKEN has required permissions
### Add-on Won't Install
- Verify config.yaml syntax
- Check Docker images are published to ghcr.io
- Ensure repository.yaml is in root directory
- Verify architecture support matches user's system
### Add-on Won't Start
Check add-on logs in Home Assistant:
- Go to add-on page → **Log** tab
- Look for startup errors
- Common issues:
- Port already in use
- Missing data files
- Permission errors
## 📚 Resources
- [Home Assistant Add-on Documentation](https://developers.home-assistant.io/docs/add-ons/)
- [Add-on Configuration](https://developers.home-assistant.io/docs/add-ons/configuration)
- [Add-on Testing](https://developers.home-assistant.io/docs/add-ons/testing)
- [GitHub Actions](https://docs.github.com/en/actions)
## ✅ Checklist
Before first release:
- [ ] All code tested and working
- [ ] Version set in config.yaml
- [ ] CHANGELOG.md updated
- [ ] Icons added (icon.png, logo.png)
- [ ] README.md reviewed
- [ ] DOCS.md reviewed
- [ ] GitHub Actions workflow tested
- [ ] Repository URL correct in config files
- [ ] Git tag created (v1.0.0)
- [ ] Docker images published to ghcr.io
- [ ] Test installation in Home Assistant
## 🎉 You're Ready!
Once you've completed the checklist above, your Home Assistant Add-on is ready for users!
Share it with the community:
- Home Assistant Forums
- Reddit r/homeassistant
- GitHub Discussions
- Discord servers
-261
View File
@@ -1,261 +0,0 @@
# Strix - Home Assistant Add-on
## 🎯 Что создано
Полностью готовый Home Assistant Add-on для Strix с автоматической сборкой и публикацией.
## 📁 Структура
```
homeassistant-addon/
├── config.yaml # Конфигурация аддона для HA
├── Dockerfile # Multi-arch Docker образ
├── build.yaml # Настройки сборки (aarch64/amd64/armv7)
├── run.sh # Скрипт запуска с интеграцией HA
├── README.md # Документация для пользователей (EN)
├── README-RU.md # Документация для пользователей (RU)
├── DOCS.md # Подробная документация
├── CHANGELOG.md # История версий
├── INSTALLATION.md # Инструкция по установке и публикации
├── icon.png.todo # Заметка про иконку 128x128
└── logo.png.todo # Заметка про логотип 256x256
```
## ✨ Возможности
- ✅ **Автоматическая сборка** через GitHub Actions
- ✅ **Multi-arch поддержка**: aarch64, amd64, armv7
- ✅ **Web UI интеграция** в боковую панель Home Assistant
- ✅ **Ingress поддержка** для бесшовной интеграции
- ✅ **Настройка через UI** Home Assistant
- ✅ **Автообновления** через Supervisor
- ✅ **Полная документация** на русском и английском
## 🚀 Быстрый старт
### 1. Завершить подготовку
```bash
# Добавить иконки (опционально, но рекомендуется)
# - icon.png (128x128px)
# - logo.png (256x256px)
# Закоммитить все изменения
git add .
git commit -m "Add Home Assistant Add-on"
git push origin main
```
### 2. Создать релиз
```bash
# Создать тег версии
git tag v1.0.0
git push origin v1.0.0
```
GitHub Actions автоматически:
- Соберет бинарники для всех архитектур
- Создаст Docker образы
- Опубликует в GitHub Container Registry
### 3. Установить в Home Assistant
1. **Supervisor****Add-on Store****⋮** → **Repositories**
2. Добавить: `https://github.com/eduard256/Strix`
3. Найти **Strix Camera Discovery**
4. Нажать **Install**
5. Нажать **Start**
6. Нажать **Open Web UI**
## ⚙️ Конфигурация
Пользователи могут настроить через UI Home Assistant:
```yaml
log_level: info # debug, info, warn, error
port: 4567 # Порт веб-интерфейса
strict_validation: true # Строгая валидация потоков
```
## 🔄 Обновление
### Новые функции/исправления
```bash
git add .
git commit -m "feat: новая функция"
git push origin main
```
Автоматически создастся dev-сборка.
### Новый релиз
```bash
# Обновить версию
sed -i 's/^version:.*/version: "1.1.0"/' homeassistant-addon/config.yaml
# Обновить CHANGELOG.md
nano homeassistant-addon/CHANGELOG.md
# Закоммитить и создать тег
git add .
git commit -m "release: v1.1.0"
git tag v1.1.0
git push origin main v1.1.0
```
## 🏗️ Как это работает
### GitHub Actions Workflow
Файл: `.github/workflows/addon.yml`
При пуше в `main` или создании тега `v*`:
1. **Build Stage** (для каждой архитектуры):
- Собирает Go бинарник
- Копирует данные и WebUI
- Создает Docker образ
- Публикует в ghcr.io
2. **Update Repository**:
- Обновляет версию в config.yaml
- Обновляет repository.yaml
- Коммитит изменения (только для тегов)
### Docker Images
Публикуются в GitHub Container Registry:
- `ghcr.io/eduard256/strix-addon-aarch64:latest`
- `ghcr.io/eduard256/strix-addon-amd64:latest`
- `ghcr.io/eduard256/strix-addon-armv7:latest`
Версионные теги:
- `ghcr.io/eduard256/strix-addon-aarch64:1.0.0`
- и т.д.
## 📦 Что включено
### Runtime зависимости
- **ffmpeg** - для валидации RTSP потоков
- **ca-certificates** - для HTTPS
- **tzdata** - для корректных временных меток
- **wget** - для healthcheck
### Данные приложения
- **База камер** - 3,600+ моделей
- **WebUI** - встроенный веб-интерфейс
- **API** - RESTful API для автоматизации
## 🔒 Безопасность
- Запуск от non-root пользователя (UID 1000)
- Минимальный Alpine образ
- Отсутствие хранения credentials
- Работа только в локальной сети
## 📚 Документация
- **README.md** - Краткое описание для пользователей
- **DOCS.md** - Полная документация по использованию
- **CHANGELOG.md** - История изменений
- **INSTALLATION.md** - Инструкция для разработчиков
## 🎨 TODO (опционально)
1. **Иконки**:
- Создать `icon.png` (128x128px)
- Создать `logo.png` (256x256px)
- Стиль: сова или камера, синий/белый (Home Assistant style)
2. **Улучшения**:
- Добавить скриншоты в README.md
- Создать видео-инструкцию
- Перевести DOCS.md на русский
3. **Интеграции**:
- Автоматическое добавление камер в HA
- Генератор конфигов для go2rtc
- Генератор конфигов для Frigate
## 🤝 Публикация
### Вариант 1: Кастомный репозиторий (Рекомендуется)
Пользователи добавляют вручную:
```
https://github.com/eduard256/Strix
```
**Плюсы**:
- Полный контроль
- Быстрые обновления
- Нет процесса одобрения
### Вариант 2: Home Assistant Community Add-ons
Отправить в официальный репозиторий:
https://github.com/home-assistant/addons
**Плюсы**:
- Официальное признание
- Проще найти пользователям
- Автообновления
**Минусы**:
- Строгие требования
- Процесс ревью
- Медленные обновления
## 🐛 Решение проблем
### Сборка не прошла
Проверить GitHub Actions:
```
https://github.com/eduard256/Strix/actions
```
### Аддон не устанавливается
- Проверить синтаксис config.yaml
- Убедиться что образы опубликованы в ghcr.io
- Проверить repository.yaml в корне
### Аддон не запускается
Смотреть логи в Home Assistant:
- Страница аддона → вкладка **Log**
- Частые проблемы:
- Порт занят
- Отсутствуют файлы данных
- Ошибки прав доступа
## ✅ Чеклист перед релизом
- [ ] Код протестирован
- [ ] Версия установлена в config.yaml
- [ ] CHANGELOG.md обновлен
- [ ] Иконки добавлены (опционально)
- [ ] README.md проверен
- [ ] DOCS.md проверен
- [ ] GitHub Actions протестирован
- [ ] Создан git тег (v1.0.0)
- [ ] Docker образы опубликованы
- [ ] Тестовая установка в Home Assistant
## 🎉 Готово!
После выполнения чеклиста ваш Home Assistant Add-on готов к использованию!
Поделитесь с сообществом:
- Форум Home Assistant
- Reddit r/homeassistant
- GitHub Discussions
- Discord серверы
---
**Вопросы?** Создайте Issue на GitHub: https://github.com/eduard256/Strix/issues
-145
View File
@@ -1,145 +0,0 @@
# Home Assistant Add-on: Strix Camera Discovery
![Supports aarch64 Architecture][aarch64-shield]
![Supports amd64 Architecture][amd64-shield]
![Supports armv7 Architecture][armv7-shield]
Strix is a smart IP camera stream discovery system that automatically finds and validates camera streams. It eliminates the need for manual URL configuration by using ONVIF discovery, comprehensive camera database, and intelligent stream testing.
## About
This add-on provides Strix - an intelligent camera discovery service for Home Assistant. It includes:
- **3,600+ Camera Models Database** - Comprehensive coverage of IP camera brands and models
- **ONVIF Discovery** - Automatic camera detection on your network
- **Smart Stream Testing** - Validates RTSP, HTTP, MJPEG, and JPEG snapshot URLs
- **Real-time Updates** - Server-Sent Events (SSE) for live discovery progress
- **Web Interface** - Beautiful UI for easy camera management
- **RESTful API** - Integrate with your automation workflows
## Installation
1. Add this repository to your Home Assistant Add-on Store:
- Click on "Add-on Store" in the Home Assistant Supervisor panel
- Click the three dots menu (top right) and select "Repositories"
- Add the URL: `https://github.com/eduard256/Strix`
- Click "Add"
2. Find "Strix Camera Discovery" in the add-on store and click "Install"
3. After installation, click "Start" to run the add-on
4. Open the Web UI by clicking "Open Web UI" button
## Configuration
```yaml
log_level: info
port: 4567
strict_validation: true
```
### Option: `log_level`
The `log_level` option controls the level of log output by the addon.
- `debug` - Shows detailed debug information
- `info` - Normal (default) log level
- `warn` - Only warnings and errors
- `error` - Only errors
### Option: `port`
The `port` option allows you to change the port on which Strix runs. Default is `4567`.
### Option: `strict_validation`
When enabled (default), Strix performs stricter stream validation:
- Verifies minimum image sizes for snapshots
- Requires at least one video stream in RTSP sources
- Reduces false positives
## How to use
1. **Open the Web UI** - Click "Open Web UI" in the add-on panel or navigate to `http://homeassistant.local:4567`
2. **Search for Camera** - Enter your camera brand/model (e.g., "Hikvision DS-2CD2032")
3. **Discover Streams** - Enter camera IP, credentials, and click "Discover"
4. **Get Stream URLs** - Copy working stream URLs for use in Home Assistant
## Example: Adding discovered camera to Home Assistant
After discovering a camera stream, add it to your `configuration.yaml`:
```yaml
camera:
- platform: generic
name: Front Door Camera
still_image_url: http://192.168.1.100/snapshot.jpg
stream_source: rtsp://admin:password@192.168.1.100:554/stream1
```
Or use with go2rtc for better performance:
```yaml
go2rtc:
streams:
front_door: rtsp://admin:password@192.168.1.100:554/stream1
```
## API Endpoints
The add-on exposes the following API endpoints:
### Health Check
```bash
GET http://homeassistant.local:4567/api/v1/health
```
### Camera Search
```bash
POST http://homeassistant.local:4567/api/v1/cameras/search
Content-Type: application/json
{
"query": "hikvision",
"limit": 10
}
```
### Stream Discovery (SSE)
```bash
POST http://homeassistant.local:4567/api/v1/streams/discover
Content-Type: application/json
{
"target": "192.168.1.100",
"model": "hikvision ds-2cd2032",
"username": "admin",
"password": "password",
"timeout": 240,
"max_streams": 10
}
```
## Support
Got questions or issues?
- [GitHub Issues](https://github.com/eduard256/Strix/issues)
- [Home Assistant Community](https://community.home-assistant.io/)
## Contributing
This is an active open-source project. We are always open to people who want to
use the code or contribute to it.
## License
MIT License - see the [LICENSE](https://github.com/eduard256/Strix/blob/main/LICENSE) file for details
[aarch64-shield]: https://img.shields.io/badge/aarch64-yes-green.svg
[amd64-shield]: https://img.shields.io/badge/amd64-yes-green.svg
[armv7-shield]: https://img.shields.io/badge/armv7-yes-green.svg
-11
View File
@@ -1,11 +0,0 @@
build_from:
aarch64: ghcr.io/home-assistant/aarch64-base:3.20
amd64: ghcr.io/home-assistant/amd64-base:3.20
armv7: ghcr.io/home-assistant/armv7-base:3.20
labels:
org.opencontainers.image.title: "Strix Camera Discovery"
org.opencontainers.image.description: "Smart IP camera stream discovery system"
org.opencontainers.image.source: "https://github.com/eduard256/Strix"
org.opencontainers.image.licenses: "MIT"
args:
STRIX_VERSION: "1.0.0"
-33
View File
@@ -1,33 +0,0 @@
name: Strix - Camera Stream Discovery
version: "1.0.1"
slug: strix
description: Smart IP camera stream discovery system with ONVIF support and comprehensive camera database
url: https://github.com/eduard256/Strix
arch:
- aarch64
- amd64
- armv7
init: false
startup: application
boot: auto
host_network: true
panel_icon: mdi:cctv
panel_title: Strix
panel_admin: false
webui: http://[HOST]:4567
ingress: true
ingress_port: 4567
ingress_stream: true
ports:
4567/tcp: 4567
ports_description:
4567/tcp: Web interface and API
options:
log_level: info
port: 4567
strict_validation: true
schema:
log_level: list(debug|info|warn|error)
port: port
strict_validation: bool
image: ghcr.io/eduard256/strix-addon-{arch}
-6
View File
@@ -1,6 +0,0 @@
TODO: Add 128x128px PNG icon for the add-on
The icon should represent Strix (owl/camera theme)
Recommended: Use a simple owl silhouette or camera icon in Home Assistant style
Place the file as: homeassistant-addon/icon.png
For now, you can use any 128x128 PNG image as a placeholder.
-6
View File
@@ -1,6 +0,0 @@
TODO: Add 256x256px PNG logo for the add-on
The logo should represent Strix branding
Recommended: Higher resolution version of the icon
Place the file as: homeassistant-addon/logo.png
For now, you can use any 256x256 PNG image as a placeholder.
-40
View File
@@ -1,40 +0,0 @@
#!/usr/bin/with-contenv bashio
# Get configuration from Home Assistant
LOG_LEVEL=$(bashio::config 'log_level')
PORT=$(bashio::config 'port')
STRICT_VALIDATION=$(bashio::config 'strict_validation')
# Print banner
bashio::log.info "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
bashio::log.info " ____ _ _ "
bashio::log.info " / ___|| |_ _ __(_)_ __"
bashio::log.info " \___ \| __| '__| \ \/ /"
bashio::log.info " ___) | |_| | | |> < "
bashio::log.info " |____/ \__|_| |_/_/\_\\"
bashio::log.info ""
bashio::log.info " Smart IP Camera Stream Discovery System"
bashio::log.info "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
# Set environment variables
export STRIX_LOG_LEVEL="${LOG_LEVEL}"
export STRIX_LOG_FORMAT="json"
export STRIX_API_LISTEN=":${PORT}"
export STRIX_DATA_PATH="/app/data"
bashio::log.info "Starting Strix with the following configuration:"
bashio::log.info " - Log Level: ${LOG_LEVEL}"
bashio::log.info " - Port: ${PORT}"
bashio::log.info " - Strict Validation: ${STRICT_VALIDATION}"
bashio::log.info " - Data Path: ${STRIX_DATA_PATH}"
# Check if ffprobe is available
if command -v ffprobe &> /dev/null; then
bashio::log.info "FFProbe found: $(ffprobe -version | head -n1)"
else
bashio::log.warning "FFProbe not found, stream validation will be limited"
fi
# Start Strix
bashio::log.info "Starting Strix server..."
exec /app/strix
+11 -15
View File
@@ -302,11 +302,13 @@ func (s *Scanner) collectStreams(ctx context.Context, req models.StreamDiscovery
"model", req.Model,
"limit", req.ModelLimit)
// Search for similar models
cameras, err := s.searchEngine.SearchByModel(req.Model, 0.8, req.ModelLimit)
// Search for cameras using intelligent brand+model search
searchResp, err := s.searchEngine.Search(req.Model, req.ModelLimit)
if err != nil {
s.logger.Error("model search failed", err)
} else {
cameras := searchResp.Cameras
// Collect entries from all matching cameras
var entries []models.CameraEntry
for _, camera := range cameras {
@@ -409,26 +411,20 @@ func (s *Scanner) testStreamsConcurrently(ctx context.Context, streams []models.
defer cancelProgress()
go func() {
ticker := time.NewTicker(3 * time.Second)
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
lastTested := int32(0)
for {
select {
case <-progressCtx.Done():
return
case <-ticker.C:
currentTested := atomic.LoadInt32(&tested)
// Only send if there's been progress
if currentTested != lastTested {
_ = streamWriter.SendJSON("progress", models.ProgressMessage{
Tested: int(currentTested),
Found: int(atomic.LoadInt32(&found)),
Remaining: len(streams) - int(currentTested),
})
lastTested = currentTested
}
// Send progress every second to prevent WriteTimeout
_ = streamWriter.SendJSON("progress", models.ProgressMessage{
Tested: int(atomic.LoadInt32(&tested)),
Found: int(atomic.LoadInt32(&found)),
Remaining: len(streams) - int(atomic.LoadInt32(&tested)),
})
}
}
}()
+32 -28
View File
@@ -174,34 +174,38 @@ func (b *Builder) replacePlaceholders(urlPath string, ctx BuildContext) string {
// Common placeholders
replacements := map[string]string{
"[CHANNEL]": strconv.Itoa(ctx.Channel),
"[channel]": strconv.Itoa(ctx.Channel),
"{channel}": strconv.Itoa(ctx.Channel), // BUBBLE protocol uses {channel}
"{CHANNEL}": strconv.Itoa(ctx.Channel),
"[WIDTH]": strconv.Itoa(ctx.Width),
"[width]": strconv.Itoa(ctx.Width),
"[HEIGHT]": strconv.Itoa(ctx.Height),
"[height]": strconv.Itoa(ctx.Height),
"[USERNAME]": ctx.Username,
"[username]": ctx.Username,
"[PASSWORD]": ctx.Password,
"[password]": ctx.Password,
"[PASWORD]": ctx.Password, // Handle typo in database
"[pasword]": ctx.Password,
"[USER]": ctx.Username,
"[user]": ctx.Username,
"[PASS]": ctx.Password,
"[pass]": ctx.Password,
"[PWD]": ctx.Password,
"[pwd]": ctx.Password,
"[IP]": ctx.IP,
"[ip]": ctx.IP,
"[PORT]": strconv.Itoa(ctx.Port),
"[port]": strconv.Itoa(ctx.Port),
"[AUTH]": auth, // base64(username:password) for basic auth
"[auth]": auth,
"[TOKEN]": "", // Empty for now
"[token]": "",
"[CHANNEL]": strconv.Itoa(ctx.Channel),
"[channel]": strconv.Itoa(ctx.Channel),
"[CHANNEL+1]": strconv.Itoa(ctx.Channel + 1), // For Hikvision-style channels (101, 201, 301...)
"[channel+1]": strconv.Itoa(ctx.Channel + 1),
"{CHANNEL}": strconv.Itoa(ctx.Channel), // BUBBLE protocol uses {channel}
"{channel}": strconv.Itoa(ctx.Channel),
"{CHANNEL+1}": strconv.Itoa(ctx.Channel + 1),
"{channel+1}": strconv.Itoa(ctx.Channel + 1),
"[WIDTH]": strconv.Itoa(ctx.Width),
"[width]": strconv.Itoa(ctx.Width),
"[HEIGHT]": strconv.Itoa(ctx.Height),
"[height]": strconv.Itoa(ctx.Height),
"[USERNAME]": ctx.Username,
"[username]": ctx.Username,
"[PASSWORD]": ctx.Password,
"[password]": ctx.Password,
"[PASWORD]": ctx.Password, // Handle typo in database
"[pasword]": ctx.Password,
"[USER]": ctx.Username,
"[user]": ctx.Username,
"[PASS]": ctx.Password,
"[pass]": ctx.Password,
"[PWD]": ctx.Password,
"[pwd]": ctx.Password,
"[IP]": ctx.IP,
"[ip]": ctx.IP,
"[PORT]": strconv.Itoa(ctx.Port),
"[port]": strconv.Itoa(ctx.Port),
"[AUTH]": auth, // base64(username:password) for basic auth
"[auth]": auth,
"[TOKEN]": "", // Empty for now
"[token]": "",
}
// Replace all placeholders
+1 -1
View File
@@ -73,7 +73,7 @@ func Load() *Config {
Server: ServerConfig{
Listen: ":4567", // Default listen address
ReadTimeout: 30 * time.Second,
WriteTimeout: 30 * time.Second,
WriteTimeout: 5 * time.Minute, // Increased for SSE long-polling
},
Database: DatabaseConfig{
DataPath: dataPath,
-3
View File
@@ -1,3 +0,0 @@
name: Strix Home Assistant Add-ons
url: https://github.com/eduard256/Strix
maintainer: eduard256
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "webui",
"version": "1.0.0",
"version": "1.0.4",
"type": "module",
"description": "",
"main": "index.js",
+382 -103
View File
@@ -79,6 +79,37 @@ body {
overflow-x: hidden;
}
/* ===== MOCK MODE BADGE ===== */
.mock-badge {
position: fixed;
top: 1rem;
right: 1rem;
z-index: 9999;
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
background: rgba(245, 158, 11, 0.15);
border: 1px solid var(--warning);
border-radius: 6px;
color: var(--warning);
font-size: var(--text-xs);
font-weight: 600;
letter-spacing: 0.05em;
box-shadow: 0 4px 12px rgba(245, 158, 11, 0.2);
backdrop-filter: blur(10px);
animation: fadeIn var(--transition-base);
}
.mock-badge svg {
animation: pulse 2s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
/* ===== LAYOUT ===== */
#app {
min-height: 100vh;
@@ -555,148 +586,173 @@ body {
margin-bottom: var(--space-6);
}
/* ===== CAROUSEL ===== */
.carousel-wrapper {
position: relative;
/* ===== STREAMS LIST ===== */
.streams-list {
display: flex;
align-items: center;
gap: var(--space-4);
margin-bottom: var(--space-4);
flex-direction: column;
gap: var(--space-3);
padding: var(--space-2);
}
.carousel {
flex: 1;
/* Custom scrollbar */
.streams-list::-webkit-scrollbar {
width: 8px;
}
.streams-list::-webkit-scrollbar-track {
background: var(--bg-secondary);
border-radius: 4px;
}
.streams-list::-webkit-scrollbar-thumb {
background: var(--purple-primary);
border-radius: 4px;
}
.streams-list::-webkit-scrollbar-thumb:hover {
background: var(--purple-light);
}
/* Stream item */
.stream-item {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 8px;
transition: all var(--transition-base);
overflow: hidden;
}
.carousel-track {
display: flex;
transition: transform var(--transition-slow);
}
.stream-card {
flex: 0 0 100%;
width: 100%;
padding: var(--space-6);
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 12px;
transition: all var(--transition-base);
}
.stream-card:hover {
.stream-item:hover {
border-color: var(--purple-primary);
box-shadow: 0 8px 24px var(--purple-glow);
box-shadow: 0 4px 12px var(--purple-glow);
}
.stream-type {
.stream-item.expanded {
border-color: var(--purple-primary);
}
/* Stream item header */
.stream-item-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--space-4);
padding: var(--space-4);
cursor: pointer;
}
.stream-item-main {
display: flex;
align-items: center;
gap: var(--space-3);
flex: 1;
min-width: 0;
}
.stream-info-left {
display: flex;
flex-direction: column;
gap: var(--space-2);
flex: 1;
min-width: 0;
}
.stream-type-badge {
display: flex;
align-items: center;
gap: var(--space-2);
font-size: var(--text-sm);
font-weight: 600;
color: var(--purple-primary);
margin-bottom: var(--space-4);
text-transform: uppercase;
letter-spacing: 0.05em;
white-space: nowrap;
}
.stream-type svg {
.stream-type-badge svg {
width: 20px;
height: 20px;
flex-shrink: 0;
}
.stream-url {
.stream-url-preview {
font-family: var(--font-mono);
font-size: var(--text-xs);
color: var(--text-secondary);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.stream-toggle {
background: none;
border: none;
padding: var(--space-2);
cursor: pointer;
color: var(--text-secondary);
transition: all var(--transition-fast);
display: flex;
align-items: center;
justify-content: center;
}
.stream-toggle:hover {
color: var(--purple-primary);
}
.stream-toggle .chevron {
transition: transform var(--transition-fast);
}
.stream-item.expanded .stream-toggle .chevron {
transform: rotate(180deg);
}
.btn-use-stream {
flex-shrink: 0;
white-space: nowrap;
padding: var(--space-3) var(--space-4);
font-size: var(--text-sm);
}
/* Stream item details */
.stream-item-details {
max-height: 0;
overflow: hidden;
transition: max-height var(--transition-base);
padding: 0 var(--space-4);
}
.stream-item-details.visible {
max-height: 500px;
padding: 0 var(--space-4) var(--space-4) var(--space-4);
}
.stream-url-full {
font-family: var(--font-mono);
font-size: var(--text-sm);
color: var(--text-primary);
word-break: break-all;
margin-bottom: var(--space-4);
margin-bottom: var(--space-3);
padding: var(--space-3);
background: var(--bg-tertiary);
border-radius: 6px;
border: 1px solid var(--border-color);
}
.stream-meta {
.stream-meta-item {
font-size: var(--text-sm);
color: var(--text-secondary);
margin-bottom: var(--space-2);
}
.stream-actions {
margin-top: var(--space-6);
.stream-meta-item:last-child {
margin-bottom: 0;
}
.carousel-arrow {
flex-shrink: 0;
width: 48px;
height: 48px;
background: var(--bg-elevated);
border: 1px solid var(--border-color);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all var(--transition-fast);
color: var(--text-secondary);
}
.carousel-arrow:hover:not(:disabled) {
background: var(--purple-primary);
border-color: var(--purple-primary);
color: white;
box-shadow: 0 4px 12px var(--purple-glow);
}
.carousel-arrow:disabled {
opacity: 0.3;
cursor: not-allowed;
}
@media (max-width: 767px) {
.carousel-wrapper {
flex-direction: column;
gap: var(--space-3);
}
.carousel-arrow {
display: none;
}
}
.carousel-info {
text-align: center;
}
.carousel-counter {
font-size: var(--text-sm);
color: var(--text-secondary);
margin-bottom: var(--space-3);
}
.carousel-dots {
display: flex;
justify-content: center;
gap: var(--space-2);
}
.carousel-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: rgba(139, 92, 246, 0.3);
border: none;
cursor: pointer;
transition: all var(--transition-base);
padding: 0;
}
.carousel-dot.active {
width: 24px;
border-radius: 4px;
background: var(--purple-primary);
box-shadow: 0 0 8px var(--purple-glow);
.meta-label {
font-weight: 600;
color: var(--text-primary);
}
/* ===== SELECTED STREAM INFO ===== */
@@ -718,6 +774,9 @@ body {
}
.stream-label {
display: flex;
align-items: center;
gap: var(--space-2);
font-size: var(--text-xs);
font-weight: 600;
color: var(--text-tertiary);
@@ -976,6 +1035,104 @@ body {
transform: translateY(0);
}
/* Button with tooltip wrapper */
.button-with-tooltip {
position: relative;
width: 100%;
}
.button-with-tooltip .btn-generate {
display: flex;
align-items: center;
justify-content: center;
gap: var(--space-2);
}
/* Button with tooltip in secondary-actions */
.secondary-actions .button-with-tooltip {
flex: 1.2;
width: auto;
}
.secondary-actions .button-with-tooltip .btn {
width: 100%;
display: flex;
align-items: center;
justify-content: center;
gap: var(--space-2);
}
.secondary-actions .button-with-tooltip:last-child {
flex: 0.8;
}
/* Info icon inside button */
.info-icon-button {
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
width: 18px;
height: 18px;
cursor: help;
color: rgba(255, 255, 255, 0.7);
transition: color var(--transition-fast);
}
.info-icon-button:hover {
color: rgba(255, 255, 255, 1);
}
.info-icon-button svg {
width: 18px;
height: 18px;
}
/* Info icon inside outline button */
.info-icon-button-outline {
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
width: 18px;
height: 18px;
cursor: help;
color: var(--text-tertiary);
transition: color var(--transition-fast);
}
.info-icon-button-outline:hover {
color: var(--purple-primary);
}
.info-icon-button-outline svg {
width: 18px;
height: 18px;
}
/* Info icon inside stream type badge */
.info-icon-stream {
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
width: 16px;
height: 16px;
margin-left: var(--space-2);
cursor: help;
color: var(--text-tertiary);
transition: color var(--transition-fast);
}
.info-icon-stream:hover {
color: var(--purple-primary);
}
.info-icon-stream svg {
width: 16px;
height: 16px;
}
.frigate-output-section {
margin-top: var(--space-6);
padding-top: var(--space-6);
@@ -987,6 +1144,128 @@ body {
display: none;
}
/* ===== TOOLTIPS ===== */
.label-with-info {
display: flex;
align-items: center;
gap: var(--space-2);
}
.info-icon {
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
width: 16px;
height: 16px;
cursor: help;
color: var(--text-tertiary);
transition: color var(--transition-fast);
}
.info-icon:hover {
color: var(--purple-primary);
}
.info-icon svg {
width: 16px;
height: 16px;
}
.tooltip {
position: absolute;
bottom: calc(100% + 8px);
left: 50%;
transform: translateX(-50%);
background: var(--bg-elevated);
border: 1px solid var(--purple-primary);
border-radius: 8px;
padding: var(--space-4);
width: 320px;
max-width: 90vw;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.6), 0 0 0 1px var(--purple-glow);
z-index: 1000;
opacity: 0;
visibility: hidden;
transition: opacity var(--transition-fast), visibility var(--transition-fast);
pointer-events: none;
}
/* Tooltip opens downward */
.tooltip.tooltip-down {
bottom: auto;
top: calc(100% + 8px);
}
.info-icon:hover .tooltip {
opacity: 1;
visibility: visible;
}
.tooltip::after {
content: '';
position: absolute;
top: 100%;
left: 50%;
transform: translateX(-50%);
border: 6px solid transparent;
border-top-color: var(--purple-primary);
}
/* Arrow for downward tooltip */
.tooltip.tooltip-down::after {
top: auto;
bottom: 100%;
border-top-color: transparent;
border-bottom-color: var(--purple-primary);
}
.tooltip-title {
font-weight: 600;
color: var(--purple-primary);
margin-bottom: var(--space-2);
font-size: var(--text-sm);
}
.tooltip-text {
font-size: var(--text-xs);
line-height: 1.5;
color: var(--text-secondary);
margin-bottom: var(--space-3);
}
.tooltip-text:last-child {
margin-bottom: 0;
}
.tooltip-examples {
margin-top: var(--space-3);
padding-top: var(--space-3);
border-top: 1px solid var(--border-color);
}
.tooltip-examples-title {
font-weight: 600;
color: var(--text-primary);
font-size: var(--text-xs);
margin-bottom: var(--space-2);
}
.tooltip-example {
font-family: var(--font-mono);
font-size: var(--text-xs);
color: var(--purple-light);
background: var(--bg-secondary);
padding: var(--space-1) var(--space-2);
border-radius: 4px;
margin-bottom: var(--space-1);
display: block;
}
.tooltip-example:last-child {
margin-bottom: 0;
}
/* ===== UTILITIES ===== */
.hidden {
display: none !important;
+14
View File
@@ -0,0 +1,14 @@
#!/bin/bash
# Simple development server for Strix WebUI
# This allows you to test the UI without running the Go backend
PORT=${1:-8080}
echo "Starting development server on port $PORT"
echo "Open: http://localhost:$PORT?mock=true"
echo ""
echo "Press Ctrl+C to stop"
# Use Python's built-in HTTP server
cd "$(dirname "$0")"
python3 -m http.server $PORT
+374 -82
View File
@@ -5,9 +5,18 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<meta name="theme-color" content="#0a0a0f">
<title>Strix - Camera Stream Discovery</title>
<link rel="stylesheet" href="/css/main.css">
<link rel="stylesheet" href="css/main.css">
</head>
<body>
<!-- Mock Mode Indicator -->
<div id="mock-mode-badge" class="mock-badge hidden">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
<path d="M8 1v6l4 2" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
<circle cx="8" cy="8" r="7" stroke="currentColor" stroke-width="2" fill="none"/>
</svg>
MOCK MODE
</div>
<div id="app">
<!-- Screen 1: Initial Address Input -->
<div id="screen-address" class="screen active">
@@ -33,7 +42,30 @@
</div>
<div class="form-group">
<label for="network-address" class="label">Network Address</label>
<label for="network-address" class="label label-with-info">
Network Address
<span class="info-icon">
<svg viewBox="0 0 16 16" fill="none">
<circle cx="8" cy="8" r="7" stroke="currentColor" stroke-width="1.5"/>
<path d="M8 7v4M8 5v.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
</svg>
<div class="tooltip tooltip-down">
<div class="tooltip-title">Network Address</div>
<p class="tooltip-text">Enter the network location of your IP camera. This can be an IP address, hostname, or a complete RTSP URL.</p>
<div class="tooltip-examples">
<div class="tooltip-examples-title">Accepted formats:</div>
<code class="tooltip-example">192.168.1.100 - IP address only</code>
<code class="tooltip-example">camera.local - Hostname/mDNS</code>
<code class="tooltip-example">rtsp://user:pass@192.168.1.100/stream - Full URL</code>
</div>
<p class="tooltip-text"><strong>Where to find it:</strong><br>Check your camera's web interface, router's DHCP leases page, or network scanner app. Most cameras use addresses in the 192.168.x.x range.</p>
<p class="tooltip-text"><strong>Next steps:</strong><br>After entering the address, click "Check Address" to validate the camera connection and proceed to stream discovery.</p>
</div>
</span>
</label>
<input
type="text"
id="network-address"
@@ -73,7 +105,26 @@
<h2 class="screen-title">Camera Configuration</h2>
<div class="form-group">
<label for="address-validated" class="label">Network Address</label>
<label for="address-validated" class="label label-with-info">
Network Address
<span class="info-icon">
<svg viewBox="0 0 16 16" fill="none">
<circle cx="8" cy="8" r="7" stroke="currentColor" stroke-width="1.5"/>
<path d="M8 7v4M8 5v.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
</svg>
<div class="tooltip tooltip-down">
<div class="tooltip-title">Network Address</div>
<p class="tooltip-text">The IP address, hostname, or full RTSP URL of your camera. This is the network location where the camera can be reached.</p>
<div class="tooltip-examples">
<div class="tooltip-examples-title">Examples:</div>
<code class="tooltip-example">192.168.1.100</code>
<code class="tooltip-example">camera.local</code>
<code class="tooltip-example">rtsp://admin:pass@192.168.1.100</code>
</div>
<p class="tooltip-text">Find it in your camera's network settings or router's device list (DHCP leases).</p>
</div>
</span>
</label>
<div class="input-validated">
<input
type="text"
@@ -88,7 +139,27 @@
</div>
<div class="form-group">
<label for="camera-model" class="label">Camera Model <span class="optional">(optional)</span></label>
<label for="camera-model" class="label label-with-info">
Camera Model <span class="optional">(optional)</span>
<span class="info-icon">
<svg viewBox="0 0 16 16" fill="none">
<circle cx="8" cy="8" r="7" stroke="currentColor" stroke-width="1.5"/>
<path d="M8 7v4M8 5v.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
</svg>
<div class="tooltip tooltip-down">
<div class="tooltip-title">Camera Model</div>
<p class="tooltip-text">The manufacturer and model of your IP camera. This helps the system use optimized stream paths for your specific camera brand.</p>
<div class="tooltip-examples">
<div class="tooltip-examples-title">Examples:</div>
<code class="tooltip-example">Hikvision: DS-2CD2142FWD</code>
<code class="tooltip-example">Dahua: IPC-HDW4433C</code>
<code class="tooltip-example">Amcrest: IP4M-1041</code>
<code class="tooltip-example">Reolink: RLC-410</code>
</div>
<p class="tooltip-text">Find it on the camera label, in the camera's web interface (Device Info), or in your purchase documentation. Leave empty for auto-detection.</p>
</div>
</span>
</label>
<div class="autocomplete-wrapper">
<input
type="text"
@@ -104,18 +175,52 @@
</div>
<div class="form-group">
<label for="username" class="label">Username</label>
<label for="username" class="label label-with-info">
Username
<span class="info-icon">
<svg viewBox="0 0 16 16" fill="none">
<circle cx="8" cy="8" r="7" stroke="currentColor" stroke-width="1.5"/>
<path d="M8 7v4M8 5v.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
</svg>
<div class="tooltip">
<div class="tooltip-title">Username</div>
<p class="tooltip-text">The authentication username for accessing your camera's RTSP stream. This is required for most IP cameras to access video feeds.</p>
<div class="tooltip-examples">
<div class="tooltip-examples-title">Common defaults:</div>
<code class="tooltip-example">admin</code>
<code class="tooltip-example">root</code>
<code class="tooltip-example">user</code>
</div>
<p class="tooltip-text">Find it in your camera setup documentation or the camera's web interface under User Management. Change default credentials for security.</p>
</div>
</span>
</label>
<input
type="text"
id="username"
class="input"
value="admin"
placeholder="admin"
autocomplete="off"
>
</div>
<div class="form-group">
<label for="password" class="label">Password</label>
<label for="password" class="label label-with-info">
Password
<span class="info-icon">
<svg viewBox="0 0 16 16" fill="none">
<circle cx="8" cy="8" r="7" stroke="currentColor" stroke-width="1.5"/>
<path d="M8 7v4M8 5v.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
</svg>
<div class="tooltip">
<div class="tooltip-title">Password</div>
<p class="tooltip-text">The authentication password for your camera's RTSP stream. This credential is used together with the username to access the video feed.</p>
<p class="tooltip-text">For security reasons, always use a strong, unique password and avoid default passwords like "12345" or "password".</p>
<p class="tooltip-text">Find it in your camera's documentation, setup guide, or change it via the camera's web interface under Security/User Management settings.</p>
</div>
</span>
</label>
<div class="input-password-wrapper">
<input
type="password"
@@ -134,23 +239,65 @@
</div>
</div>
<div class="form-group">
<label for="channel" class="label label-with-info">
Channel
<span class="info-icon">
<svg viewBox="0 0 16 16" fill="none">
<circle cx="8" cy="8" r="7" stroke="currentColor" stroke-width="1.5"/>
<path d="M8 7v4M8 5v.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
</svg>
<div class="tooltip">
<div class="tooltip-title">Channel Number</div>
<p class="tooltip-text">The channel number identifies which specific camera or video input to access on the device.</p>
<p class="tooltip-text"><strong>For standalone IP cameras:</strong> Always use 0 (default). Single cameras don't use channel numbers.</p>
<p class="tooltip-text"><strong>For NVR/DVR systems ONLY:</strong> Each connected camera has its own channel number. Channel numbering typically starts from 0.</p>
<div class="tooltip-examples">
<div class="tooltip-examples-title">NVR/DVR channel values:</div>
<code class="tooltip-example">0 - First camera on NVR/DVR</code>
<code class="tooltip-example">1 - Second camera on NVR/DVR</code>
<code class="tooltip-example">2-15 - Additional cameras (for 4, 8, 16-channel NVRs)</code>
</div>
<p class="tooltip-text">Check your NVR's camera list in the device web interface to see the correct channel assignment for each camera.</p>
</div>
</span>
</label>
<input
type="number"
id="channel"
class="input"
value="0"
min="0"
max="255"
>
</div>
<details class="advanced-section">
<summary class="advanced-toggle">Advanced</summary>
<div class="advanced-content">
<div class="form-group">
<label for="channel" class="label">Channel</label>
<input
type="number"
id="channel"
class="input"
value="0"
min="0"
max="255"
>
</div>
<div class="form-group">
<label class="label">Resolution <span class="optional">(optional)</span></label>
<label class="label label-with-info">
Resolution <span class="optional">(optional)</span>
<span class="info-icon">
<svg viewBox="0 0 16 16" fill="none">
<circle cx="8" cy="8" r="7" stroke="currentColor" stroke-width="1.5"/>
<path d="M8 7v4M8 5v.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
</svg>
<div class="tooltip">
<div class="tooltip-title">Resolution Filter</div>
<p class="tooltip-text">Optionally filter discovered streams by specific resolution. Leave empty to find all available resolutions. Use this to target specific stream qualities.</p>
<div class="tooltip-examples">
<div class="tooltip-examples-title">Common resolutions:</div>
<code class="tooltip-example">1920 × 1080 - Full HD (main stream)</code>
<code class="tooltip-example">1280 × 720 - HD (sub stream)</code>
<code class="tooltip-example">640 × 480 - VGA (low quality)</code>
<code class="tooltip-example">3840 × 2160 - 4K Ultra HD</code>
</div>
<p class="tooltip-text">Tip: Leave empty for initial discovery, then use specific values to find particular stream types (main vs sub streams).</p>
</div>
</span>
</label>
<div class="input-row">
<input
type="number"
@@ -169,7 +316,26 @@
</div>
<div class="form-group">
<label for="max-streams" class="label">Max Streams</label>
<label for="max-streams" class="label label-with-info">
Max Streams
<span class="info-icon">
<svg viewBox="0 0 16 16" fill="none">
<circle cx="8" cy="8" r="7" stroke="currentColor" stroke-width="1.5"/>
<path d="M8 7v4M8 5v.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
</svg>
<div class="tooltip">
<div class="tooltip-title">Maximum Streams</div>
<p class="tooltip-text">The maximum number of stream URLs to test during discovery. Higher values increase scan time but may find more stream variants. Lower values speed up discovery.</p>
<div class="tooltip-examples">
<div class="tooltip-examples-title">Recommended values:</div>
<code class="tooltip-example">5 - Quick scan (faster)</code>
<code class="tooltip-example">10 - Balanced (default)</code>
<code class="tooltip-example">20-50 - Thorough scan (slower)</code>
</div>
<p class="tooltip-text">Purpose: Controls how many different RTSP URL patterns are tested. Most cameras have 2-5 valid streams (main, sub, mobile, etc.).</p>
</div>
</span>
</label>
<input
type="number"
id="max-streams"
@@ -207,46 +373,11 @@
<p id="progress-text" class="progress-text">Starting scan...</p>
</div>
<div class="stats">
<div class="stat">
<span class="stat-value" id="stat-tested">0</span>
<span class="stat-label">Tested</span>
</div>
<div class="stat">
<span class="stat-value stat-primary" id="stat-found">0</span>
<span class="stat-label">Found</span>
</div>
<div class="stat">
<span class="stat-value" id="stat-remaining">0</span>
<span class="stat-label">Remaining</span>
</div>
</div>
<div id="streams-section" class="streams-section hidden">
<h3 class="section-title">Found Connections</h3>
<div class="carousel-wrapper">
<button id="carousel-prev" class="carousel-arrow carousel-arrow-left" aria-label="Previous stream">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none">
<path d="M15 18l-6-6 6-6" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>
</button>
<div class="carousel">
<div id="carousel-track" class="carousel-track"></div>
</div>
<button id="carousel-next" class="carousel-arrow carousel-arrow-right" aria-label="Next stream">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none">
<path d="M9 18l6-6-6-6" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>
</button>
</div>
<div class="carousel-info">
<p id="carousel-counter" class="carousel-counter">Stream 1 of 1</p>
<div id="carousel-dots" class="carousel-dots"></div>
</div>
<div id="streams-list" class="streams-list"></div>
</div>
</div>
</div>
@@ -265,13 +396,51 @@
<div class="stream-selection-container">
<div class="selected-stream-info">
<p class="stream-label">Main Stream</p>
<div class="stream-label">
<span>Main Stream</span>
<span class="info-icon">
<svg viewBox="0 0 16 16" fill="none">
<circle cx="8" cy="8" r="7" stroke="currentColor" stroke-width="1.5"/>
<path d="M8 7v4M8 5v.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
</svg>
<div class="tooltip tooltip-down">
<div class="tooltip-title">Main Stream</div>
<p class="tooltip-text">The primary high-resolution video stream from your camera. This stream is typically used for recording and high-quality viewing.</p>
<div class="tooltip-examples">
<div class="tooltip-examples-title">Common uses:</div>
<code class="tooltip-example">Recording to disk</code>
<code class="tooltip-example">Live HD viewing</code>
<code class="tooltip-example">High-quality playback</code>
</div>
<p class="tooltip-text">Resolution is usually 1080p (1920×1080) or higher. Higher resolution means better quality but requires more bandwidth and storage.</p>
</div>
</span>
</div>
<p id="selected-main-type" class="selected-type"></p>
<p id="selected-main-url" class="selected-url"></p>
</div>
<div id="sub-stream-info" class="selected-stream-info sub-stream hidden">
<p class="stream-label">Sub Stream</p>
<div class="stream-label">
<span>Sub Stream</span>
<span class="info-icon">
<svg viewBox="0 0 16 16" fill="none">
<circle cx="8" cy="8" r="7" stroke="currentColor" stroke-width="1.5"/>
<path d="M8 7v4M8 5v.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
</svg>
<div class="tooltip tooltip-down">
<div class="tooltip-title">Sub Stream</div>
<p class="tooltip-text">A secondary lower-resolution video stream from your camera. This stream is optimized for object detection and reduces CPU usage.</p>
<div class="tooltip-examples">
<div class="tooltip-examples-title">Common uses:</div>
<code class="tooltip-example">Motion detection</code>
<code class="tooltip-example">Object detection (person, car)</code>
<code class="tooltip-example">Low-bandwidth monitoring</code>
</div>
<p class="tooltip-text">Resolution is usually 640×480 or 720p. Using a sub stream for detection significantly improves performance while maintaining recording quality on the main stream.</p>
</div>
</span>
</div>
<p id="selected-sub-type" class="selected-type"></p>
<p id="selected-sub-url" class="selected-url"></p>
<button id="btn-remove-sub" class="btn-remove-sub">Remove Sub Stream</button>
@@ -280,24 +449,41 @@
<div class="tabs">
<div class="tabs-scroll">
<button class="tab active" data-tab="url">URL</button>
<button class="tab active" data-tab="frigate">Frigate</button>
<button class="tab" data-tab="go2rtc">Go2RTC</button>
<button class="tab" data-tab="frigate">Frigate</button>
<button class="tab" data-tab="url">URL</button>
</div>
</div>
<div class="tab-content">
<div class="tab-pane active" data-pane="url">
<pre id="config-url" class="config-code"></pre>
</div>
<div class="tab-pane" data-pane="go2rtc">
<pre id="config-go2rtc" class="config-code"></pre>
</div>
<div class="tab-pane" data-pane="frigate">
<div class="tab-pane active" data-pane="frigate">
<!-- Input section for existing config -->
<div class="frigate-input-section">
<label class="frigate-label">
Your Current Frigate Config
<label class="frigate-label label-with-info">
Your Current Frigate Config <span class="optional">(optional)</span>
<span class="info-icon">
<svg viewBox="0 0 16 16" fill="none">
<circle cx="8" cy="8" r="7" stroke="currentColor" stroke-width="1.5"/>
<path d="M8 7v4M8 5v.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
</svg>
<div class="tooltip tooltip-down">
<div class="tooltip-title">Frigate Configuration</div>
<p class="tooltip-text">You can either create a new Frigate config or add this camera to your existing configuration.</p>
<p class="tooltip-text"><strong>Option 1: New Config (Recommended for beginners)</strong><br>Leave the example config below as-is, and the system will generate a complete working configuration for you.</p>
<p class="tooltip-text"><strong>Option 2: Add to Existing Config</strong><br>If you already have Frigate running, paste your current config.yml here. The system will intelligently add this camera without breaking your existing setup.</p>
<div class="tooltip-examples">
<div class="tooltip-examples-title">Where to find your config.yml:</div>
<code class="tooltip-example">Docker: /config/config.yml</code>
<code class="tooltip-example">Home Assistant addon: /config/frigate.yml</code>
<code class="tooltip-example">Standalone: /etc/frigate/config.yml</code>
</div>
<p class="tooltip-text">The generator will preserve all your existing cameras and settings, only adding the new camera configuration.</p>
</div>
</span>
<span class="hint">Paste your existing config.yml or leave the example below</span>
</label>
<textarea
@@ -308,16 +494,72 @@
</div>
<!-- Generate button -->
<button id="btn-generate-frigate" class="btn btn-primary btn-generate">
Generate Config
</button>
<div class="button-with-tooltip">
<button id="btn-generate-frigate" class="btn btn-primary btn-generate">
Generate Config
<span class="info-icon info-icon-button">
<svg viewBox="0 0 16 16" fill="none">
<circle cx="8" cy="8" r="7" stroke="currentColor" stroke-width="1.5"/>
<path d="M8 7v4M8 5v.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
</svg>
<div class="tooltip">
<div class="tooltip-title">Generate Configuration</div>
<p class="tooltip-text">This button will process your camera streams and generate a ready-to-use Frigate configuration.</p>
<p class="tooltip-text"><strong>What happens:</strong></p>
<div class="tooltip-examples">
<div class="tooltip-examples-title">Configuration includes:</div>
<code class="tooltip-example">Go2RTC streams setup</code>
<code class="tooltip-example">Camera with detect & record roles</code>
<code class="tooltip-example">Object tracking (person, car, etc.)</code>
<code class="tooltip-example">Recording settings</code>
</div>
<p class="tooltip-text">If you provided an existing config, your camera will be added to it. Otherwise, a complete new configuration will be created.</p>
<p class="tooltip-text">After generation, use Copy or Download buttons to save your config.</p>
</div>
</span>
</button>
</div>
<!-- Output section (hidden by default) -->
<div id="frigate-output-section" class="frigate-output-section hidden">
<label class="frigate-label">Updated Config (Camera Added)</label>
<label class="frigate-label label-with-info">
Updated Config (Camera Added)
<span class="info-icon">
<svg viewBox="0 0 16 16" fill="none">
<circle cx="8" cy="8" r="7" stroke="currentColor" stroke-width="1.5"/>
<path d="M8 7v4M8 5v.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
</svg>
<div class="tooltip tooltip-down">
<div class="tooltip-title">Generated Configuration</div>
<p class="tooltip-text">This is your complete Frigate configuration with the camera successfully added.</p>
<p class="tooltip-text"><strong>What's included:</strong></p>
<div class="tooltip-examples">
<div class="tooltip-examples-title">Configuration sections:</div>
<code class="tooltip-example">go2rtc: Stream definitions</code>
<code class="tooltip-example">cameras: Camera with roles</code>
<code class="tooltip-example">objects: Person, car tracking</code>
<code class="tooltip-example">record: Recording settings</code>
</div>
<p class="tooltip-text"><strong>How to use:</strong><br>Copy or download this configuration and save it as <code>config.yml</code> in your Frigate directory. Restart Frigate to apply the changes.</p>
<p class="tooltip-text">If you added to existing config, your previous cameras and settings are preserved - only the new camera was added.</p>
</div>
</span>
</label>
<pre id="config-frigate" class="config-code"></pre>
</div>
</div>
<div class="tab-pane" data-pane="go2rtc">
<pre id="config-go2rtc" class="config-code"></pre>
</div>
<div class="tab-pane" data-pane="url">
<pre id="config-url" class="config-code"></pre>
</div>
</div>
<div class="actions">
@@ -337,15 +579,65 @@
</div>
<div class="secondary-actions">
<button id="btn-add-sub-stream" class="btn btn-primary">
<svg width="20" height="20" viewBox="0 0 20 20" fill="none">
<path d="M10 4v12M4 10h12" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>
Add Sub Stream
</button>
<button id="btn-new-search" class="btn btn-outline">
Add Another Camera
</button>
<div class="button-with-tooltip">
<button id="btn-add-sub-stream" class="btn btn-primary">
<svg width="20" height="20" viewBox="0 0 20 20" fill="none">
<path d="M10 4v12M4 10h12" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>
Add Sub Stream
<span class="info-icon info-icon-button">
<svg viewBox="0 0 16 16" fill="none">
<circle cx="8" cy="8" r="7" stroke="currentColor" stroke-width="1.5"/>
<path d="M8 7v4M8 5v.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
</svg>
<div class="tooltip">
<div class="tooltip-title">Add Sub Stream</div>
<p class="tooltip-text">Add a secondary lower-resolution stream for efficient object detection and motion monitoring.</p>
<p class="tooltip-text"><strong>Why add a sub stream?</strong></p>
<div class="tooltip-examples">
<div class="tooltip-examples-title">Benefits:</div>
<code class="tooltip-example">Reduces CPU usage by 50-70%</code>
<code class="tooltip-example">Faster object detection</code>
<code class="tooltip-example">Lower bandwidth consumption</code>
<code class="tooltip-example">Main stream quality preserved</code>
</div>
<p class="tooltip-text"><strong>How it works:</strong><br>After clicking, you'll return to the stream list where you can select a lower-resolution stream (usually 640×480 or 720p). Frigate will use this for detection while recording the main stream in full quality.</p>
<p class="tooltip-text"><strong>Recommended:</strong> Most IP cameras support multiple streams. Using a sub stream is highly recommended for optimal Frigate performance.</p>
</div>
</span>
</button>
</div>
<div class="button-with-tooltip">
<button id="btn-new-search" class="btn btn-outline">
Add Another Camera
<span class="info-icon info-icon-button-outline">
<svg viewBox="0 0 16 16" fill="none">
<circle cx="8" cy="8" r="7" stroke="currentColor" stroke-width="1.5"/>
<path d="M8 7v4M8 5v.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
</svg>
<div class="tooltip">
<div class="tooltip-title">Add Another Camera</div>
<p class="tooltip-text">Start the configuration process for a new camera from the beginning.</p>
<p class="tooltip-text"><strong>⚠️ Important - Save First!</strong><br>Before clicking this button, make sure to save your current configuration using Copy or Download buttons above. This will reset the form.</p>
<p class="tooltip-text"><strong>What happens:</strong></p>
<div class="tooltip-examples">
<div class="tooltip-examples-title">The process will:</div>
<code class="tooltip-example">1. Return to address input screen</code>
<code class="tooltip-example">2. Clear current camera settings</code>
<code class="tooltip-example">3. Start fresh discovery</code>
<code class="tooltip-example">4. Generate new config for next camera</code>
</div>
<p class="tooltip-text">You can then add the new camera to your saved Frigate config by pasting it in the config field.</p>
</div>
</span>
</button>
</div>
</div>
</div>
</div>
@@ -354,6 +646,6 @@
<!-- Toast Notification -->
<div id="toast" class="toast hidden"></div>
<script type="module" src="/js/main.js"></script>
<script type="module" src="js/main.js"></script>
</body>
</html>
+11 -2
View File
@@ -1,15 +1,24 @@
import { MockCameraSearch } from '../mock/mock-data.js';
export class CameraSearchAPI {
constructor(baseURL = null) {
constructor(baseURL = null, useMock = false) {
// Use relative URLs since API and UI are on the same port
if (!baseURL) {
this.baseURL = '';
} else {
this.baseURL = baseURL;
}
this.useMock = useMock;
this.mockAPI = useMock ? new MockCameraSearch() : null;
}
async search(query, limit = 10) {
const response = await fetch(`${this.baseURL}/api/v1/cameras/search`, {
// Use mock API if enabled
if (this.useMock) {
return await this.mockAPI.search(query, limit);
}
const response = await fetch(`${this.baseURL}api/v1/cameras/search`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
+14 -3
View File
@@ -1,5 +1,7 @@
import { MockStreamDiscovery } from '../mock/mock-data.js';
export class StreamDiscoveryAPI {
constructor(baseURL = null) {
constructor(baseURL = null, useMock = false) {
// Use relative URLs since API and UI are on the same port
if (!baseURL) {
this.baseURL = '';
@@ -7,14 +9,20 @@ export class StreamDiscoveryAPI {
this.baseURL = baseURL;
}
this.eventSource = null;
this.useMock = useMock;
this.mockAPI = useMock ? new MockStreamDiscovery() : null;
}
discover(request, callbacks) {
this.close();
const url = new URL(`${this.baseURL}/api/v1/streams/discover`, window.location.origin);
// Use mock API if enabled
if (this.useMock) {
this.mockAPI.discover(request, callbacks);
return;
}
fetch(url, {
fetch(`${this.baseURL}api/v1/streams/discover`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@@ -93,6 +101,9 @@ export class StreamDiscoveryAPI {
}
close() {
if (this.useMock && this.mockAPI) {
this.mockAPI.close();
}
if (this.eventSource) {
this.eventSource.close();
this.eventSource = null;
+82 -32
View File
@@ -1,18 +1,36 @@
import { CameraSearchAPI } from './api/camera-search.js';
import { StreamDiscoveryAPI } from './api/stream-discovery.js';
import { MockCameraAPI } from './mock/mock-camera-api.js';
import { MockStreamAPI } from './mock/mock-stream-api.js';
import { SearchForm } from './ui/search-form.js';
import { StreamCarousel } from './ui/stream-carousel.js';
import { StreamList } from './ui/stream-list.js';
import { ConfigPanel } from './ui/config-panel.js';
import { FrigateGenerator } from './config-generators/frigate/index.js';
import { showToast } from './utils/toast.js';
class StrixApp {
constructor() {
this.cameraAPI = new CameraSearchAPI();
this.streamAPI = new StreamDiscoveryAPI();
// Check if mock mode is enabled via URL parameter
const urlParams = new URLSearchParams(window.location.search);
const isMockMode = urlParams.get('mock') === 'true';
if (isMockMode) {
console.log('🎭 Mock mode enabled - using fake data');
this.cameraAPI = new MockCameraAPI();
this.streamAPI = new MockStreamAPI();
// Show mock mode badge
const mockBadge = document.getElementById('mock-mode-badge');
if (mockBadge) {
mockBadge.classList.remove('hidden');
}
} else {
this.cameraAPI = new CameraSearchAPI();
this.streamAPI = new StreamDiscoveryAPI();
}
this.searchForm = new SearchForm();
this.carousel = new StreamCarousel();
this.streamList = new StreamList();
this.configPanel = new ConfigPanel();
this.currentAddress = '';
@@ -20,15 +38,41 @@ class StrixApp {
this.selectedMainStream = null;
this.selectedSubStream = null;
this.isSelectingSubStream = false;
this.frigateConfigGenerated = false; // Track if Frigate config has been generated
this.init();
}
init() {
this.setupEventListeners();
this.prefillNetworkAddress();
this.showScreen('address');
}
/**
* Pre-fill network address input with smart default based on server IP
*/
prefillNetworkAddress() {
const hostname = window.location.hostname;
const input = document.getElementById('network-address');
// Skip if localhost or empty
if (!hostname || hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '0.0.0.0') {
return;
}
// Check if hostname is an IP address (matches pattern like 192.168.1.1)
const ipPattern = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/;
const match = hostname.match(ipPattern);
if (match) {
// Extract first three octets (e.g., "192.168.1." from "192.168.1.254")
const networkPrefix = `${match[1]}.${match[2]}.${match[3]}.`;
input.value = networkPrefix;
input.placeholder = `${networkPrefix}100`;
}
}
setupEventListeners() {
// Screen 1: Address input
document.getElementById('btn-check-address').addEventListener('click', () => this.checkAddress());
@@ -77,24 +121,6 @@ class StrixApp {
this.showScreen('config');
});
// Carousel navigation
document.getElementById('carousel-prev').addEventListener('click', () => {
this.carousel.prev();
});
document.getElementById('carousel-next').addEventListener('click', () => {
this.carousel.next();
});
// Keyboard navigation
document.addEventListener('keydown', (e) => {
const currentScreen = document.querySelector('.screen.active').id;
if (currentScreen === 'screen-discovery') {
if (e.key === 'ArrowLeft') this.carousel.prev();
if (e.key === 'ArrowRight') this.carousel.next();
}
});
// Screen 4: Configuration output
document.getElementById('btn-back-to-streams').addEventListener('click', () => {
this.isSelectingSubStream = false;
@@ -155,10 +181,12 @@ class StrixApp {
try {
const urlObj = new URL(url);
// Extract credentials
// Extract credentials (only override if provided in URL)
if (urlObj.username) {
document.getElementById('username').value = urlObj.username;
}
// If no username in URL, keep the default "admin" value
if (urlObj.password) {
document.getElementById('password').value = urlObj.password;
}
@@ -285,9 +313,6 @@ class StrixApp {
resetDiscoveryUI() {
document.getElementById('progress-fill').style.width = '0%';
document.getElementById('progress-text').textContent = 'Starting scan...';
document.getElementById('stat-tested').textContent = '0';
document.getElementById('stat-found').textContent = '0';
document.getElementById('stat-remaining').textContent = '0';
document.getElementById('streams-section').classList.add('hidden');
this.currentStreams = [];
}
@@ -298,9 +323,6 @@ class StrixApp {
document.getElementById('progress-fill').style.width = `${percentage}%`;
document.getElementById('progress-text').textContent = `Testing streams... ${Math.round(percentage)}%`;
document.getElementById('stat-tested').textContent = data.tested;
document.getElementById('stat-found').textContent = data.found;
document.getElementById('stat-remaining').textContent = data.remaining;
}
handleStreamFound(data) {
@@ -312,8 +334,8 @@ class StrixApp {
streamsSection.classList.remove('hidden');
}
// Update carousel
this.carousel.render(this.currentStreams, (stream, index) => {
// Update stream list
this.streamList.render(this.currentStreams, (stream, index) => {
this.selectStream(stream, index);
});
}
@@ -338,16 +360,22 @@ class StrixApp {
// Selecting main stream
this.selectedMainStream = stream;
this.selectedSubStream = null;
this.frigateConfigGenerated = false; // Reset Frigate config state
this.configPanel.render(this.selectedMainStream, this.selectedSubStream);
this.updateSubStreamUI();
this.showScreen('output');
// Hide action buttons initially since Frigate tab is active by default
document.querySelector('.actions').style.display = 'none';
} else {
// Selecting sub stream
this.selectedSubStream = stream;
this.isSelectingSubStream = false;
this.frigateConfigGenerated = false; // Reset Frigate config state
this.configPanel.render(this.selectedMainStream, this.selectedSubStream);
this.updateSubStreamUI();
this.showScreen('output');
// Hide action buttons initially since Frigate tab is active by default
document.querySelector('.actions').style.display = 'none';
}
}
@@ -369,8 +397,16 @@ class StrixApp {
removeSubStream() {
this.selectedSubStream = null;
this.frigateConfigGenerated = false; // Reset Frigate config state when sub stream is removed
this.configPanel.render(this.selectedMainStream, this.selectedSubStream);
this.updateSubStreamUI();
// Hide action buttons if on Frigate tab
const activeTab = document.querySelector('.tab.active').dataset.tab;
if (activeTab === 'frigate') {
document.querySelector('.actions').style.display = 'none';
}
showToast('Sub stream removed');
}
@@ -395,6 +431,10 @@ class StrixApp {
document.getElementById('config-frigate').textContent = newConfig;
document.getElementById('frigate-output-section').classList.remove('hidden');
// Mark as generated and show action buttons
this.frigateConfigGenerated = true;
document.querySelector('.actions').style.display = 'flex';
// Scroll to result
document.getElementById('frigate-output-section').scrollIntoView({
behavior: 'smooth',
@@ -429,6 +469,16 @@ class StrixApp {
// Update tab panes
document.querySelectorAll('.tab-pane').forEach(p => p.classList.remove('active'));
document.querySelector(`.tab-pane[data-pane="${tabName}"]`).classList.add('active');
// Show/hide action buttons based on tab and Frigate config state
const actionsContainer = document.querySelector('.actions');
if (tabName === 'frigate' && !this.frigateConfigGenerated) {
// Hide buttons on Frigate tab until config is generated
actionsContainer.style.display = 'none';
} else {
// Show buttons for other tabs or after Frigate config is generated
actionsContainer.style.display = 'flex';
}
}
copyConfig() {
@@ -482,7 +532,7 @@ class StrixApp {
document.getElementById('camera-model').value = '';
document.getElementById('camera-model').disabled = false;
document.getElementById('camera-model').placeholder = 'Start typing...';
document.getElementById('username').value = '';
document.getElementById('username').value = 'admin'; // Reset to default value
document.getElementById('password').value = '';
document.getElementById('channel').value = '0';
document.getElementById('max-streams').value = '10';
+49
View File
@@ -0,0 +1,49 @@
// Mock implementation of CameraSearchAPI for development
export class MockCameraAPI {
constructor() {
this.mockCameras = [
{ brand: "Hikvision", model: "DS-2CD2042WD-I" },
{ brand: "Hikvision", model: "DS-2CD2142FWD-I" },
{ brand: "Hikvision", model: "DS-2CD2032-I" },
{ brand: "Hikvision", model: "DS-2CD2385G1-I" },
{ brand: "Dahua", model: "IPC-HFW4431R-Z" },
{ brand: "Dahua", model: "IPC-HDBW4433R-ZS" },
{ brand: "Dahua", model: "DH-IPC-HFW2431S-S-S2" },
{ brand: "Dahua", model: "IPC-HDW2531T-AS-S2" },
{ brand: "Axis", model: "M3046-V" },
{ brand: "Axis", model: "P3245-LVE" },
{ brand: "Axis", model: "M5525-E" },
{ brand: "Uniview", model: "IPC322SR3-DVS28-F" },
{ brand: "Uniview", model: "IPC2124SR3-DPF40" },
{ brand: "Reolink", model: "RLC-410" },
{ brand: "Reolink", model: "RLC-520A" },
{ brand: "Reolink", model: "RLC-810A" },
{ brand: "TP-Link", model: "VIGI C300HP-4" },
{ brand: "TP-Link", model: "VIGI C540V" },
{ brand: "Amcrest", model: "IP8M-2496EW" },
{ brand: "Amcrest", model: "IP4M-1041B" },
{ brand: "Foscam", model: "FI9900P" },
{ brand: "Foscam", model: "R5" },
];
}
async search(query, limit = 10) {
// Simulate network delay
await this.delay(150);
const lowerQuery = query.toLowerCase();
const filtered = this.mockCameras.filter(camera => {
const searchText = `${camera.brand} ${camera.model}`.toLowerCase();
return searchText.includes(lowerQuery);
});
return {
cameras: filtered.slice(0, limit),
total: filtered.length
};
}
delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
+209
View File
@@ -0,0 +1,209 @@
// Mock data for development and testing
export const MOCK_CAMERAS = [
{ brand: "Hikvision", model: "DS-2CD2143G0-I" },
{ brand: "Hikvision", model: "DS-2CD2385G1-I" },
{ brand: "Hikvision", model: "DS-2CD2T85G1-I8" },
{ brand: "Dahua", model: "IPC-HFW5831E-Z5E" },
{ brand: "Dahua", model: "IPC-HDW5831R-ZE" },
{ brand: "Axis", model: "M3046-V" },
{ brand: "Axis", model: "P3245-LVE" },
{ brand: "Uniview", model: "IPC2324LB-ADZK-G" },
{ brand: "Reolink", model: "RLC-810A" },
{ brand: "TP-Link", model: "VIGI C540V" }
];
export const MOCK_STREAMS = [
{
url: "rtsp://admin:password@192.168.1.100:554/stream1",
type: "FFMPEG",
resolution: "1920x1080",
codec: "h264",
fps: 25,
bitrate: 4096,
audio: true
},
{
url: "rtsp://admin:password@192.168.1.100:554/stream2",
type: "FFMPEG",
resolution: "640x360",
codec: "h264",
fps: 15,
bitrate: 512,
audio: true
},
{
url: "http://admin:password@192.168.1.100:80/onvif/device_service",
type: "ONVIF",
resolution: "1920x1080",
codec: "h264",
fps: 25,
bitrate: 4096,
audio: false
},
{
url: "rtsp://admin:password@192.168.1.100/live/main",
type: "FFMPEG",
resolution: "2560x1440",
codec: "h265",
fps: 30,
bitrate: 6144,
audio: true
},
{
url: "rtsp://admin:password@192.168.1.100/live/sub",
type: "FFMPEG",
resolution: "704x576",
codec: "h264",
fps: 15,
bitrate: 768,
audio: false
},
{
url: "rtsp://admin:password@192.168.1.100:554/ch01/0",
type: "FFMPEG",
resolution: "3840x2160",
codec: "h265",
fps: 25,
bitrate: 8192,
audio: true
},
{
url: "rtsp://admin:password@192.168.1.100:554/ch01/1",
type: "FFMPEG",
resolution: "1280x720",
codec: "h264",
fps: 20,
bitrate: 2048,
audio: false
},
{
url: "http://admin:password@192.168.1.100:8080/video.mjpeg",
type: "MJPEG",
resolution: "1920x1080",
codec: "mjpeg",
fps: 10,
bitrate: 3072,
audio: false
},
{
url: "rtsp://admin:password@192.168.1.100/h264_stream",
type: "FFMPEG",
resolution: "1920x1080",
codec: "h264",
fps: 30,
bitrate: 4096,
audio: true
},
{
url: "http://admin:password@192.168.1.100:8081/stream.m3u8",
type: "HLS",
resolution: "1920x1080",
codec: "h264",
fps: 25,
bitrate: 4096,
audio: true
}
];
// Mock Camera Search API
export class MockCameraSearch {
async search(query, limit = 10) {
// Simulate network delay
await this.delay(100);
const results = MOCK_CAMERAS.filter(camera => {
const searchStr = `${camera.brand} ${camera.model}`.toLowerCase();
return searchStr.includes(query.toLowerCase());
});
return {
cameras: results.slice(0, limit),
total: results.length
};
}
delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
// Mock Stream Discovery API
export class MockStreamDiscovery {
constructor() {
this.isRunning = false;
this.timeoutId = null;
}
discover(request, callbacks) {
this.isRunning = true;
let tested = 0;
const totalToTest = 516;
const foundStreams = [...MOCK_STREAMS];
// Initial progress
callbacks.onProgress({
tested: 0,
found: 0,
remaining: totalToTest
});
// Simulate progressive testing
const progressInterval = setInterval(() => {
if (!this.isRunning) {
clearInterval(progressInterval);
return;
}
tested += Math.floor(Math.random() * 30) + 20;
if (tested > totalToTest) tested = totalToTest;
callbacks.onProgress({
tested: tested,
found: foundStreams.length,
remaining: totalToTest - tested
});
if (tested >= totalToTest) {
clearInterval(progressInterval);
}
}, 200);
// Send found streams progressively
let streamIndex = 0;
const streamInterval = setInterval(() => {
if (!this.isRunning) {
clearInterval(streamInterval);
return;
}
if (streamIndex < foundStreams.length) {
callbacks.onStreamFound({
stream: foundStreams[streamIndex]
});
streamIndex++;
} else {
clearInterval(streamInterval);
}
}, 800);
// Complete after ~7.7 seconds
this.timeoutId = setTimeout(() => {
if (!this.isRunning) return;
callbacks.onComplete({
total_found: foundStreams.length,
duration: 7.7
});
this.isRunning = false;
}, 7700);
}
close() {
this.isRunning = false;
if (this.timeoutId) {
clearTimeout(this.timeoutId);
this.timeoutId = null;
}
}
}
+145
View File
@@ -0,0 +1,145 @@
// Mock implementation of StreamDiscoveryAPI for development
export class MockStreamAPI {
constructor() {
this.mockStreams = [
{
url: "rtsp://192.168.1.100:554/Streaming/Channels/101",
path: "/Streaming/Channels/101",
type: "FFMPEG",
resolution: "1920x1080",
codec: "H.264",
fps: 25,
bitrate: 4096000,
has_audio: true
},
{
url: "http://192.168.1.100/snap.jpg",
path: "/snap.jpg",
type: "JPEG",
resolution: "1920x1080",
codec: "JPEG",
fps: 1,
bitrate: 0,
has_audio: false
},
{
url: "http://192.168.1.100/video.mjpg",
path: "/video.mjpg",
type: "MJPEG",
resolution: "1280x720",
codec: "MJPEG",
fps: 10,
bitrate: 2048000,
has_audio: false
},
{
url: "http://192.168.1.100/stream/live.m3u8",
path: "/stream/live.m3u8",
type: "HLS",
resolution: "1920x1080",
codec: "H.264",
fps: 25,
bitrate: 3072000,
has_audio: true
},
{
url: "http://192.168.1.100/videostream.cgi?user=admin&pwd=12345",
path: "/videostream.cgi?user=admin&pwd=12345",
type: "HTTP_VIDEO",
resolution: "1280x960",
codec: "H.264",
fps: 20,
bitrate: 2048000,
has_audio: false
},
{
url: "bubble://192.168.1.100:34567/bubble/live?ch=0&stream=0",
path: "/bubble/live?ch=0&stream=0",
type: "BUBBLE",
resolution: "1920x1080",
codec: "H.264",
fps: 25,
bitrate: 3072000,
has_audio: true
},
{
url: "rtsp://192.168.1.100:554/cam/realmonitor?channel=1&subtype=0",
path: "/cam/realmonitor?channel=1&subtype=0",
type: "ONVIF",
resolution: "2560x1440",
codec: "H.265",
fps: 30,
bitrate: 6144000,
has_audio: true
}
];
}
discover(request, callbacks) {
const totalToScan = 150;
const streamsToFind = this.mockStreams;
let tested = 0;
let found = 0;
const startTime = Date.now();
// Simulate progressive discovery
const interval = setInterval(() => {
const increment = Math.floor(Math.random() * 8) + 3;
tested = Math.min(tested + increment, totalToScan);
const remaining = totalToScan - tested;
// Send progress event
if (callbacks.onProgress) {
callbacks.onProgress({
tested: tested,
found: found,
remaining: remaining
});
}
// Randomly find streams
if (found < streamsToFind.length && Math.random() > 0.6) {
const stream = streamsToFind[found];
found++;
if (callbacks.onStreamFound) {
callbacks.onStreamFound({
stream: stream
});
}
}
// Complete when done
if (tested >= totalToScan) {
clearInterval(interval);
// Send any remaining streams
while (found < streamsToFind.length) {
const stream = streamsToFind[found];
found++;
if (callbacks.onStreamFound) {
callbacks.onStreamFound({
stream: stream
});
}
}
const duration = (Date.now() - startTime) / 1000;
if (callbacks.onComplete) {
callbacks.onComplete({
total_tested: totalToScan,
total_found: found,
duration: duration
});
}
}
}, 400);
}
close() {
// Nothing to close in mock mode
}
}
+265
View File
@@ -0,0 +1,265 @@
export class StreamList {
constructor() {
this.listContainer = document.getElementById('streams-list');
this.streams = [];
this.onUseCallback = null;
this.expandedIndex = null;
}
render(streams, onUseCallback) {
this.streams = streams;
this.onUseCallback = onUseCallback;
// Render stream items
this.listContainer.innerHTML = streams.map((stream, index) => this.renderItem(stream, index)).join('');
// Attach event listeners
this.attachEventListeners();
}
renderItem(stream, index) {
const icon = this.getStreamIcon(stream.type);
const isExpanded = this.expandedIndex === index;
const truncatedUrl = this.truncateURL(stream.url, 60);
return `
<div class="stream-item ${isExpanded ? 'expanded' : ''}" data-index="${index}">
<div class="stream-item-header" data-index="${index}">
<div class="stream-item-main">
<div class="stream-info-left">
<div class="stream-type-badge">
${icon}
<span>${stream.type}</span>
${this.getStreamTypeTooltip(stream.type)}
</div>
<div class="stream-url-preview">${truncatedUrl}</div>
</div>
<button class="stream-toggle" data-index="${index}" aria-label="Toggle details">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" class="chevron">
<path d="M4 6l4 4 4-4" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>
</button>
</div>
<button class="btn btn-primary btn-use-stream" data-index="${index}">Use Stream</button>
</div>
<div class="stream-item-details ${isExpanded ? 'visible' : ''}">
<div class="stream-url-full">${stream.url}</div>
${stream.resolution ? `<div class="stream-meta-item"><span class="meta-label">Resolution:</span> ${stream.resolution}</div>` : ''}
${stream.codec ? `<div class="stream-meta-item"><span class="meta-label">Codec:</span> ${stream.codec}${stream.fps ? `${stream.fps} fps` : ''}${stream.bitrate ? `${Math.round(stream.bitrate / 1000)} Kbps` : ''}</div>` : ''}
${stream.has_audio ? '<div class="stream-meta-item"><span class="meta-label">Audio:</span> Yes</div>' : ''}
</div>
</div>
`;
}
truncateURL(url, maxLength = 60) {
if (url.length <= maxLength) {
return url;
}
return url.substring(0, maxLength) + '...';
}
getStreamIcon(type) {
const icons = {
'FFMPEG': '<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><rect x="3" y="4" width="14" height="12" rx="1" stroke="currentColor" stroke-width="1.5"/><circle cx="7" cy="8" r="1" fill="currentColor"/><path d="M14 14l-3-2-3 2V8l3 2 3-2v6z" fill="currentColor"/></svg>',
'ONVIF': '<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="2" fill="currentColor"/><circle cx="10" cy="10" r="5" stroke="currentColor" stroke-width="1.5" stroke-dasharray="2 2"/><circle cx="10" cy="10" r="8" stroke="currentColor" stroke-width="1.5" stroke-dasharray="3 3"/></svg>',
'JPEG': '<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><rect x="3" y="4" width="14" height="12" rx="1" stroke="currentColor" stroke-width="1.5"/><circle cx="7" cy="8" r="1" fill="currentColor"/><path d="M3 13l4-4 3 3 5-5" stroke="currentColor" stroke-width="1.5"/></svg>',
'MJPEG': '<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><rect x="2" y="4" width="7" height="12" rx="1" stroke="currentColor" stroke-width="1.5"/><rect x="11" y="4" width="7" height="12" rx="1" stroke="currentColor" stroke-width="1.5"/><path d="M5 8l2 2-2 2M14 8l2 2-2 2" stroke="currentColor" stroke-width="1.5"/></svg>',
'HLS': '<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="7" stroke="currentColor" stroke-width="1.5"/><path d="M10 6v8M6 10h8" stroke="currentColor" stroke-width="1.5"/></svg>',
'HTTP_VIDEO': '<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M7 6l6 4-6 4V6z" fill="currentColor"/><circle cx="10" cy="10" r="8" stroke="currentColor" stroke-width="1.5"/></svg>',
'BUBBLE': '<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="7" stroke="currentColor" stroke-width="1.5"/><circle cx="7" cy="9" r="1.5" fill="currentColor"/><circle cx="10" cy="9" r="1.5" fill="currentColor"/><circle cx="13" cy="9" r="1.5" fill="currentColor"/><path d="M6 13q2 2 4 2t4-2" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>'
};
return icons[type] || icons['FFMPEG'];
}
getStreamTypeTooltip(type) {
const tooltips = {
'FFMPEG': `
<span class="info-icon info-icon-stream">
<svg viewBox="0 0 16 16" fill="none">
<circle cx="8" cy="8" r="7" stroke="currentColor" stroke-width="1.5"/>
<path d="M8 7v4M8 5v.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
</svg>
<div class="tooltip tooltip-down">
<div class="tooltip-title">FFMPEG Stream</div>
<p class="tooltip-text">Standard video stream decoded by FFmpeg. Most compatible and widely supported format for RTSP, HTTP, and other protocols.</p>
<div class="tooltip-examples">
<div class="tooltip-examples-title">Features:</div>
<code class="tooltip-example"> Universal compatibility</code>
<code class="tooltip-example"> Supports H.264, H.265, MJPEG</code>
<code class="tooltip-example"> Works with most cameras</code>
<code class="tooltip-example"> Best for recording</code>
</div>
<p class="tooltip-text"><strong>Best for:</strong> Main streams, recording, high-quality playback. Default choice for most use cases.</p>
</div>
</span>
`,
'ONVIF': `
<span class="info-icon info-icon-stream">
<svg viewBox="0 0 16 16" fill="none">
<circle cx="8" cy="8" r="7" stroke="currentColor" stroke-width="1.5"/>
<path d="M8 7v4M8 5v.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
</svg>
<div class="tooltip tooltip-down">
<div class="tooltip-title">ONVIF Stream</div>
<p class="tooltip-text">Industry standard protocol for IP cameras. Discovered via ONVIF specification, ensuring maximum compatibility with camera features.</p>
<div class="tooltip-examples">
<div class="tooltip-examples-title">Features:</div>
<code class="tooltip-example"> Standardized protocol</code>
<code class="tooltip-example"> Auto-discovery support</code>
<code class="tooltip-example"> PTZ control capable</code>
<code class="tooltip-example"> Vendor-independent</code>
</div>
<p class="tooltip-text"><strong>Best for:</strong> Enterprise cameras, systems requiring standardization, cameras with PTZ controls.</p>
</div>
</span>
`,
'JPEG': `
<span class="info-icon info-icon-stream">
<svg viewBox="0 0 16 16" fill="none">
<circle cx="8" cy="8" r="7" stroke="currentColor" stroke-width="1.5"/>
<path d="M8 7v4M8 5v.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
</svg>
<div class="tooltip tooltip-down">
<div class="tooltip-title">JPEG Snapshot</div>
<p class="tooltip-text">Single static image endpoint. Can be converted to video stream by repeatedly fetching images.</p>
<div class="tooltip-examples">
<div class="tooltip-examples-title">Features:</div>
<code class="tooltip-example"> Low bandwidth</code>
<code class="tooltip-example"> Simple HTTP request</code>
<code class="tooltip-example"> No streaming protocol needed</code>
<code class="tooltip-example"> Limited framerate (1-10 fps)</code>
</div>
<p class="tooltip-text"><strong>Best for:</strong> Thumbnails, snapshots, very low bandwidth scenarios. Not recommended for recording.</p>
</div>
</span>
`,
'MJPEG': `
<span class="info-icon info-icon-stream">
<svg viewBox="0 0 16 16" fill="none">
<circle cx="8" cy="8" r="7" stroke="currentColor" stroke-width="1.5"/>
<path d="M8 7v4M8 5v.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
</svg>
<div class="tooltip tooltip-down">
<div class="tooltip-title">MJPEG Stream</div>
<p class="tooltip-text">Motion JPEG - sequence of JPEG images transmitted continuously. Simple but bandwidth-intensive.</p>
<div class="tooltip-examples">
<div class="tooltip-examples-title">Features:</div>
<code class="tooltip-example"> Simple HTTP streaming</code>
<code class="tooltip-example"> No complex codecs</code>
<code class="tooltip-example"> Frame-by-frame</code>
<code class="tooltip-example"> High bandwidth usage</code>
</div>
<p class="tooltip-text"><strong>Best for:</strong> Sub streams, low-latency monitoring, simple camera integration. Higher bandwidth than H.264.</p>
</div>
</span>
`,
'HLS': `
<span class="info-icon info-icon-stream">
<svg viewBox="0 0 16 16" fill="none">
<circle cx="8" cy="8" r="7" stroke="currentColor" stroke-width="1.5"/>
<path d="M8 7v4M8 5v.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
</svg>
<div class="tooltip tooltip-down">
<div class="tooltip-title">HLS Stream</div>
<p class="tooltip-text">HTTP Live Streaming - Apple's adaptive bitrate streaming protocol. Delivers video in small chunks over HTTP.</p>
<div class="tooltip-examples">
<div class="tooltip-examples-title">Features:</div>
<code class="tooltip-example"> Adaptive bitrate</code>
<code class="tooltip-example"> Wide browser support</code>
<code class="tooltip-example"> Firewall-friendly (HTTP)</code>
<code class="tooltip-example"> Higher latency (5-30s)</code>
</div>
<p class="tooltip-text"><strong>Best for:</strong> Web playback, public streaming, CDN delivery. Not ideal for real-time monitoring.</p>
</div>
</span>
`,
'HTTP_VIDEO': `
<span class="info-icon info-icon-stream">
<svg viewBox="0 0 16 16" fill="none">
<circle cx="8" cy="8" r="7" stroke="currentColor" stroke-width="1.5"/>
<path d="M8 7v4M8 5v.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
</svg>
<div class="tooltip tooltip-down">
<div class="tooltip-title">HTTP Video Stream</div>
<p class="tooltip-text">Generic HTTP-based video stream. Simple protocol that works over standard web connections.</p>
<div class="tooltip-examples">
<div class="tooltip-examples-title">Features:</div>
<code class="tooltip-example"> Simple HTTP protocol</code>
<code class="tooltip-example"> No special ports</code>
<code class="tooltip-example"> Firewall-friendly</code>
<code class="tooltip-example"> Direct browser playback</code>
</div>
<p class="tooltip-text"><strong>Best for:</strong> Quick viewing, simple setups, scenarios where RTSP ports are blocked.</p>
</div>
</span>
`,
'BUBBLE': `
<span class="info-icon info-icon-stream">
<svg viewBox="0 0 16 16" fill="none">
<circle cx="8" cy="8" r="7" stroke="currentColor" stroke-width="1.5"/>
<path d="M8 7v4M8 5v.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
</svg>
<div class="tooltip tooltip-down">
<div class="tooltip-title">BUBBLE / DVRIP Protocol</div>
<p class="tooltip-text">Proprietary protocol for Chinese DVR/NVR cameras. Also known as: ESeeCloud, dvr163, DVR-IP, NetSurveillance, Sofia protocol, XMeye SDK.</p>
<div class="tooltip-examples">
<div class="tooltip-examples-title">Compatible brands:</div>
<code class="tooltip-example">XMEye, Floureon, ZOSI</code>
<code class="tooltip-example">Sannce, Annke, DVR163</code>
<code class="tooltip-example">ESeeCloud, NetSurveillance</code>
</div>
<div class="tooltip-examples">
<div class="tooltip-examples-title">Features:</div>
<code class="tooltip-example"> Proprietary protocol</code>
<code class="tooltip-example"> Go2RTC converts to standard</code>
<code class="tooltip-example"> Two-way audio support</code>
<code class="tooltip-example"> TCP only (port 34567)</code>
</div>
<p class="tooltip-text"><strong>Note:</strong> Automatically converted to standard RTSP format by Go2RTC. Works seamlessly with Frigate without additional configuration.</p>
</div>
</span>
`
};
return tooltips[type] || '';
}
attachEventListeners() {
// Click on header to toggle
this.listContainer.querySelectorAll('.stream-item-header').forEach(header => {
header.addEventListener('click', (e) => {
// Don't toggle if clicking "Use Stream" button
if (e.target.closest('.btn-use-stream')) {
return;
}
const index = parseInt(header.dataset.index);
this.toggleExpand(index);
});
});
// Use Stream buttons
this.listContainer.querySelectorAll('.btn-use-stream').forEach(btn => {
btn.addEventListener('click', (e) => {
e.stopPropagation(); // Prevent toggle
const index = parseInt(e.target.dataset.index);
if (this.onUseCallback) {
this.onUseCallback(this.streams[index], index);
}
});
});
}
toggleExpand(index) {
if (this.expandedIndex === index) {
// Collapse if already expanded
this.expandedIndex = null;
} else {
// Expand new item
this.expandedIndex = index;
}
// Re-render to update state
this.render(this.streams, this.onUseCallback);
}
}