说实话,我第一次写这个程序的时候,完全是因为自己记性太差了,女朋友总说我连我们第一次约会哪天都记不住,我嘴硬,心里却偷偷较劲:行,我用代码来记,但真正动起手来才发现,一周年点蜡烛视频365这个事儿,比想象中复杂得多,你想啊,要拍365天,每天点一根蜡烛,最后剪成一周年纪念视频——这份仪式感,代码得兜得住。
用什么思路来构造这个蜡烛视频程序
我先列一下核心需求,既然是Golang写,那肯定得轻量、并发、好维护,目标其实就几个:
- 每天生成一张“点蜡烛”的画面(可以是纯数字渲染,也可以是模拟蜡烛图)
- 按日期顺序拼接成视频
- 视频长度控制在合理范围
- 能在本地跑,也能部署成小服务
但这中间有个坑:365张图,一张张手动点,肯定不现实,所以核心逻辑其实是“批量生成 + 时间戳对齐”。
第一步:用Go生成每天一根蜡烛的画面
我一开始想用第三方图像库,但后来发现,用Go的标准库image加image/color和image/png就能搞定,虽然简单,但效果够用。
核心结构大概是这样:
type CandleDay struct {
Date time.Time
DayIndex int // 1到365
ImagePath string
}
每个CandleDay就代表一天,我写了个函数generateCandleImage,参数是日期和序号,返回一张渲染好的PNG。蜡烛本体用一个矩形加一个椭圆火焰,颜色随天数渐变——从第1天的暖黄到第365天的深红,这样最后视频里能看到颜色在慢慢变化,挺有仪式感的。
关键点:每张蜡烛图右下角写上当天日期,左上角写上“第X天”,这样拼接出来,观众一眼就知道是第几天。
第二步:并发生成,别让CPU闲着
生成365张图,如果串行跑,哪怕每张只需0.5秒,也得三分钟。但Go有goroutine啊,我开了8个worker,一个channel往里塞任务,主goroutine等结果,代码写出来就这个样子:
func batchGenerate(startDate time.Time, count int) []CandleDay {
tasks := make(chan int, count)
results := make(chan CandleDay, count)
// 启动worker
for w := 1; w <= 8; w++ {
go worker(tasks, results, startDate)
}
// 发任务
for i := 1; i <= count; i++ {
tasks <- i
}
close(tasks)
// 收结果
var candles []CandleDay
for i := 1; i <= count; i++ {
candles = append(candles, <-results)
}
return candles
}
实际跑下来,365张图生成用时从3分多钟压到了28秒,差不多10倍提升,goroutine就是为这种场景生的。

