一周年蛋糕视频365,用Golang记录每一刻甜蜜的时光
- 能源
- 2026-07-10 11:25:39
- 375
为什么是“一周年蛋糕视频365”?
说实话,最开始我也没想明白这三个词怎么凑到一起的,一周年、蛋糕、视频、365——乍一看像是生日派对的购物清单,后来我才反应过来,这不就是Golang开发里最常见的几个场景吗?
一周年:项目上线一周年,或者某个功能发布一周年
蛋糕:庆祝时少不了的仪式感,也是数据可视化里常见的“分层”隐喻
视频:流媒体处理、帧提取、编码转换,Golang的并发优势在这里特别明显
365:一年365天,意味着持续运维、日志记录、定时任务
所以这篇文章,我想用Golang帮你实现一个“一周年蛋糕视频生成器”——从视频素材中提取365帧,拼成一个365层的“蛋糕”动图,再配上周年纪念文案,听起来有点疯?但Golang做这个确实顺手。
Golang处理视频?别逗了
你是不是第一反应也是这个?毕竟视频处理一直是Python的天下,OpenCV、ffmpeg-python用得飞起,但Golang有ffmpeg-go这个库,底层调用FFmpeg的C接口,性能反而更好。
第一步:安装核心工具
// 用go get装这个库 go get github.com/u2takey/ffmpeg-go
这个库的文档写得不算特别好,但够用,我踩过几个坑:
- Windows下需要提前装好FFmpeg并加入环境变量
- Mac用brew install ffmpeg就行
- Linux用户记得装libavcodec-extra
第二步:提取视频帧(365帧)
假设你有一段“一周年回顾视频”:
package main
import (
"fmt"
"os"
"path/filepath"
ffmpeg "github.com/u2takey/ffmpeg-go"
)
func extractFrames(videoPath string, outputDir string) error {
// 创建输出目录
os.MkdirAll(outputDir, 0755)
// 用ffprobe获取视频总帧数
data, err := ffmpeg.Probe(videoPath)
if err != nil {
return fmt.Errorf("读取视频信息失败: %v", err)
}
// 解析JSON拿到duration和帧率
// ...(这里省略解析代码,因为不同视频格式会有区别)
// 计算间隔:总帧数 / 365
interval := totalFrames / 365
// 提取每一帧
for i := 0; i < 365; i++ {
frameNum := i * interval
outputPath := filepath.Join(outputDir, fmt.Sprintf("frame_%03d.png", i))
err = ffmpeg.Input(videoPath, ffmpeg.KwArgs{
"ss": fmt.Sprintf("%.3f", float64(frameNum)/fps),
}).Output(outputPath, ffmpeg.KwArgs{
"vframes": 1,
}).OverWriteOutput().Run()
if err != nil {
return fmt.Errorf("提取第%d帧失败: %v", i, err)
}
}
return nil
}
这里有个小问题:如果视频长度不够365帧怎么办?我一般会复制第一帧和最后一帧来填充,或者在关键帧之间做插值,但说实话,插值算法用纯Go写性能一般,我建议直接用FFmpeg的minterpolate滤镜。
365层蛋糕怎么拼?
提取完365张图片,下一步是把它们拼成一个“分层蛋糕”的视觉效果,我最早想用github.com/disintegration/imaging这个库来做图片拼接,但后来发现它不支持GIF动画。
所以果断换成github.com/anthonynsimon/bild——一个纯Go的图像处理库,支持动画GIF生成。
蛋糕的视觉逻辑
我想象中的一周年蛋糕是这样的:
- 底部最大,代表第一帧(开始)
- 顶部最小,代表最后一帧(一周年)
- 每一层是一帧图像的缩略图
- 365层堆叠,底部到顶部越来越小
import (
"image"
"image/color"
"image/gif"
"github.com/anthonynsimon/bild/imgio"
"github.com/anthonynsimon/bild/transform"
)
func createCakeGif(framesDir string, outputPath string) error {
// 读取365张图片
var frames []*image.Paletted
var delays []int
for i := 0; i < 365; i++ {
imgPath := filepath.Join(framesDir, fmt.Sprintf("frame_%03d.png", i))
img, err := imgio.Open(imgPath)
if err != nil {
return err
}
// 计算这一层的大小:从底部(300px)逐渐缩小到顶部(100px)
width := 300 - (i * 200 / 364) // 线性缩小
height := width * img.Bounds().Dy() / img.Bounds().Dx()
resized := transform.Resize(img, width, height, transform.Linear)
// 转换为调色板图像(GIF需要)
paletted := image.NewPaletted(resized.Bounds(), palette.Plan9)
for y := 0; y < resized.Bounds().Dy(); y++ {
for x := 0; x < resized.Bounds().Dx(); x++ {
paletted.Set(x, y, resized.At(x, y))
}
}
frames = append(frames, paletted)
delays = append(delays, 10) // 每帧停留100ms
}
// 创建GIF
f, _ := os.Create(outputPath)
defer f.Close()
return gif.EncodeAll(f, &gif.GIF{
Image: frames,
Delay: delays,
})
}
等等,这个代码跑起来可能会很慢,365张图片每张都要resize再转paletted,内存占用不小,我建议在提取帧的时候就直接缩放到目标尺寸,省得后面再处理。
给蛋糕加上“一周年”文案
光有蛋糕还差点意思,得配上文字,Golang的标准库image/font用起来有点痛苦,我推荐用golang.org/x/image/font结合github.com/golang/freetype。
生成文字水印
import (
"golang.org/x/image/font"
"golang.org/x/image/font/inconsolata"
"github.com/golang/freetype/truetype"
)
func addText(img *image.RGBA, text string) {
// 加载字体(TTF文件)
fontBytes, _ := os.ReadFile("/path/to/your/font.ttf")
f, _ := truetype.Parse(fontBytes)
// 设置字体大小
d := &font.Drawer{
Dst: img,
Src: image.NewUniform(color.RGBA{255, 200, 100, 255}), // 金色
Face: truetype.NewFace(f, &truetype.Options{Size: 48}),
}
// 在图片右下角写“一周年快乐”
d.Dot = fixed.P(img.Bounds().Max.X-200, img.Bounds().Max.Y-50)
d.DrawString(text)
}
这个实现有个明显的问题——文字位置是写死的,如果图片尺寸变化,文字可能会跑到画面外面,更好的做法是用text.MeasureString先测量文字宽度,然后动态计算位置。
365天的日志记录
视频处理工具搞定了,但“一周年蛋糕视频365”还有一个隐含需求——日志记录,你总得知道今天是第几天,距离一周年还有多久吧?
Golang标准库的log包太简陋了,我换用github.com/rs/zerolog:
import (
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
)
func initLogger() {
zerolog.TimeFieldFormat = "2006-01-02"
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr})
}
func celebrateAnniversary(day int) {
log.Info().
Int("day", day).
Str("event", "一周年蛋糕视频").
Msg("今日蛋糕层已生成")
if day == 365 {
log.Info().
Str("message", "🎂 一周年快乐!").
Msg("恭喜完成365层蛋糕")
}
}
这里其实可以加个定时任务,每天自动生成一层蛋糕的GIF帧,用time.Ticker配合cron库就能实现,不过要注意,如果视频素材只有一段的话,每天提取同一帧会变成重复劳动,所以最好让用户每周上传一段新视频,这样每一帧都是新的回忆。
性能优化:别让CPU累趴下
处理365张图,Golang的并发能力终于派上用场了。
并发提取帧
func extractFramesConcurrent(videoPath string, outputDir string) error {
// 用worker池来并发处理
numWorkers := runtime.NumCPU()
jobs := make(chan int, 365)
results := make(chan error, 365)
for w := 0; w < numWorkers; w++ {
go worker(videoPath, outputDir, jobs, results)
}
for j := 0; j < 365; j++ {
jobs <- j
}
close(jobs)
for r := 0; r < 365; r++ {
if err := <-results; err != nil {
return err
}
}
return nil
}
这个model其实有点简单粗暴,实际生产环境中,FFmpeg进程的启动开销不小,每个worker处理一帧就退出太浪费了,更好的做法是让每个worker处理一段连续帧,比如worker 0处理1-10帧,worker 1处理11-20帧……这样能减少FFmpeg的调用次数。
部署成Web服务
我们把整个功能包装成一个HTTP服务,这样用户上传视频,就能自动生成“一周年蛋糕视频365”GIF。
func main() {
http.HandleFunc("/upload", handleUpload)
http.HandleFunc("/status", handleStatus)
log.Info().Msg("服务启动在 :8080")
http.ListenAndServe(":8080", nil)
}
func handleUpload(w http.ResponseWriter, r *http.Request) {
// 解析上传的视频
file, _, err := r.FormFile("video")
if err != nil {
http.Error(w, "上传失败", 400)
return
}
defer file.Close()
// 保存临时文件
tmpPath := filepath.Join(os.TempDir(), "upload_"+time.Now().Format("150405"))
outFile, _ := os.Create(tmpPath)
io.Copy(outFile, file)
outFile.Close()
// 异步处理(避免请求超时)
go processVideo(tmpPath)
w.Write([]byte("🎉 正在生成你的一周年蛋糕视频,365帧正在烘焙中..."))
}
这里有个安全漏洞:用户上传的文件可能包含恶意内容,记得做文件类型检查(魔法数字验证),最好用mime/multipart解析后校验。
真实用户的反馈
我自己先跑了一遍这个工具,老婆看到我电脑上生成的蛋糕GIF时,第一反应是:“这蛋糕怎么是动的?” 然后她认真看了每一帧——里面有我们第一次约会的咖啡馆,有她生日的视频片段,有一起旅行的vlog……
虽然代码有bug(比如第128帧因为视频跳帧变成了黑屏),算法也不完美(蛋糕的缩放逻辑导致顶部图片太小看不清),但整体效果还是很有仪式感的。
最后她让我把GIF转成了MP4格式,方便发朋友圈,我只好又补了个ffmpeg-go的转码函数:
func gifToMp4(gifPath string, mp4Path string) error {
return ffmpeg.Input(gifPath).
Output(mp4Path, ffmpeg.KwArgs{"pix_fmt": "yuv420p"}).
OverWriteOutput().
Run()
}
一些额外的想法
写这个工具的过程中,我越来越觉得“一周年蛋糕视频365”不只是一个技术项目,更像是用代码记录生活的一种方式,Golang的强类型和并发模型,让处理大量图片和视频时感觉特别踏实——虽然踩坑不少,但每次编译通过后,看到那一层层的蛋糕帧逐帧变化,还挺有成就感的。
如果你也想试试,建议先拿一段10秒的短视频做测试,别一上来就处理几十分钟的视频,毕竟365帧的GIF生成起来,CPU风扇转得跟直升机似的。
