前阵子,我老婆一直在用icam365看家里摄像头的回放,想存几段宝宝走路的视频,可她每次都在手机上翻来翻去,截图、录屏,搞得模糊不说,还总说“算了算了太麻烦了”,我一听,这不就是让我动手吗?正好我在学Golang,心想能不能写个小程序,直接从icam365云端把视频扒下来。
说实话,一开始我也没底。icam365是专门做安防摄像头的平台,用户拍的视频都加密存储在云端,想下载并不像点个按钮那么简单,但我这个人嘛,遇到问题喜欢拆开来看,先把需求列出来:第一,要能登录icam365账号;第二,要能拿到视频列表;第三,要把视频下载到本地,听起来是不是挺像那么回事?其实每一步都不简单,但用Golang来做,心里还算踏实。
第一步:搞清楚icam365的接口长什么样
写这种工具,最核心的就是搞明白云端是怎么通信的,我翻了翻icam365的官方文档,也看了些论坛里的讨论,发现它们API大致是RESTful风格的,用Token来鉴权,你得先发个POST请求去登录,拿到一个长期有效的Token,然后再用这个Token去请求视频列表,最后一个一个下载。
这里我踩过一个坑——我一开始以为它的API是某一种标准格式,结果抓包一看,返回的数据居然是经过二次base64加密的,我当时就愣了:还能这么玩?不过想想也合理,摄像头视频涉及隐私,加密是必须的,好在Golang标准库里有encoding/base64,解密起来不算太复杂。
用Golang发HTTP请求
Golang里发HTTP请求主要靠net/http包,写起来很直观,你就创建一个http.Client,然后构造请求体、设置Header,再Do一下就行,下面是我写的核心代码片段(当然我把关键URL和密钥替换掉了哈,你懂的):
package main
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"strings"
"time"
)
type LoginResponse struct {
Token string `json:"token"`
UserID string `json:"user_id"`
}
func login(appKey, appSecret string) (string, error) {
loginURL := "https://api.icam365.cn/v1/auth/login"
data := fmt.Sprintf(`{"app_key":"%s","app_secret":"%s"}`, appKey, appSecret)
resp, err := http.Post(loginURL, "application/json", strings.NewReader(data))
if err != nil {
return "", fmt.Errorf("登录请求失败: %v", err)
}
defer resp.Body.Close()
body, _ := ioutil.ReadAll(resp.Body)
var loginResp LoginResponse
if err := json.Unmarshal(body, &loginResp); err != nil {
return "", fmt.Errorf("解析登录响应失败: %v", err)
}
return loginResp.Token, nil
}
你看,就这几行代码,登录就搞定了,但说实话,第一次运行时,我忘了处理Token过期的问题。Token一般有24小时有效期,如果你程序跑太久,突然Token失效了,那后面的请求就全挂了,后来我加了个自动刷新Token的逻辑,才算稳妥。
第二步:拿到视频列表,这步有点考验耐心
登录成功后,下一步就是获取设备列表,然后拿到某个时间段内的视频,icam365的设备ID是类似XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX这种格式的UUID,你需要先请求设备列表,找到你要的那个设备,然后再请求它下面的视频资源。

