用Go语言制作闺蜜365天纪念日视频,一份带着代码温度的礼物

为什么我要用Go语言来搞这个视频?说实话,一开始我想用Python,但后来发现Go在处理视频文件时意外地顺手——它的并发模型让多段素...

为什么我要用Go语言来搞这个视频?

说实话,一开始我想用Python,但后来发现Go在处理视频文件时意外地顺手——它的并发模型让多段素材拼接变得特别自然,而且编译出来的单一二进制文件丢给闺蜜直接就能跑,不用装环境,这大概就是程序员表达感情的方式吧。

先搞清楚我们要什么效果

365天纪念日视频,核心是记录这一年的点点滴滴,我们需要:

  • 按时间顺序排列的照片或短视频片段
  • 每段素材加上日期标签和简短寄语
  • 背景音乐贯穿始终
  • 转场效果不用太花哨,自然的淡入淡出就行
  • 结尾来个彩蛋

对了,别想着用FFmpeg命令行解决一切,虽然FFmpeg很强大,但纯命令行调参够你折腾一整天,我们用Go调用FFmpeg的API(通过exec包),让代码替我们处理重复工作。

准备工作:装好工具

你需要安装:

  1. Go 1.18以上版本
  2. FFmpeg(要确保在PATH里能找到)
  3. 你的素材(照片、视频片段、音乐文件)

检查FFmpeg是否安装:

ffmpeg -version

如果报错,先去ffmpeg.org下载安装,Mac用户用brew install ffmpeg就行,Linux用户走apt或yum,Windows用户记得加环境变量。

第一步:把素材整理好

我习惯按月份建文件夹,每个文件夹里再按日期命名文件。

素材/
├── 01月/
│   ├── 2024-01-01_新年第一张自拍.jpg
│   ├── 2024-01-15_一起滑雪.mp4
│   └── ...
├── 02月/
│   ├── 2024-02-14_情人节礼物.jpg
│   └── ...
...

这样命名方便程序按时间排序,Go处理这个很简单:

package main
import (
    "fmt"
    "io/ioutil"
    "path/filepath"
    "sort"
    "strings"
)
type MediaFile struct {
    Path string
    Date string // YYYY-MM-DD格式
}
func loadMediaFiles(root string) ([]MediaFile, error) {
    var files []MediaFile
    err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
        if info.IsDir() {
            return nil
        }
        // 假设文件名格式为 YYYY-MM-DD_描述.扩展名
        base := filepath.Base(path)
        parts := strings.SplitN(base, "_", 2)
        if len(parts) < 2 {
            return nil // 跳过不符合命名规则的文件
        }
        files = append(files, MediaFile{
            Path: path,
            Date: parts[0],
        })
        return nil
    })
    if err != nil {
        return nil, fmt.Errorf("遍历素材目录失败: %v", err)
    }
    // 按日期排序
    sort.Slice(files, func(i, j int) bool {
        return files[i].Date < files[j].Date
    })
    return files, nil
}

这里有个小坑:Go的time包解析日期很严格,如果文件名里日期格式不统一,解析会报错,我建议统一用YYY-MM-DD格式,少走弯路。

第二步:把图片转成视频

FFmpeg处理图片转视频的命令大致是:

ffmpeg -loop 1 -i image.jpg -c:v libx264 -t 3 -pix_fmt yuv420p output.mp4

这里-t 3表示每张图片显示3秒,我们可以根据素材的情感分量调整时长——比如生日那天的照片多停留1秒,吵架后又和好的照片多停留2秒。

用Go封装一下:

import (
    "os/exec"
    "path/filepath"
)
func imageToVideo(imagePath, outputPath string, duration int) error {
    cmd := exec.Command("ffmpeg",
        "-loop", "1",
        "-i", imagePath,
        "-c:v", "libx264",
        "-t", fmt.Sprintf("%d", duration),
        "-pix_fmt", "yuv420p",
        "-vf", "scale=1920:1080:force_original_aspect_ratio=decrease,pad=1920:1080:(ow-iw)/2:(oh-ih)/2",
        "-y",
        outputPath,
    )
    // 这里用-pix_fmt yuv420p是为了兼容性,很多播放器不认识其他格式
    return cmd.Run()
}

