当前位置:首页 > 健康 > 正文

用Go语言,我给自己造了一个第一个365天视频生成器

  • 健康
  • 2026-08-13 06:32:00
  • 15
摘要: 为什么我非要自己写代码,而不是用现成模板?说真的,年初的时候我刷到好多人在晒“第一个365天视频”——就是把每天拍的一秒钟拼起来...

为什么我非要自己写代码,而不是用现成模板?

说真的,年初的时候我刷到好多人在晒“第一个365天视频”——就是把每天拍的一秒钟拼起来,变成五分钟的时光缩影,我也想要啊,但那些在线工具要么要会员,要么水印糊一脸,有的还偷偷压缩画质,我一拍大腿:我可是写Go的,这活儿不该求人

然后我就掉坑里了,查了一圈,发现并没有一个现成的库是专门干“按日期排序、拼接视频、加转场、配BGM”这整套流程的,得,自己造轮子吧。

第一步:定需求,简单到不行,也难到不行

我想象中的流程是:

  1. 把365个小视频文件按日期命名放在一个文件夹里
  2. 程序读文件名,解析出时间戳
  3. 按时间顺序拼接
  4. 加上片头和片尾
  5. 输出一个完整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的避坑建议

  1. 别用os.Stat拿时间,信文件名
  2. 所有视频先统一-r 30,不然音画不同步
  3. 转场别贪多,365个片段全加转场你会等到地老天荒
  4. 写个deferrecover(),渲染到一半崩了至少能定位错误
  5. 记得留个临时文件清理函数

代码不复杂,复杂的是你愿意为了一年的碎片花上整个周末,但我跟你讲,值。

用Go语言,我给自己造了一个第一个365天视频生成器