diff --git a/.vscode/launch.json b/.vscode/launch.json
new file mode 100644
index 0000000..da1b27c
--- /dev/null
+++ b/.vscode/launch.json
@@ -0,0 +1,23 @@
+{
+ // 使用 IntelliSense 了解相关属性。
+ // 悬停以查看现有属性的描述。
+ // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
+ "version": "0.2.0",
+ "configurations": [
+
+ {
+ "name": "Launch Package",
+ "type": "go",
+ "request": "launch",
+ "mode": "auto",
+ "program": "${fileDirname}"
+ },
+ {
+ "name": "Launch Test",
+ "type": "go",
+ "request": "launch",
+ "mode": "test",
+ "program": "${fileDirname}"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/DATABASE_README.md b/DATABASE_README.md
new file mode 100644
index 0000000..60973b0
--- /dev/null
+++ b/DATABASE_README.md
@@ -0,0 +1,275 @@
+# 数据库集成功能说明
+
+本项目已扩展支持MySQL和SQL Server数据库,用于管理摄像头信息,实现动态配置和实时预览功能。
+
+## 功能特性
+
+- ✅ 支持MySQL和SQL Server数据库
+- ✅ 摄像头信息的增删改查
+- ✅ Web界面管理摄像头
+- ✅ 实时预览新增摄像头
+- ✅ 数据库与配置文件双模式支持
+- ✅ 自动数据库表创建
+- ✅ 数据库连接状态监控
+
+## 配置说明
+
+### 1. 配置文件设置
+
+在 `config.json` 中添加数据库配置:
+
+```json
+{
+ "server": {
+ "database_enabled": true,
+ // ... 其他服务器配置
+ },
+ "database": {
+ "enabled": true,
+ "type": "mysql",
+ "host": "localhost",
+ "port": 3306,
+ "username": "root",
+ "password": "password",
+ "database": "rtsp_cameras",
+ "table_name": "cameras"
+ }
+}
+```
+
+### 2. 数据库类型配置
+
+#### MySQL配置
+```json
+"database": {
+ "enabled": true,
+ "type": "mysql",
+ "host": "localhost",
+ "port": 3306,
+ "username": "root",
+ "password": "password",
+ "database": "rtsp_cameras",
+ "table_name": "cameras"
+}
+```
+
+#### SQL Server配置
+```json
+"database": {
+ "enabled": true,
+ "type": "sqlserver",
+ "host": "localhost",
+ "port": 1433,
+ "username": "sa",
+ "password": "YourPassword123",
+ "database": "rtsp_cameras",
+ "table_name": "cameras"
+}
+```
+
+## 数据库初始化
+
+### 1. 使用提供的SQL脚本
+
+执行 `database_init.sql` 文件来创建数据库和表:
+
+```bash
+# MySQL
+mysql -u root -p < database_init.sql
+
+# SQL Server
+sqlcmd -S localhost -U sa -P YourPassword123 -i database_init.sql
+```
+
+### 2. 手动创建(MySQL)
+
+```sql
+CREATE DATABASE rtsp_cameras CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
+USE rtsp_cameras;
+
+CREATE TABLE cameras (
+ id VARCHAR(36) PRIMARY KEY,
+ name VARCHAR(255) NOT NULL,
+ ip VARCHAR(45) NOT NULL,
+ username VARCHAR(100) DEFAULT NULL,
+ password VARCHAR(255) DEFAULT NULL,
+ rtsp_url VARCHAR(500) NOT NULL,
+ enabled BOOLEAN DEFAULT TRUE,
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
+);
+```
+
+### 3. 手动创建(SQL Server)
+
+```sql
+CREATE DATABASE rtsp_cameras;
+GO
+USE rtsp_cameras;
+GO
+
+CREATE TABLE cameras (
+ id NVARCHAR(36) PRIMARY KEY,
+ name NVARCHAR(255) NOT NULL,
+ ip NVARCHAR(45) NOT NULL,
+ username NVARCHAR(100) NULL,
+ password NVARCHAR(255) NULL,
+ rtsp_url NVARCHAR(500) NOT NULL,
+ enabled BIT DEFAULT 1,
+ created_at DATETIME2 DEFAULT GETDATE(),
+ updated_at DATETIME2 DEFAULT GETDATE()
+);
+```
+
+## API接口
+
+### 摄像头管理接口
+
+| 方法 | 路径 | 描述 |
+|------|------|------|
+| GET | `/cameras` | 获取所有摄像头列表 |
+| POST | `/camera/add` | 添加新摄像头 |
+| GET | `/camera/{id}` | 获取单个摄像头信息 |
+| PUT | `/camera/{id}` | 更新摄像头信息 |
+| DELETE | `/camera/{id}` | 删除摄像头 |
+| POST | `/cameras/refresh` | 刷新摄像头列表 |
+| GET | `/database/status` | 获取数据库状态 |
+
+### 请求示例
+
+#### 添加摄像头
+```bash
+curl -X POST http://localhost:8083/camera/add \
+ -H "Content-Type: application/json" \
+ -d '{
+ "name": "测试摄像头",
+ "ip": "192.168.1.100",
+ "username": "admin",
+ "password": "admin123",
+ "rtsp_url": "rtsp://192.168.1.100:554/stream1",
+ "enabled": true
+ }'
+```
+
+#### 更新摄像头
+```bash
+curl -X PUT http://localhost:8083/camera/{camera_id} \
+ -H "Content-Type: application/json" \
+ -d '{
+ "name": "更新的摄像头名称",
+ "ip": "192.168.1.101",
+ "username": "admin",
+ "password": "newpassword",
+ "rtsp_url": "rtsp://192.168.1.101:554/stream1",
+ "enabled": true
+ }'
+```
+
+## Web界面使用
+
+### 1. 访问摄像头管理页面
+
+打开浏览器访问:`http://localhost:8083/pages/cameras`
+
+### 2. 功能说明
+
+- **摄像头列表**:显示所有摄像头的基本信息和状态
+- **添加摄像头**:点击"添加摄像头"按钮,填写摄像头信息
+- **编辑摄像头**:点击摄像头卡片上的"编辑"按钮
+- **删除摄像头**:点击摄像头卡片上的"删除"按钮
+- **实时预览**:点击摄像头卡片上的"预览"按钮
+- **刷新列表**:点击"刷新"按钮重新加载摄像头列表
+- **数据库状态**:页面顶部显示数据库连接状态
+
+## 运行模式
+
+### 1. 数据库模式
+
+当 `database.enabled = true` 时:
+- 摄像头信息从数据库加载
+- 所有配置变更保存到数据库
+- 配置文件中的streams配置被忽略
+
+### 2. 配置文件模式
+
+当 `database.enabled = false` 时:
+- 摄像头信息从config.json加载
+- 配置变更保存到配置文件
+- 数据库功能不可用
+
+### 3. 混合模式
+
+可以在运行时动态切换模式,但建议重启应用以确保配置生效。
+
+## 依赖包
+
+项目添加了以下Go依赖包:
+
+```go
+// MySQL驱动
+github.com/go-sql-driver/mysql v1.7.1
+
+// SQL Server驱动
+github.com/denisenkom/go-mssqldb v0.12.3
+
+// UUID生成
+github.com/google/uuid v1.3.0
+```
+
+## 安装依赖
+
+```bash
+go mod tidy
+```
+
+## 编译运行
+
+```bash
+# 编译
+go build -o rtsp-to-web
+
+# 运行
+./rtsp-to-web -config config.json
+```
+
+## 故障排除
+
+### 1. 数据库连接失败
+
+- 检查数据库服务是否运行
+- 验证连接参数(主机、端口、用户名、密码)
+- 确认数据库存在
+- 检查防火墙设置
+
+### 2. 表不存在错误
+
+- 运行数据库初始化脚本
+- 检查数据库权限
+- 确认表名配置正确
+
+### 3. 摄像头无法预览
+
+- 检查RTSP URL是否正确
+- 验证摄像头网络连接
+- 确认用户名密码正确
+- 检查摄像头是否支持RTSP协议
+
+### 4. Web界面无法访问
+
+- 确认HTTP服务端口配置
+- 检查防火墙设置
+- 验证demo模式是否启用
+
+## 注意事项
+
+1. **密码安全**:数据库中的密码以明文存储,生产环境建议加密
+2. **并发访问**:多个实例同时访问数据库时注意数据一致性
+3. **备份恢复**:定期备份数据库数据
+4. **性能优化**:大量摄像头时考虑数据库索引优化
+5. **网络安全**:生产环境建议使用HTTPS和数据库SSL连接
+
+## 更新日志
+
+- v1.0.0: 初始版本,支持MySQL和SQL Server
+- 添加Web界面管理功能
+- 支持实时预览和动态配置
\ No newline at end of file
diff --git a/RTSPtoWeb.exe b/RTSPtoWeb.exe
new file mode 100644
index 0000000..e72d860
Binary files /dev/null and b/RTSPtoWeb.exe differ
diff --git a/RTSPtoWeb.go b/RTSPtoWeb.go
index 83cc934..2962376 100644
--- a/RTSPtoWeb.go
+++ b/RTSPtoWeb.go
@@ -10,6 +10,9 @@ import (
)
func main() {
+ println("Starting RTSPtoWeb server...")
+ // Set correct log level after Storage initialization
+ SetLogLevel()
log.WithFields(logrus.Fields{
"module": "main",
"func": "main",
diff --git a/apiHTTPCamera.go b/apiHTTPCamera.go
new file mode 100644
index 0000000..b33faa8
--- /dev/null
+++ b/apiHTTPCamera.go
@@ -0,0 +1,415 @@
+package main
+
+import (
+ "net/http"
+ "time"
+
+ "github.com/gin-gonic/gin"
+ "github.com/google/uuid"
+ "github.com/sirupsen/logrus"
+)
+
+// CameraRequest 摄像头请求结构
+type CameraRequest struct {
+ Name string `json:"name" binding:"required"`
+ IP string `json:"ip" binding:"required"`
+ Username string `json:"username"`
+ Password string `json:"password"`
+ RTSPURL string `json:"rtsp_url" binding:"required"`
+ Enabled bool `json:"enabled"`
+}
+
+// CameraResponse 摄像头响应结构
+type CameraResponse struct {
+ ID string `json:"id"`
+ Name string `json:"name"`
+ IP string `json:"ip"`
+ Username string `json:"username"`
+ Password string `json:"password,omitempty"` // 密码在响应中可选
+ RTSPURL string `json:"rtsp_url"`
+ Enabled bool `json:"enabled"`
+ Status string `json:"status"`
+ CreatedAt time.Time `json:"created_at"`
+ UpdatedAt time.Time `json:"updated_at"`
+}
+
+// HTTPAPIServerCameras 获取所有摄像头列表
+func HTTPAPIServerCameras(c *gin.Context) {
+ if !Storage.Server.DatabaseEnabled {
+ c.JSON(http.StatusServiceUnavailable, gin.H{
+ "error": "Database is not enabled",
+ })
+ return
+ }
+
+ if Storage.dbManager == nil {
+ c.JSON(http.StatusInternalServerError, gin.H{
+ "error": "Database manager not initialized",
+ })
+ return
+ }
+
+ cameras, err := Storage.dbManager.GetAllCameras()
+ if err != nil {
+ log.WithFields(logrus.Fields{
+ "module": "api",
+ "func": "HTTPAPIServerCameras",
+ "call": "GetAllCameras",
+ }).Errorln(err.Error())
+ c.JSON(http.StatusInternalServerError, gin.H{
+ "error": err.Error(),
+ })
+ return
+ }
+
+ // 转换为响应格式
+ var response []CameraResponse
+ for _, camera := range cameras {
+ status := "offline"
+ if stream, exists := Storage.Streams[camera.ID]; exists {
+ if len(stream.Channels) > 0 {
+ for _, channel := range stream.Channels {
+ if channel.runLock {
+ status = "online"
+ break
+ }
+ }
+ }
+ }
+
+ response = append(response, CameraResponse{
+ ID: camera.ID,
+ Name: camera.Name,
+ IP: camera.IP,
+ Username: camera.Username,
+ // Password: camera.Password, // 不返回密码
+ RTSPURL: camera.RTSPURL,
+ Enabled: camera.Enabled,
+ Status: status,
+ CreatedAt: camera.CreatedAt,
+ UpdatedAt: camera.UpdatedAt,
+ })
+ }
+
+ c.JSON(http.StatusOK, gin.H{
+ "cameras": response,
+ "count": len(response),
+ })
+}
+
+// HTTPAPIServerCameraAdd 添加新摄像头
+func HTTPAPIServerCameraAdd(c *gin.Context) {
+ if !Storage.Server.DatabaseEnabled {
+ c.JSON(http.StatusServiceUnavailable, gin.H{
+ "error": "Database is not enabled",
+ })
+ return
+ }
+
+ var req CameraRequest
+ if err := c.ShouldBindJSON(&req); err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{
+ "error": err.Error(),
+ })
+ return
+ }
+
+ // 生成新的UUID
+ cameraID := uuid.New().String()
+
+ // 创建摄像头记录
+ camera := Camera{
+ ID: cameraID,
+ Name: req.Name,
+ IP: req.IP,
+ Username: req.Username,
+ Password: req.Password,
+ RTSPURL: req.RTSPURL,
+ Enabled: req.Enabled,
+ CreatedAt: time.Now(),
+ UpdatedAt: time.Now(),
+ }
+
+ // 保存到数据库
+ err := Storage.dbManager.CreateCamera(&camera)
+ if err != nil {
+ log.WithFields(logrus.Fields{
+ "module": "api",
+ "func": "HTTPAPIServerCameraAdd",
+ "call": "CreateCamera",
+ }).Errorln(err.Error())
+ c.JSON(http.StatusInternalServerError, gin.H{
+ "error": err.Error(),
+ })
+ return
+ }
+
+ // 转换为流配置并添加到内存
+ stream := CameraToStream(camera)
+ err = Storage.StreamAdd(cameraID, stream)
+ if err != nil {
+ log.WithFields(logrus.Fields{
+ "module": "api",
+ "func": "HTTPAPIServerCameraAdd",
+ "call": "StreamAdd",
+ }).Errorln(err.Error())
+ c.JSON(http.StatusInternalServerError, gin.H{
+ "error": err.Error(),
+ })
+ return
+ }
+
+ c.JSON(http.StatusCreated, gin.H{
+ "message": "Camera added successfully",
+ "camera_id": cameraID,
+ })
+}
+
+// HTTPAPIServerCameraUpdate 更新摄像头
+func HTTPAPIServerCameraUpdate(c *gin.Context) {
+ if !Storage.Server.DatabaseEnabled {
+ c.JSON(http.StatusServiceUnavailable, gin.H{
+ "error": "Database is not enabled",
+ })
+ return
+ }
+
+ cameraID := c.Param("uuid")
+ if cameraID == "" {
+ c.JSON(http.StatusBadRequest, gin.H{
+ "error": "Camera ID is required",
+ })
+ return
+ }
+
+ var req CameraRequest
+ if err := c.ShouldBindJSON(&req); err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{
+ "error": err.Error(),
+ })
+ return
+ }
+
+ // 检查摄像头是否存在
+ existingCamera, err := Storage.dbManager.GetCameraByID(cameraID)
+ if err != nil {
+ log.WithFields(logrus.Fields{
+ "module": "api",
+ "func": "HTTPAPIServerCameraUpdate",
+ "call": "GetCameraByID",
+ }).Errorln(err.Error())
+ c.JSON(http.StatusInternalServerError, gin.H{
+ "error": err.Error(),
+ })
+ return
+ }
+
+ if existingCamera == nil {
+ c.JSON(http.StatusNotFound, gin.H{
+ "error": "Camera not found",
+ })
+ return
+ }
+
+ // 更新摄像头信息
+ updatedCamera := Camera{
+ ID: cameraID,
+ Name: req.Name,
+ IP: req.IP,
+ Username: req.Username,
+ Password: req.Password,
+ RTSPURL: req.RTSPURL,
+ Enabled: req.Enabled,
+ CreatedAt: existingCamera.CreatedAt,
+ UpdatedAt: time.Now(),
+ }
+
+ // 更新数据库
+ err = Storage.dbManager.UpdateCamera(&updatedCamera)
+ if err != nil {
+ log.WithFields(logrus.Fields{
+ "module": "api",
+ "func": "HTTPAPIServerCameraUpdate",
+ "call": "UpdateCamera",
+ }).Errorln(err.Error())
+ c.JSON(http.StatusInternalServerError, gin.H{
+ "error": err.Error(),
+ })
+ return
+ }
+
+ // 更新内存中的流配置
+ stream := CameraToStream(updatedCamera)
+ err = Storage.StreamEdit(cameraID, stream)
+ if err != nil {
+ log.WithFields(logrus.Fields{
+ "module": "api",
+ "func": "HTTPAPIServerCameraUpdate",
+ "call": "StreamEdit",
+ }).Errorln(err.Error())
+ c.JSON(http.StatusInternalServerError, gin.H{
+ "error": err.Error(),
+ })
+ return
+ }
+
+ c.JSON(http.StatusOK, gin.H{
+ "message": "Camera updated successfully",
+ })
+}
+
+// HTTPAPIServerCameraDelete 删除摄像头
+func HTTPAPIServerCameraDelete(c *gin.Context) {
+ if !Storage.Server.DatabaseEnabled {
+ c.JSON(http.StatusServiceUnavailable, gin.H{
+ "error": "Database is not enabled",
+ })
+ return
+ }
+
+ cameraID := c.Param("uuid")
+ if cameraID == "" {
+ c.JSON(http.StatusBadRequest, gin.H{
+ "error": "Camera ID is required",
+ })
+ return
+ }
+
+ // 检查摄像头是否存在
+ existingCamera, err := Storage.dbManager.GetCameraByID(cameraID)
+ if err != nil {
+ log.WithFields(logrus.Fields{
+ "module": "api",
+ "func": "HTTPAPIServerCameraDelete",
+ "call": "GetCameraByID",
+ }).Errorln(err.Error())
+ c.JSON(http.StatusInternalServerError, gin.H{
+ "error": err.Error(),
+ })
+ return
+ }
+
+ if existingCamera == nil {
+ c.JSON(http.StatusNotFound, gin.H{
+ "error": "Camera not found",
+ })
+ return
+ }
+
+ // 从内存中删除流
+ err = Storage.StreamDelete(cameraID)
+ if err != nil {
+ log.WithFields(logrus.Fields{
+ "module": "api",
+ "func": "HTTPAPIServerCameraDelete",
+ "call": "StreamDelete",
+ }).Errorln(err.Error())
+ c.JSON(http.StatusInternalServerError, gin.H{
+ "error": err.Error(),
+ })
+ return
+ }
+
+ c.JSON(http.StatusOK, gin.H{
+ "message": "Camera deleted successfully",
+ })
+}
+
+// HTTPAPIServerCameraGet 获取单个摄像头信息
+func HTTPAPIServerCameraGet(c *gin.Context) {
+ if !Storage.Server.DatabaseEnabled {
+ c.JSON(http.StatusServiceUnavailable, gin.H{
+ "error": "Database is not enabled",
+ })
+ return
+ }
+
+ cameraID := c.Param("uuid")
+ if cameraID == "" {
+ c.JSON(http.StatusBadRequest, gin.H{
+ "error": "Camera ID is required",
+ })
+ return
+ }
+
+ camera, err := Storage.dbManager.GetCameraByID(cameraID)
+ if err != nil {
+ log.WithFields(logrus.Fields{
+ "module": "api",
+ "func": "HTTPAPIServerCameraGet",
+ "call": "GetCameraByID",
+ }).Errorln(err.Error())
+ c.JSON(http.StatusInternalServerError, gin.H{
+ "error": err.Error(),
+ })
+ return
+ }
+
+ if camera == nil {
+ c.JSON(http.StatusNotFound, gin.H{
+ "error": "Camera not found",
+ })
+ return
+ }
+
+ // 检查流状态
+ status := "offline"
+ if stream, exists := Storage.Streams[camera.ID]; exists {
+ if len(stream.Channels) > 0 {
+ for _, channel := range stream.Channels {
+ if channel.runLock {
+ status = "online"
+ break
+ }
+ }
+ }
+ }
+
+ response := CameraResponse{
+ ID: camera.ID,
+ Name: camera.Name,
+ IP: camera.IP,
+ Username: camera.Username,
+ // Password: camera.Password, // 不返回密码
+ RTSPURL: camera.RTSPURL,
+ Enabled: camera.Enabled,
+ Status: status,
+ CreatedAt: camera.CreatedAt,
+ UpdatedAt: camera.UpdatedAt,
+ }
+
+ c.JSON(http.StatusOK, response)
+}
+
+// HTTPAPIServerCameraRefresh 刷新摄像头列表(从数据库重新加载)
+func HTTPAPIServerCameraRefresh(c *gin.Context) {
+ if !Storage.Server.DatabaseEnabled {
+ c.JSON(http.StatusServiceUnavailable, gin.H{
+ "error": "Database is not enabled",
+ })
+ return
+ }
+
+ err := Storage.RefreshStreamsFromDatabase()
+ if err != nil {
+ log.WithFields(logrus.Fields{
+ "module": "api",
+ "func": "HTTPAPIServerCameraRefresh",
+ "call": "RefreshStreamsFromDatabase",
+ }).Errorln(err.Error())
+ c.JSON(http.StatusInternalServerError, gin.H{
+ "error": err.Error(),
+ })
+ return
+ }
+
+ c.JSON(http.StatusOK, gin.H{
+ "message": "Cameras refreshed successfully",
+ })
+}
+
+// HTTPAPIServerDatabaseStatus 获取数据库状态
+func HTTPAPIServerDatabaseStatus(c *gin.Context) {
+ status := Storage.GetDatabaseStatus()
+ c.JSON(http.StatusOK, status)
+}
diff --git a/apiHTTPPages.go b/apiHTTPPages.go
new file mode 100644
index 0000000..ef503a5
--- /dev/null
+++ b/apiHTTPPages.go
@@ -0,0 +1,14 @@
+package main
+
+import (
+ "net/http"
+
+ "github.com/gin-gonic/gin"
+)
+
+// HTTPAPICameraManagement 摄像头管理页面
+func HTTPAPICameraManagement(c *gin.Context) {
+ c.HTML(http.StatusOK, "cameras.tmpl", gin.H{
+ "title": "摄像头管理",
+ })
+}
diff --git a/apiHTTPRouter.go b/apiHTTPRouter.go
index 675dc3a..1d73019 100644
--- a/apiHTTPRouter.go
+++ b/apiHTTPRouter.go
@@ -1,12 +1,18 @@
package main
import (
+ "io/ioutil"
"net/http"
"os"
+ "path/filepath"
+ "strings"
"time"
"github.com/gin-gonic/autotls"
"github.com/gin-gonic/gin"
+ "github.com/gomarkdown/markdown"
+ "github.com/gomarkdown/markdown/html"
+ "github.com/gomarkdown/markdown/parser"
"github.com/sirupsen/logrus"
)
@@ -57,7 +63,9 @@ func HTTPAPIServer() {
public.Any("/pages/multiview/full", HTTPAPIFullScreenMultiView)
public.GET("/pages/documentation", HTTPAPIServerDocumentation)
public.GET("/pages/player/all/:uuid/:channel", HTTPAPIPlayAll)
+ public.GET("/pages/cameras", HTTPAPICameraManagement)
public.StaticFS("/static", http.Dir(Storage.ServerHTTPDir()+"/static"))
+ public.GET("/docs/*filepath", HTTPAPIMarkdownRenderer)
}
/*
@@ -78,6 +86,18 @@ func HTTPAPIServer() {
privat.POST("/streams/multi/control/add", HTTPAPIServerStreamsMultiControlAdd)
privat.POST("/streams/multi/control/delete", HTTPAPIServerStreamsMultiControlDelete)
+ /*
+ Camera Management elements
+ */
+
+ privat.GET("/cameras", HTTPAPIServerCameras)
+ privat.POST("/camera/add", HTTPAPIServerCameraAdd)
+ privat.GET("/camera/:uuid", HTTPAPIServerCameraGet)
+ privat.PUT("/camera/:uuid", HTTPAPIServerCameraUpdate)
+ privat.DELETE("/camera/:uuid", HTTPAPIServerCameraDelete)
+ privat.POST("/cameras/refresh", HTTPAPIServerCameraRefresh)
+ privat.GET("/database/status", HTTPAPIServerDatabaseStatus)
+
/*
Stream Channel elements
*/
@@ -286,6 +306,83 @@ func HTTPAPIFullScreenMultiView(c *gin.Context) {
})
}
+// HTTPAPIMarkdownRenderer renders markdown files as HTML
+func HTTPAPIMarkdownRenderer(c *gin.Context) {
+ filePath := c.Param("filepath")
+ // Remove leading slash
+ if strings.HasPrefix(filePath, "/") {
+ filePath = filePath[1:]
+ }
+
+ // Construct full file path
+ fullPath := filepath.Join("./docs", filePath)
+
+ // Check if file exists and has .md extension
+ if !strings.HasSuffix(filePath, ".md") {
+ c.String(http.StatusNotFound, "只支持 .md 文件")
+ return
+ }
+
+ // Read markdown file
+ markdownBytes, err := ioutil.ReadFile(fullPath)
+ if err != nil {
+ c.String(http.StatusNotFound, "文件未找到")
+ return
+ }
+
+ // Create markdown parser with extensions
+ extensions := parser.CommonExtensions | parser.AutoHeadingIDs | parser.NoEmptyLineBeforeBlock
+ p := parser.NewWithExtensions(extensions)
+ doc := p.Parse(markdownBytes)
+
+ // Create HTML renderer with extensions
+ htmlFlags := html.CommonFlags | html.HrefTargetBlank
+ opts := html.RendererOptions{Flags: htmlFlags}
+ renderer := html.NewRenderer(opts)
+
+ // Render markdown to HTML
+ htmlContent := markdown.Render(doc, renderer)
+
+ // Create HTML content by concatenating strings
+ htmlStart := `
+
+
+
+
+ API 文档
+
+
+
+
+
+
+
`
+
+ htmlEnd := `
+
+
+`
+
+ // Combine HTML parts with markdown content
+ finalHTML := htmlStart + string(htmlContent) + htmlEnd
+
+ c.Header("Content-Type", "text/html; charset=utf-8")
+ c.String(http.StatusOK, finalHTML)
+}
+
// CrossOrigin Access-Control-Allow-Origin any methods
func CrossOrigin() gin.HandlerFunc {
return func(c *gin.Context) {
diff --git a/config.json b/config.json
index 65067f7..7e38d3a 100644
--- a/config.json
+++ b/config.json
@@ -23,6 +23,16 @@
"audio": true
}
},
+ "database": {
+ "enabled": false,
+ "type": "mysql",
+ "host": "localhost",
+ "port": 3306,
+ "username": "root",
+ "password": "password",
+ "database": "rtsp_cameras",
+ "table_name": "cameras"
+ },
"streams": {
"27aec28e-6181-4753-9acd-0456a75f0289": {
"channels": {
diff --git a/database.go b/database.go
new file mode 100644
index 0000000..3567e49
--- /dev/null
+++ b/database.go
@@ -0,0 +1,271 @@
+package main
+
+import (
+ "database/sql"
+ "fmt"
+ "time"
+
+ _ "github.com/denisenkom/go-mssqldb"
+ _ "github.com/go-sql-driver/mysql"
+ "github.com/google/uuid"
+)
+
+// DatabaseConfig 数据库配置结构
+type DatabaseConfig struct {
+ Enabled bool `json:"enabled"`
+ Type string `json:"type"` // mysql 或 sqlserver
+ Host string `json:"host"`
+ Port int `json:"port"`
+ Database string `json:"database"`
+ Username string `json:"username"`
+ Password string `json:"password"`
+ SSLMode string `json:"ssl_mode,omitempty"` // for mysql
+}
+
+// Camera 摄像头数据库模型
+type Camera struct {
+ ID string `json:"id" db:"id"`
+ Name string `json:"name" db:"name"`
+ IP string `json:"ip" db:"ip"`
+ Username string `json:"username" db:"username"`
+ Password string `json:"password" db:"password"`
+ RTSPURL string `json:"rtsp_url" db:"rtsp_url"`
+ Enabled bool `json:"enabled" db:"enabled"`
+ OnDemand bool `json:"on_demand" db:"on_demand"`
+ Audio bool `json:"audio" db:"audio"`
+ Debug bool `json:"debug" db:"debug"`
+ CreatedAt time.Time `json:"created_at" db:"created_at"`
+ UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
+}
+
+// DatabaseManager 数据库管理器
+type DatabaseManager struct {
+ db *sql.DB
+ config DatabaseConfig
+}
+
+// NewDatabaseManager 创建数据库管理器
+func NewDatabaseManager(config DatabaseConfig) (*DatabaseManager, error) {
+ if !config.Enabled {
+ return nil, nil
+ }
+
+ var dsn string
+ switch config.Type {
+ case "mysql":
+ dsn = fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local",
+ config.Username, config.Password, config.Host, config.Port, config.Database)
+ if config.SSLMode != "" {
+ dsn += "&tls=" + config.SSLMode
+ }
+ case "sqlserver":
+ dsn = fmt.Sprintf("server=%s;port=%d;database=%s;user id=%s;password=%s",
+ config.Host, config.Port, config.Database, config.Username, config.Password)
+ default:
+ return nil, fmt.Errorf("unsupported database type: %s", config.Type)
+ }
+
+ db, err := sql.Open(config.Type, dsn)
+ if err != nil {
+ return nil, fmt.Errorf("failed to open database: %v", err)
+ }
+
+ // 测试连接
+ if err := db.Ping(); err != nil {
+ return nil, fmt.Errorf("failed to ping database: %v", err)
+ }
+
+ // 设置连接池参数
+ db.SetMaxOpenConns(25)
+ db.SetMaxIdleConns(5)
+ db.SetConnMaxLifetime(5 * time.Minute)
+
+ dm := &DatabaseManager{
+ db: db,
+ config: config,
+ }
+
+ // 初始化数据库表
+ if err := dm.initTables(); err != nil {
+ return nil, fmt.Errorf("failed to initialize tables: %v", err)
+ }
+
+ return dm, nil
+}
+
+// initTables 初始化数据库表
+func (dm *DatabaseManager) initTables() error {
+ var createTableSQL string
+
+ switch dm.config.Type {
+ case "mysql":
+ createTableSQL = `
+ CREATE TABLE IF NOT EXISTS cameras (
+ id VARCHAR(36) PRIMARY KEY,
+ name VARCHAR(255) NOT NULL,
+ ip VARCHAR(45) NOT NULL,
+ username VARCHAR(100),
+ password VARCHAR(100),
+ rtsp_url TEXT NOT NULL,
+ enabled BOOLEAN DEFAULT TRUE,
+ on_demand BOOLEAN DEFAULT FALSE,
+ audio BOOLEAN DEFAULT TRUE,
+ debug BOOLEAN DEFAULT FALSE,
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ INDEX idx_enabled (enabled),
+ INDEX idx_ip (ip)
+ )`
+ case "sqlserver":
+ createTableSQL = `
+ IF NOT EXISTS (SELECT * FROM sysobjects WHERE name='cameras' AND xtype='U')
+ CREATE TABLE cameras (
+ id NVARCHAR(36) PRIMARY KEY,
+ name NVARCHAR(255) NOT NULL,
+ ip NVARCHAR(45) NOT NULL,
+ username NVARCHAR(100),
+ password NVARCHAR(100),
+ rtsp_url NTEXT NOT NULL,
+ enabled BIT DEFAULT 1,
+ on_demand BIT DEFAULT 0,
+ audio BIT DEFAULT 1,
+ debug BIT DEFAULT 0,
+ created_at DATETIME2 DEFAULT GETDATE(),
+ updated_at DATETIME2 DEFAULT GETDATE()
+ )`
+ }
+
+ _, err := dm.db.Exec(createTableSQL)
+ return err
+}
+
+// GetAllCameras 获取所有摄像头
+func (dm *DatabaseManager) GetAllCameras() ([]Camera, error) {
+ query := `SELECT id, name, ip, username, password, rtsp_url, enabled, on_demand, audio, debug, created_at, updated_at FROM cameras WHERE enabled = ?`
+
+ rows, err := dm.db.Query(query, true)
+ if err != nil {
+ return nil, err
+ }
+ defer rows.Close()
+
+ var cameras []Camera
+ for rows.Next() {
+ var camera Camera
+ err := rows.Scan(&camera.ID, &camera.Name, &camera.IP, &camera.Username,
+ &camera.Password, &camera.RTSPURL, &camera.Enabled, &camera.OnDemand,
+ &camera.Audio, &camera.Debug, &camera.CreatedAt, &camera.UpdatedAt)
+ if err != nil {
+ return nil, err
+ }
+ cameras = append(cameras, camera)
+ }
+
+ return cameras, nil
+}
+
+// GetCameraByID 根据ID获取摄像头
+func (dm *DatabaseManager) GetCameraByID(id string) (*Camera, error) {
+ query := `SELECT id, name, ip, username, password, rtsp_url, enabled, on_demand, audio, debug, created_at, updated_at FROM cameras WHERE id = ?`
+
+ var camera Camera
+ err := dm.db.QueryRow(query, id).Scan(&camera.ID, &camera.Name, &camera.IP,
+ &camera.Username, &camera.Password, &camera.RTSPURL, &camera.Enabled,
+ &camera.OnDemand, &camera.Audio, &camera.Debug, &camera.CreatedAt, &camera.UpdatedAt)
+
+ if err != nil {
+ if err == sql.ErrNoRows {
+ return nil, nil
+ }
+ return nil, err
+ }
+
+ return &camera, nil
+}
+
+// CreateCamera 创建摄像头
+func (dm *DatabaseManager) CreateCamera(camera *Camera) error {
+ if camera.ID == "" {
+ camera.ID = uuid.New().String()
+ }
+ camera.CreatedAt = time.Now()
+ camera.UpdatedAt = time.Now()
+
+ query := `INSERT INTO cameras (id, name, ip, username, password, rtsp_url, enabled, on_demand, audio, debug, created_at, updated_at)
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
+
+ _, err := dm.db.Exec(query, camera.ID, camera.Name, camera.IP, camera.Username,
+ camera.Password, camera.RTSPURL, camera.Enabled, camera.OnDemand,
+ camera.Audio, camera.Debug, camera.CreatedAt, camera.UpdatedAt)
+
+ return err
+}
+
+// UpdateCamera 更新摄像头
+func (dm *DatabaseManager) UpdateCamera(camera *Camera) error {
+ camera.UpdatedAt = time.Now()
+
+ query := `UPDATE cameras SET name=?, ip=?, username=?, password=?, rtsp_url=?,
+ enabled=?, on_demand=?, audio=?, debug=?, updated_at=? WHERE id=?`
+
+ _, err := dm.db.Exec(query, camera.Name, camera.IP, camera.Username,
+ camera.Password, camera.RTSPURL, camera.Enabled, camera.OnDemand,
+ camera.Audio, camera.Debug, camera.UpdatedAt, camera.ID)
+
+ return err
+}
+
+// DeleteCamera 删除摄像头
+func (dm *DatabaseManager) DeleteCamera(id string) error {
+ query := `DELETE FROM cameras WHERE id = ?`
+ _, err := dm.db.Exec(query, id)
+ return err
+}
+
+// Close 关闭数据库连接
+func (dm *DatabaseManager) Close() error {
+ if dm.db != nil {
+ return dm.db.Close()
+ }
+ return nil
+}
+
+// CameraToStream 将摄像头转换为流配置
+func CameraToStream(camera Camera) StreamST {
+ return StreamST{
+ Name: camera.Name,
+ Channels: map[string]ChannelST{
+ "0": {
+ Name: camera.Name,
+ URL: camera.RTSPURL,
+ OnDemand: camera.OnDemand,
+ Debug: camera.Debug,
+ Audio: camera.Audio,
+ },
+ },
+ }
+}
+
+// StreamToCamera 将流配置转换为摄像头
+func StreamToCamera(id string, stream StreamST) Camera {
+ camera := Camera{
+ ID: id,
+ Name: stream.Name,
+ Enabled: true,
+ CreatedAt: time.Now(),
+ UpdatedAt: time.Now(),
+ }
+
+ // 获取第一个通道的配置
+ if len(stream.Channels) > 0 {
+ for _, channel := range stream.Channels {
+ camera.RTSPURL = channel.URL
+ camera.OnDemand = channel.OnDemand
+ camera.Debug = channel.Debug
+ camera.Audio = channel.Audio
+ break
+ }
+ }
+
+ return camera
+}
diff --git a/database_init.sql b/database_init.sql
new file mode 100644
index 0000000..e213c79
--- /dev/null
+++ b/database_init.sql
@@ -0,0 +1,101 @@
+-- MySQL数据库初始化脚本
+-- 创建数据库
+CREATE DATABASE IF NOT EXISTS rtsp_cameras CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
+
+-- 使用数据库
+USE rtsp_cameras;
+
+-- 创建摄像头表
+CREATE TABLE IF NOT EXISTS cameras (
+ id VARCHAR(36) PRIMARY KEY COMMENT '摄像头唯一标识符',
+ name VARCHAR(255) NOT NULL COMMENT '摄像头名称',
+ ip VARCHAR(45) NOT NULL COMMENT 'IP地址',
+ username VARCHAR(100) DEFAULT NULL COMMENT '用户名',
+ password VARCHAR(255) DEFAULT NULL COMMENT '密码',
+ rtsp_url VARCHAR(500) NOT NULL COMMENT 'RTSP URL',
+ enabled BOOLEAN DEFAULT TRUE COMMENT '是否启用',
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+ INDEX idx_name (name),
+ INDEX idx_ip (ip),
+ INDEX idx_enabled (enabled),
+ INDEX idx_created_at (created_at)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='摄像头信息表';
+
+-- 插入示例数据
+INSERT INTO cameras (id, name, ip, username, password, rtsp_url, enabled) VALUES
+('27aec28e-6181-4753-9acd-0456a75f0289', '测试摄像头1', '192.168.1.100', 'admin', 'admin123', 'rtsp://192.168.1.100:554/stream1', TRUE),
+('38bfd39f-7292-5864-0bde-1567b86f1390', '测试摄像头2', '192.168.1.101', 'admin', 'admin123', 'rtsp://192.168.1.101:554/stream1', TRUE)
+ON DUPLICATE KEY UPDATE
+ name = VALUES(name),
+ ip = VALUES(ip),
+ username = VALUES(username),
+ password = VALUES(password),
+ rtsp_url = VALUES(rtsp_url),
+ enabled = VALUES(enabled),
+ updated_at = CURRENT_TIMESTAMP;
+
+-- SQL Server数据库初始化脚本(注释版本)
+/*
+-- 创建数据库
+IF NOT EXISTS (SELECT * FROM sys.databases WHERE name = 'rtsp_cameras')
+BEGIN
+ CREATE DATABASE rtsp_cameras;
+END
+GO
+
+-- 使用数据库
+USE rtsp_cameras;
+GO
+
+-- 创建摄像头表
+IF NOT EXISTS (SELECT * FROM sysobjects WHERE name='cameras' AND xtype='U')
+BEGIN
+ CREATE TABLE cameras (
+ id NVARCHAR(36) PRIMARY KEY,
+ name NVARCHAR(255) NOT NULL,
+ ip NVARCHAR(45) NOT NULL,
+ username NVARCHAR(100) NULL,
+ password NVARCHAR(255) NULL,
+ rtsp_url NVARCHAR(500) NOT NULL,
+ enabled BIT DEFAULT 1,
+ created_at DATETIME2 DEFAULT GETDATE(),
+ updated_at DATETIME2 DEFAULT GETDATE()
+ );
+
+ -- 创建索引
+ CREATE INDEX idx_cameras_name ON cameras(name);
+ CREATE INDEX idx_cameras_ip ON cameras(ip);
+ CREATE INDEX idx_cameras_enabled ON cameras(enabled);
+ CREATE INDEX idx_cameras_created_at ON cameras(created_at);
+END
+GO
+
+-- 创建更新触发器
+IF NOT EXISTS (SELECT * FROM sys.triggers WHERE name = 'tr_cameras_update')
+BEGIN
+ EXEC('CREATE TRIGGER tr_cameras_update ON cameras
+ AFTER UPDATE
+ AS
+ BEGIN
+ UPDATE cameras
+ SET updated_at = GETDATE()
+ WHERE id IN (SELECT id FROM inserted);
+ END');
+END
+GO
+
+-- 插入示例数据
+IF NOT EXISTS (SELECT * FROM cameras WHERE id = '27aec28e-6181-4753-9acd-0456a75f0289')
+BEGIN
+ INSERT INTO cameras (id, name, ip, username, password, rtsp_url, enabled) VALUES
+ ('27aec28e-6181-4753-9acd-0456a75f0289', N'测试摄像头1', N'192.168.1.100', N'admin', N'admin123', N'rtsp://192.168.1.100:554/stream1', 1);
+END
+
+IF NOT EXISTS (SELECT * FROM cameras WHERE id = '38bfd39f-7292-5864-0bde-1567b86f1390')
+BEGIN
+ INSERT INTO cameras (id, name, ip, username, password, rtsp_url, enabled) VALUES
+ ('38bfd39f-7292-5864-0bde-1567b86f1390', N'测试摄像头2', N'192.168.1.101', N'admin', N'admin123', N'rtsp://192.168.1.101:554/stream1', 1);
+END
+GO
+*/
\ No newline at end of file
diff --git a/docs/api.md b/docs/api.md
index e0a3f1f..66e258b 100644
--- a/docs/api.md
+++ b/docs/api.md
@@ -1,31 +1,31 @@
# RTSPtoWeb API
- * [Streams](#streams)
- * [List streams](#list-streams)
- * [Add a stream](#add-a-stream)
- * [Update a stream](#update-a-stream)
- * [Reload a stream](#reload-a-stream)
- * [Get stream info](#get-stream-info)
- * [Delete a stream](#delete-a-stream)
- * [Channels](#channels)
- * [Add a channel to a stream](#add-a-channel-to-a-stream)
- * [Update a stream channel](#update-a-stream-channel)
- * [Reload a stream channel](#reload-a-stream-channel)
- * [Get stream channel info](#get-stream-channel-info)
- * [Get stream channel codec](#get-stream-channel-codec)
- * [Delete a stream channel](#delete-a-stream-channel)
- * [Video endpoints](#video-endpoints)
+ * [流管理](#streams)
+ * [列出流](#list-streams)
+ * [添加流](#add-a-stream)
+ * [更新流](#update-a-stream)
+ * [重新加载流](#reload-a-stream)
+ * [获取流信息](#get-stream-info)
+ * [删除流](#delete-a-stream)
+ * [通道管理](#channels)
+ * [向流添加通道](#add-a-channel-to-a-stream)
+ * [更新流通道](#update-a-stream-channel)
+ * [重新加载流通道](#reload-a-stream-channel)
+ * [获取流通道信息](#get-stream-channel-info)
+ * [获取流通道编解码器](#get-stream-channel-codec)
+ * [删除流通道](#delete-a-stream-channel)
+ * [视频端点](#video-endpoints)
* [HLS](#hls)
* [HLS-LL](#hls-ll)
* [MSE](#mse)
* [WebRTC](#webrtc)
* [RTSP](#rtsp)
-## Streams
+## 流管理
-### List streams
+### 列出流
-#### Request
+#### 请求
`GET /streams`
@@ -33,7 +33,7 @@
curl http://demo:demo@127.0.0.1:8083/streams
```
-#### Response
+#### 响应
```json
{
@@ -81,9 +81,9 @@ curl http://demo:demo@127.0.0.1:8083/streams
}
```
-### Add a stream
+### 添加流
-#### Request
+#### 请求
`POST /stream/{STREAM_ID}/add`
@@ -113,7 +113,7 @@ curl \
http://demo:demo@127.0.0.1:8083/stream/{STREAM_ID}/add
```
-#### Response
+#### 响应
```json
{
@@ -122,9 +122,9 @@ curl \
}
```
-### Update a stream
+### 更新流
-#### Request
+#### 请求
`POST /stream/{STREAM_ID}/edit`
@@ -154,7 +154,7 @@ curl \
http://demo:demo@127.0.0.1:8083/stream/{STREAM_ID}/edit
```
-#### Response
+#### 响应
```json
{
@@ -163,9 +163,9 @@ curl \
}
```
-### Reload a stream
+### 重新加载流
-#### Request
+#### 请求
`GET /stream/{STREAM_ID}/reload`
@@ -173,7 +173,7 @@ curl \
curl http://demo:demo@127.0.0.1:8083/stream/{STREAM_ID}/reload
```
-#### Response
+#### 响应
```json
{
@@ -182,9 +182,9 @@ curl http://demo:demo@127.0.0.1:8083/stream/{STREAM_ID}/reload
}
```
-### Get stream info
+### 获取流信息
-#### Request
+#### 请求
`GET /stream/{STREAM_ID}/info`
@@ -192,7 +192,7 @@ curl http://demo:demo@127.0.0.1:8083/stream/{STREAM_ID}/reload
curl http://demo:demo@127.0.0.1:8083/stream/{STREAM_ID}/info
```
-#### Response
+#### 响应
```json
{
@@ -219,9 +219,9 @@ curl http://demo:demo@127.0.0.1:8083/stream/{STREAM_ID}/info
}
```
-### Delete a stream
+### 删除流
-#### Request
+#### 请求
`GET /stream/{STREAM_ID}/delete`
@@ -229,7 +229,7 @@ curl http://demo:demo@127.0.0.1:8083/stream/{STREAM_ID}/info
curl http://demo:demo@127.0.0.1:8083/stream/{STREAM_ID}/delete
```
-#### Response
+#### 响应
```json
{
@@ -238,11 +238,11 @@ curl http://demo:demo@127.0.0.1:8083/stream/{STREAM_ID}/delete
}
```
-## Channels
+## 通道管理
-### Add a channel to a stream
+### 向流添加通道
-#### Request
+#### 请求
`POST /stream/{STREAM_ID}/channel/{CHANNEL_ID}/add`
@@ -260,7 +260,7 @@ curl \
http://demo:demo@127.0.0.1:8083/stream/{STREAM_ID}/channel/{CHANNEL_ID}/add
```
-#### Response
+#### 响应
```json
{
@@ -269,9 +269,9 @@ curl \
}
```
-### Update a stream channel
+### 更新流通道
-#### Request
+#### 请求
`POST /stream/{STREAM_ID}/channel/{CHANNEL_ID}/edit`
@@ -289,7 +289,7 @@ curl \
http://demo:demo@127.0.0.1:8083/stream/{STREAM_ID}/channel/{CHANNEL_ID}/edit
```
-#### Response
+#### 响应
```json
{
@@ -298,9 +298,9 @@ curl \
}
```
-### Reload a stream channel
+### 重新加载流通道
-#### Request
+#### 请求
`GET /stream/{STREAM_ID}/channel/{CHANNEL_ID}/reload`
@@ -308,7 +308,7 @@ curl \
curl http://demo:demo@127.0.0.1:8083/stream/{STREAM_ID}/channel/{CHANNEL_ID}/reload
```
-#### Response
+#### 响应
```json
{
@@ -317,9 +317,9 @@ curl http://demo:demo@127.0.0.1:8083/stream/{STREAM_ID}/channel/{CHANNEL_ID}/rel
}
```
-### Get stream channel info
+### 获取流通道信息
-#### Request
+#### 请求
`GET /stream/{STREAM_ID}/channel/{CHANNEL_ID}/info`
@@ -327,7 +327,7 @@ curl http://demo:demo@127.0.0.1:8083/stream/{STREAM_ID}/channel/{CHANNEL_ID}/rel
curl http://demo:demo@127.0.0.1:8083/stream/{STREAM_ID}/channel/{CHANNEL_ID}/info
```
-#### Response
+#### 响应
```json
{
@@ -342,16 +342,16 @@ curl http://demo:demo@127.0.0.1:8083/stream/{STREAM_ID}/channel/{CHANNEL_ID}/inf
}
```
-### Get stream channel codec
+### 获取流通道编解码器
-#### Request
+#### 请求
`GET /stream/{STREAM_ID}/{CHANNEL_ID}/codec`
```bash
curl http://demo:demo@127.0.0.1:8083/stream/{STREAM_ID}/{CHANNEL_ID}/codec
```
-#### Response
+#### 响应
```json
{
"status": 1,
@@ -387,9 +387,9 @@ curl http://demo:demo@127.0.0.1:8083/stream/{STREAM_ID}/{CHANNEL_ID}/codec
}
```
-### Delete a stream channel
+### 删除流通道
-#### Request
+#### 请求
`GET /stream/{STREAM_ID}/channel/{CHANNEL_ID}/delete`
@@ -397,7 +397,7 @@ curl http://demo:demo@127.0.0.1:8083/stream/{STREAM_ID}/{CHANNEL_ID}/codec
curl http://demo:demo@127.0.0.1:8083/stream/{STREAM_ID}/channel/{CHANNEL_ID}/delete
```
-#### Response
+#### 响应
```json
{
"status": 1,
@@ -405,7 +405,7 @@ curl http://demo:demo@127.0.0.1:8083/stream/{STREAM_ID}/channel/{CHANNEL_ID}/del
}
```
-## Video endpoints
+## 视频端点
### HLS
@@ -439,7 +439,7 @@ ffplay http://127.0.0.1:8083/stream/{STREAM_ID}/channel/{CHANNEL_ID}/hlsll/live/
ws://127.0.0.1:8083/stream/{STREAM_ID}/channel/{CHANNEL_ID}/mse?uuid={STREAM_ID}&channel={CHANNEL_ID}
```
-NOTE: Use `wss` for a secure connection.
+注意:使用 `wss` 进行安全连接。
### WebRTC
@@ -451,11 +451,11 @@ http://127.0.0.1:8083/stream/{STREAM_ID}/channel/{CHANNEL_ID}/webrtc
#### Request
-The request is an HTTP `POST` with a FormData parameter `data` that is a base64 encoded SDP offer (e.g. `v=0...`) from a WebRTC client.
+请求是一个 HTTP `POST`,包含 FormData 参数 `data`,该参数是来自 WebRTC 客户端的 base64 编码的 SDP offer(例如 `v=0...`)。
-#### Response
+#### 响应
-The response is a base64 encoded SDP Answer.
+响应是一个 base64 编码的 SDP Answer。
### RTSP
diff --git a/go.mod b/go.mod
index 8cff25f..0ce2133 100644
--- a/go.mod
+++ b/go.mod
@@ -4,9 +4,12 @@ go 1.19
require (
github.com/deepch/vdk v0.0.20
+ github.com/denisenkom/go-mssqldb v0.12.3
github.com/gin-gonic/autotls v0.0.5
github.com/gin-gonic/gin v1.9.0
+ github.com/go-sql-driver/mysql v1.7.1
github.com/gobwas/ws v1.1.0
+ github.com/google/uuid v1.3.0
github.com/hashicorp/go-version v1.6.0
github.com/imdario/mergo v0.3.15
github.com/liip/sheriff v0.11.1
@@ -23,7 +26,9 @@ require (
github.com/gobwas/httphead v0.1.0 // indirect
github.com/gobwas/pool v0.2.1 // indirect
github.com/goccy/go-json v0.10.0 // indirect
- github.com/google/uuid v1.3.0 // indirect
+ github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe // indirect
+ github.com/golang-sql/sqlexp v0.1.0 // indirect
+ github.com/gomarkdown/markdown v0.0.0-20250311123330-531bef5e742b // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.4 // indirect
github.com/leodido/go-urn v1.2.2 // indirect
diff --git a/go.sum b/go.sum
index 6c2a1da..62f9f6c 100644
--- a/go.sum
+++ b/go.sum
@@ -1,3 +1,6 @@
+github.com/Azure/azure-sdk-for-go/sdk/azcore v0.19.0/go.mod h1:h6H6c8enJmmocHUbLiiGY6sx7f9i+X3m1CHdd5c6Rdw=
+github.com/Azure/azure-sdk-for-go/sdk/azidentity v0.11.0/go.mod h1:HcM1YX14R7CJcghJGOYCgdezslRSVzqwLf/q+4Y2r/0=
+github.com/Azure/azure-sdk-for-go/sdk/internal v0.7.0/go.mod h1:yqy467j36fJxcRV2TzfVZ1pCb5vxm4BtZPUdYWe/Xo8=
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
github.com/bytedance/sonic v1.8.3 h1:pf6fGl5eqWYKkx1RcD4qpuX+BIUaduv/wTm5ekWJ80M=
github.com/bytedance/sonic v1.8.3/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
@@ -9,6 +12,9 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/deepch/vdk v0.0.20 h1:GNQjfgapEEzaownzfRWYdQw+SqchW2yC1ha+/d0Bl24=
github.com/deepch/vdk v0.0.20/go.mod h1:774MjElr4PMgmXuTi9HXrK0nM0phMXE80W/g+vyTpAc=
+github.com/denisenkom/go-mssqldb v0.12.3 h1:pBSGx9Tq67pBOTLmxNuirNTeB8Vjmf886Kx+8Y+8shw=
+github.com/denisenkom/go-mssqldb v0.12.3/go.mod h1:k0mtMFOnU+AihqFxPMiF05rtiDrorD1Vrm1KEz5hxDo=
+github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
@@ -24,6 +30,8 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.11.2 h1:q3SHpufmypg+erIExEKUmsgmhDTyhcJ38oeKGACXohU=
github.com/go-playground/validator/v10 v10.11.2/go.mod h1:NieE624vt4SCTJtD87arVLvdmjPAeV8BQlHtMnw9D7s=
+github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI=
+github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
@@ -33,6 +41,10 @@ github.com/gobwas/ws v1.1.0 h1:7RFti/xnNkMJnrK7D1yQ/iCIB5OrrY/54/H930kIbHA=
github.com/gobwas/ws v1.1.0/go.mod h1:nzvNcVha5eUziGrbxFCo6qFIojQHjJV5cLYIbezhfL0=
github.com/goccy/go-json v0.10.0 h1:mXKd9Qw4NuzShiRlOXKews24ufknHO7gx30lsDyokKA=
github.com/goccy/go-json v0.10.0/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
+github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY=
+github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
+github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
+github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
@@ -42,6 +54,8 @@ github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvq
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
+github.com/gomarkdown/markdown v0.0.0-20250311123330-531bef5e742b h1:EY/KpStFl60qA17CptGXhwfZ+k1sFNJIUNR8DdbcuUk=
+github.com/gomarkdown/markdown v0.0.0-20250311123330-531bef5e742b/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
@@ -77,6 +91,7 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
+github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
@@ -126,6 +141,7 @@ github.com/pion/udp/v2 v2.0.1 h1:xP0z6WNux1zWEjhC7onRA3EwwSliXqu1ElUZAQhUP54=
github.com/pion/udp/v2 v2.0.1/go.mod h1:B7uvTMP00lzWdyMr/1PVZXtV3wpPIxBRd4Wl6AksXn8=
github.com/pion/webrtc/v3 v3.1.58 h1:husXqiKQuk6gbOqJlPHs185OskAyxUW6iAEgHghgCrc=
github.com/pion/webrtc/v3 v3.1.58/go.mod h1:jJdqoqGBlZiE3y8Z1tg1fjSkyEDCZLL+foypUBn0Lhk=
+github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
@@ -157,7 +173,9 @@ golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU=
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
@@ -172,6 +190,7 @@ golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
+golang.org/x/net v0.0.0-20210610132358-84b48f89b13b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
@@ -253,9 +272,11 @@ gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMy
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
diff --git a/loggingLog.go b/loggingLog.go
index 96ac945..5fd76ff 100644
--- a/loggingLog.go
+++ b/loggingLog.go
@@ -1,8 +1,6 @@
package main
import (
- "io/ioutil"
-
"github.com/sirupsen/logrus"
)
@@ -10,11 +8,20 @@ var log = logrus.New()
func init() {
//TODO: next add write to file
- if !debug {
- log.SetOutput(ioutil.Discard)
- }
+ // Temporarily disable log output redirection for debugging
+ // if !debug {
+ // log.SetOutput(ioutil.Discard)
+ // }
log.SetFormatter(&logrus.TextFormatter{
FullTimestamp: true,
})
- log.SetLevel(Storage.ServerLogLevel())
+ // Set debug level temporarily - avoid circular dependency
+ log.SetLevel(logrus.DebugLevel)
+}
+
+// SetLogLevel sets the log level after Storage is initialized
+func SetLogLevel() {
+ if Storage != nil {
+ log.SetLevel(Storage.ServerLogLevel())
+ }
}
diff --git a/storageConfig.go b/storageConfig.go
index 0afbfe3..079fb36 100644
--- a/storageConfig.go
+++ b/storageConfig.go
@@ -20,7 +20,7 @@ import (
var debug bool
var configFile string
-//NewStreamCore do load config file
+// NewStreamCore do load config file
func NewStreamCore() *StorageST {
flag.BoolVar(&debug, "debug", true, "set debug mode")
flag.StringVar(&configFile, "config", "config.json", "config patch (/etc/server/config.json or config.json)")
@@ -46,9 +46,35 @@ func NewStreamCore() *StorageST {
os.Exit(1)
}
debug = tmp.Server.Debug
+
+ // 初始化数据库连接
+ if tmp.Database.Enabled {
+ err = tmp.InitDatabase()
+ if err != nil {
+ log.WithFields(logrus.Fields{
+ "module": "config",
+ "func": "NewStreamCore",
+ "call": "InitDatabase",
+ }).Errorln(err.Error())
+ os.Exit(1)
+ }
+
+ // 从数据库加载流配置
+ err = tmp.LoadStreamsFromDatabase()
+ if err != nil {
+ log.WithFields(logrus.Fields{
+ "module": "config",
+ "func": "NewStreamCore",
+ "call": "LoadStreamsFromDatabase",
+ }).Errorln(err.Error())
+ os.Exit(1)
+ }
+ }
+
+ // 处理流配置(从配置文件或数据库)
for i, i2 := range tmp.Streams {
for i3, i4 := range i2.Channels {
- channel := tmp.ChannelDefaults
+ var channel ChannelST
err = mergo.Merge(&channel, i4)
if err != nil {
log.WithFields(logrus.Fields{
@@ -66,10 +92,11 @@ func NewStreamCore() *StorageST {
}
tmp.Streams[i] = i2
}
+
return &tmp
}
-//ClientDelete Delete Client
+// ClientDelete Delete Client
func (obj *StorageST) SaveConfig() error {
log.WithFields(logrus.Fields{
"module": "config",
diff --git a/storageDatabase.go b/storageDatabase.go
new file mode 100644
index 0000000..053b920
--- /dev/null
+++ b/storageDatabase.go
@@ -0,0 +1,237 @@
+package main
+
+import (
+ "fmt"
+ "time"
+
+ "github.com/sirupsen/logrus"
+)
+
+// InitDatabase 初始化数据库连接
+func (obj *StorageST) InitDatabase() error {
+ obj.mutex.Lock()
+ defer obj.mutex.Unlock()
+
+ if !obj.Database.Enabled {
+ log.WithFields(logrus.Fields{
+ "module": "database",
+ "func": "InitDatabase",
+ }).Infoln("Database is disabled")
+ return nil
+ }
+
+ dbManager, err := NewDatabaseManager(obj.Database)
+ if err != nil {
+ log.WithFields(logrus.Fields{
+ "module": "database",
+ "func": "InitDatabase",
+ "call": "NewDatabaseManager",
+ }).Errorln(err.Error())
+ return err
+ }
+
+ obj.dbManager = dbManager
+ log.WithFields(logrus.Fields{
+ "module": "database",
+ "func": "InitDatabase",
+ }).Infoln("Database initialized successfully")
+
+ return nil
+}
+
+// LoadStreamsFromDatabase 从数据库加载流配置
+func (obj *StorageST) LoadStreamsFromDatabase() error {
+ obj.mutex.Lock()
+ defer obj.mutex.Unlock()
+
+ if obj.dbManager == nil {
+ return fmt.Errorf("database manager not initialized")
+ }
+
+ cameras, err := obj.dbManager.GetAllCameras()
+ if err != nil {
+ log.WithFields(logrus.Fields{
+ "module": "database",
+ "func": "LoadStreamsFromDatabase",
+ "call": "GetAllCameras",
+ }).Errorln(err.Error())
+ return err
+ }
+
+ // 清空现有流配置(如果启用数据库模式)
+ if obj.Server.DatabaseEnabled {
+ obj.Streams = make(map[string]StreamST)
+ }
+
+ // 将摄像头转换为流配置
+ for _, camera := range cameras {
+ stream := CameraToStream(camera)
+ obj.Streams[camera.ID] = stream
+ log.WithFields(logrus.Fields{
+ "module": "database",
+ "func": "LoadStreamsFromDatabase",
+ "camera": camera.Name,
+ "id": camera.ID,
+ }).Infoln("Loaded camera from database")
+ }
+
+ log.WithFields(logrus.Fields{
+ "module": "database",
+ "func": "LoadStreamsFromDatabase",
+ "count": len(cameras),
+ }).Infoln("Loaded cameras from database")
+
+ return nil
+}
+
+// SyncStreamToDatabase 同步流配置到数据库
+func (obj *StorageST) SyncStreamToDatabase(streamID string, stream StreamST) error {
+ if obj.dbManager == nil || !obj.Server.DatabaseEnabled {
+ return nil // 数据库未启用,跳过同步
+ }
+
+ // 检查摄像头是否已存在
+ existingCamera, err := obj.dbManager.GetCameraByID(streamID)
+ if err != nil {
+ return err
+ }
+
+ camera := StreamToCamera(streamID, stream)
+
+ if existingCamera == nil {
+ // 创建新摄像头
+ err = obj.dbManager.CreateCamera(&camera)
+ if err != nil {
+ log.WithFields(logrus.Fields{
+ "module": "database",
+ "func": "SyncStreamToDatabase",
+ "call": "CreateCamera",
+ "id": streamID,
+ }).Errorln(err.Error())
+ return err
+ }
+ log.WithFields(logrus.Fields{
+ "module": "database",
+ "func": "SyncStreamToDatabase",
+ "id": streamID,
+ "name": camera.Name,
+ }).Infoln("Created camera in database")
+ } else {
+ // 更新现有摄像头
+ camera.CreatedAt = existingCamera.CreatedAt // 保持创建时间
+ err = obj.dbManager.UpdateCamera(&camera)
+ if err != nil {
+ log.WithFields(logrus.Fields{
+ "module": "database",
+ "func": "SyncStreamToDatabase",
+ "call": "UpdateCamera",
+ "id": streamID,
+ }).Errorln(err.Error())
+ return err
+ }
+ log.WithFields(logrus.Fields{
+ "module": "database",
+ "func": "SyncStreamToDatabase",
+ "id": streamID,
+ "name": camera.Name,
+ }).Infoln("Updated camera in database")
+ }
+
+ return nil
+}
+
+// DeleteStreamFromDatabase 从数据库删除流配置
+func (obj *StorageST) DeleteStreamFromDatabase(streamID string) error {
+ if obj.dbManager == nil || !obj.Server.DatabaseEnabled {
+ return nil // 数据库未启用,跳过删除
+ }
+
+ err := obj.dbManager.DeleteCamera(streamID)
+ if err != nil {
+ log.WithFields(logrus.Fields{
+ "module": "database",
+ "func": "DeleteStreamFromDatabase",
+ "call": "DeleteCamera",
+ "id": streamID,
+ }).Errorln(err.Error())
+ return err
+ }
+
+ log.WithFields(logrus.Fields{
+ "module": "database",
+ "func": "DeleteStreamFromDatabase",
+ "id": streamID,
+ }).Infoln("Deleted camera from database")
+
+ return nil
+}
+
+// RefreshStreamsFromDatabase 刷新数据库中的流配置
+func (obj *StorageST) RefreshStreamsFromDatabase() error {
+ if obj.dbManager == nil || !obj.Server.DatabaseEnabled {
+ return nil
+ }
+
+ // 停止所有现有流
+ obj.StopAll()
+
+ // 等待流停止
+ time.Sleep(2 * time.Second)
+
+ // 重新加载流配置
+ err := obj.LoadStreamsFromDatabase()
+ if err != nil {
+ return err
+ }
+
+ // 启动所有流
+ obj.StreamChannelRunAll()
+
+ log.WithFields(logrus.Fields{
+ "module": "database",
+ "func": "RefreshStreamsFromDatabase",
+ }).Infoln("Refreshed streams from database")
+
+ return nil
+}
+
+// GetDatabaseStatus 获取数据库状态
+func (obj *StorageST) GetDatabaseStatus() map[string]interface{} {
+ status := map[string]interface{}{
+ "enabled": obj.Database.Enabled,
+ "type": obj.Database.Type,
+ "host": obj.Database.Host,
+ "port": obj.Database.Port,
+ "database": obj.Database.Database,
+ "connected": false,
+ "error": nil,
+ }
+
+ if obj.dbManager != nil && obj.dbManager.db != nil {
+ if err := obj.dbManager.db.Ping(); err == nil {
+ status["connected"] = true
+ } else {
+ status["error"] = err.Error()
+ }
+ }
+
+ return status
+}
+
+// CloseDatabase 关闭数据库连接
+func (obj *StorageST) CloseDatabase() error {
+ obj.mutex.Lock()
+ defer obj.mutex.Unlock()
+
+ if obj.dbManager != nil {
+ err := obj.dbManager.Close()
+ obj.dbManager = nil
+ log.WithFields(logrus.Fields{
+ "module": "database",
+ "func": "CloseDatabase",
+ }).Infoln("Database connection closed")
+ return err
+ }
+
+ return nil
+}
diff --git a/storageStream.go b/storageStream.go
index 1401b25..f727830 100644
--- a/storageStream.go
+++ b/storageStream.go
@@ -42,10 +42,20 @@ func (obj *StorageST) StreamAdd(uuid string, val StreamST) error {
}
}
obj.Streams[uuid] = val
- err := obj.SaveConfig()
+
+ // 同步到数据库
+ err := obj.SyncStreamToDatabase(uuid, val)
if err != nil {
return err
}
+
+ // 如果未启用数据库,保存到配置文件
+ if !obj.Server.DatabaseEnabled {
+ err = obj.SaveConfig()
+ if err != nil {
+ return err
+ }
+ }
return nil
}
@@ -72,10 +82,20 @@ func (obj *StorageST) StreamEdit(uuid string, val StreamST) error {
}
}
obj.Streams[uuid] = val
- err := obj.SaveConfig()
+
+ // 同步到数据库
+ err := obj.SyncStreamToDatabase(uuid, val)
if err != nil {
return err
}
+
+ // 如果未启用数据库,保存到配置文件
+ if !obj.Server.DatabaseEnabled {
+ err = obj.SaveConfig()
+ if err != nil {
+ return err
+ }
+ }
return nil
}
return ErrorStreamNotFound
@@ -120,10 +140,20 @@ func (obj *StorageST) StreamDelete(uuid string) error {
}
}
delete(obj.Streams, uuid)
- err := obj.SaveConfig()
+
+ // 从数据库删除
+ err := obj.DeleteStreamFromDatabase(uuid)
if err != nil {
return err
}
+
+ // 如果未启用数据库,保存到配置文件
+ if !obj.Server.DatabaseEnabled {
+ err = obj.SaveConfig()
+ if err != nil {
+ return err
+ }
+ }
return nil
}
return ErrorStreamNotFound
diff --git a/storageStruct.go b/storageStruct.go
index f6223ce..2656465 100644
--- a/storageStruct.go
+++ b/storageStruct.go
@@ -12,20 +12,20 @@ import (
var Storage = NewStreamCore()
-//Default stream type
+// Default stream type
const (
MSE = iota
WEBRTC
RTSP
)
-//Default stream status type
+// Default stream status type
const (
OFFLINE = iota
ONLINE
)
-//Default stream errors
+// Default stream errors
var (
Success = "success"
ErrorStreamNotFound = errors.New("stream not found")
@@ -43,15 +43,17 @@ var (
ErrorStreamUnauthorized = errors.New("stream request unauthorized")
)
-//StorageST main storage struct
+// StorageST main storage struct
type StorageST struct {
mutex sync.RWMutex
Server ServerST `json:"server" groups:"api,config"`
Streams map[string]StreamST `json:"streams,omitempty" groups:"api,config"`
ChannelDefaults ChannelST `json:"channel_defaults,omitempty" groups:"api,config"`
+ Database DatabaseConfig `json:"database,omitempty" groups:"api,config"`
+ dbManager *DatabaseManager // 数据库管理器,不序列化
}
-//ServerST server storage section
+// ServerST server storage section
type ServerST struct {
Debug bool `json:"debug" groups:"api,config"`
LogLevel logrus.Level `json:"log_level" groups:"api,config"`
@@ -74,15 +76,16 @@ type ServerST struct {
Token Token `json:"token,omitempty" groups:"api,config"`
WebRTCPortMin uint16 `json:"webrtc_port_min" groups:"api,config"`
WebRTCPortMax uint16 `json:"webrtc_port_max" groups:"api,config"`
+ DatabaseEnabled bool `json:"database_enabled" groups:"api,config"`
}
-//Token auth
+// Token auth
type Token struct {
Enable bool `json:"enable" groups:"api,config"`
Backend string `json:"backend" groups:"api,config"`
}
-//ServerST stream storage section
+// ServerST stream storage section
type StreamST struct {
Name string `json:"name,omitempty" groups:"api,config"`
Channels map[string]ChannelST `json:"channels,omitempty" groups:"api,config"`
@@ -107,7 +110,7 @@ type ChannelST struct {
hlsMuxer *MuxerHLS `json:"-"`
}
-//ClientST client storage section
+// ClientST client storage section
type ClientST struct {
mode int
signals chan int
@@ -116,7 +119,7 @@ type ClientST struct {
socket net.Conn
}
-//SegmentOld HLS cache section
+// SegmentOld HLS cache section
type SegmentOld struct {
dur time.Duration
data []*av.Packet
diff --git a/test.exe b/test.exe
new file mode 100644
index 0000000..0a01176
Binary files /dev/null and b/test.exe differ
diff --git a/web/templates/add_stream.tmpl b/web/templates/add_stream.tmpl
index 9e8e086..4bbf1c2 100644
--- a/web/templates/add_stream.tmpl
+++ b/web/templates/add_stream.tmpl
@@ -3,12 +3,12 @@
-
Add stream
+ 添加流
- - Home
- - Add stream
+ - 首页
+ - 添加流
@@ -25,35 +25,35 @@
@@ -63,8 +63,8 @@
-
-
+
+
diff --git a/web/templates/cameras.tmpl b/web/templates/cameras.tmpl
new file mode 100644
index 0000000..7f3219e
--- /dev/null
+++ b/web/templates/cameras.tmpl
@@ -0,0 +1,408 @@
+
+
+
+
摄像头管理 - RTSPtoWeb
+
+
+
+
+
+
+
+
+
+
+
+
摄像头管理
+
+
+
+
+
+
+
+
+ 数据库状态: 检查中...
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/web/templates/documentation.tmpl b/web/templates/documentation.tmpl
index c64e543..85d3325 100644
--- a/web/templates/documentation.tmpl
+++ b/web/templates/documentation.tmpl
@@ -3,12 +3,12 @@
-
Documentation
+ 文档
- - Home
- - Documentation
+ - 首页
+ - 文档
@@ -20,10 +20,10 @@
diff --git a/web/templates/edit_stream.tmpl b/web/templates/edit_stream.tmpl
index 24aa85a..1391e5e 100644
--- a/web/templates/edit_stream.tmpl
+++ b/web/templates/edit_stream.tmpl
@@ -3,12 +3,12 @@
-
Edit stream
+ 编辑流
- - Home
- - Edit stream
+ - 首页
+ - 编辑流
@@ -23,42 +23,42 @@
@@ -70,7 +70,7 @@
-
-
+
+
diff --git a/web/templates/head.tmpl b/web/templates/head.tmpl
index 7d939b4..8606a55 100644
--- a/web/templates/head.tmpl
+++ b/web/templates/head.tmpl
@@ -25,12 +25,12 @@
- Home
+ 首页
@@ -54,17 +54,17 @@
">
- Dashboard
+ 仪表板
-
+
- Streams list
+ 流列表
@@ -72,7 +72,7 @@
active
{{end}}">
- Add stream
+ 添加流
@@ -81,17 +81,17 @@
active
{{end}}">
-
Full multiview
+
全屏多视图
-
+
- Documentation
+ 文档
diff --git a/web/templates/index.tmpl b/web/templates/index.tmpl
index ea354dd..a7a2375 100644
--- a/web/templates/index.tmpl
+++ b/web/templates/index.tmpl
@@ -3,12 +3,12 @@
@@ -19,7 +19,17 @@
-
Streams ({{ len .streams}})
+
+
视频流 ({{ len .streams}})
+
+
@@ -31,7 +41,7 @@
@@ -47,18 +57,18 @@
-
Channel: {{$k}}
+ 通道: {{$k}}
{{end}}
- Previous
+ 上一个
- Next
+ 下一个
@@ -70,7 +80,7 @@
MSE
@@ -78,7 +88,7 @@
HLS
@@ -86,7 +96,7 @@
WebRTC
@@ -94,7 +104,7 @@
ALL
@@ -107,8 +117,8 @@
-
Edit
-
Delete
+
编辑
+
删除
diff --git a/web/templates/stream_list.tmpl b/web/templates/stream_list.tmpl
index f5954c9..b5fb4a9 100644
--- a/web/templates/stream_list.tmpl
+++ b/web/templates/stream_list.tmpl
@@ -3,12 +3,12 @@
@@ -63,7 +63,7 @@
MSE
@@ -71,7 +71,7 @@
HLS
@@ -79,7 +79,7 @@
WebRTC
@@ -87,7 +87,7 @@
ALL
@@ -100,8 +100,8 @@
- Edit
- Delete
+ 编辑
+ 删除