从第一天到第365天,用Go语言打造你的专属成长视频记录系统

你有没有想过,如果每天给自己录一段15秒的视频,坚持一年,最后拼成一条完整的365天视频,那会是什么感觉?我试过,说实话,前30天你会觉...

你有没有想过,如果每天给自己录一段15秒的视频,坚持一年,最后拼成一条完整的365天视频,那会是什么感觉?我试过,说实话,前30天你会觉得好玩,第100天开始怀疑人生,第200天纯粹靠习惯硬撑,但到了第365天,当所有片段串起来那一刻,你整个人会起鸡皮疙瘩。

问题来了——手机相册里躺着几百段视频,怎么按日期排序?怎么拼接?怎么添加日期水印?手动处理?开什么玩笑,那得花一整天,这时候,Go语言就是你的最佳帮手,我花了大概三个晚上,用Go写了个小工具,把整个流程自动化了,今天就把这套思路完整拆给你看。

为什么是Go而不是Python或FFmpeg脚本?

先别急,我知道你会问,Python写脚本确实快,但有个致命伤:处理视频的依赖库装起来能把你逼疯,FFmpeg命令行虽然强大,但你要处理的是“按日期组织文件→批量处理→生成缩略图→拼接”这一整个流程,纯靠命令行脚本会让你的shell代码丑得没法看。

Go的好处在于:

  • 编译成单个二进制文件,扔到服务器或者NAS上就能跑
  • 标准库的ospath/filepath处理文件遍历极其顺手
  • 配合github.com/u2takey/ffmpeg-go这个库,调用FFmpeg的API像写Go代码一样自然
  • 并发处理视频片段的时候,goroutine比Python的多线程靠谱多了

我自己的使用场景是这样的:每天用手机录一段当天最有代表性的画面,然后通过SyncThing自动同步到家里的NAS,Go程序每天凌晨检查新文件,自动处理,整个过程不用我碰任何命令。

核心设计:三阶段管道

我的处理逻辑其实很简单,拆成三个阶段,用Go的管道模式串起来:

type VideoProcessor struct {
    inputDir  string
    outputDir string
    datePattern string // "2006-01-02"
}

扫描与排序

这个阶段负责把输入目录里所有视频文件找出来,按文件名里的日期排序,这里我踩过一个坑——手机导出的视频文件名可能不带日期,所以你得从文件的修改时间(ModTime)或者EXIF信息里提取。

func (vp *VideoProcessor) scanAndSort() ([]VideoFile, error) {
    var videos []VideoFile
    err := filepath.Walk(vp.inputDir, func(path string, info os.FileInfo, err error) error {
        if err != nil || info.IsDir() {
            return err
        }
        ext := strings.ToLower(filepath.Ext(path))
        if ext == ".mp4" || ext == ".mov" {
            modTime := info.ModTime()
            videos = append(videos, VideoFile{
                Path: path,
                Date: modTime.Format("2006-01-02"),
            })
        }
        return nil
    })
    // 按日期排序
    sort.Slice(videos, func(i, j int) bool {
        return videos[i].Date < videos[j].Date
    })
    return videos, nil
}

这里有个细节值得注意:排序不仅仅是字典序,你得保证日期是标准的YYYY-MM-DD格式,不然2023-12-31会排在2024-01-01前面。

批量标准化处理

这个阶段的目标是把所有视频统一成相同的分辨率、帧率和编码格式,为什么?因为手机录像有时候竖屏有时候横屏,分辨率也千奇百怪,如果不统一,最后拼接出来的视频画面会疯狂跳变。

从第一天到第365天,用Go语言打造你的专属成长视频记录系统

我用的是ffmpeg-go库,代码看起来这样:

func (vp *VideoProcessor) normalize(video VideoFile) error {
    err := ffmpeg.Input(video.Path).
        Output(vp.outputDir+"/normalized/"+video.Date+".mp4",
            ffmpeg.KwArgs{
                "vf": "scale=1080:1920:force_original_aspect_ratio=decrease,pad=1080:1920:(ow-iw)/2:(oh-ih)/2",
                "r":  "30",
                "c:v": "libx264",
                "preset": "medium",
                "crf": "23",
            }).
        OverwriteOutput().
        ErrorToStdOut().
        Run()
    return err
}

这个vf参数的意思是:把视频缩放后居中放在1080x1920的黑色背景上,因为我的视频都是竖屏拍的,统一成竖屏尺寸,到时候拼接就顺滑了。

拼接与时间轴

最后一个阶段是拼接,这里有两种思路:简单拼接叠加时间轴,简单拼接就是concat demuxer,把文件列表写进文本文件然后交给FFmpeg,但更有意思的做法是给每个视频片段加上日期水印,并且在切换时用Xfade转场。

我最初用的是简单拼接,代码巨短:

func concatVideos(videoList []string, outputPath string) error {
    listFile, _ := os.Create("concat_list.txt")
    defer listFile.Close()
    for _, v := range videoList {
        fmt.Fprintf(listFile, "file '%s'\n", v)
    }
    err := ffmpeg.Input("concat_list.txt", ffmpeg.KwArgs{"f": "concat", "safe": "0"}).
        Output(outputPath, ffmpeg.KwArgs{"c": "copy"}).
        OverwriteOutput().Run()
    return err
}

但后来觉得每天切换太生硬,就改成了用xfade滤镜,每个片段0.5秒交叉溶解,代码会复杂一点,但效果完全不一样——看着一年的时间在眼前淡入淡出,那感觉真的挺奇妙的。

额外的优化:加载提示与并发处理

