27 Commits

Author SHA1 Message Date
eduard256 eedce14731 Merge branch 'develop' 2025-11-26 13:01:32 +03:00
eduard256 9975aa71de Release v1.0.8: Use host network mode for Docker deployments 2025-11-26 13:01:27 +03:00
eduard256 38e4af230f Use host network mode for Docker deployments
- Update docker run command to use --network host
- Update docker-compose.yml to use network_mode: host
- Update docker-compose.full.yml to use network_mode: host
- Remove port mappings as they are not needed with host network
2025-11-26 12:57:45 +03:00
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
33 changed files with 4055 additions and 1844 deletions
+36
View File
@@ -5,6 +5,42 @@ 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.8] - 2025-11-26
### Changed
- Updated Docker deployment to use host network mode for better compatibility
- Modified docker-compose.yml to use `network_mode: host`
- Updated installation commands to use `--network host` flag
- Removed port mappings as they are not needed with host network mode
### Improved
- Better compatibility with unprivileged LXC containers
- Simplified Docker networking configuration
- Direct network access for improved camera discovery performance
## [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
+516 -177
View File
@@ -1,217 +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)
### Docker (Recommended)
---
## Your Problem?
- ❌ 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
## Solution
-**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
---
## 🚀 Installation (One Command)
### 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 --network host --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.4"
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 -4
View File
@@ -9,13 +9,10 @@ services:
image: eduard256/strix:latest
container_name: strix
restart: unless-stopped
ports:
- "4567:4567"
network_mode: host
environment:
- STRIX_LOG_LEVEL=info
- STRIX_LOG_FORMAT=json
networks:
- cameras
healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:4567/api/v1/health"]
interval: 30s
+1 -3
View File
@@ -7,9 +7,7 @@ services:
# build: .
container_name: strix
restart: unless-stopped
ports:
- "4567:4567"
network_mode: host
environment:
# Logging configuration
+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,
+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
+372 -80
View File
@@ -8,6 +8,15 @@
<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>
+10 -1
View File
@@ -1,14 +1,23 @@
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) {
// 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: {
+14 -1
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,11 +9,19 @@ export class StreamDiscoveryAPI {
this.baseURL = baseURL;
}
this.eventSource = null;
this.useMock = useMock;
this.mockAPI = useMock ? new MockStreamDiscovery() : null;
}
discover(request, callbacks) {
this.close();
// Use mock API if enabled
if (this.useMock) {
this.mockAPI.discover(request, callbacks);
return;
}
fetch(`${this.baseURL}api/v1/streams/discover`, {
method: 'POST',
headers: {
@@ -91,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);
}
}