一年前的今天,你们在一起了,或者,你开始了某个项目,又或者,你养了一只猫,不管是什么,一周年是个大日子,我想做个纪念视频,时长正好365秒——一秒代表一天,一秒一个画面,听起来浪漫是吧?但我是程序员,我不会用Premiere,我只会写Go,那怎么办?用Go语言硬撸一个视频生成器。
你可能觉得,写视频?那不是设计师的活儿吗?别急,Go语言的标准库加上几个第三方包,能干的事情比你想象的多,365秒的视频,换成代码来生成,反而更可控,更精确,还能自动化,今天我就手把手带你走一遍,用Go写一个“一周年纪念视频365秒”的完整流程,中间我会说说坑,也会聊聊取舍,反正就是边想边写,别指望我一开始就完美。
为什么是365秒?为什么是Go?
先解释一下这个数字,365秒,正好6分5秒,一秒代表一天,一年刚好,这个视频的逻辑很简单:每一帧是一个日子,365帧连起来就是一年,你可以把照片、文字、或者小动画按天排列,比如第1天是初见,第100天是第一次旅行,第365天是今天,这种线性叙事,用代码生成比手工剪辑方便一百倍。
那为什么用Go?因为Go处理并发、处理文件、处理二进制数据都很顺手,Go编译出来是个单文件,扔到服务器或者本地都能跑,不需要装一堆依赖,我试过用Python做类似的事情,但OpenCV的安装包动不动就几百兆,Go这边一个go get就搞定,Go的image库是标准库自带的,画图、拼接、生成帧都很直接。
但说实话,Go不是做视频的主流语言。 主流是FFmpeg、Python、或者JavaScript,Go可以调用FFmpeg的底层接口,或者直接操作帧数据,我们今天的方案是:用Go生成每一帧的图片,然后用FFmpeg把它们合成视频,Go负责图片生成,FFmpeg负责编码,分工明确,各干各的。
第一步:设计视频的数据结构
先别急着写代码,你得想清楚,365秒的视频,每秒钟放什么内容?我打算这样设计:
- 每一秒一帧,帧率就是1fps(没错,一秒一帧,因为一秒代表一天)。
- 每帧是一张图片,尺寸我选1920x1080,主流视频尺寸。
- :背景图(或者纯色)、日期文字、一张小照片(如果有)、一句短文案。
数据结构用Go的struct来表示,大概长这样:
type DayFrame struct {
Day int // 第几天,从1到365
Date string // 日期,quot;2023-03-15"
PhotoPath string // 这一天的照片路径,如果没照片就留空
Caption string // 一句话文案,quot;第一次一起做饭"
BGColor color.RGBA // 背景色,可以按色调渐变
}
然后整个视频就是一个[]DayFrame切片,长度365,你需要提前准备好365天的数据,你可以手动填,也可以用程序从某个记录里读(比如微博、朋友圈、或者自己写的日记API)。
这里有个坑:不是每天都有照片,一周年视频,如果365天每天都有照片,那得是多充实的一年?大多数人可能只有几十张,所以我们的代码要能处理“缺照片”的情况——没照片就只显示文字和颜色背景,甚至可以用渐变或者动画曲线代替。
第二步:生成单帧图片(核心逻辑)
Go标准库的image包能画图。image.NewRGBA创建一张空白位图,然后用draw包往上面画文字、贴图片,我试过,画文字有点麻烦,因为Go标准库不支持直接渲染字体(它只支持位图字体),所以我们需要第三方库:golang.org/x/image/font 和 golang.org/x/image/font/basicfont。
但basicfont只有一种等宽字体,很丑,想好看,得用TrueType字体,那就需要golang.org/x/image/font/opentype或者github.com/golang/freetype,我推荐用freetype,加载一个.ttf文件,设置字号,然后画文字。
画文字的流程大概是这样的:
- 加载字体文件(比如你喜欢的黑体或者手写体)。
- 创建一个
freetype.Context,设置字体、大小、DPI。 - 在图片上指定位置绘制文字字符串。
写一段伪代码:
func drawText(img *image.RGBA, text string, x, y int, fontFace font.Face, col color.Color) {
point := freetype.Pt(x, y)
ctx := freetype.NewContext()
ctx.SetDst(img)
ctx.SetSrc(image.NewUniform(col))
ctx.SetFont(fontFace)
ctx.SetFontSize(24)
ctx.DrawString(text, point)
}
但别忘了,画中文需要字体文件支持中文。 系统自带的字体比如msyh.ttc(微软雅黑)或者NotoSansSC-Regular.otf都可以,你需要把字体文件放在项目目录里,或者用Go的embed包嵌进去。
贴照片:用image.Decode打开照片文件,然后缩放到指定区域,再用draw.Draw覆盖到帧图片上,缩放推荐用golang.org/x/image/draw包,它能做高质量的双线性缩放。
每帧生成后,保存成JPEG或者PNG,但我建议保存成PNG,因为JPEG有损压缩,多帧累积后画质下降明显,虽然体积大一点,但视频质量更重要。
第三步:批量生成所有帧(并发处理)
365秒的视频,就是365张图片,单张生成可能需要几十毫秒,串行下来也就十几秒,但如果你想要效率,可以用Go的goroutine并发生成。
设计一个worker池:开10个goroutine,每个从chan DayFrame里取一个帧数据,生成图片,写入到文件夹frames/下,文件命名比如frame_0001.png,这样所有帧生成完,耗时可能只有一两秒(取决于图片大小和字体渲染)。
但这里有个陷阱:图片写入磁盘是IO操作,并发写入同一个文件夹可能会造成竞争。 解决办法是每个goroutine写入不同文件,或者用互斥锁保护写入,最简单是每个goroutine单独创建文件,文件名带goroutine ID,最后再排序,但更简单的是串行写入——反正365张图片,串行也就几秒,不值得为并发引入复杂性。
我实际写的时候,选择串行,因为我想让每帧的顺序完全可控,不想在并发时出现“第100帧比第99帧先生成完”的情况,那会让最后排序变得麻烦,你可以说我不够“Go style”,但视频生成这种线性任务,串行更清晰。
串行生成的Go代码长这样:
for i, frame := range frames {
img := generateFrame(frame)
filename := fmt.Sprintf("frames/frame_%04d.png", i+1)
f, _ := os.Create(filename)
png.Encode(f, img)
f.Close()
fmt.Printf("Generated frame %d/365\n", i+1)
}
看到没?我连错误处理都省了(示例代码别学我,实际项目要加),跑完之后,frames/目录下就有365个PNG文件。
第四步:用FFmpeg合成视频
Go图片都生成好了,最后一步:合成视频,这部分Go做不了,得靠FFmpeg,但你可以在Go里用os/exec调用FFmpeg命令。
合成命令:
ffmpeg -framerate 1 -i frames/frame_%04d.png -c:v libx264 -pix_fmt yuv420p -r 30 -vf "setpts=30*PTS" output.mp4
解释一下:
-framerate 1:输入帧率是1fps(一秒一帧)。-i frames/frame_%04d.png:输入文件名模式。-c:v libx264:用H.264编码。-pix_fmt yuv420p:像素格式,兼容性最好。-r 30:输出视频帧率设为30fps。-vf "setpts=30*PTS":让每一帧驻留30个输出帧,从而实现1秒的时长。
这样生成的视频,第一帧显示1秒,第二帧显示1秒……一共365秒,但注意,FFmpeg的输入帧率是1,输出帧率是30,所以每个输入帧被复制30次,导致输出视频实际帧数 = 365 * 30 = 10950帧,时长 = 10950 / 30 = 365秒,没错,就是你要的365秒。
如果你想要带声音,比如背景音乐,再加一个音频流:
ffmpeg -framerate 1 -i frames/frame_%04d.png -i music.mp3 -c:v libx264 -pix_fmt yuv420p -r 30 -shortest -vf "setpts=30*PTS" output_with_audio.mp4
音乐长度最好也是365秒左右,或者你能循环。-shortest参数保证视频和音频中较短的结束。
第五步:处理边界情况(实战中的真实感)
写代码的时候,肯定会遇到一堆破事,我列几个我遇到的:
字体文件找不到
你代码里写死了字体路径,但换台机器就跑不起来了,解决方案:用go:embed把字体文件嵌入到二进制里。
//go:embed fonts/NotoSansSC-Regular.otf var fontData []byte
然后从fontData里解析字体,而不是从文件系统加载,这样编译出来的单文件自带字体,直接拷贝到任何机器都能跑。
照片尺寸不一致
有的照片是竖屏,有的是横屏,有的还带EXIF旋转信息,你需要统一处理:先读EXIF(用github.com/rwcarlsen/goexif/exif包),然后旋转成正向,再缩放到固定区域,注意保持宽高比,多余部分裁剪或者加黑边,我选择加黑边,因为裁剪可能丢掉重要内容。
文字超长
文案如果太长,超出画面边界就丑了,你得根据文字长度动态调整字号,或者换行,简单做法:限制文案最多20个字符,超了就截断加省略号,或者写一个自动换行函数,在指定宽度内分割成多行。
Go里没有现成的自动换行函数,你得自己算。font.MeasureString可以测量字符串宽度,然后暴力切分。
性能问题
生成365张1920x1080的PNG,每张可能3-5MB,总共1-2GB,磁盘空间没问题,但生成时间可能比想象的长,我实测,单张带一张照片和文字渲染,大概200ms,串行就是73秒,73秒生成一个视频,也能接受,但如果用并发,可以压到10秒以内。
你确定要并发吗?我开始也写了并发版本,但调试时发现,因为每帧数据不同,goroutine之间没有共享状态,确实快了,但输出文件顺序乱了,后来我加了文件名排序,才解决,最终我选择串行,就是图个稳妥。
额外的点子:让视频动起来
静态图片一秒一张,看久了会无聊,你可以加入一些微动效:比如文字渐入、照片缩放、背景颜色渐变,但这些需要每帧之间有关联,也就是说你不能独立地生成每一帧,而是需要前后帧的数据。
比如你想让背景颜色从红色渐变到蓝色,第1帧是红色,第365帧是蓝色,中间的帧需要按线性插值,Go实现很简单:
func lerpColor(c1, c2 color.RGBA, t float64) color.RGBA {
return color.RGBA{
R: uint8(float64(c1.R) + (float64(c2.R)-float64(c1.R))*t),
G: uint8(float64(c1.G) + (float64(c2.G)-float64(c1.G))*t),
B: uint8(float64(c1.B) + (float64(c2.B)-float64(c1.B))*t),
A: 255,
}
}
其中t = float64(day) / 365,这样所有帧的颜色就是连续的。
照片也能做缓慢拉近或者平移效果(Ken Burns effect),但需要你在帧生成时记录上一帧的裁剪区域,然后稍微移动,这个就复杂了,我不展开,但Go是可以做的。
最终的文件结构
你写完代码后,项目应该长这样:
my-anniversary-video/
├── main.go // 主程序
├── fonts/
│ └── NotoSansSC-Regular.otf // 嵌入字体
├── photos/
│ ├── day_001.jpg
│ ├── day_002.jpg
│ └── ... // 365天的照片,可能不全
├── frames/ // 生成帧图片的目录
│ ├── frame_0001.png
│ ├── frame_0002.png
│ └── ...
└── output.mp4 // 最终视频
编译运行:
go build -o anniversary ./anniversary ffmpeg -framerate 1 -i frames/frame_%04d.png -c:v libx264 -pix_fmt yuv420p -r 30 -vf "setpts=30*PTS" output.mp4
你也可以把FFmpeg调用直接写在Go程序里,一条命令搞定。
一些你可能想问的
Q: 我没照片怎么办?
那就纯文字+背景渐变色,文案可以是每一句情话、每一件小事、或者每天的天气,365句“早安”也能凑出来。
Q: 365秒太长了,能不能只选重要日子?
当然可以,365秒只是一个概念,你可以按天做,但只显示有事件的日子,其他日子用空白画面填充,最终视频还是365秒,但只有几十帧有内容。
Q: 视频质量怎么保证?
使用高码率-b:v 20M,或者直接无损编码-crf 0(但文件会非常大),建议用-crf 18,视觉无损,文件大小适中。
Q: 我不懂Go,能学吗?
本文默认你有一些Go基础,但如果你想做纪念视频,又不想学编程,那你其实可以用别的工具,本文更像是一篇“程序员怎么用代码表达浪漫”的案例,你如果啥都不会,那就学Go,反正学两天就能写这个项目了。
最后想说的(但不是总结)
写这个项目的时候,我一直在想,纪念视频到底纪念的是什么?是那些照片吗?是那些日期吗?不完全是,是那些被记住的瞬间,而代码生成的视频,不过是把这些瞬间用一种精确到你无法反驳的方式排列出来——每秒一张,不多不少。
我女朋友看了我生成的视频,她说觉得有点奇怪,因为有的帧她笑得很丑,有的帧根本没对上焦,但她说很喜欢那种“被程序记住了”的感觉,大概这就是代码的情书吧。

你在写的时候,也可能会发现,有些日子你想不起来发生了什么,那就空着,别怕,真实的纪念就是不完美的。
去写吧,Go语言的小程序,配上一段365秒的视频,今年的一周年,明年还能改改参数再用。
本文来自作者[kyadmin]投稿,不代表be365立场,如若转载,请注明出处:http://www.uss1.cn/nba/179.html
评论列表(4条)
我是be365的签约作者“kyadmin”!
希望本篇文章《用Go语言打造一周年纪念视频,365秒的浪漫,代码也能写情书》能对你有所帮助!
本站[be365]内容主要涵盖:be365,be365网站,be365网址,ball365体育,Be365排名
本文概览:一年前的今天,你们在一起了,或者,你开始了某个项目,又或者,你养了一只猫,不管是什么,一周年是个大日子,我想做个纪念视频,时长正好365...