用Go语言,把365天压缩成一段求婚视频
- 健康
- 2026-09-02 21:41:57
- 52
先别急着写代码——你需要的不是编辑器,是“时间轴”
我昨天在地铁上刷到一个朋友求婚成功的朋友圈,视频里姑娘哭得稀里哗啦,但真正让我愣住的,是视频最后那行小字:“过去365天,我每天录了3秒钟”,好家伙,我第一反应是——这得写个脚本来处理吧?第二反应是——用Go语言干这事儿,简直像是给自行车装火箭,但仔细想想,又很合理。
很多人以为做这种“365天每日3秒”的视频,难点在剪辑软件,其实真正的坑在文件管理,你想想,一天录一段,手机里躺着的视频可能叫VID_20250213_083012.mp4,也可能叫IMG_20250213_184455.mov,甚至可能某天喝多了录了个横屏又竖屏的怪胎,这时候,你需要的是批量重命名、按时间排序、统一编码格式——这些恰恰是Go语言最舒服的领域。
第一步:把365个“碎片”变成有序的“档案”
我不建议你用bash脚本干这事儿,因为跨平台(你女朋友可能用iPhone,你剪辑用Windows)时,文件名里的中文表情符号、空格、时区偏移会把你逼疯。用Go写个小工具,十分钟搞定。
package main
import (
"fmt"
"io/fs"
"log"
"os"
"path/filepath"
"sort"
"strings"
"time"
)
func main() {
root := "./daily_raw" // 你把每天的视频丢这里
var files []string
filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
if err != nil || d.IsDir() {
return nil
}
if strings.HasSuffix(strings.ToLower(d.Name()), ".mp4") ||
strings.HasSuffix(strings.ToLower(d.Name()), ".mov") {
files = append(files, path)
}
return nil
})
// 关键:解析文件修改时间,并且按时间排序
sort.Slice(files, func(i, j int) bool {
ti, _ := getModTime(files[i])
tj, _ := getModTime(files[j])
return ti.Before(tj)
})
// 重命名成 001.mp4, 002.mp4 ... 保证ffmpeg拼接顺序正确
outDir := "./sorted"
os.MkdirAll(outDir, 0755)
for i, f := range files {
ext := strings.ToLower(filepath.Ext(f))
newName := fmt.Sprintf("%03d%s", i+1, ext)
// 你用io.Copy拷贝也行,直接rename也行,但小心跨盘符
os.Rename(f, filepath.Join(outDir, newName))
}
fmt.Printf("整理完成,共 %d 个文件\n", len(files))
}
func getModTime(path string) (time.Time, error) {
// 这里有个细节:如果你用手机传输,modTime可能变成传输时间
// 更可靠的做法是从文件名解析时间戳,但这里先简化为modTime
info, err := os.Stat(path)
if err != nil {
return time.Time{}, err
}
return info.ModTime(), nil
}
你看,代码里我都写了注释——“modTime可能不准”,这就是现实,我当年做这个的时候,犯的第一个错误就是信任文件修改时间,结果发现,用微信传视频到电脑上,所有文件的modTime都变成了“,最后我只能靠文件名里带的日期(比如VID_20250213)去硬解析,所以真正健壮的代码应该先尝试解析文件名,解析失败再回退到modTime,但你猜怎么着?Go标准库的time.Parse足够用了,但你需要写至少三种日期格式模板,因为手机厂商的命名习惯不一样。
第二步:ffmpeg不是万能的,但配合Go就是yyds
整理好顺序后,理论上一条命令就能拼视频:
ffmpeg -f concat -safe 0 -i list.txt -c copy output.mp4
但现实是,365个视频,可能一部分是60fps,一部分是30fps,码率分辨率全不一样,直接concat会得到一部“扭曲”的片子,所以你需要先用ffmpeg统一转码成中间格式,
ffmpeg -i 001.mp4 -vf "scale=1920:1080:force_original_aspect_ratio=decrease,pad=1920:1080:(ow-iw)/2:(oh-ih)/2" -r 30 -c:v libx264 -preset veryfast -crf 23 -an temp_001.mp4
要写365条这种命令?疯了,用Go的os/exec包去循环调用ffmpeg,顺便处理并发(比如同时跑4个转码任务,因为你的CPU有8个核),还有更聪明的办法:不用转码所有视频,只用ffmpeg的filter_complex配合select和trim,在合成时直接抽取每段的前3秒,但说实话,那命令长度能绕地球一圈。我的建议是分两步走:先用Go统一点压缩,再concat,别嫌浪费磁盘空间,中间文件生成个10GB很正常,你事先要检查df -h。
第三步:那段“求婚文案”怎么做进视频里?
你可能想给每个片段叠加日期字幕,第53天”,或者最后来一句“嫁给我好吗”,在Go里,你不需要直接操作像素,而是用Go生成字幕文件,比如subtitles.srt,然后让ffmpeg去烧录。
for i := 1; i <= 365; i++ {
fmt.Fprintf(srtFile, "%d\n", i)
// 计算这一天的实际日期
day := startDate.AddDate(0, 0, i-1)
// SRT时间格式:HH:MM:SS,mmm --> HH:MM:SS,mmm
// 每段占3秒,偏移量是 (i-1)*3 秒
startSec := (i-1) * 3
endSec := i * 3
fmt.Fprintf(srtFile, "%02d:%02d:%02d,000 --> %02d:%02d:%02d,000\n",
startSec/3600, (startSec%3600)/60, startSec%60,
endSec/3600, (endSec%3600)/60, endSec%60)
fmt.Fprintf(srtFile, "Day %d - %s\n\n", i, day.Format("Jan 2"))
}
你看,困难的是你需要的包越来越多——时间处理、并发控制、文件系统操作,但Go的强项恰恰是让你用比较少的代码把这些串起来,而且编译出来的二进制是静态链接的,丢到女朋友的MacBook上,连Go环境都不用装,双击就能跑。
彩蛋:别只做视频——用Go写个“回忆API”
既然你都用Go写了整理脚本,那顺手做个更酷的——把每天那个3秒的视频缩略图,配合当时的天气数据、你的心跳(智能手表导出)或者当天的微博关键词,生成一份“时间胶囊HTML”,这其实用Go的html/template就能做,最后输出一个自包含的.html文件,里面有365张小卡片,鼠标悬停就播放那3秒的GIF,求婚那天,你先把视频播完,然后打开这个页面说:“你看,这是我们这一年的每一秒。”她绝对会愣住,然后哭得比看视频还惨。
Go不是唯一选择,Python加OpenCV也能干,但Go的好处是你不用担心依赖冲突——我在写脚本的时候,最烦的就是Python的虚拟环境又坏了,而且Go的goroutine在处理文件转码这种IO密集任务时,逻辑上非常清爽,跑完365个文件的转码,CPU利用率能稳定在70%多,风扇狂转,但进程一点不卡。
关于速度和忠告
我得提醒你:不要想着当天求婚当天做,整个流程,从收集素材到清理坏帧(某天你喝多了拍模糊了,得去掉),到生成字幕,到转码,到最终渲染,保守估计你花一个周末的下午+晚上,我用Go写这个工具,不是因为炫技,是因为它让我能清晰地管理每一步的错误,比如某个文件解码失败,Go程序直接panic然后告诉你文件名,你就知道那个文件其实只有0.5秒,得删掉或者补录。
别忘了备份原始素材,我用的压缩参数是crf 23,虽然视觉无损,但毕竟是二次编码,万一你求婚成功后又想要4K原版呢?所以我的脚本里最后会生成一份sha256sums.txt,用来校验你的原始文件是否有改动。
临门一脚的浪漫
代码写完,视频渲染完,你大概会有一种“追了365天,终于要交卷”的踏实感,但浪漫和代码不一样——代码逻辑错了会报错,恋爱里的小失误可能反而成了回忆,比如我处理到第365天时,发现文件名是mp4,因为我某天多录了三次,那一刻我没改代码,反而哈哈大笑,因为这恰好证明了我那天的激动和紧张。
最后那3秒,别加什么滤镜,别加什么过场动画,就放你早上起床脸都没洗,对着镜头傻笑的样子,然后字幕打上:“第365天,想问你要一个答案。”
这时候,你的Go程序已经安静地退出了,剩下的,交给戒指和你颤抖的手。
