你刷短视频的时候,有没有刷到过那种“365天每日一帧”的延时视频?就是把一整年的照片或者视频片段,每天截一帧,然后拼成一个超长视频,我去年突发奇想,决定也搞一个,结果你猜怎么着?我在365dni截了一个视频——用Go语言写的,前后折腾了大概两周。

为什么非要用Go?
说实话,最开始我想用Python,毕竟Python写脚本快嘛,OpenCV一装,十行代码就能跑,但我很快就遇到问题了:处理365个视频文件,每个文件可能几十秒到几分钟不等,Python那个GIL(全局解释器锁)真的让人抓狂,我想做一个能命令行调用的工具,最好还能带个简单的Web界面,Go编译成单个二进制文件,丢到服务器就能跑,这诱惑太大了。
我的硬件配置
先说一下我的“实验室”环境,挺简陋的:
| 组件 | 型号/版本 |
|---|---|
| CPU | AMD Ryzen 5 5600X |
| 内存 | 32GB DDR4 |
| 系统 | Ubuntu 22.04 |
| Go版本 | 22.2 |
| 视频格式 | 大部分是MP4,少数MOV |
硬盘是1TB的NVMe SSD,365个视频,加起来大概120GB,不算大,但绝对不算小。
核心思路:别想太复杂
刚开始我犯了一个错误:我想一次性把所有视频加载到内存里,结果?内存直接爆了,后来我换了个思路——流式处理,一个一个视频读,读一个处理一个,处理完就释放,这样内存占用稳定在200MB左右。
关键代码片段(简化版)
package main
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"time"
)
func CaptureFrame(videoPath string, timestamp float64, outputPath string) error {
cmd := exec.Command("ffmpeg",
"-ss", fmt.Sprintf("%.2f", timestamp),
"-i", videoPath,
"-frames:v", "1",
"-q:v", "2",
outputPath,
)
return cmd.Run()
}
你看,核心就是调ffmpeg,你别笑,真的,99%的视频处理问题,ffmpeg都能搞定,我们做的只是用Go把它串起来。
遇到的那些坑
坑一:时间戳计算
视频时长不是固定的,有的视频10秒,有的5分钟,如果固定在第5秒截帧,短视频就崩了,所以我得先检测每个视频的时长:
func GetVideoDuration(videoPath string) (float64, error) {
cmd := exec.Command("ffprobe",
"-v", "error",
"-show_entries", "format=duration",
"-of", "default=noprint_wrappers=1:nokey=1",
videoPath,
)
out, err := cmd.Output()
if err != nil {
return 0, err
}
duration, _ := strconv.ParseFloat(strings.TrimSpace(string(out)), 64)
return duration, nil
}
然后根据每天对应的百分比来算时间戳,比如第100天,就是duration * (100/365)。
坑二:并发控制
一开始我开365个goroutine同时跑,结果CPU直接100%,ffmpeg进程互相抢资源,反而更慢,后来用带缓冲的channel加worker pool,限制同时运行的ffmpeg数量:
const maxConcurrent = 4
sem := make(chan struct{}, maxConcurrent)
for _, video := range videos {
sem <- struct{}{}
go func(v string) {
defer func() { <-sem }()
// 处理视频...
}(video)
}
4个并发是最佳平衡点,多了反而慢,少了浪费。
坑三:文件名排序
你猜我犯的最蠢的错误是什么?文件名的自然排序,Go默认的字符串排序会把“video10.mp4”排在“video2.mp4”前面,结果第2天的视频和第10天的视频搞反了,最后用了natsort库解决:
import "github.com/facette/natsort" natsort.Sort(files)
命令行界面:简单够用
我写了一个CLI,参数不多,但够用:
$ 365dni --input ./videos/ --output ./frames/ --format jpg
支持--help查看所有选项:
--input:视频文件夹路径--output:输出帧的文件夹--format:jpg或png(默认jpg)--concurrent:并发数(默认4)--start-date:第一天的日期(用于命名文件)
加个Web界面玩玩
后来我觉得命令行还是不够直观,就顺手写了个HTTP服务器,用net/http包,很简单:
http.HandleFunc("/upload", uploadHandler)
http.HandleFunc("/status", statusHandler)
http.Handle("/frames/", http.StripPrefix("/frames/", http.FileServer(http.Dir("./frames"))))
前端就一个HTML页面,上传视频、查看进度、下载结果,就这些,没加什么复杂的东西。
真实使用感受
最终结果呢?我处理了365个视频,大概花了6个小时,生成的帧图片大约500MB,然后用ffmpeg把这些帧合成一个视频:
ffmpeg -framerate 24 -pattern_type glob -i 'frames/*.jpg' -c:v libx264 output.mp4
合成完一看,还挺有成就感的,有些天的视频特别美——比如第47天,我的猫第一次跳到窗台上;第203天,我在阳台种的花开了,当然也有糊掉的,比如第88天,那天我手抖没拍好,画面全糊了,但我觉得这才是真实的生活嘛。
一些小建议
如果你也想搞类似的工具,给你几个建议:
- 别自己实现编解码:用ffmpeg,别自己造轮子,Go的
os/exec调ffmpeg就是最佳实践。 - 处理错误要优雅:有些视频可能损坏了,别让整个程序崩溃,加个
defer recover或者错误通道。 - 日志很重要:我用
log/slog包,记录每个视频的处理状态,后来发现有个视频处理失败,一看日志,原来是文件名里有个特殊字符。 - 考虑边缘情况:有些视频只有1秒怎么办?有些视频是竖屏的又怎么办?我统一加了黑边填充。
代码写完后,我就把项目推到了GitHub,现在偶尔还会有人提issue,说在某个系统上跑不了,我就一个一个修,挺有意思的。
你也能写
说真的,这个项目没什么高深的算法,就是一步一个脚印地解决问题。Go语言的强项就是这种“土办法但靠谱”的活儿,你不是非得写微服务、高并发才用Go,用它来处理日常的文件、视频、文本也是一把好手。
最后我的视频终于剪好了,虽然画面切换的生涩感很明显,帧与帧之间跳得有点不连贯,但那是实实在在的365天啊,我截了一个视频,更像截下了一整年的碎片,有时候技术就是这样,不是为了做出多酷炫的东西,就是为了把脑子里那个“我想试试”的念头变成现实。
本文来自作者[kyadmin]投稿,不代表be365立场,如若转载,请注明出处:http://www.hljbesthome.com/lvyou/1506.html
评论列表(4条)
我是be365的签约作者“kyadmin”!
希望本篇文章《在365dni截了一个视频,我用Go语言写了一个剪辑工具》能对你有所帮助!
本站[be365]内容主要涵盖:be365,be365网站,be365网址,ball365体育,Be365排名
本文概览:你刷短视频的时候,有没有刷到过那种“365天每日一帧”的延时视频?就是把一整年的照片或者视频片段,每天截一帧,然后拼成一个超长视频,我去...