用Go语言,我给自己造了一个第一个365天视频生成器
- 健康
- 2026-08-13 06:32:00
- 25
为什么我非要自己写代码,而不是用现成模板?
说真的,年初的时候我刷到好多人在晒“第一个365天视频”——就是把每天拍的一秒钟拼起来,变成五分钟的时光缩影,我也想要啊,但那些在线工具要么要会员,要么水印糊一脸,有的还偷偷压缩画质,我一拍大腿:我可是写Go的,这活儿不该求人。
然后我就掉坑里了,查了一圈,发现并没有一个现成的库是专门干“按日期排序、拼接视频、加转场、配BGM”这整套流程的,得,自己造轮子吧。
第一步:定需求,简单到不行,也难到不行
我想象中的流程是:
- 把365个小视频文件按日期命名放在一个文件夹里
- 程序读文件名,解析出时间戳
- 按时间顺序拼接
- 加上片头和片尾
- 输出一个完整mp4
听起来是不是特简单?但坑在于“分辨率不一致”,你手机竖着拍的、横着拍的、别人发来的旧视频,全是五花八门的尺寸,如果直接拼,出来的画面会忽大忽小,像心电图一样抽搐。
第二步:用Go调FFmpeg,核心就三行命令
Go本身不处理视频,它是靠os/exec去调用FFmpeg的,我的核心逻辑其实就一个函数:
func runFFmpeg(args ...string) error {
cmd := exec.Command("ffmpeg", args...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd.Run()
}
然后拼转码参数,关键点是先把所有视频统一转成同样的分辨率,比如都改成1280x720,帧率统一30fps,音频统一AAC,这一步不做,后面全白搭。
转码统一格式:
runFFmpeg("-i", inputPath, "-vf", "scale=1280:720:force_original_aspect_ratio=decrease,pad=1280:720:(ow-iw)/2:(oh-ih)/2", "-r", "30", tempFile)
这段代码里pad是精髓,它会用黑边填充,而不是拉伸变形,不然你的脸会被拉成马脸,真的。
第三步:排序才是灵魂,文件名别瞎起
我一开始用filepath.Walk读文件,然后按os.Stat().ModTime()排序,结果发现一个问题:如果你是跨年拍的,或者是把旧视频导进来,修改时间根本不靠谱。
所以最后我定了个规矩,文件名必须是YYYY-MM-DD_HHmmss.mp4这种格式,然后程序里用正则解析:
re := regexp.MustCompile(`(\d{4})-(\d{2})-(\d{2})_(\d{6})`)
解析出时间后,转成time.Time,再排序。这一步是纯Go标准库的活儿,不需要外部依赖。 排序用sort.Slice,简单粗暴:
sort.Slice(videos, func(i, j int) bool {
return videos[i].t.Before(videos[j].t)
})
第四步:拼接不是简单concat,要处理转场
如果直接用FFmpeg的concat demuxer,黑屏生硬不说,而且如果你的视频时长长短不一,声音会断,我试过用xfade滤镜做交叉淡化转场,效果很不错,但代价是渲染时间爆炸——365个视频每个转场1秒,光转场就364秒,再加上渲染时间,我那台M1 Mac跑了整整40分钟。
而且xfade的语法特别绕,需要计算每个片段的偏移量,你可以直接用concat filter更稳妥,但转场效果就没了,我最后折中了一下:每天24小时的视频,我用拼接,不转场;每周的边界加一个0.5秒的淡入淡出,代码逻辑就是:
if i % 7 == 0 {
// 前一天片段加上fade out
// 后一天片段加上fade in
}
第五步:片头片尾,用Go生成文字卡片
我不想额外做图片,所以直接用FFmpeg的drawtext滤镜生成文字,这里有个小坑:FFmpeg默认不带中文字体,你得指定一个.ttf文件路径,我放了个微软雅黑在项目里,
runFFmpeg("-f", "lavfi", "-i", "color=c=black:s=1280x720:r=30", "-vf", "drawtext=text='我的365天':fontfile=msyh.ttf:fontsize=72:fontcolor=white:x=(w-text_w)/2:y=(h-text_h)/2", "-t", "3", intro.mp4)
性能不是重点,但崩溃恢复是
我最崩溃的一刻是渲染到第300个视频时内存爆了——因为我把所有视频都读进了内存数组,还解析了元数据,后来改成流式处理:每转码完一个片段,立即写入临时文件,只保存当前拼接列表。
伪代码就是这样:
| 步骤 | 操作 | 内存占用 |
|---|---|---|
| 1 | 读取文件名 | 极低 |
| 2 | 转码片段 | 单片段大小 |
| 3 | 写入concat列表 | 一个路径字符串 |
| 4 | 最后统一concat | 低 |
这样最占内存的就只有单个视频片段。
那天晚上我盯着进度条发呆
程序跑完的那一刻,我点开生成的mp4,前10秒就是平平无奇的生活碎片:早上挤地铁、中午吃面、晚上加班,但到了第200秒左右,突然出现了去年冬天那场大雪,我穿着那件红色羽绒服在楼下傻站着笑。说实话,那一瞬间我鼻子有点酸。
这个工具不是多专业,在剪辑师眼里就是个玩具,但它让我理解了什么叫“用代码记录生活”。365个文件,每个只有一秒,但合在一起就是整个人生里某个绕不过去的拐角。
如果你也想搞,送你几个Go的避坑建议
- 别用
os.Stat拿时间,信文件名 - 所有视频先统一
-r 30,不然音画不同步 - 转场别贪多,365个片段全加转场你会等到地老天荒
- 写个
defer的recover(),渲染到一半崩了至少能定位错误 - 记得留个临时文件清理函数
代码不复杂,复杂的是你愿意为了一年的碎片花上整个周末,但我跟你讲,值。
