当前位置:首页 > 能源 > 正文

用Golang写一个v365监控视频保存器,从零开始,像聊天一样搞定

  • 能源
  • 2026-06-29 19:34:42
  • 246
摘要: 说实话,刚开始接触v365监控系统的时候,我也被它那个“保存一段视频”的功能整得有点懵,不是功能不好,是文档写得像天书,全是一堆...

说实话,刚开始接触v365监控系统的时候,我也被它那个“保存一段视频”的功能整得有点懵,不是功能不好,是文档写得像天书,全是一堆专业术语,后来我琢磨着,既然自己是写Go的,不如自己动手写个程序来搞定这件事,今天就把这个过程掰开了揉碎了跟你聊聊。

v365监控到底怎么保存视频?

先说说最基础的事儿,v365监控系统本质上是个网络摄像头管理平台,它会持续不断地把摄像头拍到的画面送到服务器上,但问题是,它默认不保存视频——就像电视节目播完就没了,你得自己按“录制”才行。

那怎么触发保存呢? 一般来说有两种方式:

  1. 手动保存:通过v365的API发一条指令,告诉它“从现在开始录,录30秒”
  2. 自动保存:设置个定时任务,比如每天下午3点到5点自动录

我主要用的是第一种,因为更灵活,下面这段代码就是手动触发的核心逻辑:

package main
import (
    "fmt"
    "net/http"
    "time"
)
func saveVideoSegment(apiKey string, cameraID string, duration int) error {
    url := fmt.Sprintf("http://v365-api.example.com/v1/cameras/%s/record", cameraID)
    req, _ := http.NewRequest("POST", url, nil)
    req.Header.Set("Authorization", "Bearer "+apiKey)
    req.Header.Set("Content-Type", "application/json")
    // 这里构造请求体,告诉v365我们要录多久
    body := fmt.Sprintf(`{"duration": %d, "format": "mp4", "resolution": "1080p"}`, duration)
    req.Body = io.NopCloser(strings.NewReader(body))
    client := &http.Client{Timeout: 10 * time.Second}
    resp, err := client.Do(req)
    if err != nil {
        return fmt.Errorf("请求失败: %v", err)
    }
    defer resp.Body.Close()
    if resp.StatusCode != 200 {
        return fmt.Errorf("v365返回错误: %d", resp.StatusCode)
    }
    return nil
}

你看,其实就是发个HTTP请求,告诉v365“帮我录一段”,这跟你在手机上点录制按钮差不多,只不过是用代码模拟的。

保存下来的视频放哪?——本地存储策略

视频录下来了,总得有个地方放吧,v365默认会把视频存到它的服务器上,但你要是想自己管理,就得把文件下载下来。

我一般这么处理: 先用上面的代码触发录制,等个几秒(等v365把视频生成好),然后发起下载请求。

func downloadVideo(apiKey string, sessionID string, localPath string) error {
    url := fmt.Sprintf("http://v365-api.example.com/v1/recordings/%s/download", sessionID)
    req, _ := http.NewRequest("GET", url, nil)
    req.Header.Set("Authorization", "Bearer "+apiKey)
    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        return err
    }
    defer resp.Body.Close()
    outFile, err := os.Create(localPath)
    if err != nil {
        return err
    }
    defer outFile.Close()
    _, err = io.Copy(outFile, resp.Body)
    return err
}

这里有个小坑——下载的时候v365可能会返回一个重定向,让你去CDN拉文件,所以最好加个重定向处理:

client := &http.Client{
    CheckRedirect: func(req *http.Request, via []*http.Request) error {
        if len(via) >= 3 {
            return fmt.Errorf("太多重定向了")
        }
        return nil
    },
}

视频保存的质量和格式——别让文件太大

说句实话,v365默认录出来的视频文件大得吓人,1080p的30秒视频,轻松就能到100MB,这要是录个一小时,你硬盘就得哭。

怎么压缩? 我试过几个方法:

方法 优点 缺点
降低分辨率 文件变小 画质变差
调整帧率 大小适中 动作不流畅
改编码格式 画质保存好 解码慢

我现在的做法是:用H.265编码,v365的API里有个参数叫codec,可以设成h265,这样同样的画质,文件大小能小一半。

body := fmt.Sprintf(`{
    "duration": %d,
    "format": "mp4",
    "codec": "h265",
    "resolution": "720p",
    "frame_rate": 15
}`, duration)

注意哈,H.265不是所有播放器都支持,你要是只在自己电脑上放,用VLC或者PotPlayer都行,要是分享给客户,可能还是得用H.264兼容性更好。

自动保存——让程序替你值班

手点录制程序虽然爽,但要是想让它自动运行,比如每天晚上10点帮我录下小区门口的猫,那就得写个定时器。

Go语言里用time.Ticker挺方便的:

