用Go语言实现一年365天卡点视频,从零开始的自制心法
- 房产
- 2026-08-14 05:02:18
- 71
我最近迷上了那种365天卡点视频——每天一张照片,最后用0.5秒的切换速度拼成一年的人生缩略,说实话,看别人做的成品很爽,但自己想做的时候,网上搜到的工具要么收费,要么水印大得吓人,有一天我突然想:我好歹是个写Go的,为什么不自己写个生成器?
先说清楚:这活儿Go到底能不能干?
很多朋友一听“视频处理”就条件反射觉得得用Python或者专门的视频编辑软件,但Go在这件事上其实有它独特的优势——编译成单个二进制文件,扔到服务器上就能跑,而且处理大量文件时并发能力是真的强,我用的核心库是ffmpeg的Go绑定,本质上是调用系统里的FFmpeg命令,但用Go包了一层,写起来优雅得多。
我的环境是Windows开发机,部署到一台Linux小服务器上跑定时任务,如果你只在本机玩,完全够用。
第一步:准备素材文件名要规范,不然你会哭
你的365张照片不可能都叫IMG_20230101.jpg这种杂乱的名字,我踩过一个坑:手机导出的照片命名完全不规律,有的还带空格,Go里写个批量重命名的小工具很简单:
package main
import (
"fmt"
"os"
"path/filepath"
"strings"
)
func main() {
dir := "./photos"
files, _ := os.ReadDir(dir)
for i, f := range files {
oldName := filepath.Join(dir, f.Name())
ext := strings.ToLower(filepath.Ext(f.Name()))
newName := fmt.Sprintf("%s/%03d%s", dir, i+1, ext) // 001.jpg
os.Rename(oldName, newName)
}
fmt.Println("搞定了,共处理", len(files), "个文件")
}
这就是个热身,关键点:统一成三位数编号,因为卡点视频讲究有序切换,编号乱了你后面排序的时候会疯掉。
第二步:用Go调FFmpeg生成视频的两种思路
思路A:直接命令拼接(简单粗暴,适合新手)
我在Go里用os/exec包,直接调FFmpeg命令,典型的命令长这样:
cmd := exec.Command("ffmpeg",
"-framerate", "30", // 帧率
"-i", "photos/%03d.jpg", // 输入模式
"-vf", "scale=1080:1920,setsar=1", // 缩放成竖屏
"-c:v", "libx264", // 编码
"-pix_fmt", "yuv420p",
"output.mp4")
cmd.Run()
但这里有个致命问题:365张图,每张0.5秒,总时长也就3分钟,直接这么跑,生成的视频卡顿感很明显,因为FFmpeg默认会按输入帧率均匀分配时间,你需要的“卡点”效果,其实是每张图片展示0.5秒然后硬切,这对FFmpeg来说很简单。
思路B:用Go生成过滤图脚本(灵活,支持自定义节奏)
真正的卡点视频,往往不是均匀的0.5秒,比如前半年快、后半年慢,或者配合音乐节奏变化,这时候我建议用Go生成一个FFmpeg滤镜脚本,核心是先计算每张图的持续时间:
type Photo struct {
Path string
Duration float64 // 这张图停留几秒
}
func buildFilter(photos []Photo) string {
var parts []string
for i, p := range photos {
// 每张图作为一个输入流
parts = append(parts, fmt.Sprintf(
"[%d:v]scale=1080:1920,setpts=PTS-%d/TB[v%d]",
i, i, i))
}
// 然后用 concat 连接
return strings.Join(parts, ";") + "; concat=n=365:v=1:a=0[out]"
}
这个自己写起来很绕,但其实有个取巧的办法:让Go直接把每张图处理成等长的视频片段,再拼接,比如每张图生成一个0.5秒的mp4,然后合并,缺点是慢,但逻辑直白。
第三步:处理图片缩放和裁剪——这里有个大坑
手机照片大多3:4或者9:16,而卡点视频通常做9:16竖屏,如果用简单的scale=1080:1920,会拉伸变形,非常丑,正确做法是先裁剪再缩放:
// 先居中裁剪到 9:16,再缩放 "-vf", "scale=1080:1920:force_original_aspect_ratio=increase,crop=1080:1920"
这个参数的意思是:先放大让较短的边达到目标尺寸,然后居中裁剪掉多余部分,代价是边缘会丢一点,但照片的主题一般都在中心,问题不大。
如果你像我一样有点强迫症,还可以用smartblur配合轻微锐化:
"-vf", "scale=1080:1920:force_original_aspect_ratio=increase,crop=1080:1920,unsharp=5:5:0.8:3:3:0.4"
第四步:音频对齐——卡点视频的灵魂
真正让人“爽”的卡点,靠的是重音和画面切换同步,如果你没有现成的卡点音乐,可以用Go写个小程序分析音乐的节拍点(需要加载音频库,比如beep),但我嫌麻烦,直接手动在音乐时间轴上标注了每张图的切换时间,然后让Go按照这个时间表生成滤镜脚本。
| 音乐时间点 | 对应照片 | 停留时长 |
|---|---|---|
| 0s ~ 0.5s | jpg | 5s |
| 5s ~ 1.0s | jpg | 5s |
| 0s ~ 0.0s |
我写了一个简单的文本配置文件,Go程序读它,然后生成FFmpeg的concat脚本,这样改音乐或者改照片顺序都不用碰代码。
第五步:完整代码骨架
下面是我的实际项目结构(简略版):
func main() {
// 1. 读取照片列表
photos := loadPhotos("./photos") // 返回 sorted []string
// 2. 读取时间配置
times := loadTiming("./timing.txt") // 每张图的时间戳
// 3. 生成滤镜
filter := buildConcatFilter(photos, times)
// 4. 执行FFmpeg
output := "./final_365.mp4"
runFFmpeg(filter, output, times[len(times)-1].End)
fmt.Println("视频生成完毕!", output)
}
其中runFFmpeg里我用exec.Command,但加了-threads参数来利用多核CPU,实测365张1080x1920图片,3分钟视频,在四核CPU上大概要跑40秒左右。完全能接受。
第六步:给视频加上字幕和日期水印
这个其实很加分,我想在每张照片左下角显示“Day 1”、“Day 36”这样的角标,FFmpeg的drawtext滤镜能办到,但我当初写的时候有个bug:中文日期的字体路径在Windows和Linux上不一样,折腾了好久。
// Linux 下的字体示例 "-vf", "drawtext=fontfile=/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf:text='Day 1':fontsize=48:fontcolor=white:box=1:boxcolor=black@0.5:boxborderw=10:x=50:y=50"
如果你不想这么麻烦,直接用Go生成PNG水印图再叠加,也行,但我懒,所以用drawtext硬刚。
第七步:常见问题排查(亲身踩过)
- 图片文件名里有空格 —— 记得
filepath.Base处理 - 视频画面卡顿 —— 检查是不是所有图片分辨率一样?混合分辨率会导致concat失败
- 输出文件巨大 —— 365张1080p图片,静态画面多,编码后也就几十MB,如果你用默认设置还很大,建议加
-crf 23 - 在服务器上跑乱码 —— 终端编码问题,设置
os.Setenv("LANG", "en_US.UTF-8")往往能救急
最后的操作建议
我自己现在每周都用这套Go工具跑一次上个月的月度卡点视频,然后攒到年底合成365天版,你可以先把流程跑通,再慢慢优化滤镜质量。
对了,如果在处理过程中你发现ffmpeg命令行参数记不住,有个技巧:先用纯命令行手动调通一组参数,再写进Go代码里固化下来,别直接逼自己一次写对。
写这个工具的过程,比看任何教程都让我理解FFmpeg的滤镜体系,当你看到一年前的照片在0.5秒的切换里飞过,那种感觉啊,跟买了个模板套出来的完全不一样。
