Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0c0d743594 | |||
| 787919d20b | |||
| e9dc04178e | |||
| 915c1dec1b | |||
| e6828d8a22 | |||
| eedce14731 | |||
| 9975aa71de | |||
| 38e4af230f | |||
| 031e494787 | |||
| de389588ce | |||
| 4c03ad8d3c | |||
| d569a76700 | |||
| a405d6198f | |||
| 4143c267cd | |||
| 19e58db70f |
@@ -5,6 +5,54 @@ 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/),
|
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).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [1.0.9] - 2025-12-11
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Fixed real-time SSE streaming in Home Assistant Ingress mode
|
||||||
|
- SSE events now arrive immediately instead of being buffered until completion
|
||||||
|
|
||||||
|
### Technical
|
||||||
|
- Added automatic detection of Home Assistant Ingress via X-Ingress-Path header
|
||||||
|
- Implemented 64KB padding for SSE events to overcome aiohttp buffer in HA Supervisor
|
||||||
|
- Adjusted progress update interval to 3 seconds in Ingress mode to reduce traffic
|
||||||
|
- Normal mode (Docker/direct access) remains unchanged
|
||||||
|
|
||||||
|
## [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
|
## [0.1.0] - 2025-11-06
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -38,7 +38,7 @@
|
|||||||
### Ubuntu / Debian
|
### Ubuntu / Debian
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo apt update && command -v docker >/dev/null 2>&1 || curl -fsSL https://get.docker.com | sudo sh && docker run -d --name strix -p 4567:4567 --restart unless-stopped eduard256/strix:latest
|
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
|
||||||
```
|
```
|
||||||
|
|
||||||
Open **http://YOUR_SERVER_IP:4567**
|
Open **http://YOUR_SERVER_IP:4567**
|
||||||
|
|||||||
+1
-1
@@ -20,7 +20,7 @@ import (
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
// Version is the application version
|
// Version is the application version
|
||||||
Version = "1.0.4"
|
Version = "1.0.9"
|
||||||
|
|
||||||
// Banner is the application banner
|
// Banner is the application banner
|
||||||
Banner = `
|
Banner = `
|
||||||
|
|||||||
+64
-28
@@ -4,6 +4,42 @@
|
|||||||
"last_updated": "2025-10-17",
|
"last_updated": "2025-10-17",
|
||||||
"source": "ispyconnect.com",
|
"source": "ispyconnect.com",
|
||||||
"entries": [
|
"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": [
|
"models": [
|
||||||
"3628-675",
|
"3628-675",
|
||||||
@@ -313,15 +349,6 @@
|
|||||||
"port": 0,
|
"port": 0,
|
||||||
"url": ""
|
"url": ""
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"models": [
|
|
||||||
"IPC-300"
|
|
||||||
],
|
|
||||||
"type": "FFMPEG",
|
|
||||||
"protocol": "rtsp",
|
|
||||||
"port": 8554,
|
|
||||||
"url": "/Streaming/Channels/101"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"models": [
|
"models": [
|
||||||
"IPC-340HD",
|
"IPC-340HD",
|
||||||
@@ -465,15 +492,6 @@
|
|||||||
"port": 0,
|
"port": 0,
|
||||||
"url": "snapshot.jpg"
|
"url": "snapshot.jpg"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"models": [
|
|
||||||
"IPC-740"
|
|
||||||
],
|
|
||||||
"type": "FFMPEG",
|
|
||||||
"protocol": "rtsp",
|
|
||||||
"port": 8554,
|
|
||||||
"url": "/Streaming/Channels/102"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"models": [
|
"models": [
|
||||||
"IP-CAM",
|
"IP-CAM",
|
||||||
@@ -631,16 +649,6 @@
|
|||||||
"port": 80,
|
"port": 80,
|
||||||
"url": "/videostream.asf?user=[USERNAME]&pwd=[PASSWORD]&resolution=320x240"
|
"url": "/videostream.asf?user=[USERNAME]&pwd=[PASSWORD]&resolution=320x240"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"models": [
|
|
||||||
"PX3615",
|
|
||||||
"SK7008-T1F1"
|
|
||||||
],
|
|
||||||
"type": "FFMPEG",
|
|
||||||
"protocol": "rtsp",
|
|
||||||
"port": 554,
|
|
||||||
"url": "/Streaming/channels/401"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"models": [
|
"models": [
|
||||||
"PX-3615-675"
|
"PX-3615-675"
|
||||||
@@ -722,6 +730,34 @@
|
|||||||
"protocol": "http",
|
"protocol": "http",
|
||||||
"port": 82,
|
"port": 82,
|
||||||
"url": "/cgi/mjpg/mjpg.cgi"
|
"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
@@ -4,6 +4,42 @@
|
|||||||
"last_updated": "2025-10-17",
|
"last_updated": "2025-10-17",
|
||||||
"source": "ispyconnect.com",
|
"source": "ispyconnect.com",
|
||||||
"entries": [
|
"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": [
|
"models": [
|
||||||
"10550",
|
"10550",
|
||||||
@@ -320,31 +356,6 @@
|
|||||||
"port": 554,
|
"port": 554,
|
||||||
"url": "/s2"
|
"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": [
|
"models": [
|
||||||
"IPCB54611B",
|
"IPCB54611B",
|
||||||
@@ -635,6 +646,31 @@
|
|||||||
"protocol": "rtsp",
|
"protocol": "rtsp",
|
||||||
"port": 554,
|
"port": 554,
|
||||||
"url": "/mpeg4/media.amp?resolution=640x480"
|
"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"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -4,6 +4,24 @@
|
|||||||
"last_updated": "2025-10-17",
|
"last_updated": "2025-10-17",
|
||||||
"source": "ispyconnect.com",
|
"source": "ispyconnect.com",
|
||||||
"entries": [
|
"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": [
|
"models": [
|
||||||
"WIFI-5MP-30"
|
"WIFI-5MP-30"
|
||||||
|
|||||||
@@ -4,6 +4,24 @@
|
|||||||
"last_updated": "2025-10-17",
|
"last_updated": "2025-10-17",
|
||||||
"source": "ispyconnect.com",
|
"source": "ispyconnect.com",
|
||||||
"entries": [
|
"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": [
|
"models": [
|
||||||
"mega216"
|
"mega216"
|
||||||
|
|||||||
+127
-91
@@ -4,6 +4,42 @@
|
|||||||
"last_updated": "2025-10-17",
|
"last_updated": "2025-10-17",
|
||||||
"source": "ispyconnect.com",
|
"source": "ispyconnect.com",
|
||||||
"entries": [
|
"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": [
|
"models": [
|
||||||
"NVR",
|
"NVR",
|
||||||
@@ -220,55 +256,6 @@
|
|||||||
"port": 0,
|
"port": 0,
|
||||||
"url": "snapshot.jpg?user=[USERNAME]&pwd=[PASSWORD]&strm=[CHANNEL]"
|
"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": [
|
"models": [
|
||||||
"141CS",
|
"141CS",
|
||||||
@@ -498,39 +485,6 @@
|
|||||||
"port": 554,
|
"port": 554,
|
||||||
"url": "/onvif2"
|
"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": [
|
"models": [
|
||||||
"2MP",
|
"2MP",
|
||||||
@@ -659,15 +613,6 @@
|
|||||||
"port": 80,
|
"port": 80,
|
||||||
"url": "/cgi-bin/snapshot.cgi?chn=4&u=[USERNAME]&p=[PASSWORD]"
|
"url": "/cgi-bin/snapshot.cgi?chn=4&u=[USERNAME]&p=[PASSWORD]"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"models": [
|
|
||||||
"DVR"
|
|
||||||
],
|
|
||||||
"type": "FFMPEG",
|
|
||||||
"protocol": "rtsp",
|
|
||||||
"port": 0,
|
|
||||||
"url": "/Streaming/Channels/201"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"models": [
|
"models": [
|
||||||
"h264",
|
"h264",
|
||||||
@@ -851,6 +796,97 @@
|
|||||||
"protocol": "rtsp",
|
"protocol": "rtsp",
|
||||||
"port": 0,
|
"port": 0,
|
||||||
"url": "/h264/ch1/main/av_stream"
|
"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"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -4,6 +4,24 @@
|
|||||||
"last_updated": "2025-10-17",
|
"last_updated": "2025-10-17",
|
||||||
"source": "ispyconnect.com",
|
"source": "ispyconnect.com",
|
||||||
"entries": [
|
"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": [
|
"models": [
|
||||||
"Other"
|
"Other"
|
||||||
|
|||||||
@@ -4,6 +4,42 @@
|
|||||||
"last_updated": "2025-10-17",
|
"last_updated": "2025-10-17",
|
||||||
"source": "ispyconnect.com",
|
"source": "ispyconnect.com",
|
||||||
"entries": [
|
"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": [
|
"models": [
|
||||||
"Outdoor Smart Home Camera",
|
"Outdoor Smart Home Camera",
|
||||||
|
|||||||
+1097
-1052
File diff suppressed because it is too large
Load Diff
+200
-83
@@ -4,6 +4,123 @@
|
|||||||
"last_updated": "2025-10-17",
|
"last_updated": "2025-10-17",
|
||||||
"source": "ispyconnect.com",
|
"source": "ispyconnect.com",
|
||||||
"entries": [
|
"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": [
|
"models": [
|
||||||
"040",
|
"040",
|
||||||
@@ -47,69 +164,6 @@
|
|||||||
"port": 554,
|
"port": 554,
|
||||||
"url": "/Streaming/Channels/1"
|
"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": [
|
"models": [
|
||||||
"DC-I200",
|
"DC-I200",
|
||||||
@@ -221,16 +275,6 @@
|
|||||||
"port": 554,
|
"port": 554,
|
||||||
"url": "/h264_stream"
|
"url": "/h264_stream"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"models": [
|
|
||||||
"ds-i200",
|
|
||||||
"VDP-D2201"
|
|
||||||
],
|
|
||||||
"type": "FFMPEG",
|
|
||||||
"protocol": "rtsp",
|
|
||||||
"port": 555,
|
|
||||||
"url": "/Streaming/Channels/102"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"models": [
|
"models": [
|
||||||
"Ds-i203"
|
"Ds-i203"
|
||||||
@@ -240,16 +284,6 @@
|
|||||||
"port": 8000,
|
"port": 8000,
|
||||||
"url": "/"
|
"url": "/"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"models": [
|
|
||||||
"DS-I214(B)",
|
|
||||||
"DS-I405"
|
|
||||||
],
|
|
||||||
"type": "FFMPEG",
|
|
||||||
"protocol": "rtsp",
|
|
||||||
"port": 554,
|
|
||||||
"url": "/ISAPI/Streaming/Channels/102"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"models": [
|
"models": [
|
||||||
"DS-I220",
|
"DS-I220",
|
||||||
@@ -310,6 +344,89 @@
|
|||||||
"protocol": "rtsp",
|
"protocol": "rtsp",
|
||||||
"port": 554,
|
"port": 554,
|
||||||
"url": "/onvif1"
|
"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"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -4,6 +4,24 @@
|
|||||||
"last_updated": "2025-10-17",
|
"last_updated": "2025-10-17",
|
||||||
"source": "ispyconnect.com",
|
"source": "ispyconnect.com",
|
||||||
"entries": [
|
"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": [
|
"models": [
|
||||||
"0010/0020",
|
"0010/0020",
|
||||||
@@ -647,15 +665,6 @@
|
|||||||
"port": 0,
|
"port": 0,
|
||||||
"url": "cam[CHANNEL]/h264"
|
"url": "cam[CHANNEL]/h264"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"models": [
|
|
||||||
"FCS-3084"
|
|
||||||
],
|
|
||||||
"type": "FFMPEG",
|
|
||||||
"protocol": "rtsp",
|
|
||||||
"port": 0,
|
|
||||||
"url": "/Streaming/Channels/102"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"models": [
|
"models": [
|
||||||
"FCS-4051",
|
"FCS-4051",
|
||||||
@@ -770,6 +779,15 @@
|
|||||||
"protocol": "http",
|
"protocol": "http",
|
||||||
"port": 80,
|
"port": 80,
|
||||||
"url": "/cgi-bin/video.jpg"
|
"url": "/cgi-bin/video.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"models": [
|
||||||
|
"FCS-3084"
|
||||||
|
],
|
||||||
|
"type": "FFMPEG",
|
||||||
|
"protocol": "rtsp",
|
||||||
|
"port": 0,
|
||||||
|
"url": "/Streaming/Channels/102"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
+36
-18
@@ -4,6 +4,24 @@
|
|||||||
"last_updated": "2025-10-17",
|
"last_updated": "2025-10-17",
|
||||||
"source": "ispyconnect.com",
|
"source": "ispyconnect.com",
|
||||||
"entries": [
|
"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": [
|
"models": [
|
||||||
"0v600-365-kd",
|
"0v600-365-kd",
|
||||||
@@ -153,24 +171,6 @@
|
|||||||
"port": 0,
|
"port": 0,
|
||||||
"url": "snapshot.jpg?account=[USERNAME]&password=[PASSWORD]"
|
"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": [
|
"models": [
|
||||||
"CAM-1",
|
"CAM-1",
|
||||||
@@ -375,6 +375,24 @@
|
|||||||
"protocol": "http",
|
"protocol": "http",
|
||||||
"port": 80,
|
"port": 80,
|
||||||
"url": "/cgi-bin/snapshot.cgi?chn=0&u=[USERNAME]&p=[PASSWORD]"
|
"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"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -4,6 +4,24 @@
|
|||||||
"last_updated": "2025-10-17",
|
"last_updated": "2025-10-17",
|
||||||
"source": "ispyconnect.com",
|
"source": "ispyconnect.com",
|
||||||
"entries": [
|
"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": [
|
"models": [
|
||||||
"801",
|
"801",
|
||||||
|
|||||||
+128
-92
@@ -4,6 +4,42 @@
|
|||||||
"last_updated": "2025-10-17",
|
"last_updated": "2025-10-17",
|
||||||
"source": "ispyconnect.com",
|
"source": "ispyconnect.com",
|
||||||
"entries": [
|
"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": [
|
"models": [
|
||||||
"005FTCD",
|
"005FTCD",
|
||||||
@@ -588,58 +624,6 @@
|
|||||||
"port": 554,
|
"port": 554,
|
||||||
"url": "/ch05/1"
|
"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": [
|
"models": [
|
||||||
"887"
|
"887"
|
||||||
@@ -874,37 +858,6 @@
|
|||||||
"port": 0,
|
"port": 0,
|
||||||
"url": "/Streaming/Unicast/channels/401"
|
"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": [
|
"models": [
|
||||||
"DVR4",
|
"DVR4",
|
||||||
@@ -942,15 +895,6 @@
|
|||||||
"port": 80,
|
"port": 80,
|
||||||
"url": "/?action=stream"
|
"url": "/?action=stream"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"models": [
|
|
||||||
"DVR8-4500"
|
|
||||||
],
|
|
||||||
"type": "FFMPEG",
|
|
||||||
"protocol": "rtsp",
|
|
||||||
"port": 554,
|
|
||||||
"url": "/Streaming/Channels/301"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"models": [
|
"models": [
|
||||||
"DVR8-4500",
|
"DVR8-4500",
|
||||||
@@ -1314,6 +1258,98 @@
|
|||||||
"protocol": "rtsp",
|
"protocol": "rtsp",
|
||||||
"port": 0,
|
"port": 0,
|
||||||
"url": "/Streaming/Channels/2"
|
"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"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -4,6 +4,24 @@
|
|||||||
"last_updated": "2025-10-17",
|
"last_updated": "2025-10-17",
|
||||||
"source": "ispyconnect.com",
|
"source": "ispyconnect.com",
|
||||||
"entries": [
|
"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": [
|
"models": [
|
||||||
"TBK-BUL8841Z"
|
"TBK-BUL8841Z"
|
||||||
|
|||||||
@@ -31,6 +31,54 @@
|
|||||||
"notes": "Common RTSP sub stream for ONVIF cameras",
|
"notes": "Common RTSP sub stream for ONVIF cameras",
|
||||||
"model_count": 9998
|
"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",
|
"url": "/ch2",
|
||||||
"type": "FFMPEG",
|
"type": "FFMPEG",
|
||||||
|
|||||||
@@ -9,13 +9,10 @@ services:
|
|||||||
image: eduard256/strix:latest
|
image: eduard256/strix:latest
|
||||||
container_name: strix
|
container_name: strix
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
network_mode: host
|
||||||
- "4567:4567"
|
|
||||||
environment:
|
environment:
|
||||||
- STRIX_LOG_LEVEL=info
|
- STRIX_LOG_LEVEL=info
|
||||||
- STRIX_LOG_FORMAT=json
|
- STRIX_LOG_FORMAT=json
|
||||||
networks:
|
|
||||||
- cameras
|
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:4567/api/v1/health"]
|
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:4567/api/v1/health"]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
|
|||||||
+1
-3
@@ -7,9 +7,7 @@ services:
|
|||||||
# build: .
|
# build: .
|
||||||
container_name: strix
|
container_name: strix
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
network_mode: host
|
||||||
ports:
|
|
||||||
- "4567:4567"
|
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
# Logging configuration
|
# Logging configuration
|
||||||
|
|||||||
@@ -302,11 +302,13 @@ func (s *Scanner) collectStreams(ctx context.Context, req models.StreamDiscovery
|
|||||||
"model", req.Model,
|
"model", req.Model,
|
||||||
"limit", req.ModelLimit)
|
"limit", req.ModelLimit)
|
||||||
|
|
||||||
// Search for similar models
|
// Search for cameras using intelligent brand+model search
|
||||||
cameras, err := s.searchEngine.SearchByModel(req.Model, 0.8, req.ModelLimit)
|
searchResp, err := s.searchEngine.Search(req.Model, req.ModelLimit)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.logger.Error("model search failed", err)
|
s.logger.Error("model search failed", err)
|
||||||
} else {
|
} else {
|
||||||
|
cameras := searchResp.Cameras
|
||||||
|
|
||||||
// Collect entries from all matching cameras
|
// Collect entries from all matching cameras
|
||||||
var entries []models.CameraEntry
|
var entries []models.CameraEntry
|
||||||
for _, camera := range cameras {
|
for _, camera := range cameras {
|
||||||
@@ -409,7 +411,14 @@ func (s *Scanner) testStreamsConcurrently(ctx context.Context, streams []models.
|
|||||||
defer cancelProgress()
|
defer cancelProgress()
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
ticker := time.NewTicker(1 * time.Second)
|
// Use longer interval for Ingress mode to reduce traffic (padding is ~64KB per event)
|
||||||
|
// Normal mode: 1 second, Ingress mode: 3 seconds
|
||||||
|
progressInterval := 1 * time.Second
|
||||||
|
if streamWriter.IsIngress() {
|
||||||
|
progressInterval = 3 * time.Second
|
||||||
|
}
|
||||||
|
|
||||||
|
ticker := time.NewTicker(progressInterval)
|
||||||
defer ticker.Stop()
|
defer ticker.Stop()
|
||||||
|
|
||||||
for {
|
for {
|
||||||
@@ -417,7 +426,7 @@ func (s *Scanner) testStreamsConcurrently(ctx context.Context, streams []models.
|
|||||||
case <-progressCtx.Done():
|
case <-progressCtx.Done():
|
||||||
return
|
return
|
||||||
case <-ticker.C:
|
case <-ticker.C:
|
||||||
// Send progress every second to prevent WriteTimeout
|
// Send progress to prevent WriteTimeout and show scanning activity
|
||||||
_ = streamWriter.SendJSON("progress", models.ProgressMessage{
|
_ = streamWriter.SendJSON("progress", models.ProgressMessage{
|
||||||
Tested: int(atomic.LoadInt32(&tested)),
|
Tested: int(atomic.LoadInt32(&tested)),
|
||||||
Found: int(atomic.LoadInt32(&found)),
|
Found: int(atomic.LoadInt32(&found)),
|
||||||
|
|||||||
@@ -174,34 +174,38 @@ func (b *Builder) replacePlaceholders(urlPath string, ctx BuildContext) string {
|
|||||||
|
|
||||||
// Common placeholders
|
// Common placeholders
|
||||||
replacements := map[string]string{
|
replacements := map[string]string{
|
||||||
"[CHANNEL]": strconv.Itoa(ctx.Channel),
|
"[CHANNEL]": strconv.Itoa(ctx.Channel),
|
||||||
"[channel]": strconv.Itoa(ctx.Channel),
|
"[channel]": strconv.Itoa(ctx.Channel),
|
||||||
"{channel}": strconv.Itoa(ctx.Channel), // BUBBLE protocol uses {channel}
|
"[CHANNEL+1]": strconv.Itoa(ctx.Channel + 1), // For Hikvision-style channels (101, 201, 301...)
|
||||||
"{CHANNEL}": strconv.Itoa(ctx.Channel),
|
"[channel+1]": strconv.Itoa(ctx.Channel + 1),
|
||||||
"[WIDTH]": strconv.Itoa(ctx.Width),
|
"{CHANNEL}": strconv.Itoa(ctx.Channel), // BUBBLE protocol uses {channel}
|
||||||
"[width]": strconv.Itoa(ctx.Width),
|
"{channel}": strconv.Itoa(ctx.Channel),
|
||||||
"[HEIGHT]": strconv.Itoa(ctx.Height),
|
"{CHANNEL+1}": strconv.Itoa(ctx.Channel + 1),
|
||||||
"[height]": strconv.Itoa(ctx.Height),
|
"{channel+1}": strconv.Itoa(ctx.Channel + 1),
|
||||||
"[USERNAME]": ctx.Username,
|
"[WIDTH]": strconv.Itoa(ctx.Width),
|
||||||
"[username]": ctx.Username,
|
"[width]": strconv.Itoa(ctx.Width),
|
||||||
"[PASSWORD]": ctx.Password,
|
"[HEIGHT]": strconv.Itoa(ctx.Height),
|
||||||
"[password]": ctx.Password,
|
"[height]": strconv.Itoa(ctx.Height),
|
||||||
"[PASWORD]": ctx.Password, // Handle typo in database
|
"[USERNAME]": ctx.Username,
|
||||||
"[pasword]": ctx.Password,
|
"[username]": ctx.Username,
|
||||||
"[USER]": ctx.Username,
|
"[PASSWORD]": ctx.Password,
|
||||||
"[user]": ctx.Username,
|
"[password]": ctx.Password,
|
||||||
"[PASS]": ctx.Password,
|
"[PASWORD]": ctx.Password, // Handle typo in database
|
||||||
"[pass]": ctx.Password,
|
"[pasword]": ctx.Password,
|
||||||
"[PWD]": ctx.Password,
|
"[USER]": ctx.Username,
|
||||||
"[pwd]": ctx.Password,
|
"[user]": ctx.Username,
|
||||||
"[IP]": ctx.IP,
|
"[PASS]": ctx.Password,
|
||||||
"[ip]": ctx.IP,
|
"[pass]": ctx.Password,
|
||||||
"[PORT]": strconv.Itoa(ctx.Port),
|
"[PWD]": ctx.Password,
|
||||||
"[port]": strconv.Itoa(ctx.Port),
|
"[pwd]": ctx.Password,
|
||||||
"[AUTH]": auth, // base64(username:password) for basic auth
|
"[IP]": ctx.IP,
|
||||||
"[auth]": auth,
|
"[ip]": ctx.IP,
|
||||||
"[TOKEN]": "", // Empty for now
|
"[PORT]": strconv.Itoa(ctx.Port),
|
||||||
"[token]": "",
|
"[port]": strconv.Itoa(ctx.Port),
|
||||||
|
"[AUTH]": auth, // base64(username:password) for basic auth
|
||||||
|
"[auth]": auth,
|
||||||
|
"[TOKEN]": "", // Empty for now
|
||||||
|
"[token]": "",
|
||||||
}
|
}
|
||||||
|
|
||||||
// Replace all placeholders
|
// Replace all placeholders
|
||||||
|
|||||||
+67
-5
@@ -5,9 +5,20 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// IngressPaddingSize is the padding size for Home Assistant Ingress mode.
|
||||||
|
// HA Supervisor uses aiohttp with 64KB buffer for StreamResponse.
|
||||||
|
// We need to fill this buffer to force immediate delivery of SSE events.
|
||||||
|
IngressPaddingSize = 64 * 1024 // 64KB
|
||||||
|
|
||||||
|
// IngressHeader is the header that Home Assistant Ingress adds to requests
|
||||||
|
IngressHeader = "X-Ingress-Path"
|
||||||
|
)
|
||||||
|
|
||||||
// Event represents a Server-Sent Event
|
// Event represents a Server-Sent Event
|
||||||
type Event struct {
|
type Event struct {
|
||||||
ID string
|
ID string
|
||||||
@@ -253,8 +264,9 @@ func generateClientID() string {
|
|||||||
|
|
||||||
// StreamWriter provides a simple interface for writing SSE events
|
// StreamWriter provides a simple interface for writing SSE events
|
||||||
type StreamWriter struct {
|
type StreamWriter struct {
|
||||||
client *Client
|
client *Client
|
||||||
server *Server
|
server *Server
|
||||||
|
isIngress bool // True when running through Home Assistant Ingress proxy
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewStreamWriter creates a new stream writer for a client
|
// NewStreamWriter creates a new stream writer for a client
|
||||||
@@ -275,6 +287,9 @@ func (s *Server) NewStreamWriter(w http.ResponseWriter, r *http.Request) (*Strea
|
|||||||
// Send initial flush to establish connection
|
// Send initial flush to establish connection
|
||||||
flusher.Flush()
|
flusher.Flush()
|
||||||
|
|
||||||
|
// Detect Home Assistant Ingress mode by checking for X-Ingress-Path header
|
||||||
|
isIngress := r.Header.Get(IngressHeader) != ""
|
||||||
|
|
||||||
// Create client
|
// Create client
|
||||||
ctx, cancel := context.WithCancel(r.Context())
|
ctx, cancel := context.WithCancel(r.Context())
|
||||||
client := &Client{
|
client := &Client{
|
||||||
@@ -287,8 +302,9 @@ func (s *Server) NewStreamWriter(w http.ResponseWriter, r *http.Request) (*Strea
|
|||||||
}
|
}
|
||||||
|
|
||||||
return &StreamWriter{
|
return &StreamWriter{
|
||||||
client: client,
|
client: client,
|
||||||
server: s,
|
server: s,
|
||||||
|
isIngress: isIngress,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -304,7 +320,48 @@ func (sw *StreamWriter) SendEvent(eventType string, data interface{}) error {
|
|||||||
return fmt.Errorf("response does not support flushing")
|
return fmt.Errorf("response does not support flushing")
|
||||||
}
|
}
|
||||||
|
|
||||||
return sw.server.writeEvent(sw.client.Response, flusher, event)
|
// Use Ingress-aware write method
|
||||||
|
return sw.writeEventWithIngress(sw.client.Response, flusher, event)
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeEventWithIngress writes an event and adds padding for Ingress mode
|
||||||
|
func (sw *StreamWriter) writeEventWithIngress(w http.ResponseWriter, flusher http.Flusher, event Event) error {
|
||||||
|
// Write the event using standard method
|
||||||
|
if err := sw.server.writeEvent(w, flusher, event); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// In Ingress mode, add padding to fill the 64KB buffer and force immediate delivery
|
||||||
|
if sw.isIngress {
|
||||||
|
if err := sw.writePadding(w, flusher); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// writePadding writes SSE comment padding to fill proxy buffers.
|
||||||
|
// SSE comments (lines starting with ':') are ignored by clients.
|
||||||
|
func (sw *StreamWriter) writePadding(w http.ResponseWriter, flusher http.Flusher) error {
|
||||||
|
// Create padding using SSE comments which are ignored by clients
|
||||||
|
// Each line is ": " + padding content + "\n"
|
||||||
|
// We need ~64KB to fill the aiohttp StreamResponse buffer
|
||||||
|
const lineSize = 1024 // 1KB per line
|
||||||
|
const numLines = 64 // 64 lines = 64KB
|
||||||
|
|
||||||
|
paddingLine := ": " + strings.Repeat(".", lineSize-4) + "\n" // -4 for ": " and "\n"
|
||||||
|
|
||||||
|
for i := 0; i < numLines; i++ {
|
||||||
|
if _, err := fmt.Fprint(w, paddingLine); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flush the padding
|
||||||
|
flusher.Flush()
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// SendJSON sends JSON data as an event
|
// SendJSON sends JSON data as an event
|
||||||
@@ -312,6 +369,11 @@ func (sw *StreamWriter) SendJSON(eventType string, v interface{}) error {
|
|||||||
return sw.SendEvent(eventType, v)
|
return sw.SendEvent(eventType, v)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsIngress returns true if running through Home Assistant Ingress proxy
|
||||||
|
func (sw *StreamWriter) IsIngress() bool {
|
||||||
|
return sw.isIngress
|
||||||
|
}
|
||||||
|
|
||||||
// SendMessage sends a simple message
|
// SendMessage sends a simple message
|
||||||
func (sw *StreamWriter) SendMessage(message string) error {
|
func (sw *StreamWriter) SendMessage(message string) error {
|
||||||
return sw.SendEvent("message", map[string]string{"message": message})
|
return sw.SendEvent("message", map[string]string{"message": message})
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "webui",
|
"name": "webui",
|
||||||
"version": "1.0.4",
|
"version": "1.0.9",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
|
|||||||
+146
-1
@@ -590,10 +590,155 @@ body {
|
|||||||
.streams-list {
|
.streams-list {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: var(--space-3);
|
gap: var(--space-6);
|
||||||
padding: var(--space-2);
|
padding: var(--space-2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ===== STREAM GROUPS ===== */
|
||||||
|
.stream-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stream-group-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-2);
|
||||||
|
padding-bottom: var(--space-2);
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
transition: color var(--transition-fast);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stream-group-header:hover {
|
||||||
|
color: var(--purple-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stream-group-toggle {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
transition: all var(--transition-fast);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stream-group-toggle .chevron {
|
||||||
|
transition: transform var(--transition-fast);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stream-group.collapsed .stream-group-toggle .chevron {
|
||||||
|
transform: rotate(-90deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stream-group.collapsed .stream-group-content {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stream-group-title {
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stream-group-count {
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
font-weight: 400;
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stream-group-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stream-group-empty {
|
||||||
|
padding: var(--space-4);
|
||||||
|
text-align: center;
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border: 1px dashed var(--border-color);
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== STREAM SUBGROUPS (Main/Sub/Other within Recommended) ===== */
|
||||||
|
.stream-subgroup {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stream-subgroup:not(:last-child) {
|
||||||
|
margin-bottom: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stream-subgroup-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-2);
|
||||||
|
padding-left: var(--space-2);
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
transition: color var(--transition-fast);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stream-subgroup-header:hover {
|
||||||
|
color: var(--purple-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stream-subgroup-toggle {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
transition: all var(--transition-fast);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stream-subgroup-toggle .chevron {
|
||||||
|
transition: transform var(--transition-fast);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stream-subgroup.collapsed .stream-subgroup-toggle .chevron {
|
||||||
|
transform: rotate(-90deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stream-subgroup.collapsed .stream-subgroup-content {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stream-subgroup-title {
|
||||||
|
font-size: var(--text-xs);
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stream-subgroup-count {
|
||||||
|
font-size: var(--text-xs);
|
||||||
|
font-weight: 400;
|
||||||
|
color: var(--text-disabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stream-subgroup-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
/* Custom scrollbar */
|
/* Custom scrollbar */
|
||||||
.streams-list::-webkit-scrollbar {
|
.streams-list::-webkit-scrollbar {
|
||||||
width: 8px;
|
width: 8px;
|
||||||
|
|||||||
@@ -252,6 +252,15 @@ export class FrigateGenerator {
|
|||||||
return lines;
|
return lines;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build RTSP path with optional ?mp4 suffix for BUBBLE streams
|
||||||
|
*/
|
||||||
|
static buildRtspPath(streamName, streamType) {
|
||||||
|
const basePath = `rtsp://127.0.0.1:8554/${streamName}`;
|
||||||
|
// Add ?mp4 parameter only for BUBBLE streams to enable recording in Frigate
|
||||||
|
return streamType === 'BUBBLE' ? `${basePath}?mp4` : basePath;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate camera lines for cameras section
|
* Generate camera lines for cameras section
|
||||||
*/
|
*/
|
||||||
@@ -264,11 +273,14 @@ export class FrigateGenerator {
|
|||||||
|
|
||||||
if (cameraInfo.subStream) {
|
if (cameraInfo.subStream) {
|
||||||
// Use sub for detect, main for record
|
// Use sub for detect, main for record
|
||||||
lines.push(` - path: rtsp://127.0.0.1:8554/${cameraInfo.subStreamName}`);
|
const subPath = this.buildRtspPath(cameraInfo.subStreamName, cameraInfo.subStream.type);
|
||||||
|
const mainPath = this.buildRtspPath(cameraInfo.mainStreamName, cameraInfo.mainStream.type);
|
||||||
|
|
||||||
|
lines.push(` - path: ${subPath}`);
|
||||||
lines.push(' input_args: preset-rtsp-restream');
|
lines.push(' input_args: preset-rtsp-restream');
|
||||||
lines.push(' roles:');
|
lines.push(' roles:');
|
||||||
lines.push(' - detect');
|
lines.push(' - detect');
|
||||||
lines.push(` - path: rtsp://127.0.0.1:8554/${cameraInfo.mainStreamName}`);
|
lines.push(` - path: ${mainPath}`);
|
||||||
lines.push(' input_args: preset-rtsp-restream');
|
lines.push(' input_args: preset-rtsp-restream');
|
||||||
lines.push(' roles:');
|
lines.push(' roles:');
|
||||||
lines.push(' - record');
|
lines.push(' - record');
|
||||||
@@ -280,7 +292,9 @@ export class FrigateGenerator {
|
|||||||
lines.push(` Sub Stream: ${cameraInfo.subStreamName} # Низкое разрешение (опционально)`);
|
lines.push(` Sub Stream: ${cameraInfo.subStreamName} # Низкое разрешение (опционально)`);
|
||||||
} else {
|
} else {
|
||||||
// Use main for both detect and record
|
// Use main for both detect and record
|
||||||
lines.push(` - path: rtsp://127.0.0.1:8554/${cameraInfo.mainStreamName}`);
|
const mainPath = this.buildRtspPath(cameraInfo.mainStreamName, cameraInfo.mainStream.type);
|
||||||
|
|
||||||
|
lines.push(` - path: ${mainPath}`);
|
||||||
lines.push(' input_args: preset-rtsp-restream');
|
lines.push(' input_args: preset-rtsp-restream');
|
||||||
lines.push(' roles:');
|
lines.push(' roles:');
|
||||||
lines.push(' - detect');
|
lines.push(' - detect');
|
||||||
|
|||||||
+12
-1
@@ -315,6 +315,11 @@ class StrixApp {
|
|||||||
document.getElementById('progress-text').textContent = 'Starting scan...';
|
document.getElementById('progress-text').textContent = 'Starting scan...';
|
||||||
document.getElementById('streams-section').classList.add('hidden');
|
document.getElementById('streams-section').classList.add('hidden');
|
||||||
this.currentStreams = [];
|
this.currentStreams = [];
|
||||||
|
// Reset stream list state for fresh discovery
|
||||||
|
this.streamList.selectionMode = 'main';
|
||||||
|
this.streamList.collapsedGroups.clear();
|
||||||
|
this.streamList.collapsedSubgroups.clear();
|
||||||
|
this.streamList.needsSmartDefaults = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
handleProgress(data) {
|
handleProgress(data) {
|
||||||
@@ -334,7 +339,7 @@ class StrixApp {
|
|||||||
streamsSection.classList.remove('hidden');
|
streamsSection.classList.remove('hidden');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update stream list
|
// Update stream list (smart defaults applied automatically on first render)
|
||||||
this.streamList.render(this.currentStreams, (stream, index) => {
|
this.streamList.render(this.currentStreams, (stream, index) => {
|
||||||
this.selectStream(stream, index);
|
this.selectStream(stream, index);
|
||||||
});
|
});
|
||||||
@@ -391,6 +396,12 @@ class StrixApp {
|
|||||||
document.getElementById('frigate-output-section').classList.add('hidden');
|
document.getElementById('frigate-output-section').classList.add('hidden');
|
||||||
document.getElementById('config-frigate').textContent = '';
|
document.getElementById('config-frigate').textContent = '';
|
||||||
|
|
||||||
|
// Set stream list to sub selection mode (will collapse Main, show Sub)
|
||||||
|
this.streamList.setSelectionMode('sub');
|
||||||
|
this.streamList.render(this.currentStreams, (stream, index) => {
|
||||||
|
this.selectStream(stream, index);
|
||||||
|
});
|
||||||
|
|
||||||
showToast('Select a sub stream from available streams');
|
showToast('Select a sub stream from available streams');
|
||||||
this.showScreen('discovery');
|
this.showScreen('discovery');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
export class MockStreamAPI {
|
export class MockStreamAPI {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.mockStreams = [
|
this.mockStreams = [
|
||||||
|
// RTSP Main streams (1920x1080)
|
||||||
{
|
{
|
||||||
url: "rtsp://192.168.1.100:554/Streaming/Channels/101",
|
url: "rtsp://192.168.1.100:554/Streaming/Channels/101",
|
||||||
path: "/Streaming/Channels/101",
|
path: "/Streaming/Channels/101",
|
||||||
@@ -12,6 +13,27 @@ export class MockStreamAPI {
|
|||||||
bitrate: 4096000,
|
bitrate: 4096000,
|
||||||
has_audio: true
|
has_audio: true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
url: "rtsp://192.168.1.100:554/live/main",
|
||||||
|
path: "/live/main",
|
||||||
|
type: "FFMPEG",
|
||||||
|
resolution: "1920x1080",
|
||||||
|
codec: "H.264",
|
||||||
|
fps: 30,
|
||||||
|
bitrate: 4608000,
|
||||||
|
has_audio: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: "rtsp://192.168.1.100:554/stream1",
|
||||||
|
path: "/stream1",
|
||||||
|
type: "FFMPEG",
|
||||||
|
resolution: "1920x1080",
|
||||||
|
codec: "H.265",
|
||||||
|
fps: 25,
|
||||||
|
bitrate: 3584000,
|
||||||
|
has_audio: true
|
||||||
|
},
|
||||||
|
// JPEG snapshots (5 items in different positions)
|
||||||
{
|
{
|
||||||
url: "http://192.168.1.100/snap.jpg",
|
url: "http://192.168.1.100/snap.jpg",
|
||||||
path: "/snap.jpg",
|
path: "/snap.jpg",
|
||||||
@@ -22,16 +44,124 @@ export class MockStreamAPI {
|
|||||||
bitrate: 0,
|
bitrate: 0,
|
||||||
has_audio: false
|
has_audio: false
|
||||||
},
|
},
|
||||||
|
// RTSP Sub streams (640x480)
|
||||||
|
{
|
||||||
|
url: "rtsp://192.168.1.100:554/Streaming/Channels/102",
|
||||||
|
path: "/Streaming/Channels/102",
|
||||||
|
type: "FFMPEG",
|
||||||
|
resolution: "640x480",
|
||||||
|
codec: "H.264",
|
||||||
|
fps: 5,
|
||||||
|
bitrate: 512000,
|
||||||
|
has_audio: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: "rtsp://192.168.1.100:554/live/sub",
|
||||||
|
path: "/live/sub",
|
||||||
|
type: "FFMPEG",
|
||||||
|
resolution: "640x480",
|
||||||
|
codec: "H.264",
|
||||||
|
fps: 10,
|
||||||
|
bitrate: 768000,
|
||||||
|
has_audio: false
|
||||||
|
},
|
||||||
|
// JPEG #2
|
||||||
|
{
|
||||||
|
url: "http://192.168.1.100/cgi-bin/snapshot.cgi",
|
||||||
|
path: "/cgi-bin/snapshot.cgi",
|
||||||
|
type: "JPEG",
|
||||||
|
resolution: "1920x1080",
|
||||||
|
codec: "JPEG",
|
||||||
|
fps: 1,
|
||||||
|
bitrate: 0,
|
||||||
|
has_audio: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: "rtsp://192.168.1.100:554/stream2",
|
||||||
|
path: "/stream2",
|
||||||
|
type: "FFMPEG",
|
||||||
|
resolution: "640x480",
|
||||||
|
codec: "H.264",
|
||||||
|
fps: 15,
|
||||||
|
bitrate: 640000,
|
||||||
|
has_audio: false
|
||||||
|
},
|
||||||
|
// ONVIF streams
|
||||||
|
{
|
||||||
|
url: "rtsp://192.168.1.100:554/onvif/profile0",
|
||||||
|
path: "/onvif/profile0",
|
||||||
|
type: "ONVIF",
|
||||||
|
resolution: "1920x1080",
|
||||||
|
codec: "H.264",
|
||||||
|
fps: 25,
|
||||||
|
bitrate: 4096000,
|
||||||
|
has_audio: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: "rtsp://192.168.1.100:554/onvif/profile1",
|
||||||
|
path: "/onvif/profile1",
|
||||||
|
type: "ONVIF",
|
||||||
|
resolution: "640x480",
|
||||||
|
codec: "H.264",
|
||||||
|
fps: 15,
|
||||||
|
bitrate: 512000,
|
||||||
|
has_audio: false
|
||||||
|
},
|
||||||
|
// JPEG #3
|
||||||
|
{
|
||||||
|
url: "http://192.168.1.100/image/jpeg.cgi",
|
||||||
|
path: "/image/jpeg.cgi",
|
||||||
|
type: "JPEG",
|
||||||
|
resolution: "1920x1080",
|
||||||
|
codec: "JPEG",
|
||||||
|
fps: 1,
|
||||||
|
bitrate: 0,
|
||||||
|
has_audio: false
|
||||||
|
},
|
||||||
|
// More RTSP variants
|
||||||
|
{
|
||||||
|
url: "rtsp://192.168.1.100:554/cam/realmonitor?channel=1&subtype=0",
|
||||||
|
path: "/cam/realmonitor?channel=1&subtype=0",
|
||||||
|
type: "FFMPEG",
|
||||||
|
resolution: "1920x1080",
|
||||||
|
codec: "H.265",
|
||||||
|
fps: 30,
|
||||||
|
bitrate: 5120000,
|
||||||
|
has_audio: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: "rtsp://192.168.1.100:554/cam/realmonitor?channel=1&subtype=1",
|
||||||
|
path: "/cam/realmonitor?channel=1&subtype=1",
|
||||||
|
type: "FFMPEG",
|
||||||
|
resolution: "640x480",
|
||||||
|
codec: "H.264",
|
||||||
|
fps: 10,
|
||||||
|
bitrate: 512000,
|
||||||
|
has_audio: false
|
||||||
|
},
|
||||||
|
// MJPEG
|
||||||
{
|
{
|
||||||
url: "http://192.168.1.100/video.mjpg",
|
url: "http://192.168.1.100/video.mjpg",
|
||||||
path: "/video.mjpg",
|
path: "/video.mjpg",
|
||||||
type: "MJPEG",
|
type: "MJPEG",
|
||||||
resolution: "1280x720",
|
resolution: "1920x1080",
|
||||||
codec: "MJPEG",
|
codec: "MJPEG",
|
||||||
fps: 10,
|
fps: 10,
|
||||||
bitrate: 2048000,
|
bitrate: 3072000,
|
||||||
has_audio: false
|
has_audio: false
|
||||||
},
|
},
|
||||||
|
// JPEG #4
|
||||||
|
{
|
||||||
|
url: "http://192.168.1.100/Streaming/channels/1/picture",
|
||||||
|
path: "/Streaming/channels/1/picture",
|
||||||
|
type: "JPEG",
|
||||||
|
resolution: "1920x1080",
|
||||||
|
codec: "JPEG",
|
||||||
|
fps: 1,
|
||||||
|
bitrate: 0,
|
||||||
|
has_audio: false
|
||||||
|
},
|
||||||
|
// HLS
|
||||||
{
|
{
|
||||||
url: "http://192.168.1.100/stream/live.m3u8",
|
url: "http://192.168.1.100/stream/live.m3u8",
|
||||||
path: "/stream/live.m3u8",
|
path: "/stream/live.m3u8",
|
||||||
@@ -42,16 +172,18 @@ export class MockStreamAPI {
|
|||||||
bitrate: 3072000,
|
bitrate: 3072000,
|
||||||
has_audio: true
|
has_audio: true
|
||||||
},
|
},
|
||||||
|
// HTTP Video
|
||||||
{
|
{
|
||||||
url: "http://192.168.1.100/videostream.cgi?user=admin&pwd=12345",
|
url: "http://192.168.1.100/videostream.cgi?user=admin&pwd=12345",
|
||||||
path: "/videostream.cgi?user=admin&pwd=12345",
|
path: "/videostream.cgi?user=admin&pwd=12345",
|
||||||
type: "HTTP_VIDEO",
|
type: "HTTP_VIDEO",
|
||||||
resolution: "1280x960",
|
resolution: "1920x1080",
|
||||||
codec: "H.264",
|
codec: "H.264",
|
||||||
fps: 20,
|
fps: 20,
|
||||||
bitrate: 2048000,
|
bitrate: 2560000,
|
||||||
has_audio: false
|
has_audio: false
|
||||||
},
|
},
|
||||||
|
// BUBBLE
|
||||||
{
|
{
|
||||||
url: "bubble://192.168.1.100:34567/bubble/live?ch=0&stream=0",
|
url: "bubble://192.168.1.100:34567/bubble/live?ch=0&stream=0",
|
||||||
path: "/bubble/live?ch=0&stream=0",
|
path: "/bubble/live?ch=0&stream=0",
|
||||||
@@ -59,33 +191,75 @@ export class MockStreamAPI {
|
|||||||
resolution: "1920x1080",
|
resolution: "1920x1080",
|
||||||
codec: "H.264",
|
codec: "H.264",
|
||||||
fps: 25,
|
fps: 25,
|
||||||
bitrate: 3072000,
|
bitrate: 3584000,
|
||||||
|
has_audio: true
|
||||||
|
},
|
||||||
|
// JPEG #5
|
||||||
|
{
|
||||||
|
url: "http://192.168.1.100/tmpfs/auto.jpg",
|
||||||
|
path: "/tmpfs/auto.jpg",
|
||||||
|
type: "JPEG",
|
||||||
|
resolution: "1920x1080",
|
||||||
|
codec: "JPEG",
|
||||||
|
fps: 1,
|
||||||
|
bitrate: 0,
|
||||||
|
has_audio: false
|
||||||
|
},
|
||||||
|
// Additional RTSP
|
||||||
|
{
|
||||||
|
url: "rtsp://192.168.1.100:554/h264_stream",
|
||||||
|
path: "/h264_stream",
|
||||||
|
type: "FFMPEG",
|
||||||
|
resolution: "1920x1080",
|
||||||
|
codec: "H.264",
|
||||||
|
fps: 30,
|
||||||
|
bitrate: 4096000,
|
||||||
has_audio: true
|
has_audio: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
url: "rtsp://192.168.1.100:554/cam/realmonitor?channel=1&subtype=0",
|
url: "rtsp://192.168.1.100:554/av0_0",
|
||||||
path: "/cam/realmonitor?channel=1&subtype=0",
|
path: "/av0_0",
|
||||||
type: "ONVIF",
|
type: "FFMPEG",
|
||||||
resolution: "2560x1440",
|
resolution: "1920x1080",
|
||||||
|
codec: "H.264",
|
||||||
|
fps: 25,
|
||||||
|
bitrate: 3840000,
|
||||||
|
has_audio: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: "rtsp://192.168.1.100:554/av0_1",
|
||||||
|
path: "/av0_1",
|
||||||
|
type: "FFMPEG",
|
||||||
|
resolution: "640x480",
|
||||||
|
codec: "H.264",
|
||||||
|
fps: 10,
|
||||||
|
bitrate: 512000,
|
||||||
|
has_audio: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: "rtsp://192.168.1.100:554/unicast/c1/s0/live",
|
||||||
|
path: "/unicast/c1/s0/live",
|
||||||
|
type: "FFMPEG",
|
||||||
|
resolution: "1920x1080",
|
||||||
codec: "H.265",
|
codec: "H.265",
|
||||||
fps: 30,
|
fps: 25,
|
||||||
bitrate: 6144000,
|
bitrate: 4608000,
|
||||||
has_audio: true
|
has_audio: true
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
discover(request, callbacks) {
|
discover(request, callbacks) {
|
||||||
const totalToScan = 150;
|
const totalToScan = 450;
|
||||||
const streamsToFind = this.mockStreams;
|
const streamsToFind = this.mockStreams;
|
||||||
let tested = 0;
|
let tested = 0;
|
||||||
let found = 0;
|
let found = 0;
|
||||||
|
|
||||||
const startTime = Date.now();
|
const startTime = Date.now();
|
||||||
|
|
||||||
// Simulate progressive discovery
|
// Simulate progressive discovery - 1 stream per second
|
||||||
const interval = setInterval(() => {
|
const progressInterval = setInterval(() => {
|
||||||
const increment = Math.floor(Math.random() * 8) + 3;
|
const increment = Math.floor(Math.random() * 15) + 10;
|
||||||
tested = Math.min(tested + increment, totalToScan);
|
tested = Math.min(tested + increment, totalToScan);
|
||||||
const remaining = totalToScan - tested;
|
const remaining = totalToScan - tested;
|
||||||
|
|
||||||
@@ -98,33 +272,9 @@ export class MockStreamAPI {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
// Complete when done
|
||||||
if (tested >= totalToScan) {
|
if (tested >= totalToScan) {
|
||||||
clearInterval(interval);
|
clearInterval(progressInterval);
|
||||||
|
|
||||||
// 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;
|
const duration = (Date.now() - startTime) / 1000;
|
||||||
|
|
||||||
@@ -136,7 +286,23 @@ export class MockStreamAPI {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, 400);
|
}, 300);
|
||||||
|
|
||||||
|
// Find streams at ~1 per second
|
||||||
|
const streamInterval = setInterval(() => {
|
||||||
|
if (found < streamsToFind.length) {
|
||||||
|
const stream = streamsToFind[found];
|
||||||
|
found++;
|
||||||
|
|
||||||
|
if (callbacks.onStreamFound) {
|
||||||
|
callbacks.onStreamFound({
|
||||||
|
stream: stream
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
clearInterval(streamInterval);
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
close() {
|
close() {
|
||||||
|
|||||||
@@ -4,19 +4,277 @@ export class StreamList {
|
|||||||
this.streams = [];
|
this.streams = [];
|
||||||
this.onUseCallback = null;
|
this.onUseCallback = null;
|
||||||
this.expandedIndex = null;
|
this.expandedIndex = null;
|
||||||
|
// Track collapsed state for groups and subgroups
|
||||||
|
this.collapsedGroups = new Set();
|
||||||
|
this.collapsedSubgroups = new Set();
|
||||||
|
// Selection mode: 'main' or 'sub'
|
||||||
|
this.selectionMode = 'main';
|
||||||
|
// Flag to apply smart defaults on first render after reset
|
||||||
|
this.needsSmartDefaults = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set selection mode and apply smart defaults for collapsed state
|
||||||
|
* Only resets collapsed state when mode actually changes
|
||||||
|
*/
|
||||||
|
setSelectionMode(mode) {
|
||||||
|
if (this.selectionMode === mode) return;
|
||||||
|
|
||||||
|
this.selectionMode = mode;
|
||||||
|
this.applySmartDefaults();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply smart collapsed defaults based on current selection mode and available streams
|
||||||
|
*/
|
||||||
|
applySmartDefaults() {
|
||||||
|
// Get current stream classification
|
||||||
|
const recommended = this.streams.filter(s => this.isRecommended(s));
|
||||||
|
const { main, sub, other } = this.classifyRecommendedStreams(
|
||||||
|
recommended.map((stream, i) => ({ stream, index: i }))
|
||||||
|
);
|
||||||
|
|
||||||
|
// Reset all collapsed states
|
||||||
|
this.collapsedGroups.clear();
|
||||||
|
this.collapsedSubgroups.clear();
|
||||||
|
|
||||||
|
if (this.selectionMode === 'main') {
|
||||||
|
// Main mode: show Main, collapse Sub/Other/Alternative
|
||||||
|
if (main.length > 0) {
|
||||||
|
// Has main streams - collapse everything except Main
|
||||||
|
this.collapsedGroups.add('alternative');
|
||||||
|
this.collapsedSubgroups.add('recommended-sub');
|
||||||
|
this.collapsedSubgroups.add('recommended-other');
|
||||||
|
}
|
||||||
|
// If no main streams - leave everything open
|
||||||
|
} else {
|
||||||
|
// Sub mode: show Sub, collapse Main/Other/Alternative
|
||||||
|
if (sub.length > 0) {
|
||||||
|
// Has sub streams - collapse everything except Sub
|
||||||
|
this.collapsedGroups.add('alternative');
|
||||||
|
this.collapsedSubgroups.add('recommended-main');
|
||||||
|
this.collapsedSubgroups.add('recommended-other');
|
||||||
|
}
|
||||||
|
// If no sub streams - leave everything open
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stream types considered "recommended" (standard video streams)
|
||||||
|
static RECOMMENDED_TYPES = ['FFMPEG', 'ONVIF'];
|
||||||
|
|
||||||
|
// Minimum width threshold for Main streams (HD quality)
|
||||||
|
static MIN_MAIN_WIDTH = 720;
|
||||||
|
|
||||||
|
// Minimum gap between resolutions to split Main/Sub
|
||||||
|
static MIN_GAP_FOR_SPLIT = 400;
|
||||||
|
|
||||||
|
isRecommended(stream) {
|
||||||
|
return StreamList.RECOMMENDED_TYPES.includes(stream.type);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse resolution string "1920x1080" to width number
|
||||||
|
* Returns null if resolution is missing or invalid
|
||||||
|
*/
|
||||||
|
parseResolutionWidth(resolution) {
|
||||||
|
if (!resolution) return null;
|
||||||
|
const match = resolution.match(/^(\d+)x(\d+)$/);
|
||||||
|
if (!match) return null;
|
||||||
|
return parseInt(match[1], 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Classify recommended streams into Main/Sub/Other using clustering algorithm
|
||||||
|
*
|
||||||
|
* Algorithm:
|
||||||
|
* 1. Streams with width >= 720 are candidates for Main
|
||||||
|
* 2. Streams with width < 720 go to Sub
|
||||||
|
* 3. Streams without resolution go to Other
|
||||||
|
* 4. Among Main candidates, find max gap between sorted resolutions
|
||||||
|
* 5. If gap > 400px, split into Main (higher) and Sub (lower)
|
||||||
|
*/
|
||||||
|
classifyRecommendedStreams(items) {
|
||||||
|
const main = [];
|
||||||
|
const sub = [];
|
||||||
|
const other = [];
|
||||||
|
|
||||||
|
// First pass: separate by resolution availability and threshold
|
||||||
|
const mainCandidates = []; // width >= 720
|
||||||
|
|
||||||
|
items.forEach(item => {
|
||||||
|
const width = this.parseResolutionWidth(item.stream.resolution);
|
||||||
|
|
||||||
|
if (width === null) {
|
||||||
|
other.push(item);
|
||||||
|
} else if (width < StreamList.MIN_MAIN_WIDTH) {
|
||||||
|
sub.push(item);
|
||||||
|
} else {
|
||||||
|
mainCandidates.push({ ...item, width });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// If no main candidates or only one, no need to cluster
|
||||||
|
if (mainCandidates.length <= 1) {
|
||||||
|
mainCandidates.forEach(item => main.push({ stream: item.stream, index: item.index }));
|
||||||
|
return { main, sub, other };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort candidates by width descending
|
||||||
|
mainCandidates.sort((a, b) => b.width - a.width);
|
||||||
|
|
||||||
|
// Find the largest gap between adjacent resolutions
|
||||||
|
let maxGap = 0;
|
||||||
|
let splitIndex = -1;
|
||||||
|
|
||||||
|
for (let i = 0; i < mainCandidates.length - 1; i++) {
|
||||||
|
const gap = mainCandidates[i].width - mainCandidates[i + 1].width;
|
||||||
|
if (gap > maxGap) {
|
||||||
|
maxGap = gap;
|
||||||
|
splitIndex = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If max gap is significant, split into Main and Sub
|
||||||
|
if (maxGap > StreamList.MIN_GAP_FOR_SPLIT && splitIndex >= 0) {
|
||||||
|
mainCandidates.forEach((item, i) => {
|
||||||
|
const cleanItem = { stream: item.stream, index: item.index };
|
||||||
|
if (i <= splitIndex) {
|
||||||
|
main.push(cleanItem);
|
||||||
|
} else {
|
||||||
|
sub.push(cleanItem);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// All candidates stay in Main
|
||||||
|
mainCandidates.forEach(item => {
|
||||||
|
main.push({ stream: item.stream, index: item.index });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return { main, sub, other };
|
||||||
}
|
}
|
||||||
|
|
||||||
render(streams, onUseCallback) {
|
render(streams, onUseCallback) {
|
||||||
this.streams = streams;
|
this.streams = streams;
|
||||||
this.onUseCallback = onUseCallback;
|
this.onUseCallback = onUseCallback;
|
||||||
|
|
||||||
// Render stream items
|
// Apply smart defaults on first render after reset
|
||||||
this.listContainer.innerHTML = streams.map((stream, index) => this.renderItem(stream, index)).join('');
|
if (this.needsSmartDefaults && streams.length > 0) {
|
||||||
|
this.needsSmartDefaults = false;
|
||||||
|
this.applySmartDefaults();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Split streams into groups while preserving original indices
|
||||||
|
const recommended = [];
|
||||||
|
const alternative = [];
|
||||||
|
|
||||||
|
streams.forEach((stream, index) => {
|
||||||
|
if (this.isRecommended(stream)) {
|
||||||
|
recommended.push({ stream, index });
|
||||||
|
} else {
|
||||||
|
alternative.push({ stream, index });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Render only non-empty groups
|
||||||
|
let html = '';
|
||||||
|
if (recommended.length > 0) {
|
||||||
|
html += this.renderRecommendedGroup(recommended);
|
||||||
|
}
|
||||||
|
if (alternative.length > 0) {
|
||||||
|
html += this.renderGroup('Alternative', alternative, 'alternative');
|
||||||
|
}
|
||||||
|
this.listContainer.innerHTML = html;
|
||||||
|
|
||||||
// Attach event listeners
|
// Attach event listeners
|
||||||
this.attachEventListeners();
|
this.attachEventListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render Recommended group with Main/Sub/Other subgroups
|
||||||
|
*/
|
||||||
|
renderRecommendedGroup(items) {
|
||||||
|
const { main, sub, other } = this.classifyRecommendedStreams(items);
|
||||||
|
const totalCount = items.length;
|
||||||
|
const isCollapsed = this.collapsedGroups.has('recommended');
|
||||||
|
|
||||||
|
let subgroupsHtml = '';
|
||||||
|
|
||||||
|
if (main.length > 0) {
|
||||||
|
subgroupsHtml += this.renderSubgroup('Main', main, 'recommended');
|
||||||
|
}
|
||||||
|
if (sub.length > 0) {
|
||||||
|
subgroupsHtml += this.renderSubgroup('Sub', sub, 'recommended');
|
||||||
|
}
|
||||||
|
if (other.length > 0) {
|
||||||
|
subgroupsHtml += this.renderSubgroup('Other', other, 'recommended');
|
||||||
|
}
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="stream-group stream-group-recommended ${isCollapsed ? 'collapsed' : ''}">
|
||||||
|
<div class="stream-group-header" data-group="recommended">
|
||||||
|
<button class="stream-group-toggle" aria-label="Toggle group">
|
||||||
|
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" class="chevron">
|
||||||
|
<path d="M3 4.5l3 3 3-3" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<span class="stream-group-title">Recommended</span>
|
||||||
|
<span class="stream-group-count">(${totalCount})</span>
|
||||||
|
</div>
|
||||||
|
<div class="stream-group-content">
|
||||||
|
${subgroupsHtml}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render a subgroup (Main/Sub/Other) within Recommended
|
||||||
|
*/
|
||||||
|
renderSubgroup(title, items, parentGroup) {
|
||||||
|
const subgroupKey = `${parentGroup}-${title.toLowerCase()}`;
|
||||||
|
const isCollapsed = this.collapsedSubgroups.has(subgroupKey);
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="stream-subgroup ${isCollapsed ? 'collapsed' : ''}" data-subgroup="${subgroupKey}">
|
||||||
|
<div class="stream-subgroup-header" data-subgroup="${subgroupKey}">
|
||||||
|
<button class="stream-subgroup-toggle" aria-label="Toggle subgroup">
|
||||||
|
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" class="chevron">
|
||||||
|
<path d="M2.5 3.75l2.5 2.5 2.5-2.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<span class="stream-subgroup-title">${title}</span>
|
||||||
|
<span class="stream-subgroup-count">(${items.length})</span>
|
||||||
|
</div>
|
||||||
|
<div class="stream-subgroup-content">
|
||||||
|
${items.map(({ stream, index }) => this.renderItem(stream, index)).join('')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderGroup(title, items, groupClass) {
|
||||||
|
const count = items.length;
|
||||||
|
const isCollapsed = this.collapsedGroups.has(groupClass);
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="stream-group stream-group-${groupClass} ${isCollapsed ? 'collapsed' : ''}">
|
||||||
|
<div class="stream-group-header" data-group="${groupClass}">
|
||||||
|
<button class="stream-group-toggle" aria-label="Toggle group">
|
||||||
|
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" class="chevron">
|
||||||
|
<path d="M3 4.5l3 3 3-3" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<span class="stream-group-title">${title}</span>
|
||||||
|
<span class="stream-group-count">(${count})</span>
|
||||||
|
</div>
|
||||||
|
<div class="stream-group-content">
|
||||||
|
${items.map(({ stream, index }) => this.renderItem(stream, index)).join('')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
renderItem(stream, index) {
|
renderItem(stream, index) {
|
||||||
const icon = this.getStreamIcon(stream.type);
|
const icon = this.getStreamIcon(stream.type);
|
||||||
const isExpanded = this.expandedIndex === index;
|
const isExpanded = this.expandedIndex === index;
|
||||||
@@ -225,7 +483,28 @@ export class StreamList {
|
|||||||
}
|
}
|
||||||
|
|
||||||
attachEventListeners() {
|
attachEventListeners() {
|
||||||
// Click on header to toggle
|
// Group header toggle (Recommended, Alternative)
|
||||||
|
this.listContainer.querySelectorAll('.stream-group-header').forEach(header => {
|
||||||
|
header.addEventListener('click', (e) => {
|
||||||
|
const groupKey = header.dataset.group;
|
||||||
|
if (groupKey) {
|
||||||
|
this.toggleGroup(groupKey);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Subgroup header toggle (Main, Sub, Other)
|
||||||
|
this.listContainer.querySelectorAll('.stream-subgroup-header').forEach(header => {
|
||||||
|
header.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation(); // Don't bubble to group header
|
||||||
|
const subgroupKey = header.dataset.subgroup;
|
||||||
|
if (subgroupKey) {
|
||||||
|
this.toggleSubgroup(subgroupKey);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Click on stream item header to toggle details
|
||||||
this.listContainer.querySelectorAll('.stream-item-header').forEach(header => {
|
this.listContainer.querySelectorAll('.stream-item-header').forEach(header => {
|
||||||
header.addEventListener('click', (e) => {
|
header.addEventListener('click', (e) => {
|
||||||
// Don't toggle if clicking "Use Stream" button
|
// Don't toggle if clicking "Use Stream" button
|
||||||
@@ -250,6 +529,24 @@ export class StreamList {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toggleGroup(groupKey) {
|
||||||
|
if (this.collapsedGroups.has(groupKey)) {
|
||||||
|
this.collapsedGroups.delete(groupKey);
|
||||||
|
} else {
|
||||||
|
this.collapsedGroups.add(groupKey);
|
||||||
|
}
|
||||||
|
this.render(this.streams, this.onUseCallback);
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleSubgroup(subgroupKey) {
|
||||||
|
if (this.collapsedSubgroups.has(subgroupKey)) {
|
||||||
|
this.collapsedSubgroups.delete(subgroupKey);
|
||||||
|
} else {
|
||||||
|
this.collapsedSubgroups.add(subgroupKey);
|
||||||
|
}
|
||||||
|
this.render(this.streams, this.onUseCallback);
|
||||||
|
}
|
||||||
|
|
||||||
toggleExpand(index) {
|
toggleExpand(index) {
|
||||||
if (this.expandedIndex === index) {
|
if (this.expandedIndex === index) {
|
||||||
// Collapse if already expanded
|
// Collapse if already expanded
|
||||||
|
|||||||
Reference in New Issue
Block a user