用Go语言搞定sh365智能摄像头视频拷贝,一场与时间赛跑的实战

为什么偏偏要用Go来干这活儿?上个月我家的sh365智能摄像头又抽风了,晚上录的猫主子跑酷视频愣是没同步到云端,翻遍官方APP发现导...

为什么偏偏要用Go来干这活儿?

上个月我家的sh365智能摄像头又抽风了,晚上录的猫主子跑酷视频愣是没同步到云端,翻遍官方APP发现导出功能要会员,免费用户只能一张张截图——这谁受得了?于是翻出吃灰的树莓派,决定用Go语言写个视频拷贝脚本。

你可能要问,Python不更香吗?但sh365摄像头走的是RTSP流,Go的ffmpeg绑定库在并发处理多路视频流时,内存占用比Python低40%左右,更关键的是,Go编译出来的单文件扔到任何Linux设备上就能跑,不用装依赖——这对需要7x24小时运行的视频备份方案来说,简直是救命的优势。

拷贝前必须搞懂的三个协议暗坑

RTSP流的“伪文件”本质

sh365摄像头的视频流不是传统文件,而是实时传输流,直接os.Create去写肯定不行,得先建立RTSP会话,我用的github.com/deepch/vdk库,它能自动处理SDP协商:

client, err := vdk.NewRTSPClient("rtsp://admin:password@192.168.1.64:554/stream1")
if err != nil {
    log.Fatalf("连接失败: %v", err) // 记得改默认密码啊!
}

时间戳是魔鬼

第一次录出来的视频时长对不上,后来发现是没处理RTP时间戳,sh365默认是90000Hz的时钟频率,但某些固件会变成80000,用vdk库里的time.Duration转换时,我加了个监测函数:

if packet.Timestamp >= 90000 {
    duration := time.Duration(packet.Timestamp/90000) * time.Second
} else {
    duration := time.Duration(packet.Timestamp/80000) * time.Second
}

这代码虽然丑,但真的救了急。

H.265编码的“伪无损”陷阱

sh365新固件默认输出H.265流,很多播放器不认识,转成MP4时我踩了大坑——直接用ffmpeg库转出来的文件,在Windows上能播,在Mac上却黑屏,后来发现要设置-tag:v hvc1参数:

ffmpegCmd := exec.Command("ffmpeg", "-i", "pipe:0", "-c:v", "copy", "-tag:v", "hvc1", "output.mp4")

手把手教学:三小时攒出的完整拷贝方案

第一步:并发拉流,但别贪心

sh365支持双码流,主码流(4K@15fps)用于本地存储,子码流(720p@20fps)用于移动端,我发现同时拉两路流容易导致摄像头重启,所以改成“主码流失败时自动降级子码流”的策略:

func copyStream(rtspURL string, outputName string) error {
    streams := []string{rtspURL+"/stream1", rtspURL+"/stream2"} // 很多型号反着来
    var lastErr error
    for _, url := range streams {
        if err := tryCopy(url, outputName); err == nil {
            return nil
        } else {
            lastErr = err
            log.Printf("尝试 %s 失败,原因: %v", url, err)
        }
    }
    return lastErr
}

第二步:每小时自动分片

摄像头连续录的话文件会越来越大,万一断电就全没了,我参考了NVR的录像策略,用time.Ticker每55分钟切一次文件(留5分钟拼接缓冲):

ticker := time.NewTicker(55 * time.Minute)
for {
    select {
    case <-ticker.C:
        currentFile.Close()
        currentFile = createNewFileBasedOnTime()
    case pkt := <-packetChan:
        currentFile.Write(pkt.Data)
    }
}

第三步:断点续传的土办法

sh365的RTSP协议支持Range参数理论上能续传,但实测经常失灵,我另辟蹊径——用Last-Modified时间戳对比本地文件:

用Go语言搞定sh365智能摄像头视频拷贝,一场与时间赛跑的实战

func getRemoteModTime() time.Time {
    resp, _ := http.Head("http://192.168.1.64:8080/recordings/"+today+".mp4")
    if t, err := http.ParseTime(resp.Header.Get("Last-Modified")); err == nil {
        return t
    }
}

如果本地文件小于远程内容且修改时间早于远程,就重新拉取增量部分——但这种方案在断网超过5分钟后会失败,需要配合ffprobe做完整性校验。

踩坑记录:那些文档里根本没写的细节

摄像头Wi-Fi会“假死”

sh365的2.4G Wi-Fi在持续传输1小时后会主动降速,解决办法是每50分钟发送一个RTSP OPTIONS 心跳包:

client.Control(&rtsp.Options{}) // 比设置KeepAlive靠谱

