说实话,我一开始也没想到会用Go语言来搞一周年纪念视频,你想啊,纪念视频这种东西,通常不都是用剪映、Premiere,或者至少是Python调一下OpenCV吗?但我偏不,我就是那种喜欢折腾的人,而且这一年我写的最多的语言就是Go,从命令行工具到HTTP服务,从并发爬虫到中间件开发,几乎每天都在跟它打交道,所以当365天纪念日快到了的时候,我脑子里蹦出一个念头:能不能用Go来生成一个“一周年纪念视频”?
答案是:能,而且还挺好玩。
为什么选Go?性能、并发、还有那种“硬核浪漫”
先说说我为什么非要用Go,很多人会觉得,视频处理这种活儿,应该交给C++或者Python,但Go这几年在多媒体处理方面其实已经有不少积累,更重要的是,Go的并发模型实在太适合做这种“逐帧处理+批量合成”的任务了。
你想啊,一周年纪念视频,365天,每天至少一张照片或者一段小视频片段,如果按顺序一张一张地处理,那得等到猴年马月?但Go的goroutine一开,几十个协程同时干活,CPU直接拉满,速度嗖嗖的,而且Go的标准库就自带image、image/color、image/draw这些包,虽然功能比较基础,但做一些简单的图片合成、文字叠加、渐变效果,完全够用。
更重要的是——用Go写纪念视频这件事本身,就是一种纪念,因为这一整年,我写的每一行Go代码,都在为这个视频做准备,它不只是视频,它是我这一年的编程日志。
技术选型:FFmpeg + Go,完美搭档
纯用Go标准库做视频编码是不现实的,Go的image包只能处理静态图片,没法直接生成视频流,所以我的方案是:
- Go负责所有图片/视频的预处理、合成、文字叠加
- FFmpeg负责把图片序列帧压成视频,或者做最后的拼接
这个分工非常清晰,FFmpeg是命令行的视频瑞士军刀,Go是控制中枢,我只需要在Go里把每一帧的图片生成好,然后用os/exec调用FFmpeg命令,几秒钟就能出一段高清视频。
365天,我是怎么把数据整理成“帧”的
做纪念视频的第一步,是整理素材,365天,每天我都拍了一张照片——不是那种刻意摆拍的,就是日常的随手拍,早上起床的咖啡、地铁上的夕阳、深夜写代码的屏幕、周末遛弯遇见的猫……每一张照片背后都是一天。
我把这些照片按日期命名:2023-10-01.jpg,2023-10-02.jpg,一直到2024-09-30.jpg,然后在Go里写一个扫描目录的函数:
func loadImages(dir string) ([]string, error) {
files, err := ioutil.ReadDir(dir)
if err != nil {
return nil, err
}
var paths []string
for _, f := range files {
if strings.HasSuffix(f.Name(), ".jpg") {
paths = append(paths, filepath.Join(dir, f.Name()))
}
}
sort.Strings(paths)
return paths, nil
}
注意这里排序很重要——照片必须按时间顺序排列,不然视频放出来就是乱序的,纪念的意义就没了。
每一帧都加一点“生活气息”
光是把照片串成视频太无聊了,我想在每一帧上叠加一些信息:日期、星期几、当天的关键词、甚至当天的天气和步数(如果你有可穿戴设备的数据),这就需要用到Go的image和golang.org/x/image/font包。
我写了一个函数,接受一张图片和当天的元数据,返回一张新的带文字的图片:
func addTextToImage(img image.Image, text string) image.Image {
bounds := img.Bounds()
newImg := image.NewRGBA(bounds)
draw.Draw(newImg, bounds, img, bounds.Min, draw.Src)
// 加载字体
fontData, _ := ioutil.ReadFile("fonts/NotoSansSC-Regular.ttf")
font, _ := freetype.ParseFont(fontData)
c := freetype.NewContext()
c.SetDPI(72)
c.SetFont(font)
c.SetFontSize(24)
c.SetClip(bounds)
c.SetDst(newImg)
c.SetSrc(image.NewUniform(color.RGBA{255, 255, 255, 255}))
pt := freetype.Pt(10, 30)
c.DrawString(text, pt)
return newImg
}
字体文件需要自己准备,我用的Noto Sans SC,谷歌的开源字体,直接在GitHub上搜一下就能下载。
并发处理365张图片:goroutine的威力
单线程一张张处理365张图片,每张图片叠加文字+保存,大概需要2-3秒一张,总共得等十几分钟,这太慢了,我改成用worker pool模式,开10个goroutine并发处理:
type job struct {
index int
path string
text string
}
func worker(id int, jobs <-chan job, results chan<- string) {
for j := range jobs {
img, _ := loadImage(j.path)
newImg := addTextToImage(img, j.text)
outPath := fmt.Sprintf("frames/frame_%04d.png", j.index)
saveImage(newImg, outPath)
results <- outPath
}
}
这样10个worker同时干活,处理速度直接提升了10倍,不到两分钟全部搞定,看着终端里刷刷刷的输出,那种感觉真的很爽——就像看着365天在眼前快速翻过。
用FFmpeg把帧合成视频
所有帧都生成好之后,最后一步就是合成视频,在Go里调用FFmpeg非常简单:
func makeVideo() {
cmd := exec.Command("ffmpeg",
"-framerate", "24",
"-i", "frames/frame_%04d.png",
"-c:v", "libx264",
"-pix_fmt", "yuv420p",
"anniversary_video_365.mp4")
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Run()
}
这里我用的帧率是24fps,365张图片就是大概15秒的视频,如果你想慢一点,可以降到12fps或者8fps,这样每张照片停留的时间更长,更有回忆的味道。
不只是视频,还内置了一个“时间轴”功能
做这个视频的过程中,我顺便写了一个小工具,可以把照片的元数据导出一个HTML时间轴,用Go内置的html/template包,生成一个带时间线的网页,每一张照片旁边配一段文字,这样你不仅有了视频,还有一个可以直接点开看的“365天日记”。