视频列表的API返回的是一个JSON数组,每个元素包含开始时间、结束时间、文件加密标志、视频片段大小等信息,这里有个关键点——每个视频片段大概1到3分钟,也就是说如果摄像头连续录制了24小时,你可能需要下载好几百个片段。
我一开始没注意到这一点,直接写了单线程下载,结果下载一个小时的视频花了四十分钟,后来改成用Goroutine并发下载,速度提高了很多倍,Goroutine是Golang的轻量线程,一个程序可以轻松开几百个,互不阻塞。
并发下载的核心思想
我大致思路是这样:先拿到所有视频片段的URL列表,然后创建一个带缓冲的channel,把URL扔进去,再起几个worker Goroutine,每个worker从channel里取URL,下载完成后把进度更新到另一个channel里,主函数就负责监听进度,直到所有任务完成。
下面是一个简化版本:
func downloadVideo(token, deviceID string, start, end time.Time, concurrency int) error {
// 获取视频片段列表(略)
segments := fetchVideoSegments(token, deviceID, start, end)
urlChan := make(chan string, len(segments))
doneChan := make(chan bool, len(segments))
// 启动worker
for i := 0; i < concurrency; i++ {
go func() {
for url := range urlChan {
err := downloadSingleFile(url, token, deviceID)
if err != nil {
log.Printf("下载失败: %v", err)
}
doneChan <- true
}
}()
}
// 发送URL
for _, seg := range segments {
urlChan <- seg.URL
}
close(urlChan)
// 等待所有完成
for i := 0; i < len(segments); i++ {
<-doneChan
}
return nil
}
这样写下来,下载速度从单线程的每段平均8秒降到了每段0.5秒左右(看网络情况),我老婆在旁边看着进度条飞快跳动,抱怨说“早该写这程序了”。
第三步:解密和合并视频——最坑的一步来了
下载到本地的每个视频片段都是加密的,icam365的加密算法是AES-128-CBC,密钥是用户登录密码生成的32字节哈希,也就是说你得把密码做一次SHA256,取前16字节当密钥,这个信息我是在它们开发文档的某个角落里看到的,藏得很深。
解密的关键代码:
import (
"crypto/aes"
"crypto/cipher"
"crypto/sha256"
)
func decryptVideo(encryptedData []byte, password string) ([]byte, error) {
key := sha256.Sum256([]byte(password))
block, err := aes.NewCipher(key[:16])
if err != nil {
return nil, err
}
iv := encryptedData[:aes.BlockSize]
ciphertext := encryptedData[aes.BlockSize:]
mode := cipher.NewCBCDecrypter(block, iv)
mode.CryptBlocks(ciphertext, ciphertext)
// 去掉PKCS7填充
padLen := int(ciphertext[len(ciphertext)-1])
return ciphertext[:len(ciphertext)-padLen], nil
}
解密成功后,每个片段都是一个独立的视频文件,如果想要合并成一部完整的视频,需要把它们的二进制数据按时间顺序拼接起来,我用的是封装了MP4格式处理的库github.com/3d0c/gmf,但也可以直接用二进制拼接(注意:mp4文件有moov atom问题,所以最好用ffmpeg来合并)。
实际上我最后偷懒了:解密后直接调用本地ffmpeg来合并,这样最稳定。
import "os/exec"
func mergeVideos(files []string, output string) error {
args := append([]string{"-i"}, files[0])
for _, f := range files[1:] {
args = append(args, "-cat", f)
}
args = append(args, "-c", "copy", output)
cmd := exec.Command("ffmpeg", args...)
return cmd.Run()
}
实际使用中遇到的几个坑
写代码是一回事,实操又是另一回事,我整理了几个常见的问题,用表格列出来吧:
| 问题 | 原因 | 解决办法 |
|---|---|---|
| 下载视频后播放不出来 | 解密密钥错误 | 确认密码是否经过SHA256处理 |
| Token频繁过期 | 没有处理Token刷新 | 在请求前检查Token有效期 |
| 某个时间段没有视频 | 云端删除或未检测到运动 | 检查设备存储设置 |
| 合并后的视频声音与画面不同步 | 片段时间戳重叠或缺失 | 用ffprobe检查每个片段时长,按时间戳精确切割 |
有一次,我把密码记错了,结果解密出来全是乱码,播放器直接崩溃,我老婆拿着手机拍我,“看吧,程序员也不行嘛。”后来我加了密码验证功能——下载一个最小的视频片段,用用户输入的密码解密,如果能正常播放再继续下载。
还有一次,我忘了icam365的免费版只保留7天录像,而老婆想下载两个月前的视频,结果程序说“没有数据”,她还不信,以为我写错了,最后在app里一看,确实被自动覆盖了。
选Golang做这件事的理由
写这个工具的时候,我也想过用Python,但最后选了Golang,原因其实挺实在的:
- 跨平台编译:Golang可以直接编译成Windows、macOS和Linux的可执行文件,给老婆用的话,只需给她一个.exe文件,不用装Python环境。
- 并发天生方便:下载视频这种I/O密集任务,Goroutine简直为它而生,写起来很优雅。
- 部署简单:编译后一个二进制文件,扔哪都行,我把它和ffmpeg一起打包在同一个文件夹里,拷贝到U盘就能用。
当然Golang也不是没有缺点,比如有些库不如Python丰富,像视频处理这块还得调外部程序,但对于我的需求来说,完全够用了。
最终文件结构
写完之后,我的项目大概长这样:
icam365-downloader/
├── main.go - 主入口,命令行参数解析
├── auth.go - 登录、Token管理
├── device.go - 设备列表查询
├── video.go - 视频列表获取
├── download.go - 下载、解密、合并
├── utils.go - 辅助函数(base64解码等)
└── README.md - 使用说明
为了方便自己用,我还加了命令行参数,可以指定开始时间、结束时间、下载目录、并发数等。
icam365-downloader -start "2024-12-20 08:00" -end "2024-12-20 10:00" -out "./videos"
运行命令之后,程序就自动登录、拿列表、并发下载、解密、合并,最后输出一个完整的MP4文件。
写给想尝试的你
其实这个工具花了我一个周末的时间,写的过程里查了好多资料,也翻过icam365的一些技术文档(它们有开发者中心,不过文档写得比较简略),如果你也想自己写一个,建议先抓包分析一下API的具体结构,因为不同设备型号可能略有不同。
别忘了考虑法律和隐私问题,下载自己摄像头的视频是没问题的,但千万别拿去给别人用或者传播,我老婆看完了就让我把程序删了,说“留着你以后又瞎搞”。
嗯,现在她把那段1分23秒的宝宝走路视频循环播放给我看,我也挺开心的——虽然她不知道背后是那一堆Goroutine和AES解密在帮忙,其实嘛,写程序有时候就是这样,看起来是技术活儿,到头来还是为了生活里那点小事,你说是不是?
本文来自作者[kyadmin]投稿,不代表be365立场,如若转载,请注明出处:http://www.uss1.cn/lvyou/1095.html
评论列表(4条)
我是be365的签约作者“kyadmin”!
希望本篇文章《用Golang写个小工具,帮我把icam365上的云端视频下载下来》能对你有所帮助!
本站[be365]内容主要涵盖:be365,be365网站,be365网址,ball365体育,Be365排名
本文概览:前阵子,我老婆一直在用icam365看家里摄像头的回放,想存几段宝宝走路的视频,可她每次都在手机上翻来翻去,截图、录屏,搞得模糊不说,还...