这事儿得从去年说起,我家小团子刚出生那会儿,我和他妈就商量着,要给她做一份不一样的周岁礼物,那种影楼里拍的流水线相册,总觉得少了点温度,后来刷小红书,看到有人做“365天一周岁视频”——就是每天录一小段,攒一整年,最后剪成一个几分钟的短片,当时我就想,这事儿我能干。
结果真干起来才发现,每天拍10秒不难,难的是怎么把这365段素材折腾到一起,我用手机剪吧,手都怼酸了;用Premiere吧,电脑卡成PPT,最要命的是,我翻遍全网,居然没找到能批量处理这种跨年视频的趁手工具。
然后我决定——既然我是写Go的,干脆自己写一个。
为什么是Go,不是Python?
你可能觉得奇怪,视频处理不都是Python的天下吗?OpenCV、MoviePy,哪个不比Go方便?没错,但问题在于:我要处理的是几百个视频文件,不是几十个,Python那个GIL锁,一跑多线程CPU直接拉胯,而且小团子奶奶家用的老电脑,装个Python环境都费劲,Go编译出来就是个exe,双击就能跑,对非程序员也友好。
我还有个私心——用Go写视频处理,能让程序跑得更“野”,比如我想在视频里插入随机音效:每天小团子笑出声时加个“叮”的提示音;哭的时候加个小喇叭,Go的goroutine处理这种异步操作,比Python的队列流畅得多。
先搭个骨架:用FFmpeg做苦力
别被“视频处理”吓着,Go本身不处理视频,它只是个“调度员”,真正的体力活全靠FFmpeg这个神器,你要做的,就是让Go把FFmpeg命令组装好,然后往命令行里扔。
我写的第一版代码长这样(简化版):
package main
import (
"fmt"
"os/exec"
"path/filepath"
)
func main() {
// 假设所有视频都在videos文件夹里
files, _ := filepath.Glob("videos/*.mp4")
// 生成一个临时文件列表
listFile := "filelist.txt"
f, _ := os.Create(listFile)
for _, v := range files {
fmt.Fprintf(f, "file '%s'\n", v)
}
f.Close()
// 调用FFmpeg合并
cmd := exec.Command("ffmpeg", "-f", "concat", "-safe", "0",
"-i", listFile, "-c", "copy", "final.mp4")
cmd.Run()
}
这段代码有个坑:它假设所有视频的编码、分辨率、帧率都完全一样,但现实是:老婆用iPhone拍的是HEVC,我用华为拍的是H.264,老妈用老安卓拍的分辨率才720p,直接合并,最后出来的视频要么花屏,要么音画不同步。
解决“格式乱炖”的麻烦
我后来加的自动转码逻辑,大概花了两个周末。
核心思路是:先把所有素材统一成同一个标准,比如统一成1080p、25帧、H.264编码,这个过程最耗时间,但必须做。
我把代码重构了一遍:
import (
"fmt"
"os/exec"
"path/filepath"
)
func standardizeVideo(input string, output string) error {
// 强制统一参数
args := []string{
"-i", input,
"-vf", "scale=-2:1080", // 保持宽高比,高度固定1080
"-r", "25", // 25帧每秒
"-c:v", "libx264",
"-preset", "fast", // 用快速预设,保证家里的老电脑也能跑
"-c:a", "aac",
"-y", // 覆盖输出
output,
}
cmd := exec.Command("ffmpeg", args...)
return cmd.Run()
}
这里有个小技巧:用 -2 作为宽度的参数,FFmpeg会自动计算偶数宽度的缩放值,避免因为奇数宽度导致编码报错,这个坑我摔过一次。
最难搞的是时间线:365天怎么排?
所有素材统一格式后,最大的问题来了:怎么把每天的视频按时间顺序正确排列?
我一拍脑袋:用文件名!让老婆每天拍完视频后重命名,格式是 20250201_宝宝翻身.mp4,然后我用Go读出文件名中的日期,按时间排序。
但后来发现,光按日期还不行。有时候同一天有3段视频:早上宝宝学爬,下午打疫苗哭,晚上洗澡玩水,如果简单按文件名排序,早上的可能排到晚上后面。
我加了个更精细的排序函数:
import (
"sort"
"strings"
"time"
)
type Clip struct {
Path string
Time time.Time
Sequence int // 同一天的序号
}
func sortClips(clips []Clip) {
sort.Slice(clips, func(i, j int) bool {
if clips[i].Time.Equal(clips[j].Time) {
return clips[i].Sequence < clips[j].Sequence
}
return clips[i].Time.Before(clips[j].Time)
})
}
然后在文件名里加时间戳:20250201_083000_宝宝翻身.mp4,这样连小时和分钟都有了,排序精确到秒。
配音和BGM:给视频加点生活气息
光有画面多干巴,我想在365天视频里,随机穿插一些小团子的原声:她第一次叫“爸爸”那段(其实叫的是“哒哒”),还有她打嗝的声音。
我方法是把所有音频文件放一个文件夹,程序随机挑5段插入到视频的时间轴里,但有个问题:音频长度和视频片段不匹配,比如视频片段只有3秒,但笑声有5秒,会盖掉下一段视频的声音。

