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

+

添加流

@@ -25,35 +25,35 @@
-

Main channel parameters

+

主通道 参数

- - - You can choose any name for the stream, for example "My room" or "Happy sausage" + + + 您可以为流选择任何名称,例如"我的房间"或"客厅"
- - - Enter rtsp address as instructed by your camera. Look like rtsp://<ip>:<port>/path + + + 按照摄像头说明输入rtsp地址。格式如 rtsp://<ip>:<port>/path
- + - On persistent connection, the server get data from the camera continuously. On demand, the server get data from the camera only when you click play button + 持续连接时,服务器会持续从摄像头获取数据。按需连接时,服务器只在您点击播放按钮时才从摄像头获取数据
- +
- Select this options if you want get more data about the stream + 如果您想获取更多关于流的数据,请选择此选项
@@ -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

+

文档

@@ -20,10 +20,10 @@

- API documentation is available in the GitHub repository. + API 文档 可在本地查看。

- See the project README for installation and configuration instructions. + 查看项目 README 获取安装和配置说明。

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

+

编辑流

@@ -23,42 +23,42 @@
-

Parameters main

+

参数 主要

- - - You can choose any name for the stream, for example "My room" or "Happy sausage" + + + 您可以为流选择任何名称,例如"我的房间"或"客厅"
- - - Enter rtsp address as instructed by your camera. Look like rtsp://<ip>:<port>/path + + + 按照摄像头说明输入rtsp地址。格式如 rtsp://<ip>:<port>/path
- + - On persistent connection, the server get data from the camera continuously. On demand, the server get data from the camera only when you click play button + 持续连接时,服务器会持续从摄像头获取数据。按需连接时,服务器只在您点击播放按钮时才从摄像头获取数据
- +
- Select this options if you want get more data about the stream + 如果您想获取更多关于流的数据,请选择此选项
@@ -70,7 +70,7 @@
-

Sub channel parameters

+

子通道 参数

@@ -78,34 +78,34 @@
- - - Enter rtsp address as instructed by your camera. Look like rtsp://<ip>:<port>/path + + + 按照摄像头说明输入rtsp地址。格式如 rtsp://<ip>:<port>/path
- + - On persistent connection, the server get data from the camera continuously. On demand, the server get data from the camera only when you click play button + 持续连接时,服务器会持续从摄像头获取数据。按需连接时,服务器只在您点击播放按钮时才从摄像头获取数据
- +
- Select this options if you want get more data about the stream + 如果您想获取更多关于流的数据,请选择此选项
@@ -117,8 +117,8 @@
- - + +
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 @@ @@ -54,17 +54,17 @@ ">

- Dashboard + 仪表板

- + @@ -81,17 +81,17 @@ active {{end}}"> -

Full multiview

+

全屏多视图

- + 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 @@
-

Dashboard

+

仪表板

@@ -19,7 +19,17 @@
-
Streams ({{ len .streams}})
+
+
视频流 ({{ len .streams}})
+ +
@@ -31,7 +41,7 @@

{{.Name}}

- {{len .Channels }} + {{len .Channels }}
@@ -47,18 +57,18 @@ {{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 @@
-

Streams list

+

流列表

@@ -24,7 +24,7 @@

{{.Name}}

- {{len .Channels }} + {{len .Channels }}
@@ -40,18 +40,18 @@ {{end}}
- Previous + 上一个 - Next + 下一个
@@ -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 + 编辑 + 删除