From 627409cf56a6dcce781d9e56d01c8e236261ecba Mon Sep 17 00:00:00 2001 From: eduard256 Date: Tue, 11 Nov 2025 22:32:59 +0300 Subject: [PATCH] Add Frigate config merging and camera database updates - Refactor Frigate generator to support adding cameras to existing configs - Add text-based YAML parsing to preserve formatting and comments - Implement duplicate camera/stream name detection and auto-numbering - Add support for inserting cameras into existing go2rtc and cameras sections - Update UI: add textarea for existing config input and generate button - Preserve user's existing configuration when adding new cameras - Add example config template for new users - Update ConfigPanel to initialize Frigate tab instead of auto-generating - Add FrigateGenerator import to main.js - Add custom styles for Frigate config input and output sections - Support both empty config (create from scratch) and existing config (merge) modes Camera database updates: - Add OpenIPC firmware camera support (257 models) - Add Yi-Hack firmware variants (v4, v5, Allwinner, MStar) - Add Fang-Hacks firmware support - Add OpenMiko firmware support - Update Sonoff camera models - Update Thingino firmware camera models --- data/brands/fang-hacks.json | 44 ++ data/brands/openipc.json | 257 ++++++++++ data/brands/openmiko.json | 59 +++ data/brands/sonoff.json | 42 +- data/brands/thingino.json | 149 +++++- data/brands/yi-hack-allwinner-v2.json | 90 ++++ data/brands/yi-hack-allwinner.json | 109 ++++ data/brands/yi-hack-mstar.json | 139 ++++++ data/brands/yi-hack-v4.json | 187 +++++++ data/brands/yi-hack-v5.json | 81 +++ webui/web/css/main.css | 77 +++ .../web/js/config-generators/frigate/index.js | 465 +++++++++++++----- webui/web/js/main.js | 43 ++ webui/web/js/ui/config-panel.js | 50 +- 14 files changed, 1660 insertions(+), 132 deletions(-) create mode 100644 data/brands/fang-hacks.json create mode 100644 data/brands/openipc.json create mode 100644 data/brands/openmiko.json create mode 100644 data/brands/yi-hack-allwinner-v2.json create mode 100644 data/brands/yi-hack-allwinner.json create mode 100644 data/brands/yi-hack-mstar.json create mode 100644 data/brands/yi-hack-v4.json create mode 100644 data/brands/yi-hack-v5.json diff --git a/data/brands/fang-hacks.json b/data/brands/fang-hacks.json new file mode 100644 index 0000000..03c73a8 --- /dev/null +++ b/data/brands/fang-hacks.json @@ -0,0 +1,44 @@ +{ + "brand": "fang-hacks", + "brand_id": "fang-hacks", + "last_updated": "2025-11-11", + "source": "github.com/samtap/fang-hacks", + "website": "https://github.com/samtap/fang-hacks", + "entries": [ + { + "models": [ + "XIAOFANG ARM PROCESSOR", + "XiaoFang ARM", + "Classic XiaoFang", + "ARM926EJ-S" + ], + "type": "FFMPEG", + "protocol": "rtsp", + "port": 554, + "url": "/unicast", + "notes": "Classic XiaoFang with ARM processor (not Ingenic)" + }, + { + "models": [ + "XIAOMI XIAOFANG", + "Xiaofang", + "XiaoFang" + ], + "type": "FFMPEG", + "protocol": "rtsp", + "port": 554, + "url": "/unicast", + "notes": "Xiaomi Xiaofang camera with fang-hacks" + }, + { + "models": [ + "Other" + ], + "type": "FFMPEG", + "protocol": "rtsp", + "port": 554, + "url": "/unicast", + "notes": "Generic fang-hacks installation" + } + ] +} diff --git a/data/brands/openipc.json b/data/brands/openipc.json new file mode 100644 index 0000000..95e9a26 --- /dev/null +++ b/data/brands/openipc.json @@ -0,0 +1,257 @@ +{ + "brand": "OpenIPC", + "brand_id": "openipc", + "last_updated": "2025-11-11", + "source": "openipc.org", + "website": "https://openipc.org", + "entries": [ + { + "models": [ + "MAJESTIC STREAMER", + "Generic", + "Majestic", + "Other" + ], + "type": "FFMPEG", + "protocol": "rtsp", + "port": 554, + "url": "/stream=0", + "notes": "Main stream (video0) - Majestic streamer default" + }, + { + "models": [ + "MAJESTIC STREAMER", + "Generic", + "Majestic", + "Other" + ], + "type": "FFMPEG", + "protocol": "rtsp", + "port": 554, + "url": "/stream=1", + "notes": "Sub stream (video1) - Majestic streamer" + }, + { + "models": [ + "HISILICON", + "Hi3516EV200", + "Hi3516EV300", + "Hi3516CV500", + "Hi3516DV300", + "Hi3518EV200", + "Hi3518EV300" + ], + "type": "FFMPEG", + "protocol": "rtsp", + "port": 554, + "url": "/stream=0", + "notes": "HiSilicon SoC main stream" + }, + { + "models": [ + "HISILICON", + "Hi3516EV200", + "Hi3516EV300", + "Hi3516CV500", + "Hi3516DV300", + "Hi3518EV200", + "Hi3518EV300" + ], + "type": "FFMPEG", + "protocol": "rtsp", + "port": 554, + "url": "/stream=1", + "notes": "HiSilicon SoC sub stream" + }, + { + "models": [ + "GOKE", + "GK7205V200", + "GK7205V300", + "GK7605V100" + ], + "type": "FFMPEG", + "protocol": "rtsp", + "port": 554, + "url": "/stream=0", + "notes": "Goke SoC main stream" + }, + { + "models": [ + "GOKE", + "GK7205V200", + "GK7205V300", + "GK7605V100" + ], + "type": "FFMPEG", + "protocol": "rtsp", + "port": 554, + "url": "/stream=1", + "notes": "Goke SoC sub stream" + }, + { + "models": [ + "INGENIC", + "T31", + "T30", + "T20", + "T10" + ], + "type": "FFMPEG", + "protocol": "rtsp", + "port": 554, + "url": "/stream=0", + "notes": "Ingenic SoC main stream" + }, + { + "models": [ + "INGENIC", + "T31", + "T30", + "T20", + "T10" + ], + "type": "FFMPEG", + "protocol": "rtsp", + "port": 554, + "url": "/stream=1", + "notes": "Ingenic SoC sub stream" + }, + { + "models": [ + "SIGMASTAR", + "SSC325", + "SSC335", + "SSC337", + "SSC338Q" + ], + "type": "FFMPEG", + "protocol": "rtsp", + "port": 554, + "url": "/stream=0", + "notes": "SigmaStar SoC main stream" + }, + { + "models": [ + "SIGMASTAR", + "SSC325", + "SSC335", + "SSC337", + "SSC338Q" + ], + "type": "FFMPEG", + "protocol": "rtsp", + "port": 554, + "url": "/stream=1", + "notes": "SigmaStar SoC sub stream" + }, + { + "models": [ + "NOVATEK", + "NT98562", + "NT98566" + ], + "type": "FFMPEG", + "protocol": "rtsp", + "port": 554, + "url": "/stream=0", + "notes": "Novatek SoC main stream" + }, + { + "models": [ + "XIONGMAI", + "XM530", + "XM550" + ], + "type": "FFMPEG", + "protocol": "rtsp", + "port": 554, + "url": "/stream=0", + "notes": "XiongMai SoC main stream" + }, + { + "models": [ + "AMBARELLA", + "S2L", + "S3L" + ], + "type": "FFMPEG", + "protocol": "rtsp", + "port": 554, + "url": "/stream=0", + "notes": "Ambarella SoC main stream" + }, + { + "models": [ + "Generic", + "Majestic", + "Other" + ], + "type": "JPEG", + "protocol": "http", + "port": 80, + "url": "/image.jpg", + "notes": "JPEG snapshot" + }, + { + "models": [ + "Generic", + "Majestic", + "Other" + ], + "type": "MJPEG", + "protocol": "http", + "port": 80, + "url": "/mjpeg.html", + "notes": "MJPEG stream" + }, + { + "models": [ + "Generic", + "Majestic", + "Other" + ], + "type": "FFMPEG", + "protocol": "http", + "port": 80, + "url": "/video.mp4", + "notes": "Fragmented MP4 video" + }, + { + "models": [ + "Generic", + "Majestic", + "Other" + ], + "type": "FFMPEG", + "protocol": "http", + "port": 80, + "url": "/audio.opus", + "notes": "Opus audio stream" + }, + { + "models": [ + "Generic", + "Majestic", + "Other" + ], + "type": "FFMPEG", + "protocol": "http", + "port": 80, + "url": "/audio.mp3", + "notes": "MP3 audio stream" + }, + { + "models": [ + "Generic", + "Majestic", + "Other" + ], + "type": "FFMPEG", + "protocol": "http", + "port": 80, + "url": "/audio.m4a", + "notes": "AAC audio stream" + } + ] +} diff --git a/data/brands/openmiko.json b/data/brands/openmiko.json new file mode 100644 index 0000000..d85f439 --- /dev/null +++ b/data/brands/openmiko.json @@ -0,0 +1,59 @@ +{ + "brand": "OpenMiko", + "brand_id": "openmiko", + "last_updated": "2025-11-11", + "source": "github.com/openmiko/openmiko", + "website": "https://github.com/openmiko/openmiko", + "entries": [ + { + "models": [ + "WYZE CAM V2", + "WyzeCam V2", + "Wyze V2", + "WYZEC1-JZ" + ], + "type": "FFMPEG", + "protocol": "rtsp", + "port": 8554, + "url": "/video3_unicast", + "notes": "WyzeCam V2 with OpenMiko firmware - NON-STANDARD PORT 8554" + }, + { + "models": [ + "XIAOMI XIAOFANG 1S", + "Xiaofang 1S", + "XiaoFang 1S" + ], + "type": "FFMPEG", + "protocol": "rtsp", + "port": 8554, + "url": "/video3_unicast", + "notes": "Xiaomi Xiaofang 1S with OpenMiko - NON-STANDARD PORT 8554" + }, + { + "models": [ + "ISMARTALARM SPOT+", + "iSmartAlarm Spot+", + "Spot+" + ], + "type": "FFMPEG", + "protocol": "rtsp", + "port": 8554, + "url": "/video3_unicast", + "notes": "iSmartAlarm Spot+ with OpenMiko - NON-STANDARD PORT 8554" + }, + { + "models": [ + "INGENIC T20", + "Generic T20", + "T20 based", + "Other" + ], + "type": "FFMPEG", + "protocol": "rtsp", + "port": 8554, + "url": "/video3_unicast", + "notes": "Generic Ingenic T20 based cameras - NON-STANDARD PORT 8554" + } + ] +} diff --git a/data/brands/sonoff.json b/data/brands/sonoff.json index ef4e116..6848ed2 100644 --- a/data/brands/sonoff.json +++ b/data/brands/sonoff.json @@ -1,8 +1,9 @@ { "brand": "Sonoff", "brand_id": "sonoff", - "last_updated": "2025-10-17", - "source": "ispyconnect.com", + "last_updated": "2025-11-11", + "source": "ispyconnect.com, github.com/roleoroleo/sonoff-hack", + "website": "https://github.com/roleoroleo/sonoff-hack", "entries": [ { "models": [ @@ -73,6 +74,43 @@ "protocol": "rtsp", "port": 0, "url": "live/ch00_0" + }, + { + "models": [ + "SONOFF-HACK GOKE GK7205V200", + "GK-200MP2-B with sonoff-hack", + "Goke GK7205V200", + "sonoff-hack" + ], + "type": "FFMPEG", + "protocol": "rtsp", + "port": 554, + "url": "/av_stream/ch0", + "notes": "Sonoff-hack custom firmware - Main stream" + }, + { + "models": [ + "SONOFF-HACK", + "GK-200MP2-B with sonoff-hack", + "sonoff-hack" + ], + "type": "JPEG", + "protocol": "http", + "port": 8080, + "url": "/snapshot.jpg", + "notes": "Sonoff-hack custom firmware - JPEG snapshot (port 8080)" + }, + { + "models": [ + "SONOFF-HACK GOKE GK7205V300", + "GK7205V300", + "Goke chipset" + ], + "type": "FFMPEG", + "protocol": "rtsp", + "port": 554, + "url": "/av_stream/ch0", + "notes": "Sonoff-hack for Goke GK7205V300 chipset" } ] } \ No newline at end of file diff --git a/data/brands/thingino.json b/data/brands/thingino.json index 211ed03..6f8cd54 100644 --- a/data/brands/thingino.json +++ b/data/brands/thingino.json @@ -1,8 +1,9 @@ { "brand": "Thingino", "brand_id": "thingino", - "last_updated": "2025-10-17", - "source": "ispyconnect.com", + "last_updated": "2025-11-11", + "source": "ispyconnect.com, thingino.com", + "website": "https://github.com/themactep/thingino-firmware", "entries": [ { "models": [ @@ -13,6 +14,148 @@ "protocol": "rtsp", "port": 554, "url": "/ch0" + }, + { + "models": [ + "INGENIC T31", + "T31", + "T31X", + "T31ZX", + "T31A", + "Generic T31" + ], + "type": "FFMPEG", + "protocol": "rtsp", + "port": 554, + "url": "/ch0", + "notes": "Main stream 1080p - Ingenic T31 series SoC" + }, + { + "models": [ + "INGENIC T31", + "T31", + "T31X", + "T31ZX", + "T31A", + "Generic T31" + ], + "type": "FFMPEG", + "protocol": "rtsp", + "port": 554, + "url": "/ch1", + "notes": "Sub stream 360p - Ingenic T31 series SoC" + }, + { + "models": [ + "INGENIC T20", + "T20", + "T20X", + "T20L", + "Generic T20" + ], + "type": "FFMPEG", + "protocol": "rtsp", + "port": 554, + "url": "/ch0", + "notes": "Main stream 1080p - Ingenic T20 series SoC" + }, + { + "models": [ + "INGENIC T20", + "T20", + "T20X", + "T20L", + "Generic T20" + ], + "type": "FFMPEG", + "protocol": "rtsp", + "port": 554, + "url": "/ch1", + "notes": "Sub stream 360p - Ingenic T20 series SoC" + }, + { + "models": [ + "INGENIC T10", + "T10", + "Generic T10" + ], + "type": "FFMPEG", + "protocol": "rtsp", + "port": 554, + "url": "/ch0", + "notes": "Main stream - Ingenic T10 SoC" + }, + { + "models": [ + "INGENIC T10", + "T10", + "Generic T10" + ], + "type": "FFMPEG", + "protocol": "rtsp", + "port": 554, + "url": "/ch1", + "notes": "Sub stream - Ingenic T10 SoC" + }, + { + "models": [ + "INGENIC S3L", + "S3L", + "Generic S3L" + ], + "type": "FFMPEG", + "protocol": "rtsp", + "port": 554, + "url": "/ch0", + "notes": "Main stream - Ingenic S3L SoC" + }, + { + "models": [ + "Generic", + "Other", + "Xiaomi Dafang", + "Xiaomi Xiaofang" + ], + "type": "FFMPEG", + "protocol": "rtsp", + "port": 554, + "url": "/ch0", + "notes": "Main stream 1080p - Generic Thingino installation" + }, + { + "models": [ + "Generic", + "Other", + "Xiaomi Dafang", + "Xiaomi Xiaofang" + ], + "type": "FFMPEG", + "protocol": "rtsp", + "port": 554, + "url": "/ch1", + "notes": "Sub stream 360p - Generic Thingino installation" + }, + { + "models": [ + "Generic", + "Other" + ], + "type": "JPEG", + "protocol": "http", + "port": 80, + "url": "/image.jpg", + "notes": "JPEG snapshot" + }, + { + "models": [ + "Generic", + "Other" + ], + "type": "MJPEG", + "protocol": "http", + "port": 8080, + "url": "/", + "notes": "MJPEG stream via HTTP" } ] -} \ No newline at end of file +} diff --git a/data/brands/yi-hack-allwinner-v2.json b/data/brands/yi-hack-allwinner-v2.json new file mode 100644 index 0000000..14e92ae --- /dev/null +++ b/data/brands/yi-hack-allwinner-v2.json @@ -0,0 +1,90 @@ +{ + "brand": "yi-hack-Allwinner-v2", + "brand_id": "yi-hack-allwinner-v2", + "last_updated": "2025-11-11", + "source": "github.com/roleoroleo/yi-hack-Allwinner-v2", + "website": "https://github.com/roleoroleo/yi-hack-Allwinner-v2", + "entries": [ + { + "models": [ + "ALLWINNER V2 PLATFORM", + "Yi Allwinner v2", + "Different flash layout", + "Tovendor Mini Smart Home Camera" + ], + "type": "FFMPEG", + "protocol": "rtsp", + "port": 554, + "url": "/ch0_0.h264", + "notes": "Allwinner v2 platform (different flash layout) - High resolution" + }, + { + "models": [ + "ALLWINNER V2 PLATFORM", + "Yi Allwinner v2", + "Different flash layout", + "Tovendor Mini Smart Home Camera" + ], + "type": "FFMPEG", + "protocol": "rtsp", + "port": 554, + "url": "/ch0_1.h264", + "notes": "Allwinner v2 platform (different flash layout) - Low resolution" + }, + { + "models": [ + "ALLWINNER V2 PLATFORM", + "Yi Allwinner v2", + "Different flash layout", + "Tovendor Mini Smart Home Camera" + ], + "type": "FFMPEG", + "protocol": "rtsp", + "port": 554, + "url": "/ch0_2.h264", + "notes": "Allwinner v2 platform (different flash layout) - Audio only" + }, + { + "models": [ + "YI HOME CAMERA 3", + "Yi Home Camera 3" + ], + "type": "FFMPEG", + "protocol": "rtsp", + "port": 554, + "url": "/ch0_0.h264", + "notes": "Yi Home Camera 3 - HD stream" + }, + { + "models": [ + "YI HOME CAMERA 3", + "Yi Home Camera 3" + ], + "type": "FFMPEG", + "protocol": "rtsp", + "port": 554, + "url": "/ch0_1.h264", + "notes": "Yi Home Camera 3 - SD stream" + }, + { + "models": [ + "Other" + ], + "type": "FFMPEG", + "protocol": "rtsp", + "port": 554, + "url": "/ch0_0.h264", + "notes": "Generic Allwinner v2 camera - HD stream" + }, + { + "models": [ + "Other" + ], + "type": "FFMPEG", + "protocol": "rtsp", + "port": 554, + "url": "/ch0_1.h264", + "notes": "Generic Allwinner v2 camera - SD stream" + } + ] +} diff --git a/data/brands/yi-hack-allwinner.json b/data/brands/yi-hack-allwinner.json new file mode 100644 index 0000000..7aa36a0 --- /dev/null +++ b/data/brands/yi-hack-allwinner.json @@ -0,0 +1,109 @@ +{ + "brand": "yi-hack-Allwinner", + "brand_id": "yi-hack-allwinner", + "last_updated": "2025-11-11", + "source": "github.com/roleoroleo/yi-hack-Allwinner", + "website": "https://github.com/roleoroleo/yi-hack-Allwinner", + "entries": [ + { + "models": [ + "ALLWINNER PLATFORM", + "Yi 1080p Allwinner", + "Generic Allwinner" + ], + "type": "FFMPEG", + "protocol": "rtsp", + "port": 554, + "url": "/ch0_0.h264", + "notes": "Allwinner platform - High resolution video" + }, + { + "models": [ + "ALLWINNER PLATFORM", + "Yi 1080p Allwinner", + "Generic Allwinner" + ], + "type": "FFMPEG", + "protocol": "rtsp", + "port": 554, + "url": "/ch0_1.h264", + "notes": "Allwinner platform - Low resolution video" + }, + { + "models": [ + "ALLWINNER PLATFORM", + "Yi 1080p Allwinner", + "Generic Allwinner" + ], + "type": "FFMPEG", + "protocol": "rtsp", + "port": 554, + "url": "/ch0_2.h264", + "notes": "Allwinner platform - Audio only" + }, + { + "models": [ + "YI HOME 1080P ALLWINNER", + "Yi Home Allwinner" + ], + "type": "FFMPEG", + "protocol": "rtsp", + "port": 554, + "url": "/ch0_0.h264", + "notes": "Yi Home 1080p Allwinner - HD stream" + }, + { + "models": [ + "YI HOME 1080P ALLWINNER", + "Yi Home Allwinner" + ], + "type": "FFMPEG", + "protocol": "rtsp", + "port": 554, + "url": "/ch0_1.h264", + "notes": "Yi Home 1080p Allwinner - SD stream" + }, + { + "models": [ + "YI DOME 1080P ALLWINNER", + "Yi Dome Allwinner" + ], + "type": "FFMPEG", + "protocol": "rtsp", + "port": 554, + "url": "/ch0_0.h264", + "notes": "Yi Dome 1080p Allwinner - HD stream" + }, + { + "models": [ + "YI DOME 1080P ALLWINNER", + "Yi Dome Allwinner" + ], + "type": "FFMPEG", + "protocol": "rtsp", + "port": 554, + "url": "/ch0_1.h264", + "notes": "Yi Dome 1080p Allwinner - SD stream" + }, + { + "models": [ + "Other" + ], + "type": "FFMPEG", + "protocol": "rtsp", + "port": 554, + "url": "/ch0_0.h264", + "notes": "Generic Allwinner camera - HD stream" + }, + { + "models": [ + "Other" + ], + "type": "FFMPEG", + "protocol": "rtsp", + "port": 554, + "url": "/ch0_1.h264", + "notes": "Generic Allwinner camera - SD stream" + } + ] +} diff --git a/data/brands/yi-hack-mstar.json b/data/brands/yi-hack-mstar.json new file mode 100644 index 0000000..4b6cb0a --- /dev/null +++ b/data/brands/yi-hack-mstar.json @@ -0,0 +1,139 @@ +{ + "brand": "yi-hack-MStar", + "brand_id": "yi-hack-mstar", + "last_updated": "2025-11-11", + "source": "github.com/roleoroleo/yi-hack-MStar", + "website": "https://github.com/roleoroleo/yi-hack-MStar", + "entries": [ + { + "models": [ + "MSTAR INFINITY CHIPSET", + "MStar", + "Yi Home MStar", + "Yi Dome MStar", + "Generic MStar" + ], + "type": "FFMPEG", + "protocol": "rtsp", + "port": 554, + "url": "/ch0_0.h264", + "notes": "MStar Infinity chipset - High resolution video" + }, + { + "models": [ + "MSTAR INFINITY CHIPSET", + "MStar", + "Yi Home MStar", + "Yi Dome MStar", + "Generic MStar" + ], + "type": "FFMPEG", + "protocol": "rtsp", + "port": 554, + "url": "/ch0_1.h264", + "notes": "MStar Infinity chipset - Low resolution video" + }, + { + "models": [ + "MSTAR INFINITY CHIPSET", + "MStar", + "Yi Home MStar", + "Yi Dome MStar", + "Generic MStar" + ], + "type": "FFMPEG", + "protocol": "rtsp", + "port": 554, + "url": "/ch0_2.h264", + "notes": "MStar Infinity chipset - Audio only" + }, + { + "models": [ + "YI HOME 1080P MSTAR", + "Yi 1080p MStar" + ], + "type": "FFMPEG", + "protocol": "rtsp", + "port": 554, + "url": "/ch0_0.h264", + "notes": "Yi Home 1080p MStar - HD stream" + }, + { + "models": [ + "YI HOME 1080P MSTAR", + "Yi 1080p MStar" + ], + "type": "FFMPEG", + "protocol": "rtsp", + "port": 554, + "url": "/ch0_1.h264", + "notes": "Yi Home 1080p MStar - SD stream" + }, + { + "models": [ + "YI DOME 1080P MSTAR", + "Yi Dome MStar" + ], + "type": "FFMPEG", + "protocol": "rtsp", + "port": 554, + "url": "/ch0_0.h264", + "notes": "Yi Dome 1080p MStar - HD stream" + }, + { + "models": [ + "YI DOME 1080P MSTAR", + "Yi Dome MStar" + ], + "type": "FFMPEG", + "protocol": "rtsp", + "port": 554, + "url": "/ch0_1.h264", + "notes": "Yi Dome 1080p MStar - SD stream" + }, + { + "models": [ + "AQARA CAMERA G2H", + "Aqara G2H", + "G2H" + ], + "type": "FFMPEG", + "protocol": "rtsp", + "port": 554, + "url": "/ch0_0.h264", + "notes": "Aqara Camera G2H with yi-hack-MStar - HD stream" + }, + { + "models": [ + "AQARA CAMERA G2H", + "Aqara G2H", + "G2H" + ], + "type": "FFMPEG", + "protocol": "rtsp", + "port": 554, + "url": "/ch0_1.h264", + "notes": "Aqara Camera G2H with yi-hack-MStar - SD stream" + }, + { + "models": [ + "Other" + ], + "type": "FFMPEG", + "protocol": "rtsp", + "port": 554, + "url": "/ch0_0.h264", + "notes": "Generic MStar based camera - HD stream" + }, + { + "models": [ + "Other" + ], + "type": "FFMPEG", + "protocol": "rtsp", + "port": 554, + "url": "/ch0_1.h264", + "notes": "Generic MStar based camera - SD stream" + } + ] +} diff --git a/data/brands/yi-hack-v4.json b/data/brands/yi-hack-v4.json new file mode 100644 index 0000000..257a2a5 --- /dev/null +++ b/data/brands/yi-hack-v4.json @@ -0,0 +1,187 @@ +{ + "brand": "yi-hack-v4", + "brand_id": "yi-hack-v4", + "last_updated": "2025-11-11", + "source": "github.com/TheCrypt0/yi-hack-v4", + "website": "https://github.com/TheCrypt0/yi-hack-v4", + "entries": [ + { + "models": [ + "YI HOME 720P", + "Yi Home 720p", + "17CN", + "27US", + "47CN" + ], + "type": "FFMPEG", + "protocol": "rtsp", + "port": 554, + "url": "/ch0_0.h264", + "notes": "Yi Home 720p - Hi3518e chipset - HD stream" + }, + { + "models": [ + "YI HOME 720P", + "Yi Home 720p", + "17CN", + "27US", + "47CN" + ], + "type": "FFMPEG", + "protocol": "rtsp", + "port": 554, + "url": "/ch0_1.h264", + "notes": "Yi Home 720p - Hi3518e chipset - SD stream" + }, + { + "models": [ + "YI DOME 720P", + "Yi Dome 720p", + "43US", + "63US", + "Generic Dome 720p" + ], + "type": "FFMPEG", + "protocol": "rtsp", + "port": 554, + "url": "/ch0_0.h264", + "notes": "Yi Dome 720p - Hi3518e chipset - HD stream" + }, + { + "models": [ + "YI DOME 720P", + "Yi Dome 720p", + "43US", + "63US", + "Generic Dome 720p" + ], + "type": "FFMPEG", + "protocol": "rtsp", + "port": 554, + "url": "/ch0_1.h264", + "notes": "Yi Dome 720p - Hi3518e chipset - SD stream" + }, + { + "models": [ + "YI HOME 1080P", + "Yi Home 1080p", + "48US", + "Version 1" + ], + "type": "FFMPEG", + "protocol": "rtsp", + "port": 554, + "url": "/ch0_0.h264", + "notes": "Yi Home 1080p - Hi3518e chipset - HD stream" + }, + { + "models": [ + "YI HOME 1080P", + "Yi Home 1080p", + "48US", + "Version 1" + ], + "type": "FFMPEG", + "protocol": "rtsp", + "port": 554, + "url": "/ch0_1.h264", + "notes": "Yi Home 1080p - Hi3518e chipset - SD stream" + }, + { + "models": [ + "YI DOME 1080P", + "Yi Dome 1080p", + "45US", + "65US", + "Generic Dome 1080p" + ], + "type": "FFMPEG", + "protocol": "rtsp", + "port": 554, + "url": "/ch0_0.h264", + "notes": "Yi Dome 1080p - Hi3518e chipset - HD stream" + }, + { + "models": [ + "YI DOME 1080P", + "Yi Dome 1080p", + "45US", + "65US", + "Generic Dome 1080p" + ], + "type": "FFMPEG", + "protocol": "rtsp", + "port": 554, + "url": "/ch0_1.h264", + "notes": "Yi Dome 1080p - Hi3518e chipset - SD stream" + }, + { + "models": [ + "YI CLOUD DOME 1080P", + "Yi Cloud Dome 1080p" + ], + "type": "FFMPEG", + "protocol": "rtsp", + "port": 554, + "url": "/ch0_0.h264", + "notes": "Yi Cloud Dome 1080p - Hi3518e chipset - HD stream" + }, + { + "models": [ + "YI CLOUD DOME 1080P", + "Yi Cloud Dome 1080p" + ], + "type": "FFMPEG", + "protocol": "rtsp", + "port": 554, + "url": "/ch0_1.h264", + "notes": "Yi Cloud Dome 1080p - Hi3518e chipset - SD stream" + }, + { + "models": [ + "YI OUTDOOR 1080P", + "Yi Outdoor 1080p" + ], + "type": "FFMPEG", + "protocol": "rtsp", + "port": 554, + "url": "/ch0_0.h264", + "notes": "Yi Outdoor 1080p - Hi3518e chipset - HD stream" + }, + { + "models": [ + "YI OUTDOOR 1080P", + "Yi Outdoor 1080p" + ], + "type": "FFMPEG", + "protocol": "rtsp", + "port": 554, + "url": "/ch0_1.h264", + "notes": "Yi Outdoor 1080p - Hi3518e chipset - SD stream" + }, + { + "models": [ + "HI3518E CHIPSET", + "Generic Hi3518e", + "Other" + ], + "type": "FFMPEG", + "protocol": "rtsp", + "port": 554, + "url": "/ch0_0.h264", + "notes": "Generic Yi camera with Hi3518e chipset - HD stream" + }, + { + "models": [ + "HI3518E CHIPSET", + "Generic Hi3518e", + "Other" + ], + "type": "FFMPEG", + "protocol": "rtsp", + "port": 554, + "url": "/ch0_1.h264", + "notes": "Generic Yi camera with Hi3518e chipset - SD stream" + } + ] +} diff --git a/data/brands/yi-hack-v5.json b/data/brands/yi-hack-v5.json new file mode 100644 index 0000000..c3afcfe --- /dev/null +++ b/data/brands/yi-hack-v5.json @@ -0,0 +1,81 @@ +{ + "brand": "yi-hack-v5", + "brand_id": "yi-hack-v5", + "last_updated": "2025-11-11", + "source": "github.com/alienatedsec/yi-hack-v5", + "website": "https://github.com/alienatedsec/yi-hack-v5", + "entries": [ + { + "models": [ + "HI3518EV200 CHIPSET", + "Yi Home", + "Yi Dome", + "Generic Hi3518ev200" + ], + "type": "FFMPEG", + "protocol": "rtsp", + "port": 554, + "url": "/ch0_0.h264", + "notes": "Hi3518ev200 chipset - HD video stream" + }, + { + "models": [ + "HI3518EV200 CHIPSET", + "Yi Home", + "Yi Dome", + "Generic Hi3518ev200" + ], + "type": "FFMPEG", + "protocol": "rtsp", + "port": 554, + "url": "/ch0_1.h264", + "notes": "Hi3518ev200 chipset - SD video stream" + }, + { + "models": [ + "HI3518EV200 CHIPSET", + "Yi Home", + "Yi Dome", + "Generic Hi3518ev200" + ], + "type": "FFMPEG", + "protocol": "rtsp", + "port": 554, + "url": "/ch0_2.h264", + "notes": "Hi3518ev200 chipset - Audio only stream" + }, + { + "models": [ + "HI3518EV200 CHIPSET", + "Yi Home", + "Yi Dome", + "Generic Hi3518ev200" + ], + "type": "FFMPEG", + "protocol": "rtsp", + "port": 554, + "url": "/ch0_3.h264", + "notes": "Hi3518ev200 chipset - Audio only stream (alternative)" + }, + { + "models": [ + "Other" + ], + "type": "FFMPEG", + "protocol": "rtsp", + "port": 554, + "url": "/ch0_0.h264", + "notes": "Generic - HD video stream" + }, + { + "models": [ + "Other" + ], + "type": "FFMPEG", + "protocol": "rtsp", + "port": 554, + "url": "/ch0_1.h264", + "notes": "Generic - SD video stream" + } + ] +} diff --git a/webui/web/css/main.css b/webui/web/css/main.css index 81e9374..363abe6 100644 --- a/webui/web/css/main.css +++ b/webui/web/css/main.css @@ -910,6 +910,83 @@ body { } } +/* ===== FRIGATE TAB CUSTOM STYLES ===== */ +.frigate-input-section { + margin-bottom: var(--space-6); +} + +.frigate-label { + display: block; + font-weight: 500; + margin-bottom: var(--space-2); + color: var(--text-primary); + font-size: var(--text-sm); +} + +.frigate-label .hint { + display: block; + font-weight: 400; + color: var(--text-tertiary); + font-size: var(--text-xs); + margin-top: var(--space-1); +} + +.frigate-config-input { + width: 100%; + min-height: 300px; + font-family: var(--font-mono); + font-size: var(--text-sm); + background: var(--bg-tertiary); + border: 1px solid var(--border-color); + border-radius: 8px; + padding: var(--space-3) var(--space-4); + color: var(--text-primary); + resize: vertical; + transition: all var(--transition-fast); +} + +.frigate-config-input:focus { + outline: none; + border-color: var(--border-focus); + box-shadow: 0 0 0 3px var(--purple-glow); +} + +.btn-generate { + width: 100%; + padding: var(--space-4) var(--space-6); + font-size: var(--text-lg); + font-weight: 600; + margin-bottom: var(--space-6); + background: var(--purple-primary); + color: white; + border: none; + border-radius: 8px; + cursor: pointer; + transition: all var(--transition-fast); + box-shadow: 0 4px 12px var(--purple-glow); +} + +.btn-generate:hover { + background: var(--purple-light); + box-shadow: 0 8px 24px var(--purple-glow-strong); + transform: translateY(-2px); +} + +.btn-generate:active { + transform: translateY(0); +} + +.frigate-output-section { + margin-top: var(--space-6); + padding-top: var(--space-6); + border-top: 1px solid var(--border-color); + animation: slideIn 0.3s ease-out; +} + +.frigate-output-section.hidden { + display: none; +} + /* ===== UTILITIES ===== */ .hidden { display: none !important; diff --git a/webui/web/js/config-generators/frigate/index.js b/webui/web/js/config-generators/frigate/index.js index 4efe8f9..61227f7 100644 --- a/webui/web/js/config-generators/frigate/index.js +++ b/webui/web/js/config-generators/frigate/index.js @@ -1,115 +1,365 @@ /** * Frigate NVR Configuration Generator - * Generates unified Frigate + Go2RTC YAML configs - * All cameras are routed through Frigate's built-in go2rtc for optimal performance + * Adds cameras to existing Frigate configuration + * Based on frigate-conf-generate logic */ export class FrigateGenerator { /** - * Generate complete Frigate config with embedded Go2RTC - * @param {Object} mainStream - Main stream object (used for recording) - * @param {Object} subStream - Optional sub stream object (used for detection if provided) + * Main entry point - generates config (new or adds to existing) + * @param {string} existingConfig - Existing YAML config text (or empty string) + * @param {Object} mainStream - Main stream object + * @param {Object} subStream - Optional sub stream object * @returns {string} YAML configuration string */ - static generate(mainStream, subStream = null) { - const cameraName = this.generateCameraName(mainStream); - const config = []; - - // MQTT Configuration - config.push('mqtt:'); - config.push(' enabled: false'); - config.push(''); - - // Global Record Configuration - config.push('# Global Recording Settings'); - config.push('record:'); - config.push(' enabled: true'); - config.push(' retain:'); - config.push(' days: 7'); - config.push(' mode: motion # Record only on motion detection'); - config.push(''); - - // Generate Go2RTC section - config.push('# Go2RTC Configuration (Frigate built-in)'); - config.push('go2rtc:'); - config.push(' streams:'); - - // Main stream configuration - const mainStreamName = this.generateStreamName(mainStream, 'main'); - const mainSource = this.generateGo2RTCSource(mainStream); - config.push(` '${mainStreamName}':`); - config.push(` - ${mainSource}`); - - // Sub stream configuration if provided - if (subStream) { - config.push(''); - const subStreamName = this.generateStreamName(subStream, 'sub'); - const subSource = this.generateGo2RTCSource(subStream); - config.push(` '${subStreamName}':`); - config.push(` - ${subSource}`); + static generate(existingConfig, mainStream, subStream = null) { + if (!existingConfig || existingConfig.trim() === '') { + // Create new config from scratch + return this.createNewConfig(mainStream, subStream); } - config.push(''); + // Add to existing config + return this.addToExistingConfig(existingConfig, mainStream, subStream); + } - // Generate Frigate cameras section - config.push('# Frigate Camera Configuration'); - config.push('cameras:'); - config.push(` ${cameraName}:`); - config.push(' ffmpeg:'); - config.push(' inputs:'); + /** + * Add camera to existing config (text-based, preserves everything) + */ + static addToExistingConfig(existingConfig, mainStream, subStream) { + const lines = existingConfig.split('\n'); + // Find existing camera names and stream names to avoid duplicates + const existingCameras = this.findExistingCameras(lines); + const existingStreams = this.findExistingStreams(lines); + + // Generate unique camera info + const cameraInfo = this.generateUniqueCameraInfo(mainStream, subStream, existingCameras, existingStreams); + + // Find insertion points + const go2rtcStreamIndex = this.findGo2rtcStreamsInsertionPoint(lines); + const camerasInsertIndex = this.findCamerasInsertionPoint(lines); + + if (go2rtcStreamIndex === -1 || camerasInsertIndex === -1) { + throw new Error('Could not find go2rtc streams or cameras section in config'); + } + + // Generate new stream lines + const streamLines = this.generateStreamLines(cameraInfo); + + // Generate new camera lines + const cameraLines = this.generateCameraLines(cameraInfo); + + // Insert streams into go2rtc section + lines.splice(go2rtcStreamIndex, 0, ...streamLines); + + // Insert camera into cameras section (adjust index after first insertion) + const adjustedCameraIndex = camerasInsertIndex + streamLines.length; + lines.splice(adjustedCameraIndex, 0, ...cameraLines); + + return lines.join('\n'); + } + + /** + * Find existing camera names + */ + static findExistingCameras(lines) { + const cameras = new Set(); + let inCamerasSection = false; + + for (const line of lines) { + if (line.match(/^cameras:/)) { + inCamerasSection = true; + continue; + } + + if (inCamerasSection && line.match(/^[a-z]/)) { + break; // Next top-level section + } + + if (inCamerasSection && line.match(/^\s{2}(\w+):/)) { + const match = line.match(/^\s{2}(\w+):/); + cameras.add(match[1]); + } + } + + return cameras; + } + + /** + * Find existing stream names + */ + static findExistingStreams(lines) { + const streams = new Set(); + let inStreamsSection = false; + + for (const line of lines) { + if (line.match(/^\s{2}streams:/)) { + inStreamsSection = true; + continue; + } + + if (inStreamsSection && line.match(/^[a-z]/)) { + break; // Next top-level section + } + + if (inStreamsSection && line.match(/^\s{4}'?(\w+)'?:/)) { + const match = line.match(/^\s{4}'?(\w+)'?:/); + streams.add(match[1]); + } + } + + return streams; + } + + /** + * Find where to insert new streams in go2rtc section + */ + static findGo2rtcStreamsInsertionPoint(lines) { + let inStreamsSection = false; + let lastStreamIndex = -1; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + + if (line.match(/^\s{2}streams:/)) { + inStreamsSection = true; + continue; + } + + if (inStreamsSection) { + // Check if this is a stream definition or its content + if (line.match(/^\s{4,}/)) { + lastStreamIndex = i; + } else if (line.match(/^[a-z#]/)) { + // Found next section - insert before empty line if it exists + if (lastStreamIndex >= 0 && lines[lastStreamIndex + 1]?.trim() === '') { + return lastStreamIndex + 2; // After existing empty line + } + return lastStreamIndex + 1; + } + } + } + + return lastStreamIndex + 1; + } + + /** + * Find where to insert new camera in cameras section + */ + static findCamerasInsertionPoint(lines) { + let inCamerasSection = false; + let lastCameraLineIndex = -1; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + + if (line.match(/^cameras:/)) { + inCamerasSection = true; + continue; + } + + if (inCamerasSection) { + // Check if we're still in a camera definition + if (line.match(/^\s{2}\w+:/)) { + // New camera starting + lastCameraLineIndex = i; + } else if (line.match(/^\s{2,}\S/)) { + // Still inside camera definition + lastCameraLineIndex = i; + } else if (line.match(/^[a-z]/) && !line.match(/^cameras:/)) { + // Found next top-level section + // Skip any empty lines before it + let insertIndex = lastCameraLineIndex + 1; + while (insertIndex < lines.length && lines[insertIndex].trim() === '') { + insertIndex++; + } + return insertIndex; + } else if (line.match(/^version:/)) { + // Insert before version, skip empty lines + let insertIndex = i; + while (insertIndex > 0 && lines[insertIndex - 1].trim() === '') { + insertIndex--; + } + return insertIndex; + } + } + } + + // If we reach end of file, insert at end + return lines.length; + } + + /** + * Generate unique camera info avoiding duplicates + */ + static generateUniqueCameraInfo(mainStream, subStream, existingCameras, existingStreams) { + const ip = this.extractIP(mainStream.url); + const baseName = ip ? `camera_${ip.replace(/\./g, '_').replace(/:/g, '_')}` : 'camera'; + const streamBaseName = ip ? ip.replace(/\./g, '_').replace(/:/g, '_') : 'stream'; + + // Find unique camera name + let cameraName = baseName; + let suffix = 0; + while (existingCameras.has(cameraName)) { + suffix++; + cameraName = `${baseName}_${suffix}`; + } + + // Find unique stream names + let mainStreamName = `${streamBaseName}_main${suffix ? `_${suffix}` : ''}`; + while (existingStreams.has(mainStreamName)) { + suffix++; + mainStreamName = `${streamBaseName}_main_${suffix}`; + } + + let subStreamName = null; if (subStream) { - // If sub stream exists: use it for detection, main for recording - const subStreamName = this.generateStreamName(subStream, 'sub'); - config.push(` - path: rtsp://127.0.0.1:8554/${subStreamName}`); - config.push(' input_args: preset-rtsp-restream'); - config.push(' roles:'); - config.push(' - detect'); - config.push(` - path: rtsp://127.0.0.1:8554/${mainStreamName}`); - config.push(' input_args: preset-rtsp-restream'); - config.push(' roles:'); - config.push(' - record'); + subStreamName = `${streamBaseName}_sub${suffix ? `_${suffix}` : ''}`; + while (existingStreams.has(subStreamName)) { + suffix++; + subStreamName = `${streamBaseName}_sub_${suffix}`; + } + } + + return { + cameraName, + mainStreamName, + subStreamName, + mainStream, + subStream + }; + } + + /** + * Generate stream lines for go2rtc section + */ + static generateStreamLines(cameraInfo) { + const lines = []; + + // Add main stream + const mainSource = this.generateGo2RTCSource(cameraInfo.mainStream); + lines.push(` '${cameraInfo.mainStreamName}':`); + lines.push(` - ${mainSource}`); + + // Add sub stream if provided + if (cameraInfo.subStream) { + lines.push(''); + const subSource = this.generateGo2RTCSource(cameraInfo.subStream); + lines.push(` '${cameraInfo.subStreamName}':`); + lines.push(` - ${subSource}`); + } + + lines.push(''); + return lines; + } + + /** + * Generate camera lines for cameras section + */ + static generateCameraLines(cameraInfo) { + const lines = []; + + lines.push(` ${cameraInfo.cameraName}:`); + lines.push(' ffmpeg:'); + lines.push(' inputs:'); + + if (cameraInfo.subStream) { + // Use sub for detect, main for record + lines.push(` - path: rtsp://127.0.0.1:8554/${cameraInfo.subStreamName}`); + lines.push(' input_args: preset-rtsp-restream'); + lines.push(' roles:'); + lines.push(' - detect'); + lines.push(` - path: rtsp://127.0.0.1:8554/${cameraInfo.mainStreamName}`); + lines.push(' input_args: preset-rtsp-restream'); + lines.push(' roles:'); + lines.push(' - record'); + + // Add live view configuration + lines.push(' live:'); + lines.push(' streams:'); + lines.push(` Main Stream: ${cameraInfo.mainStreamName} # HD для просмотра`); + lines.push(` Sub Stream: ${cameraInfo.subStreamName} # Низкое разрешение (опционально)`); } else { - // No sub stream: use main for both detection and recording - config.push(` - path: rtsp://127.0.0.1:8554/${mainStreamName}`); - config.push(' input_args: preset-rtsp-restream'); - config.push(' roles:'); - config.push(' - detect'); - config.push(' - record'); + // Use main for both detect and record + lines.push(` - path: rtsp://127.0.0.1:8554/${cameraInfo.mainStreamName}`); + lines.push(' input_args: preset-rtsp-restream'); + lines.push(' roles:'); + lines.push(' - detect'); + lines.push(' - record'); } - // Live view configuration - if (subStream) { - config.push(' live:'); - config.push(' streams:'); - config.push(` Main Stream: ${mainStreamName} # HD для просмотра`); - config.push(` Sub Stream: ${this.generateStreamName(subStream, 'sub')} # Низкое разрешение (опционально)`); + // Add objects configuration + lines.push(' objects:'); + lines.push(' track:'); + lines.push(' - person'); + lines.push(' - car'); + lines.push(' - cat'); + lines.push(' - dog'); + + // Add record configuration + lines.push(' record:'); + lines.push(' enabled: true'); + lines.push(''); + + return lines; + } + + /** + * Create new configuration from scratch + */ + static createNewConfig(mainStream, subStream) { + const cameraInfo = this.generateUniqueCameraInfo(mainStream, subStream, new Set(), new Set()); + const lines = []; + + // MQTT + lines.push('mqtt:'); + lines.push(' enabled: false'); + lines.push(''); + + // Record + lines.push('# Global Recording Settings'); + lines.push('record:'); + lines.push(' enabled: true'); + lines.push(' retain:'); + lines.push(' days: 7'); + lines.push(' mode: motion # Record only on motion detection'); + lines.push(''); + + // Go2RTC + lines.push('# Go2RTC Configuration (Frigate built-in)'); + lines.push('go2rtc:'); + lines.push(' streams:'); + lines.push(...this.generateStreamLines(cameraInfo)); + + // Cameras + lines.push('# Frigate Camera Configuration'); + lines.push('cameras:'); + lines.push(...this.generateCameraLines(cameraInfo)); + + // Version + lines.push('version: 0.16-0'); + + return lines.join('\n'); + } + + /** + * Extract IP address from URL + */ + static extractIP(url) { + try { + const urlObj = new URL(url); + return urlObj.hostname; + } catch (e) { + // Try to extract IP with regex + const match = url.match(/(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})/); + return match ? match[1] : null; } - - // Object detection configuration - config.push(' objects:'); - config.push(' track:'); - config.push(' - person'); - config.push(' - car'); - config.push(' - cat'); - config.push(' - dog'); - - // Recording configuration - config.push(' record:'); - config.push(' enabled: true'); - - config.push(''); - config.push('version: 0.16-0'); - - return config.join('\n'); } /** * Generate Go2RTC source configuration based on stream type - * Returns the source string for go2rtc streams section */ static generateGo2RTCSource(stream) { // Handle JPEG snapshots with exec:ffmpeg conversion - // Uses full path to ffmpeg and {{output}} for Frigate template escaping if (stream.type === 'JPEG') { return [ 'exec:/usr/lib/ffmpeg/7.0/bin/ffmpeg', @@ -122,7 +372,7 @@ export class FrigateGenerator { '-preset ultrafast', '-tune zerolatency', '-g 20', - '-f rtsp {{output}}' // Double braces for Frigate template escaping + '-f rtsp {{output}}' ].join(' '); } @@ -130,16 +380,12 @@ export class FrigateGenerator { if (stream.type === 'ONVIF') { try { const urlObj = new URL(stream.url); - // Extract credentials and host from HTTP URL const username = urlObj.username || 'admin'; const password = urlObj.password || ''; const host = urlObj.hostname; const port = urlObj.port || '80'; - - // Generate onvif:// URL return `onvif://${username}:${password}@${host}:${port}`; } catch (e) { - // If URL parsing fails, return as-is return stream.url; } } @@ -159,36 +405,7 @@ export class FrigateGenerator { } } - // For all other types (RTSP, MJPEG, HLS, HTTP-FLV, RTMP, etc.): use direct URL - // Go2RTC handles these formats natively + // For all other types: use direct URL return stream.url; } - - /** - * Generate camera name from IP address - * Format: "camera_192_168_1_100" - */ - static generateCameraName(stream) { - try { - const urlObj = new URL(stream.url); - const ip = urlObj.hostname.replace(/\./g, '_').replace(/:/g, '_'); - return `camera_${ip}`; - } catch (e) { - return 'camera'; - } - } - - /** - * Generate stream name for Go2RTC reference - * Format: "192_168_1_100_main" or "192_168_1_100_sub" - */ - static generateStreamName(stream, suffix) { - try { - const urlObj = new URL(stream.url); - const ip = urlObj.hostname.replace(/\./g, '_').replace(/:/g, '_'); - return `${ip}_${suffix}`; - } catch (e) { - return `camera_stream_${suffix}`; - } - } } diff --git a/webui/web/js/main.js b/webui/web/js/main.js index fd788df..a891e4e 100644 --- a/webui/web/js/main.js +++ b/webui/web/js/main.js @@ -3,6 +3,7 @@ import { StreamDiscoveryAPI } from './api/stream-discovery.js'; import { SearchForm } from './ui/search-form.js'; import { StreamCarousel } from './ui/stream-carousel.js'; import { ConfigPanel } from './ui/config-panel.js'; +import { FrigateGenerator } from './config-generators/frigate/index.js'; import { showToast } from './utils/toast.js'; class StrixApp { @@ -106,6 +107,9 @@ class StrixApp { document.getElementById('btn-add-sub-stream').addEventListener('click', () => this.addSubStream()); document.getElementById('btn-remove-sub').addEventListener('click', () => this.removeSubStream()); + // Frigate config generation + document.getElementById('btn-generate-frigate').addEventListener('click', () => this.generateFrigateConfig()); + document.getElementById('btn-new-search').addEventListener('click', () => { this.reset(); this.showScreen('address'); @@ -354,6 +358,11 @@ class StrixApp { } this.isSelectingSubStream = true; + + // Clear Frigate output section (but NOT the user's input textarea) + document.getElementById('frigate-output-section').classList.add('hidden'); + document.getElementById('config-frigate').textContent = ''; + showToast('Select a sub stream from available streams'); this.showScreen('discovery'); } @@ -365,6 +374,40 @@ class StrixApp { showToast('Sub stream removed'); } + /** + * Generate Frigate config by adding camera to existing config + */ + generateFrigateConfig() { + const existingConfig = document.getElementById('existing-frigate-config').value; + const mainStream = this.selectedMainStream; + const subStream = this.selectedSubStream; + + if (!mainStream) { + showToast('No main stream selected', 'error'); + return; + } + + try { + // Generate config using FrigateGenerator + const newConfig = FrigateGenerator.generate(existingConfig, mainStream, subStream); + + // Show result + document.getElementById('config-frigate').textContent = newConfig; + document.getElementById('frigate-output-section').classList.remove('hidden'); + + // Scroll to result + document.getElementById('frigate-output-section').scrollIntoView({ + behavior: 'smooth', + block: 'nearest' + }); + + showToast('Config generated successfully!'); + } catch (error) { + showToast(`Error: ${error.message}`, 'error'); + console.error('Config generation error:', error); + } + } + updateSubStreamUI() { const subStreamInfo = document.getElementById('sub-stream-info'); const addSubStreamBtn = document.getElementById('btn-add-sub-stream'); diff --git a/webui/web/js/ui/config-panel.js b/webui/web/js/ui/config-panel.js index ae044e9..876ed31 100644 --- a/webui/web/js/ui/config-panel.js +++ b/webui/web/js/ui/config-panel.js @@ -21,15 +21,59 @@ export class ConfigPanel { document.getElementById('selected-sub-url').textContent = this.maskCredentials(subStream.url); } - // Generate configs + // Generate configs for URL and Go2RTC (as before) const urlConfig = this.generateURLConfig(); const go2rtcConfig = Go2RTCGenerator.generate(mainStream, subStream); - const frigateConfig = FrigateGenerator.generate(mainStream, subStream); // Update config displays document.getElementById('config-url').textContent = urlConfig; document.getElementById('config-go2rtc').textContent = go2rtcConfig; - document.getElementById('config-frigate').textContent = frigateConfig; + + // For Frigate: initialize the tab instead of generating automatically + this.initializeFrigateTab(); + } + + /** + * Initialize Frigate tab with example config + */ + initializeFrigateTab() { + const textarea = document.getElementById('existing-frigate-config'); + const outputSection = document.getElementById('frigate-output-section'); + + // Show example config if field is empty + if (!textarea.value || textarea.value.trim() === '') { + textarea.value = this.getExampleConfig(); + } + + // Hide output section + outputSection.classList.add('hidden'); + document.getElementById('config-frigate').textContent = ''; + } + + /** + * Get example Frigate config + */ + getExampleConfig() { + return `mqtt: + enabled: false + +# Global Recording Settings +record: + enabled: true + retain: + days: 7 + mode: motion # Record only on motion detection + +# Go2RTC Configuration (Frigate built-in) +go2rtc: + streams: + # Your existing streams will be preserved here + +# Frigate Camera Configuration +cameras: + # Your existing cameras will be preserved here + +version: 0.16-0`; } generateURLConfig() {