一年365个视频,如果一个个按顺序处理,在普通电脑上得跑几个小时,我用Go的并发模型做了个优化:每个视频的标准化过程丢进goroutine,最多同时跑4个

sem := make(chan struct{}, 4)
var wg sync.WaitGroup
for _, v := range videos {
    wg.Add(1)
    go func(video VideoFile) {
        defer wg.Done()
        sem <- struct{}{}
        defer func() { <-sem }()
        vp.normalize(video)
    }(v)
}
wg.Wait()

但记住一点:拼接阶段千万不能并发,FFmpeg的concat操作对文件顺序有强依赖,并发会出乱子。

另一个实用功能是进度反馈,处理第180天的时候你就知道“到底还剩多少个”有多重要了,我用了个简单的计数器加原子操作:

var processed int32
// 在每个goroutine里
atomic.AddInt32(&processed, 1)
fmt.Printf("进度: %d/%d, 完成率 %.1f%%\n", 
    atomic.LoadInt32(&processed), len(videos), 
    float64(atomic.LoadInt32(&processed))/float64(len(videos))*100)

一个意外的收获:缩略图日历

写着写着,我灵机一动:不如顺便生成一个缩略图日历?把每天的视频抽出一帧,拼成一个像日历一样的格子——1月的格子排在左上角,12月排在右下角,这样一眼就能看到这一年里每个时间段的生活色彩。

实现上用ffmpegselect滤镜每N帧抽一帧,然后用Go的image库拼图:

func generateThumbnailCalendar(videos []VideoFile) error {
    // 为每个视频生成一张缩略图
    for i, video := range videos {
        thumbsPath := fmt.Sprintf("thumbs/%d.jpg", i)
        ffmpeg.Input(video.Path).
            Output(thumbsPath, ffmpeg.KwArgs{
                "vframes": "1",
                "ss": "5", // 取第5秒的帧
            }).
            Run()
    }
    // 然后按7列(周)来排列,每个月换行...
    // 代码较长,不贴了,核心逻辑就是往image.RGBA里画矩形
}

打印出来贴墙上,那种成就感不亚于看到最后成片的那一刻。

别忘了音频的处理

第一版的时候我偷懒,拼接时没管音频,结果发现画面是顺畅了,但背景音乐断断续续的,后来我加了音轨处理:先对每个片段的音频做归一化(loudnorm滤镜),然后把所有音轨静音,只保留环境底噪的混合。

怎么弄的?在拼接之前先单独处理音频:

ffmpeg.Input(video.Path).
    Output("audio_"+video.Date+".wav", ffmpeg.KwArgs{
        "vn": "",  // 忽略视频流
        "af": "loudnorm=I=-16:TP=-1.5:LRA=11",
    }).
    Run()

最后用amergeamix把整年的音频混成一条背景音轨,不过说实话,我最后成品视频是直接把音频全部静音,配上自己选的BGM,一年的环境音混在一起真的不好听。

关于文件命名和日期提取,我最后用了更稳妥的做法

别依赖文件修改时间——因为同步软件可能会改变ModTime,我后来改成优先从文件名里正则提取日期,实在提取不到才用ModTime

func extractDate(filename string, modTime time.Time) string {
    re := regexp.MustCompile(`(\d{4})[-_]?(\d{2})[-_]?(\d{2})`)
    matches := re.FindStringSubmatch(filename)
    if len(matches) == 4 {
        return fmt.Sprintf("%s-%s-%s", matches[1], matches[2], matches[3])
    }
    return modTime.Format("2006-01-02")
}

这样做的原因是:我有个习惯是偶尔会补录,比如某天忘记录了,第二天早上补一段,如果按修改时间排序,补录的视频会排到第二天后面,但那天的位置就空着了,所以文件名里写清楚日期是最稳的。

代码仓库里的其他小工具

写这个主程序的同时,我还顺手写了两个小工具:

  • check_drop:检查哪些日期缺失视频,比如发现3月17日没录像,提醒你补拍。
  • make_gif:把365段视频每隔10天抽一帧,做成一个动态GIF,方便发朋友圈。

这些小东西都很简单,加起来不到500行Go代码,但真的可以养成一个好习惯。

最后想说的是...

这套程序我跑了两个多月了,目前进度到第127天,说实话,最有意思的不是最后拼出来的成品,而是每天按下录制键之前想“今天有什么值得记录的”的那个瞬间,Go这个语言不煽情,但它写出来的工具能让你坚持在做一件煽情的事。

代码一点都不完美——我甚至没写单元测试,错误处理也粗暴,但有什么关系呢?它跑起来稳稳的,处理完视频第二天早上默默地把成品放在NAS的/record/365days目录下,你只需要打开那个文件夹,看一天天累积起来的文件大小从几百MB慢慢变成几个GB,那种充实的重量,比任何社交媒体的点赞都真实。

哦对了,如果你也打算做这件事,记得把代码和视频目录都备份两份,硬盘会坏,但数据不会骗你。

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

(1)

文章推荐

发表回复

本站作者才能评论

评论列表(4条)

  • kyadmin
    kyadmin 2026-08-28

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

  • kyadmin
    kyadmin 2026-08-28

    希望本篇文章《从第一天到第365天,用Go语言打造你的专属成长视频记录系统》能对你有所帮助!

  • kyadmin
    kyadmin 2026-08-28

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

  • kyadmin
    kyadmin 2026-08-28

    本文概览:你有没有想过,如果每天给自己录一段15秒的视频,坚持一年,最后拼成一条完整的365天视频,那会是什么感觉?我试过,说实话,前30天你会觉...

    联系我们

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

    关注我们