用Golang写一个v365监控视频保存器,从零开始,像聊天一样搞定
- 能源
- 2026-06-29 19:34:42
- 246
说实话,刚开始接触v365监控系统的时候,我也被它那个“保存一段视频”的功能整得有点懵,不是功能不好,是文档写得像天书,全是一堆专业术语,后来我琢磨着,既然自己是写Go的,不如自己动手写个程序来搞定这件事,今天就把这个过程掰开了揉碎了跟你聊聊。
v365监控到底怎么保存视频?
先说说最基础的事儿,v365监控系统本质上是个网络摄像头管理平台,它会持续不断地把摄像头拍到的画面送到服务器上,但问题是,它默认不保存视频——就像电视节目播完就没了,你得自己按“录制”才行。
那怎么触发保存呢? 一般来说有两种方式:
- 手动保存:通过v365的API发一条指令,告诉它“从现在开始录,录30秒”
- 自动保存:设置个定时任务,比如每天下午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秒的画面一起录进去,这个功能特别适合抓拍那种“已经发生了”的事件。
好了,今天就聊这么多,你要是照着上面的代码写,应该能跑起来保存视频了,要是遇到啥问题,或者发现我哪里写错了,随时告诉我——编程这事儿,本来就是在互相挑错中进步的。