注意那个scalepad的组合——它会把图片按比例缩放到1920x1080,不足的部分用黑边填充,不然有些竖屏照片会被拉变形。

并发处理所有图片:

func batchConvertImages(files []MediaFile, tempDir string) ([]string, error) {
    type result struct {
        index int
        path  string
        err   error
    }
    ch := make(chan result, len(files))
    for i, f := range files {
        go func(idx int, file MediaFile) {
            ext := filepath.Ext(file.Path)
            outPath := filepath.Join(tempDir, fmt.Sprintf("clip_%04d.mp4", idx))
            duration := 3 // 基础3秒
            // 特殊日期加长时间,这里可以加逻辑判断
            err := imageToVideo(file.Path, outPath, duration)
            ch <- result{idx, outPath, err}
        }(i, f)
    }
    var outFiles []string
    for i := 0; i < len(files); i++ {
        r := <-ch
        if r.err != nil {
            return nil, fmt.Errorf("处理第%d个文件失败: %v", r.index, r.err)
        }
        outFiles = append(outFiles, r.path)
    }
    return outFiles, nil
}

Go的goroutine在这里太香了——几百张图片,同时开8个goroutine去处理,速度快了好几倍。不过别开太多,FFmpeg很吃CPU,我电脑开16个直接卡成PPT。

用Go语言制作闺蜜365天纪念日视频,一份带着代码温度的礼物

第三步:拼接所有视频片段

现在我们有了一大堆小的视频片段(每张照片3秒),还有可能直接就是视频的原始素材,需要把它们合成一个完整的视频。

FFmpeg拼接视频有两种方式:

  1. concat协议(适合相同编码参数的片段)
  2. concat过滤器(更灵活,但语法复杂)

我们用concat协议,因为所有片段都是统一格式。

先生成一个文件列表:

func createConcatList(clips []string, listPath string) error {
    f, err := os.Create(listPath)
    if err != nil {
        return err
    }
    defer f.Close()
    for _, clip := range clips {
        // FFmpeg的concat列表要求路径用单引号包裹
        fmt.Fprintf(f, "file '%s'\n", clip)
    }
    return nil
}

然后运行拼接命令:

func concatClips(listPath, outputPath string) error {
    cmd := exec.Command("ffmpeg",
        "-f", "concat",
        "-safe", "0",
        "-i", listPath,
        "-c", "copy",
        "-y",
        outputPath,
    )
    return cmd.Run()
}

-safe 0允许使用相对路径,-c copy直接复制流媒体,不重新编码,速度快得飞起。但注意:如果片段之间的编码参数不一样(比如分辨率、帧率),用copy会出问题,我踩过这个坑,所以前面统一把所有图片转成了相同参数。

第四步:添加背景音乐和字幕

视频有了,但光有画面还不够,背景音乐能让情绪到位,字幕(日期和寄语)能让记忆更清晰。

加背景音乐

func addBackgroundMusic(videoPath, audioPath, outputPath string) error {
    cmd := exec.Command("ffmpeg",
        "-i", videoPath,
        "-i", audioPath,
        "-c:v", "copy",
        "-c:a", "aac",
        "-shortest",   // 以较短的流为准,一般是视频
        "-map", "0:v:0",
        "-map", "1:a:0",
        "-y",
        outputPath,
    )
    return cmd.Run()
}

如果音乐比视频长,自动截断;如果音乐短,循环播放要复杂些——需要先获取视频时长,然后循环拼接音频,再用-shortest截断,不过最简单的方式:选一首比视频长的歌。

添加日期标签字幕

这里用FFmpeg的drawtext滤镜,每段素材的起始时间不同,需要精确计算时间点。