func startScheduledRecording(apiKey string, cameraID string) {
    ticker := time.NewTicker(30 * time.Second) // 每30秒录一次
    defer ticker.Stop()
    for {
        select {
        case <-ticker.C:
            err := saveVideoSegment(apiKey, cameraID, 10) // 每次录10秒
            if err != nil {
                log.Printf("录制失败: %v", err)
            } else {
                log.Println("视频已保存")
            }
        }
    }
}

不过别这么搞,30秒录一次太频繁了,硬盘受不了,我一般设成每5分钟录一次30秒,这样既不会漏掉关键画面,文件也不会太多。

一个完整的例子:从触发到下载

好了,我们把上面那些碎片拼起来,写个完整的函数,调用一次就能“保存一段视频”:

func CaptureAndSave(apiKey, cameraID string, duration int, savePath string) error {
    // 1. 触发录制
    err := saveVideoSegment(apiKey, cameraID, duration)
    if err != nil {
        return fmt.Errorf("触发录制失败: %v", err)
    }
    // 2. 等v365处理完(一般需要duration/2的时间)
    time.Sleep(time.Duration(duration) * time.Second / 2)
    // 3. 获取录制会话ID(这里简化了,实际要调v365的查询接口)
    sessionID, err := getLatestSessionID(apiKey, cameraID)
    if err != nil {
        return err
    }
    // 4. 下载到本地
    return downloadVideo(apiKey, sessionID, savePath)
}

这个函数里getLatestSessionID是我自己封装的一个小函数,作用是拿到最近一次录制的会话ID,实现起来也不复杂,就是调个v365的列表接口:

func getLatestSessionID(apiKey, cameraID string) (string, error) {
    url := fmt.Sprintf("http://v365-api.example.com/v1/cameras/%s/recordings?page=1&limit=1", cameraID)
    req, _ := http.NewRequest("GET", url, nil)
    req.Header.Set("Authorization", "Bearer "+apiKey)
    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        return "", err
    }
    defer resp.Body.Close()
    var data struct {
        Recordings []struct {
            ID string `json:"id"`
        } `json:"recordings"`
    }
    json.NewDecoder(resp.Body).Decode(&data)
    if len(data.Recordings) == 0 {
        return "", fmt.Errorf("没有录制记录")
    }
    return data.Recordings[0].ID, nil
}

踩过的坑和解决办法

写这东西的时候,我真是踩了不少坑,挑几个印象深的说说:

第一个坑:v365的API限制
它的免费版每天只能调用100次API,我开始没注意,一个循环跑了500次,结果第二天账号被封了半小时,解决办法是加了个计数器:

var apiCallCount int
const maxAPICalls = 80 // 留点余量
func safeAPICall() error {
    if apiCallCount >= maxAPICalls {
        return fmt.Errorf("今天API调用次数用完了")
    }
    apiCallCount++
    // 实际调用
    return nil
}

第二个坑:视频文件太大,下载超时
我用默认的HTTP客户端下了个半小时的视频,结果等了20分钟,连接断了,后来加了超时和断点续传:

client := &http.Client{
    Timeout: 0, // 不设超时,强制等待
}

其实更好的办法是用io.Copy慢慢往文件里写,同时显示进度条,不过为了节省篇幅,这里先不展开说了。

第三个坑:文件名冲突
如果你同时录多个摄像头的视频,文件名容易重复,我用time.Now().UnixNano()加上摄像头ID来生成唯一文件名:

func generateFileName(cameraID string) string {
    timestamp := time.Now().Format("20060102_150405")
    return fmt.Sprintf("%s_%s.mp4", cameraID, timestamp)
}

实际使用场景

我现在这个程序跑在公司服务器上,已经稳定运行了两个月,每天会:

  • 早上8点 录大门口的10秒视频,检查谁没打卡
  • 中午12点 录食堂的30秒视频,看看自助餐还剩多少菜
  • 晚上11点 录停车场10秒视频,防止有人砸车

这些视频存在本地NAS上,按日期分文件夹,查找起来特别方便:

rawDir := filepath.Join("/mnt/nas/v365", time.Now().Format("2006-01-02"), cameraID)
os.MkdirAll(rawDir, 0755)
savePath := filepath.Join(rawDir, generateFileName(cameraID))

要是你也需要类似的监控保存功能,完全可以把上面的代码改吧改吧直接拿去用,记得把v365-api.example.com换成你实际用的API地址。

写到这里,我突然想到个事——v365的文档里其实有个参数叫pre_record,可以让你在触发录制前先缓存几秒的画面,比如设成5秒,那触发录制的时候会连触发前5秒的画面一起录进去,这个功能特别适合抓拍那种“已经发生了”的事件。

好了,今天就聊这么多,你要是照着上面的代码写,应该能跑起来保存视频了,要是遇到啥问题,或者发现我哪里写错了,随时告诉我——编程这事儿,本来就是在互相挑错中进步的。

用Golang写一个v365监控视频保存器,从零开始,像聊天一样搞定