如何用GoCV+Echo实现摄像头录制的启停分离API?
如何用Go+Echo+GoCV实现启停分离的摄像头录制功能
我正在用Go语言结合Echo框架开发后端服务,借助GoCV实现摄像头视频录制功能。希望通过两个独立API分别控制录制的启动与停止,但GoCV官方示例需要预先设定录制时长。我尝试在启动接口里用无限循环持续录制,结果导致请求挂起,直到调用停止接口才结束,这种做法不规范还会引发浏览器超时问题。相关文档很少,找不到同类示例,请问怎么实现无需预先知晓录制时长的启停分离式录制?
GoCV官方示例代码
// 功能说明: // // 本示例使用VideoCapture类从连接的摄像头捕获AVI视频, // 然后将100帧保存到磁盘上的视频文件中。 // // 运行方式: // // savevideo [摄像头ID] [视频文件] // // go run ./cmd/savevideo/main.go 0 testvideo.avi // package main import ( "fmt" "os" "gocv.io/x/gocv" ) func main() { if len(os.Args) < 3 { fmt.Println("运行方式:\n\tsavevideo [摄像头ID] [视频文件]") return } deviceID := os.Args[1] saveFile := os.Args[2] webcam, err := gocv.OpenVideoCapture(deviceID) if err != nil { fmt.Printf("打开视频捕获设备失败: %v\n", deviceID) return } defer webcam.Close() img := gocv.NewMat() defer img.Close() if ok := webcam.Read(&img); !ok { fmt.Printf("无法读取设备 %v\n", deviceID) return } writer, err := gocv.VideoWriterFile(saveFile, "MJPG", 25, img.Cols(), img.Rows(), true) if err != nil { fmt.Printf("打开视频写入设备失败: %v\n", saveFile) return } defer writer.Close() for i := 0; i < 100; i++ { if ok := webcam.Read(&img); !ok { fmt.Printf("设备已关闭: %v\n", deviceID) return } if img.Empty() { continue } writer.Write(img) } }
我的实现代码
package gocvFunctions import ( "fmt" "net/http" "github.com/labstack/echo/v4" "gocv.io/x/gocv" ) func GocvStartRecord(c echo.Context) error { fmt.Print("GocvStartRecord 启动\n") deviceID := c.QueryParam("deviceID") saveFile := c.QueryParam("saveFile") webcam, err := gocv.OpenVideoCapture(deviceID) if err != nil { fmt.Printf("打开视频捕获设备失败: %v\n", deviceID) return c.String(http.StatusInternalServerError, "打开视频捕获设备失败: "+deviceID) } defer webcam.Close() img := gocv.NewMat() defer img.Close() if ok := webcam.Read(&img); !ok { fmt.Printf("无法读取设备 %v\n", deviceID) return c.String(http.StatusInternalServerError, "无法读取设备: "+deviceID) } writer, err := gocv.VideoWriterFile(saveFile, "mp4v", 25, img.Cols(), img.Rows(), true) if err != nil { fmt.Printf("打开视频写入设备失败: %v\n", saveFile) return c.String(http.StatusInternalServerError, "打开视频写入设备失败: "+saveFile) } defer writer.Close() for /* i := 0; i < 100; i++ */ { if ok := webcam.Read(&img); !ok { fmt.Printf("设备已关闭: %v\n", deviceID) return c.String(http.StatusInternalServerError, "设备已关闭: "+deviceID) } if img.Empty() { continue } writer.Write(img) } return c.String(http.StatusOK, "GocvStartRecord 成功") } func GocvStopRecord(c echo.Context) error { fmt.Print("GocvStopRecord 启动\n") deviceID := c.QueryParam("deviceID") saveFile := c.QueryParam("saveFile") webcam, err := gocv.OpenVideoCapture(deviceID) if err != nil { fmt.Printf("打开视频捕获设备失败: %v\n", deviceID) return c.String(http.StatusInternalServerError, "打开视频捕获设备失败: "+deviceID) } defer webcam.Close() /* img := gocv.NewMat() defer img.Close() if ok := webcam.Read(&img); !ok { fmt.Printf("无法读取设备 %v\n", deviceID) return c.String(http.StatusInternalServerError, "无法读取设备: "+deviceID) } writer, err := gocv.VideoWriterFile(saveFile, "mp4v", 25, img.Cols(), img.Rows(), true) if err != nil { fmt.Printf("打开视频写入设备失败: %v\n", saveFile) return c.String(http.StatusInternalServerError, "打开视频写入设备失败: "+saveFile) } defer writer.Close() */ return c.String(http.StatusOK, "GocvStopRecord 成功") }
解决方案
核心思路是把录制逻辑放到独立的goroutine中执行,同时用全局映射维护每个设备的录制状态与控制信号,避免阻塞API请求。
步骤1:定义录制状态管理结构
package gocvFunctions import ( "sync" "gocv.io/x/gocv" ) // 录制任务结构体,维护单个设备的录制状态与资源 type RecordTask struct { IsRecording bool Webcam *gocv.VideoCapture Writer *gocv.VideoWriter StopChan chan struct{} mutex sync.Mutex } // 全局映射,存储设备ID到录制任务的对应关系 var recordTasks = make(map[string]*RecordTask) var taskMutex sync.Mutex
步骤2:改造启动接口
启动接口初始化录制资源后,立即启动goroutine执行录制,无需等待录制结束:
func GocvStartRecord(c echo.Context) error { deviceID := c.QueryParam("deviceID") saveFile := c.QueryParam("saveFile") taskMutex.Lock() defer taskMutex.Unlock() // 检查设备是否正在录制,避免重复启动 if task, exists := recordTasks[deviceID]; exists && task.IsRecording { return c.String(http.StatusConflict, "该设备正在录制中") } // 打开摄像头 webcam, err := gocv.OpenVideoCapture(deviceID) if err != nil { return c.String(http.StatusInternalServerError, "打开视频捕获设备失败: "+deviceID) } // 读取首帧获取分辨率 img := gocv.NewMat() defer img.Close() if ok := webcam.Read(&img); !ok { webcam.Close() return c.String(http.StatusInternalServerError, "无法读取设备: "+deviceID) } if img.Empty() { webcam.Close() return c.String(http.StatusInternalServerError, "设备无输出帧") } // 打开视频写入器 writer, err := gocv.VideoWriterFile(saveFile, "mp4v", 25, img.Cols(), img.Rows(), true) if err != nil { webcam.Close() return c.String(http.StatusInternalServerError, "打开视频写入设备失败: "+saveFile) } // 创建停止信号通道 stopChan := make(chan struct{}) task := &RecordTask{ IsRecording: true, Webcam: webcam, Writer: writer, StopChan: stopChan, } recordTasks[deviceID] = task // 启动goroutine执行录制逻辑 go func() { defer func() { // 录制结束后自动清理资源 task.mutex.Lock() task.Webcam.Close() task.Writer.Close() task.IsRecording = false task.mutex.Unlock() // 从全局映射移除任务(可选,根据需求保留或清理) taskMutex.Lock() delete(recordTasks, deviceID) taskMutex.Unlock() }() imgLoop := gocv.NewMat() defer imgLoop.Close() for { select { case <-stopChan: // 收到停止信号,退出录制循环 return default: // 读取帧并写入视频文件 if ok := task.Webcam.Read(&imgLoop); !ok { return } if imgLoop.Empty() { continue } task.Writer.Write(imgLoop) } } }() return c.String(http.StatusOK, "录制已启动") }
步骤3:改造停止接口
停止接口向对应录制任务发送停止信号,触发资源清理:
func GocvStopRecord(c echo.Context) error { deviceID := c.QueryParam("deviceID") taskMutex.Lock() task, exists := recordTasks[deviceID] taskMutex.Unlock() if !exists || !task.IsRecording { return c.String(http.StatusNotFound, "该设备未在录制") } task.mutex.Lock() defer task.mutex.Unlock() // 发送停止信号(关闭通道后所有读取操作会立即返回) close(task.StopChan) task.IsRecording = false return c.String(http.StatusOK, "录制已停止") }
关键注意事项
- 并发安全:用互斥锁保护全局任务映射和单个任务的状态,避免并发读写冲突。
- 资源泄漏:录制goroutine中通过defer确保摄像头和写入器在停止后正确关闭。
- 设备冲突:启动前检查设备录制状态,避免同一设备重复启动录制。
- 信号可靠性:用关闭通道的方式发送停止信号,确保goroutine能及时响应退出。
内容的提问来源于stack exchange,提问作者Luigi Gargioni
相关产品推荐
相关产品推荐