| 并发数 | 生成时间 | 内存占用 |
|---|---|---|
| 1 | 3m12s | 45MB |
| 4 | 51s | 68MB |
| 8 | 28s | 92MB |
| 16 | 27s | 145MB |
超过8个worker提升就很小了,瓶颈在磁盘写入,所以8个worker是我试出来的甜点值。
第三步:把图片拼成视频
这是最头疼的一步,Golang标准库没有视频编码器,所以我用了ffmpeg的子进程调用,Go的os/exec包直接调ffmpeg命令行:
func assembleVideo(imageDir string, outputPath string) error {
cmd := exec.Command("ffmpeg",
"-framerate", "30", // 30帧每秒
"-pattern_type", "glob",
"-i", imageDir + "/candle_*.png",
"-c:v", "libx264",
"-pix_fmt", "yuv420p",
outputPath,
)
return cmd.Run()
}
每秒30帧,每张图占一帧,365张图就是12秒多一点。但一周年视频12秒也太短了,我想让每张蜡烛图停留久一点,所以改成了每张图重复30帧,这样365天×30帧=10950帧,除以30帧每秒=365秒≈6分钟。刚好一根蜡烛燃6分钟,配一段舒缓音乐,感觉对味了。
处理日期边界和闰年问题
一开始没考虑闰年,直到我同事说:“你这程序要是2024年用,2月29号怎么办?”我才意识到,365天这个数字本身就有日期敏感性。
我是这么处理的:
func isLeapYear(year int) bool {
return year%4 == 0 && (year%100 != 0 || year%400 == 0)
}
func daysInYear(year int) int {
if isLeapYear(year) {
return 366
}
return 365
}
然后用户输入“一周年”时,自动检测起始年份,如果是闰年就生成366个蜡烛,否则365个,这样就不会漏掉2月29号那根蜡烛了。
这里有个小插曲——连蜡烛颜色渐变逻辑也得跟着改,366天的话,颜色跨度要比365天稍微拉宽一点,否则最后一天看起来颜色没变到位,我重新算了下步长:R = 255, G = 255 - (dayIndex * 255 / totalDays), B = 0,这样最后一天红色满格,绿色归零,视觉上能看出明显渐变。
输出格式和文件名规范
我让每个蜡烛图文件名包含日期和序号,方便排序和调试。
candle_20240101_001.png
candle_20240102_002.png
...
candle_20241231_365.png
这样用glob匹配的时候顺序自然就是对的。千万别用纯序号,否则跨年时“第365天”和“第1天”之间会混淆。
命令行参数设计
为了让不熟悉编程的人也能用,我用flag包做了参数:
var (
startDate = flag.String("start", "", "起始日期,格式:2006-01-02")
output = flag.String("out", "anniversary.mp4", "输出视频文件名")
fps = flag.Int("fps", 1, "每张蜡烛图停留的帧数")
)
*fps默认是30,让每张图停留约1秒,如果有用户希望视频短一点,可以改成15或10。甚至还能设成1,做成快闪版。
整体跑一遍的样子
我把代码打包成单个main.go,大概220行,编译后生成的可执行文件只有4.6MB,放到树莓派上也能跑,命令是:
./candle-365 -start 2024-01-01 -out my-anniversary.mp4
然后屏幕上会输出:
开始生成蜡烛图...
Day 001: 完成
Day 002: 完成
...
Day 365: 完成
全部图片生成完毕,耗时 28.3 秒
开始拼接视频...
视频拼接完成,输出文件:my-anniversary.mp4
整个过程加起来不到一分钟,最后拿到一个6分钟的视频,每一帧都有一根蜡烛和当天的日期,你可以在纪念日那天,从第1天放到第365天,看着蜡烛颜色从暖黄变成深红,画面本身就在讲述时间的故事。
真实使用时的几个细节
我用这个程序给自己和女朋友做了一周年视频,有个问题出现得很突然——蜡烛火焰位置固定,但图片尺寸是1920×1080,放到手机上看蜡烛太小了,后来我加了一个-size参数,默认是1080p,但可以改成720p或者480p,甚至正方形1:1适合Instagram story。
还有一个经验:视频配乐要控制音量,ffmpeg里可以加音频轨道,我直接把一首mp3嵌进去,命令是:
cmd := exec.Command("ffmpeg",
"-i", "temp_video.mp4",
"-i", "background_music.mp3",
"-c:v", "copy",
"-c:a", "aac",
"-shortest",
"final_anniversary.mp4",
)
这样视频长度由画面决定,音乐自动截断或循环。一周年点蜡烛视频365,最终成品就是一个.mp4文件,可以在任何设备上播放。
视频做好后,我女朋友看了,说我总算干了一回靠谱的事,虽然嘴上这么说,但她把那个视频存到手机里,时不时翻出来看。程序是冷的,但数据是热的,365根蜡烛,每根都对应一个真实的日子——有吵架的,有开心的,有加班的,有一起买菜做晚饭的,每一帧背后都有记忆。
本文来自作者[kyadmin]投稿,不代表be365立场,如若转载,请注明出处:http://www.hljbesthome.com/jiankang/1580.html
评论列表(4条)
我是be365的签约作者“kyadmin”!
希望本篇文章《一周年点蜡烛视频365,用Golang记录每一个值得纪念的日子》能对你有所帮助!
本站[be365]内容主要涵盖:be365,be365网站,be365网址,ball365体育,Be365排名
本文概览:说实话,我第一次写这个程序的时候,完全是因为自己记性太差了,女朋友总说我连我们第一次约会哪天都记不住,我嘴硬,心里却偷偷较劲:行,我用代...