func generateSubtitles(files []MediaFile, clipDuration int) string {
    var filters []string
    currentTime := 0.0
    for i, f := range files {
        // 提取日期(从文件名)
        date := f.Date
        // 你想加的文字
        text := fmt.Sprintf("%s", date)
        // 计算开始时间和持续时间
        start := currentTime
        duration := float64(clipDuration)
        // drawtext滤镜语法
        filter := fmt.Sprintf(
            "drawtext=text='%s':fontfile=/path/to/font.ttf:fontsize=24:fontcolor=white:x=(w-text_w)/2:y=h-50:enable='between(t,%f,%f)'",
            text, start, start+duration,
        )
        filters = append(filters, filter)
        currentTime += duration
    }
    if len(filters) == 0 {
        return ""
    }
    // 用逗号连接所有滤镜
    return strings.Join(filters, ",")
}

这里字体路径得填绝对路径,不然FFmpeg找不到字体,Windows用户路径里记得用双反斜杠或者正斜杠。

然后使用这个滤镜字符串:

func applySubtitles(videoPath, subtitleFilter, outputPath string) error {
    cmd := exec.Command("ffmpeg",
        "-i", videoPath,
        "-vf", subtitleFilter,
        "-c:a", "copy",
        "-y",
        outputPath,
    )
    return cmd.Run()
}

第五步:加入转场效果

基础的淡入淡出转场:

func addTransitions(videoPath, outputPath string, transitionDuration float64) error {
    // 先获取视频总时长
    durationCmd := exec.Command("ffprobe",
        "-v", "error",
        "-show_entries", "format=duration",
        "-of", "default=noprint_wrappers=1:nokey=1",
        videoPath,
    )
    output, _ := durationCmd.Output()
    totalDuration, _ := strconv.ParseFloat(strings.TrimSpace(string(output)), 64)
    // 应用淡入淡出:开头淡入1秒,结尾淡出1秒
    filter := fmt.Sprintf(
        "fade=t=in:st=0:d=1,fade=t=out:st=%f:d=%f",
        totalDuration-transitionDuration,
        transitionDuration,
    )
    cmd := exec.Command("ffmpeg",
        "-i", videoPath,
        "-vf", filter,
        "-c:a", "copy",
        "-y",
        outputPath,
    )
    return cmd.Run()
}

如果要在中间每个片段之间加转场,那计算量就大了——需要知道每个片段的精确切割点。我实际做的时候放弃了全加,只在开头和结尾加了淡入淡出,中间直接硬切,反而更有回忆的味道。

最终的main函数:把一切串联起来

func main() {
    // 配置
    rootDir := "./素材"
    tempDir := "./temp"
    finalVideo := "./闺蜜365天纪念日视频.mp4"
    audioFile := "./背景音乐.mp3"
    fontFile := "/usr/share/fonts/truetype/noto/NotoSansCJK-Regular.ttc" // 改你的字体路径
    os.MkdirAll(tempDir, 0755)
    defer os.RemoveAll(tempDir)
    // 1. 加载素材
    fmt.Println("📁 正在加载素材...")
    files, err := loadMediaFiles(rootDir)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("找到 %d 个素材文件\n", len(files))
    // 2. 图片转视频(并发)
    fmt.Println("🎬 转换图片为视频片段...")
    clips, err := batchConvertImages(files, tempDir)
    if err != nil {
        log.Fatal(err)
    }
    // 3. 拼接所有片段
    fmt.Println("🧩 拼接视频片段...")
    concatList := filepath.Join(tempDir, "concat.txt")
    createConcatList(clips, concatList)
    concatVideo := filepath.Join(tempDir, "concat.mp4")
    if err := concatClips(concatList, concatVideo); err != nil {
        log.Fatal(err)
    }
    // 4. 添加字幕
    fmt.Println("💬 添加日期标签...")
    subtitleFilter := generateSubtitles(files, 3)
    if subtitleFilter != "" {
        subtitledVideo := filepath.Join(tempDir, "subtitled.mp4")
        if err := applySubtitles(concatVideo, subtitleFilter, subtitledVideo); err != nil {
            log.Fatal(err)
        }
        concatVideo = subtitledVideo
    }
    // 5. 添加背景音乐
    fmt.Println("🎵 添加背景音乐...")
    withMusic := filepath.Join(tempDir, "with_music.mp4")
    if err := addBackgroundMusic(concatVideo, audioFile, withMusic); err != nil {
        log.Fatal(err)
    }
    // 6. 添加转场效果
    fmt.Println("✨ 添加淡入淡出...")
    if err := addTransitions(withMusic, finalVideo, 1.0); err != nil {
        log.Fatal(err)
    }
    fmt.Printf("✅ 视频生成完毕: %s\n", finalVideo)
}

