用Go语言打造一周年纪念视频,365秒的浪漫,代码也能写情书

一年前的今天,你们在一起了,或者,你开始了某个项目,又或者,你养了一只猫,不管是什么,一周年是个大日子,我想做个纪念视频,时长正好365...

一年前的今天,你们在一起了,或者,你开始了某个项目,又或者,你养了一只猫,不管是什么,一周年是个大日子,我想做个纪念视频,时长正好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/fontgolang.org/x/image/font/basicfont

但basicfont只有一种等宽字体,很丑,想好看,得用TrueType字体,那就需要golang.org/x/image/font/opentype或者github.com/golang/freetype,我推荐用freetype,加载一个.ttf文件,设置字号,然后画文字。

画文字的流程大概是这样的:

  1. 加载字体文件(比如你喜欢的黑体或者手写体)。
  2. 创建一个freetype.Context,设置字体、大小、DPI。
  3. 在图片上指定位置绘制文字字符串。

写一段伪代码:

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秒的浪漫,代码也能写情书

你在写的时候,也可能会发现,有些日子你想不起来发生了什么,那就空着,别怕,真实的纪念就是不完美的。

去写吧,Go语言的小程序,配上一段365秒的视频,今年的一周年,明年还能改改参数再用。

本文来自作者[kyadmin]投稿,不代表be365立场,如若转载,请注明出处:http://www.uss1.cn/nba/179.html

(10)

文章推荐

发表回复

本站作者才能评论

评论列表(4条)

  • kyadmin
    kyadmin 2026-06-27

    我是be365的签约作者“kyadmin”!

  • kyadmin
    kyadmin 2026-06-27

    希望本篇文章《用Go语言打造一周年纪念视频,365秒的浪漫,代码也能写情书》能对你有所帮助!

  • kyadmin
    kyadmin 2026-06-27

    本站[be365]内容主要涵盖:be365,be365网站,be365网址,ball365体育,Be365排名

  • kyadmin
    kyadmin 2026-06-27

    本文概览:一年前的今天,你们在一起了,或者,你开始了某个项目,又或者,你养了一只猫,不管是什么,一周年是个大日子,我想做个纪念视频,时长正好365...

    联系我们

    工作时间:周一至周五,9:30-18:30,节假日休息

    关注我们