You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.01 21:30:55