我后来用了一个笨办法:把音频裁剪成与视频片段等长,代码里用FFmpeg的 -t 参数截取音频的前n秒:
func overlayRandomAudio(videoPath string, audioPath string, outputPath string) {
// 先获取视频时长
duration := getVideoDuration(videoPath)
// 裁剪音频到相同长度
args := []string{
"-i", videoPath,
"-i", audioPath,
"-filter_complex",
fmt.Sprintf("[0:a]volume=0.8[a1];[1:a]volume=1.2[a2];[a1][a2]amix=inputs=2:duration=first"),
"-c:v", "copy",
"-y",
outputPath,
}
cmd := exec.Command("ffmpeg", args...)
cmd.Run()
}
这样每次插入的音频都会自动对齐,不会出现说话说到一半被切断的情况。
日期水印:别让视频失去时间感
365天视频最浪漫的地方,就是能通过画面感受到时光流逝,所以我决定在每段视频右上角加上当天的日期。
Go里加文字水印有两种方式:
- 用FFmpeg的drawtext滤镜 —— 需要系统装字体库,兼容性一般
- 用Go自己生成带日期的PNG图片,然后叠加到视频 —— 更折腾,但可控性强
我选了第一种,因为简单,但有个坑:中文日期需要中文字体,我在程序里内置了一个开源字体文件(这里的文件名我记不清了,好像是 “NotoSansSC-Regular.otf”),然后通过 fontfile 参数指定路径:
func addDateWatermark(input string, date string, output string) {
args := []string{
"-i", input,
"-vf", fmt.Sprintf(
"drawtext=text='%s':x=w-text_w-20:y=20:fontsize=36:fontcolor=white:fontfile='./NotoSansSC-Regular.otf'",
date,
),
"-c:a", "copy",
"-y",
output,
}
cmd := exec.Command("ffmpeg", args...)
cmd.Run()
}
这个水印加完,整个视频瞬间就有了“纪录片”的感觉。
性能优化:别让老电脑死机
小团子奶奶家那台电脑是6年前的联想笔记本,跑FFmpeg多任务直接卡死,我用了两个优化:
- 限制并行数 —— 用带缓冲的channel控制同时跑的任务不超过2个
sem := make(chan struct{}, 2) // 最多2个并发
for _, clip := range clips {
sem <- struct{}{} // 获取信号量
go func(c Clip) {
defer func() { <-sem }() // 释放
convertClip(c)
}(clip)
}
// 等待所有完成
for i := 0; i < cap(sem); i++ {
sem <- struct{}{}
}
- 用更快的编码预设 —— 预设从
medium改成veryfast,虽然文件大10%,但处理速度快了一倍
最终成品:12月31日那晚
小团子周岁生日前一晚,我跑完了最后一次脚本,程序花了3个多小时,把365个视频合成一个4分50秒的短片。
打开播放的时候,第一段是她在医院刚出生,皱巴巴的像个小老头;接着是她第一次笑、第一次翻身、第一次发烧、第一次学坐、第一次叫妈妈......看着看着,我眼睛就湿了。
老婆在旁边说:“你这个程序员老爸,终于干了件人事。”
视频最后10秒,我用Go程序自动生成了一段字幕:
“365天,24帧每秒,8,760,000帧画面。每一帧都是你长大的证据。”
一些实用建议
如果你想动手做,这几个点能省很多坑:
- 每天拍同机位 —— 固定一个角落拍10秒,方便后期做“时间流逝”效果
- 文件名格式统一 —— 最好用
YYYYMMDD_HHMMSS_描述.mp4,不然排序很头疼 - 音频素材单独存 —— 孩子的声音太珍贵,我后悔好多笑声没单独录
- 先做小样测试 —— 别一上来就全量跑,先用10段视频把流程调通
我代码开源在Github上了,仓库名是 baby-365-days,需要的可以去里搜,不过先提醒你:代码写得比较糙,注释都是中文夹杂拼音,变量名还有叫 temp1103 这种凑合用的——毕竟那是我半夜两点抱着睡着的娃写的。
本文来自作者[kyadmin]投稿,不代表be365立场,如若转载,请注明出处:http://www.uss1.cn/fnagchan/716.html
评论列表(4条)
我是be365的签约作者“kyadmin”!
希望本篇文章《用Go语言给娃剪365天周岁视频,我把自己逼成了程序员老爸》能对你有所帮助!
本站[be365]内容主要涵盖:be365,be365网站,be365网址,ball365体育,Be365排名
本文概览:这事儿得从去年说起,我家小团子刚出生那会儿,我和他妈就商量着,要给她做一份不一样的周岁礼物,那种影楼里拍的流水线相册,总觉得少了点温度,...