这个功能是我后来临时加的,但没想到女朋友最喜欢的就是这个——“你看,2023年11月15号那天,你说项目上线压力大,晚上吃了三碗泡面,哈哈哈哈。” 你看,生活就是这样,你以为忘了的东西,照片和代码都替你记着。
关于素材和版权的一点提醒
做视频用的照片、音乐、字体,一定要确认版权,我用的照片都是自己拍的,音乐是从Free Music Archive上下载的CC0协议的音乐,字体是Google Fonts的开源字体,不要随便从网上扒素材,尤其是商用的话,被找上门就不好玩了。
FFmpeg和Go本身都是开源工具,完全没问题。
性能不够?那就优化
做完第一版之后,我遇到了一个痛点:生成出来的视频太大了,365张照片,每张都是3000x4000的分辨率,合成后视频文件接近2GB,这分享起来太麻烦了。
于是我加了一个压缩步骤:
func resizeImage(img image.Image, maxWidth int) image.Image {
bounds := img.Bounds()
w := bounds.Dx()
h := bounds.Dy()
if w <= maxWidth {
return img
}
ratio := float64(maxWidth) / float64(w)
newW := int(float64(w) * ratio)
newH := int(float64(h) * ratio)
newImg := resize.Resize(uint(newW), uint(newH), img, resize.Lanczos3)
return newImg
}
用的是github.com/nfnt/resize这个库,Lanczos3插值算法,画质损失很小,但文件体积直接降到了200MB以内。
多段视频拼接:第三部分我加了“生活BGM”
我把视频分成了三段:
| 段落 | 时长 | 配乐 | |
|---|---|---|---|
| 第一章 | 365天照片快闪 | 15秒 | 轻快钢琴曲 |
| 第二章 | 精选的12段短视频 | 60秒 | 温暖吉他 |
| 第三章 | 文字卡+最后的彩蛋 | 10秒 | 安静收尾 |
每一段都是单独用Go+FFmpeg生成的,最后再用FFmpeg拼接:
func concatVideos(videos []string, output string) {
listFile := "videos.txt"
var content strings.Builder
for _, v := range videos {
content.WriteString(fmt.Sprintf("file '%s'\n", v))
}
ioutil.WriteFile(listFile, []byte(content.String()), 0644)
cmd := exec.Command("ffmpeg", "-f", "concat", "-safe", "0",
"-i", listFile, "-c", "copy", output)
cmd.Run()
}
这里的彩蛋我放了一段用Go代码生成的动态文字——“365天,谢谢你一直都在”,那段文字是用image/gif包写的,逐帧闪烁,配合FFmpeg转成视频,虽然技术上不算复杂,但看到它出现在视频最后的时候,我承认我有点眼眶发热。
一些踩坑记录,帮你少走弯路
- 字体文件路径一定要写对,我一开始用相对路径,跑起来总报错,后来改成绝对路径才搞定。
- 颜色空间要小心,用
image.NewRGBA创建新图的时候,如果与原图颜色空间不一致,合成出来颜色会偏,最好统一用color.RGBAModel。 - FFmpeg的参数顺序不能乱,比如
-i必须在-c:v之前,否则报错,建议直接复制我上面的命令别自己改。 - 帧命名必须补零。
frame_%04d.png这种格式,FFmpeg才能正确识别序列,如果文件名有跳跃,它会漏帧或者报错。 - 内存管理,365张全尺寸图片同时加载,内存占用会飙升,我改成逐张加载、处理、释放,配合
sync.Pool复用临时对象,内存稳定在300MB以内。
如果真的想做一个给自己用
你可能会说,我只是想做一个纪念视频,有必要这么折腾吗?确实,如果你只是想快速出一个视频,用剪映拖拽几下就搞定了,但用Go写这个过程,给我带来的不仅仅是最终那个视频文件。
- 我学会了
image包的底层操作 - 我摸清了FFmpeg的命令行参数
- 我用goroutine实现了一个完整的流水线
- 我把一年的数据整理成了可搜索、可重放的时间轴
而且最神奇的是,这个视频我可以每年都跑一次,只需要换一下照片目录和日期范围,就能生成新的,现在已经成了一个自动化纪念视频生成器。
我喜欢这种感觉——用代码记录生活,用技术表达感情,Go不是最酷的语言,但它快、稳、简单,就像一年365天,每一天都平平无奇,但串起来就是一部属于你自己的电影。
好了,不多说了,视频已经导出,我准备去给女朋友看了,希望到时候她还记得2023年10月15号那天的红烧牛肉面有多难吃。
本文来自作者[kyadmin]投稿,不代表be365立场,如若转载,请注明出处:http://www.hljbesthome.com/tiyu/1434.html
评论列表(4条)
我是be365的签约作者“kyadmin”!
希望本篇文章《用Golang写一周年纪念视频?365天的代码与回忆,我都给你打包好了》能对你有所帮助!
本站[be365]内容主要涵盖:be365,be365网站,be365网址,ball365体育,Be365排名
本文概览:说实话,我一开始也没想到会用Go语言来搞一周年纪念视频,你想啊,纪念视频这种东西,通常不都是用剪映、Premiere,或者至少是Pyth...