那些让我想砸电脑的坑

写这个程序时,我遇到了不少问题,分享出来,你就别踩了:

  1. 路径问题:Windows和Linux的路径分隔符不同。filepath.Join能处理,但FFmpeg的参数里路径用单引号包裹时,Windows下得小心转义。

  2. 字体找不到:drawtext需要指定字体文件,Mac上字体在/System/Library/Fonts/,Linux在/usr/share/fonts/,Windows在C:\Windows\Fonts\,最好放一个中文字体在项目目录里,用相对路径引用。

  3. 音频编码问题:有些音频格式FFmpeg不认识,统一转成aac格式比较稳妥。

  4. 内存不够:处理大量高分辨率照片时,如果同时开太多goroutine,内存会爆。我后来加了信号量限制并发数,比如限制同时运行4个goroutine。

  5. 时间戳不连续:如果某个日期有多张照片,代码还是会按顺序逐个处理,但日期标签会重复显示,可以加个判断:同一日期只显示一次标签。

让视频更有“人味儿”

纯技术框架只能做出冷冰冰的视频,想让它有温度,可以从这几个方向改进:

  • 根据照片内容调整时长:用Go调用图像识别API(比如Google Cloud Vision),检测到多人合影、笑脸时自动延长显示时间。

  • 随机加入特效:在重要的日期(生日、纪念日)前后随机插入小动画,可以用Go生成FFmpeg的滤镜参数,比如在画面上飘落花瓣(用overlay滤镜叠加透明PNG序列)。

  • 片头片尾用模板:用Go读取一个JSON配置文件,里面定义好片头片尾的文字、字体、颜色、背景色,这样下次给其他朋友做,改配置文件就行。

  • 自动配乐:根据照片的情感分析结果(开心的、温馨的、搞笑的)自动选择背景音乐风格,这个比较复杂,但Go调用第三方API也很方便。

我最后给视频加了个小彩蛋——把所有照片的EXIF信息(拍摄时间、地点)提取出来,在地图上画了一条轨迹,作为片尾动画。但代码写得太丑,就不贴出来了。

一些随想

写这个程序的整个过程,让我想起当年和闺蜜一起学编程的日子,她总说代码是冷的,我说不对,代码只是工具,温度是人给的。

用Go写视频生成,其实不是最优雅的方式——Python的moviepy库更直观,但Go编译出来的单一可执行文件,双击就能运行,这对不懂编程的闺蜜来说太友好了,而且Go的并发模型处理批量任务真的很顺手,写完这版后,我又给另一个朋友做了一个100天的版本,只改了配置文件和素材目录。

对了,记得把生成的视频先预览一遍,我有一次因为字体路径写错,所有字幕都变成了方块,还好发现得早,重新生成了一次。

最后提醒一句:FFmpeg的参数极其严格,多一个空格少一个冒号都会报错,我建议把每个命令的标准错误输出都打印出来,方便调试。Go里用cmd.StderrPipe可以捕获错误信息。

好了,代码就这些,剩下的就是打开终端,运行go build,然后把生成的可执行文件和素材放在一起,双击运行,等待进度条走完,一个带着代码温度的365天纪念日视频就出来了。

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

(5)

文章推荐

发表回复

本站作者才能评论

评论列表(4条)

  • kyadmin
    kyadmin 2026-07-13

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

  • kyadmin
    kyadmin 2026-07-13

    希望本篇文章《用Go语言制作闺蜜365天纪念日视频,一份带着代码温度的礼物》能对你有所帮助!

  • kyadmin
    kyadmin 2026-07-13

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

  • kyadmin
    kyadmin 2026-07-13

    本文概览:为什么我要用Go语言来搞这个视频?说实话,一开始我想用Python,但后来发现Go在处理视频文件时意外地顺手——它的并发模型让多段素...

    联系我们

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

    关注我们