当前位置:首页 > 能源 > 正文

一周年蛋糕视频365,用Golang记录每一刻甜蜜的时光

  • 能源
  • 2026-07-10 11:25:39
  • 375
摘要: 为什么是“一周年蛋糕视频365”?说实话,最开始我也没想明白这三个词怎么凑到一起的,一周年、蛋糕、视频、365——乍一看像是生日...

为什么是“一周年蛋糕视频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风扇转得跟直升机似的。

一周年蛋糕视频365,用Golang记录每一刻甜蜜的时光