用Golang做一周年纪念视频365?我试了,结果真香

这事儿其实挺偶然的,上个月我老婆突然说,老公,咱们结婚一周年了,我想把过去365天的照片和视频剪成一个纪念视频,我当时脑子里第一个反应是...

这事儿其实挺偶然的,上个月我老婆突然说,老公,咱们结婚一周年了,我想把过去365天的照片和视频剪成一个纪念视频,我当时脑子里第一个反应是——手机里一千多张照片,还有几十段零零散散的视频,这要是手动剪辑,不得干到猴年马月去?

我这个人吧,懒,但懒人有懒办法,既然我是写Go的,那不如试试能不能用Golang把这活儿给自动化了,说实话,一开始我心里完全没底,毕竟Golang不是专门搞视频处理的,但后来发现,这条路还真能走通。

Golang凭什么能做视频处理?

你可能以为视频处理必须用Python的moviepy或者FFmpeg的命令行,没错,FFmpeg确实是底层主力,但用Golang调用FFmpeg,有它独特的优势。

Go的并发模型太适合批处理了,你想啊,365天的素材,要按月份分类、要随机挑选、要拼接、要加转场和字幕,每一步都可能是独立的任务,用Go的goroutine,我们可以让这些任务并行跑起来,效率直接拉满。

Go编译出来是一个单文件二进制,这意味着你不需要在老婆的电脑上装Python环境、配各种依赖,直接把这个工具扔给她,双击就能跑,这才是技术宅的浪漫好吗?

到底怎么做?核心步骤拆解

下面我把自己折腾出来的流程分享出来,这套方法不需要你懂太多视频编码知识,只要会写基础的Go代码就行,关键依赖只有两个:FFmpeg(处理视频的核心工具)和 Golang的os/exec包(调用系统命令)。

第一步:整理素材清单

我写了一个小脚本去扫描指定目录下的所有图片和视频文件,这里有个值得注意的点:手机拍的视频编码五花八门,MP4、MOV、甚至一些奇怪的HEVC格式都有,为了后续处理统一,我在这一步就把所有素材的绝对路径、文件名、文件大小记录在一个结构体里。

type MediaFile struct {  
    Path     string  
    Name     string  
    Ext      string  
    IsVideo  bool  
    Duration float64  // 如果是视频,记录时长  
}  

这一步看起来简单,但实际上是后面所有操作的基础,我当时花了大概半小时,把手机里这一年的素材全导出来,放在一个叫anniversary_365的文件夹里,文件夹结构按月份分好了:2024-012024-02……一直到2024-12

第二步:核心逻辑——如何从365天里选出精华

说实话,365天的素材里,有大量重复、模糊、甚至拍糊了的照片,不可能全用上,一是视频时长受不了,二是也没人想看365张一模一样的自拍。

所以我的策略是:

策略因素 具体做法
时间均匀分布 每个月份至少选3-5个素材,确保全年都有代表
随机性 在每个月内随机挑选,避免刻意排序
按质量过滤 只选取分辨率 ≥ 1080p 的素材,低于这个的暂时不要
去重逻辑 比较两张图片的MD5哈希值,相同哈希的直接跳过

这个逻辑用Go实现非常直白,遍历每个月份子目录,把所有文件塞进一个slice,然后用rand.Shuffle打乱顺序,再按计划数量“捞”出前几个。

rand.Seed(time.Now().UnixNano())  
rand.Shuffle(len(monthFiles), func(i, j int) {  
    monthFiles[i], monthFiles[j] = monthFiles[j], monthFiles[i]  
})  
selected = append(selected, monthFiles[:min(5, len(monthFiles))]...)  

这里有个小坑,我当时差点翻车。Go 1.20之后,rand.Seed已经自动弃用了,直接用rand.New(rand.NewSource(time.Now().UnixNano()))才是正确姿势,我一开始没注意,结果每次运行结果都一样,差点以为是自己的魔法诅咒了程序。

第三步:用Golang组装视频片段

这一步是最核心,也最考验耐心的,我选用FFmpeg作为底层引擎,但是通过Go来驱动。

用Golang做一周年纪念视频365?我试了,结果真香

具体方案是:把选出来的所有素材,先统一转为统一的尺寸(我选了1920x1080,16:9横屏),并且把所有图片转为5秒钟的静帧视频,所有视频片段截取其中最精华的8秒

为什么要统一时长?因为后面拼接的时候,每个片段时间不统一,转场节奏会很乱,我试过不同时长混在一起,结果是:有的镜头一闪而过,有的拖拖拉拉,整体观感极差。

在Go里,我构建了一个FFmpeg命令的模板:

ffmpegCmd := fmt.Sprintf(`  
    -i %s  
    -vf "scale=1920:1080:force_original_aspect_ratio=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2"  
    -t 5  
    -c:v libx264  
    -crf 23  
    temp_%d.mp4  
`, inputPath, index)  

这里借助了exec.Command来执行系统命令,注意那个force_original_aspect_ratio参数,它可以保证图片和视频在被拉伸或填充时,不会被“挤变形”,我老婆最受不了画面变形,这一步的padding逻辑救了我一命。

第四步:并行处理——一年素材批量转码