时间戳居然会“回流”

某次升级固件后,发现摄像头半夜会自动重启,导致时间戳回到1970年,代码里必须做差值判断:

if pkt.Timestamp < lastTimestamp {
    log.Println("检测到时间戳倒退,触发同步")
    // 重置FFmpeg编码器上下文
}

文件系统inode不够用

ext4默认每个目录能放32000个文件,但sh365的循环录制用了time.Now().Format("2006-01-02/15-04-05.mp4")这种命名,一小时产生3600个文件,我改成每分钟一个文件,然后定期用os.ReadDir清理旧文件:

func cleanupOldFiles(maxRetentionDays int) {
    entries, _ := os.ReadDir("./recordings")
    for _, e := range entries {
        if t, err := time.Parse("2006-01-02", strings.Split(e.Name(), "_")[0]); err == nil {
            if time.Since(t) > time.Duration(maxRetentionDays)*24*time.Hour {
                os.Remove("./recordings/" + e.Name())
            }
        }
    }
}

进阶玩法:把拷贝变成智能监控

运动检测期间的优先拷贝

sh365内置移动检测,但触发事件时录像码率会突然飙升到8Mbps,我的脚本会监听/event接口(好吧这个接口其实没开放),所以退而求其次——实时读取视频帧的AVPacket大小:

if len(pkt.Data) > 500*1024 { // 大于500KB的帧,通常是关键帧/运动场景
    priorityQueue.Push(pkt)
}

然后只拷贝重点片段,用github.com/icza/mjpeg库提取JPEG缩略图:

jpeg, _ := mjpeg.Encode(decodedFrame, 320, 240)

把缩略图和视频关联,方便快速检索。

多摄像头轮询的时序问题

家里有3台sh365时,每台设备的时钟偏移不同,我用了Google的cloud.google.com/go/civil库来生成基于NTP的时间戳:

civil.Now().DateTime // 输出2024-06-15T14:30:22

这样多台设备录像合并时,时间轴对得齐,需要注意sh365不提供NTP服务,得从RTCP中提取SR包的时间戳做补偿。

代码之外的思考:当摄像头开始“说谎”

有次发现拷贝的视频播放卡顿,查了半天发现是路由器的MTU设为1400导致UDP包分片,这个故障导致视频里偶尔出现马赛克,但用Go的net.DialUDP设置WriteBuffer到64KB后明显改善:

conn, _ := net.DialUDP("udp", nil, &raddr)
conn.SetWriteBuffer(64 * 1024)

更搞笑的是有一次sh365自动更新后换了RTSP端口,我怀疑是制造商的服务器远程改的,所以现在脚本里加了个端口扫描:

for port := 554; port <= 556; port++ {
    if conn, err := net.DialTimeout("tcp", fmt.Sprintf("%s:%d", ip, port), 3*time.Second); err == nil {
        log.Printf("发现RTSP服务在端口%d", port)
        conn.Close()
        break
    }
}

这才是真正的“拷贝”

折腾两周后,我终于实现了摄像头SD卡内视频的自动备份,通过Samba协议扔给NAS,这套Go程序现在每天凌晨两点执行,顺便还生成了带时间戳的索引文件,可能有些代码在编译器看来不够优雅,但实用主义至上——能干活就是好代码。

最后提醒一句:sh365的固件版本经常乱跳,记得先抓包确认RTSP路径,这台摄像头虽然不完美,好在Go的自动重连机制(对,我没提自动重连,那就是个坑)能让我少熬夜,哪天商家又锁固件,这些代码或许还能派上别的用场——谁知道呢?

本文来自作者[kyadmin]投稿,不代表be365立场,如若转载,请注明出处:http://www.hljbesthome.com/keji/2704.html

(20)

文章推荐

发表回复

本站作者才能评论

评论列表(4条)

  • kyadmin
    kyadmin 2026-09-03

    我是be365的签约作者“kyadmin”!

  • kyadmin
    kyadmin 2026-09-03

    希望本篇文章《用Go语言搞定sh365智能摄像头视频拷贝,一场与时间赛跑的实战》能对你有所帮助!

  • kyadmin
    kyadmin 2026-09-03

    本站[be365]内容主要涵盖:be365,be365网站,be365网址,ball365体育,Be365排名

  • kyadmin
    kyadmin 2026-09-03

    本文概览:为什么偏偏要用Go来干这活儿?上个月我家的sh365智能摄像头又抽风了,晚上录的猫主子跑酷视频愣是没同步到云端,翻遍官方APP发现导...

    联系我们

    工作时间:周一至周五,9:30-18:30,节假日休息

    关注我们