如果一个一个转码,365个片段可能要跑几个小时,这时候Go的并发能力就派上用场了,我用了生产者-消费者模型:主goroutine负责遍历所有选中的素材并发送到channel,工作goroutine池负责接收任务并调用FFmpeg转码。

jobs := make(chan MediaFile, 100)  
results := make(chan string, 100)  
// 启动10个worker goroutine  
for w := 1; w <= 10; w++ {  
    go worker(w, jobs, results)  
}  

这个并行池我设了10个worker,实测下来,我的M1 MacBook Air上,同时跑10个FFmpeg进程,CPU占用率大概在70%左右,不卡顿,效率却翻了将近10倍,原来串行需要4小时的工作,并行后大约25分钟就全部转码完毕。

不过有个问题要注意:同时写临时文件时,要给每个文件起唯一的名字,否则会覆盖冲突,我用了os.CreateTemp,自动生成了带随机后缀的文件名。

第五步:拼接成片 + 加字幕和背景音乐

所有片段都准备好后,最后一步是把它们拼接起来,加上我们俩的名字和“一周年快乐”的字幕,再配上背景音乐。

拼接有两种主流做法:

  1. FFmpeg concat协议:适用于所有片段编码参数完全一致的情况
  2. FFmpeg concat demuxer:适用于参数不一致的情况,更灵活

我用了第二种方式,先创建一个filelist.txt,把所有临时文件名按顺序列出来:

file 'temp_0.mp4'  
file 'temp_1.mp4'  
file 'temp_2.mp4'  
……  

然后一条命令搞定拼接和加音乐:

ffmpeg -f concat -safe 0 -i filelist.txt -i background_music.mp3 -shortest -c:v copy -c:a aac output_final.mp4  

背景音乐选了《Beautiful in White》,是的,有点俗,但我老婆喜欢。

字幕我用的是硬字幕,也就是直接烧录到视频里,Go里用drawtext滤镜加了两行字:“Happy 1st Anniversary”和“用Golang写的,请多指教”,代码片段大致长这样:

vfFilter := `drawtext=text='Happy 1st Anniversary':fontfile=/path/to/font.ttf:fontsize=48:fontcolor=white:x=(w-text_w)/2:y=h-200:enable='between(t,0,5)'`  

这里要特别强调一句:如果想用中文,必须指定一个支持中文的字体文件,我一开始没指定,结果字幕全变成方框框,折腾了半小时才发现是字体问题。

几个让我差点崩溃的坑

做这个项目最大的收获,不是视频本身,而是一堆血泪教训,分享出来,希望后面有人用Go做视频时能避开。

  1. 中文字体编码问题:Windows上的中文字体名称和Linux不一样,导致找不到字体,通用解法是用系统自带字体,比如在macOS上用/System/Library/Fonts/STHeiti Light.ttc

  2. 视频片段首尾黑屏:转码后的临时文件,前后可能有几帧纯黑,解决办法是在拼接前,用FFmpeg的trim功能裁剪掉首尾的0.5秒。

  3. 内存占用爆炸:当素材特别多时,Go里如果一次性把所有文件路径读入内存,可能会把16GB内存吃满,解决方案是改用filepath.Walk迭代处理,配合sync.Pool复用slice内存。

  4. 时间戳漂移:不同视频片段的原始时间戳不一致,拼接后播放时会出现画面跳跃,这个我最后是用-fflags +genpts强制让每个片段重写时间戳来搞定的。

成品到底怎么样?

说实话,第一次跑出来的效果只能说“能看”,背景音乐和画面的节奏没对齐,有些片段因为原始分辨率太差,被拉大后模糊得像马赛克,但经过两次调优(把模糊素材筛选门槛提高,音乐提前裁剪到整小节),最终成品让老婆看哭了。

她说:“这视频感觉不像是机器剪的,倒像是人用心挑过的——有我们的婚礼、第一次一起做饭、旅游时被雨淋……就是那种不经意的真实感。”

我心想:其实这就是随机挑选和按月份均匀分布的功劳,在人看来是精心设计,在代码眼里只是几个数学逻辑的排列组合。

后来我把这个工具开源了,放在GitHub上叫anniversary-365,到现在为止,居然有300多颗star,还有人留言说用它给父母做了金婚纪念视频,没想到一个偷懒搞出来的小项目,能帮到这么多人。


对了,如果你也想试,建议先把素材导出来,放在一个干净文件夹里,不用太纠结于代码写的多漂亮,反正最终用户只关心视频好不好看,Go的好处就是:它不会在你老婆面前掉链子。

代码写好了,剩下的,就看你想为谁做这个视频了。

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

(11)

文章推荐

发表回复

本站作者才能评论

评论列表(4条)

  • kyadmin
    kyadmin 2026-07-16

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

  • kyadmin
    kyadmin 2026-07-16

    希望本篇文章《用Golang做一周年纪念视频365?我试了,结果真香》能对你有所帮助!

  • kyadmin
    kyadmin 2026-07-16

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

  • kyadmin
    kyadmin 2026-07-16

    本文概览:这事儿其实挺偶然的,上个月我老婆突然说,老公,咱们结婚一周年了,我想把过去365天的照片和视频剪成一个纪念视频,我当时脑子里第一个反应是...

    联系我们

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

    关注我们