Go 使用 FFmpeg 对视频进行抽帧

发布于:
更新于:

Go 使用 FFmpeg 对视频进行抽帧

本篇介绍如何使用 github.com/u2takey/ffmpeg-go 库对视频进行抽帧操作。

目录

安装依赖

要使用 ffmpeg-go 进行视频抽帧,需要安装以下依赖:

# 安装 ffmpeg-go 库
go get -u github.com/u2takey/ffmpeg-go

# 安装图像处理库(可选,用于处理抽取的帧)
go get -u github.com/disintegration/imaging

此外,您的系统上需要安装 FFmpeg 命令行工具,因为 ffmpeg-go 是对 FFmpeg 的 Go 语言封装。

基本概念

ffmpeg-go 库提供了一套流式 API,允许您构建 FFmpeg 命令管道。主要组件包括:

  • Stream: 表示媒体流,可以通过各种方法进行转换和处理
  • Input: 创建输入流
  • Output: 指定输出目标
  • Filter: 应用 FFmpeg 过滤器
  • Run: 执行命令

常用抽帧方法

抽取单帧作为封面

从视频中抽取特定帧作为封面图片:

func extractCoverFrame(inputPath, outputPath string) error {
    buf := bytes.NewBuffer(nil)
    err := ffmpeg.Input(inputPath).
        Filter("select", ffmpeg.Args{"gte(n,60)"}). // 抽取第60帧
        Output("pipe:", ffmpeg.KwArgs{"vframes": 1, "format": "image2", "vcodec": "mjpeg"}).
        WithOutput(buf, nil).
        Run()

    if err != nil {
        return fmt.Errorf("ffmpeg抽帧失败: %v", err)
    }

    // 解码JPEG图像
    img, err := imaging.Decode(buf)
    if err != nil {
        return fmt.Errorf("解码图像失败: %v", err)
    }

    // 保存图像到文件
    err = imaging.Save(img, outputPath)
    if err != nil {
        return fmt.Errorf("保存图像失败: %v", err)
    }

    return nil
}

每秒抽取一帧

使用 fps 过滤器从视频中每秒抽取一帧:

func extractFramesPerSecond(inputPath, outputDir string) error {
    err := ffmpeg.Input(inputPath).
        Filter("fps", ffmpeg.Args{"1"}). // 每秒1帧
        Output(filepath.Join(outputDir, "frame-%03d.jpg"), ffmpeg.KwArgs{"q:v": 2}).
        OverWriteOutput().
        Run()

    if err != nil {
        return fmt.Errorf("ffmpeg抽帧失败: %v", err)
    }

    return nil
}

抽取指定时间点的帧

在视频的特定时间点抽取帧:

func extractFramesAtTimePoints(inputPath, outputDir string, timePoints []float64) error {
    for _, timePoint := range timePoints {
        outputPath := filepath.Join(outputDir, fmt.Sprintf("time-%.1f.jpg", timePoint))
        
        err := ffmpeg.Input(inputPath, ffmpeg.KwArgs{"ss": timePoint}).
            Output(outputPath, ffmpeg.KwArgs{"vframes": 1, "q:v": 2}).
            OverWriteOutput().
            Run()

        if err != nil {
            return fmt.Errorf("在时间点%.1f抽帧失败: %v", timePoint, err)
        }
    }
    
    return nil
}

抽取指定帧号的帧

抽取视频中特定帧号的帧:

func extractFramesByNumber(inputPath, outputDir string, frameNumbers []int) error {
    for _, frameNum := range frameNumbers {
        outputPath := filepath.Join(outputDir, fmt.Sprintf("frame-%d.jpg", frameNum))
        
        buf := bytes.NewBuffer(nil)
        err := ffmpeg.Input(inputPath).
            Filter("select", ffmpeg.Args{fmt.Sprintf("eq(n,%d)", frameNum)}).
            Output("pipe:", ffmpeg.KwArgs{"vframes": 1, "format": "image2", "vcodec": "mjpeg"}).
            WithOutput(buf, nil).
            Run()

        if err != nil {
            return fmt.Errorf("抽取帧号%d失败: %v", frameNum, err)
        }

        // 解码并保存图像
        img, err := imaging.Decode(buf)
        if err != nil {
            return fmt.Errorf("解码帧号%d的图像失败: %v", frameNum, err)
        }

        err = imaging.Save(img, outputPath)
        if err != nil {
            return fmt.Errorf("保存帧号%d的图像失败: %v", frameNum, err)
        }
    }
    
    return nil
}

函数参数详解

基本输入输出函数

ffmpeg.Input(inputPath string, args ...ffmpeg.KwArgs) *ffmpeg.Stream

指定输入文件。

  • inputPath: 输入文件的路径
  • args: 可选的关键字参数

示例:

// 基本用法
stream := ffmpeg.Input("input.mp4")

// 带参数的用法,从2秒开始读取
stream := ffmpeg.Input("input.mp4", ffmpeg.KwArgs{"ss": 2.5})

stream.Output(outputPath string, args ...ffmpeg.KwArgs) *ffmpeg.Stream

指定输出文件。

  • outputPath: 输出文件的路径
  • args: 可选的关键字参数

示例:

// 基本用法
stream := ffmpeg.Input("input.mp4").Output("output.mp4")

// 带参数的用法,设置视频质量
stream := ffmpeg.Input("input.mp4").Output("output.jpg", ffmpeg.KwArgs{"q:v": 2})

过滤器函数

stream.Filter(filterName string, args ffmpeg.Args, kwargs ...ffmpeg.KwArgs) *ffmpeg.Stream

应用 FFmpeg 过滤器。

  • filterName: 过滤器名称
  • args: 位置参数
  • kwargs: 可选的关键字参数

示例:

// 使用 select 过滤器选择特定帧
stream := ffmpeg.Input("input.mp4").Filter("select", ffmpeg.Args{"gte(n,60)"})

// 使用 fps 过滤器设置帧率
stream := ffmpeg.Input("input.mp4").Filter("fps", ffmpeg.Args{"1"})

参数类型

ffmpeg.Args

字符串切片类型 []string,用于传递过滤器的位置参数。

示例:

// 传递单个参数
ffmpeg.Args{"1"}

// 传递表达式作为参数
ffmpeg.Args{"gte(n,60)"}

ffmpeg.KwArgs

映射类型 map[string]interface{},用于传递关键字参数。

示例:

// 设置输出帧数
ffmpeg.KwArgs{"vframes": 1}

// 设置输出格式和编码器
ffmpeg.KwArgs{"format": "image2", "vcodec": "mjpeg"}

执行和输出控制函数

stream.Run() error

执行 FFmpeg 命令。

stream.WithOutput(w io.Writer, stderr io.Writer) *ffmpeg.Stream

重定向输出到指定的 Writer。

  • w: 标准输出的目标 Writer
  • stderr: 标准错误的目标 Writer

stream.OverWriteOutput() *ffmpeg.Stream

允许覆盖已存在的输出文件。

过滤器表达式

select 过滤器

select 过滤器用于选择特定的帧,常用表达式:

  • eq(n,10): 选择第10帧
  • gte(n,60): 选择帧号大于等于60的帧
  • lte(n,100): 选择帧号小于等于100的帧
  • eq(pict_type,I): 选择I帧
  • not(mod(n,3)): 选择每3帧中的1帧

fps 过滤器

fps 过滤器用于控制输出的帧率:

  • fps=1: 每秒输出1帧
  • fps=1/5: 每5秒输出1帧
  • fps=10: 每秒输出10帧

示例代码

package main

import (
	"bytes"
	"fmt"
	"os"
	"path/filepath"

	"github.com/disintegration/imaging"
	ffmpeg "github.com/u2takey/ffmpeg-go"
)

func main() {
	// 输入视频文件路径
	inputPath := "./input.mp4"
	// 输出目录
	outputDir := "./frames"

	// 确保输出目录存在
	if err := os.MkdirAll(outputDir, 0755); err != nil {
		fmt.Printf("创建输出目录失败: %v\n", err)
		return
	}

	// 示例1: 抽取单帧作为封面
	coverPath := filepath.Join(outputDir, "cover.jpg")
	if err := extractCoverFrame(inputPath, coverPath); err != nil {
		fmt.Printf("抽取封面失败: %v\n", err)
	} else {
		fmt.Printf("成功抽取封面: %s\n", coverPath)
	}

	// 示例2: 每秒抽取一帧
	if err := extractFramesPerSecond(inputPath, outputDir); err != nil {
		fmt.Printf("每秒抽帧失败: %v\n", err)
	} else {
		fmt.Println("成功完成每秒抽帧")
	}

	// 示例3: 抽取指定时间点的帧
	timePoints := []float64{1.5, 3.0, 5.5, 8.0}
	if err := extractFramesAtTimePoints(inputPath, outputDir, timePoints); err != nil {
		fmt.Printf("抽取指定时间点的帧失败: %v\n", err)
	} else {
		fmt.Println("成功抽取指定时间点的帧")
	}

	// 示例4: 抽取指定帧号的帧
	frameNumbers := []int{10, 50, 100, 200}
	if err := extractFramesByNumber(inputPath, outputDir, frameNumbers); err != nil {
		fmt.Printf("抽取指定帧号的帧失败: %v\n", err)
	} else {
		fmt.Println("成功抽取指定帧号的帧")
	}
}

// extractCoverFrame 从视频中提取一帧作为封面
func extractCoverFrame(inputPath, outputPath string) error {
	// 从视频的第2秒处抽取一帧作为封面
	buf := bytes.NewBuffer(nil)
	err := ffmpeg.Input(inputPath).
		Filter("select", ffmpeg.Args{"gte(n,60)"}). // 抽取第60帧
		Output("pipe:", ffmpeg.KwArgs{"vframes": 1, "format": "image2", "vcodec": "mjpeg"}).
		WithOutput(buf, nil).
		Run()

	if err != nil {
		return fmt.Errorf("ffmpeg抽帧失败: %v", err)
	}

	// 解码JPEG图像
	img, err := imaging.Decode(buf)
	if err != nil {
		return fmt.Errorf("解码图像失败: %v", err)
	}

	// 保存图像到文件
	err = imaging.Save(img, outputPath)
	if err != nil {
		return fmt.Errorf("保存图像失败: %v", err)
	}

	return nil
}

// extractFramesPerSecond 每秒从视频中提取一帧
func extractFramesPerSecond(inputPath, outputDir string) error {
	// 获取视频信息
	_, err := ffmpeg.Probe(inputPath)
	if err != nil {
		return fmt.Errorf("获取视频信息失败: %v", err)
	}

	// 使用ffmpeg的fps过滤器,每秒抽取一帧
	err = ffmpeg.Input(inputPath).
		Filter("fps", ffmpeg.Args{"1"}). // 每秒1帧
		Output(filepath.Join(outputDir, "frame-%03d.jpg"), ffmpeg.KwArgs{"q:v": 2}).
		OverWriteOutput().
		Run()

	if err != nil {
		return fmt.Errorf("ffmpeg抽帧失败: %v", err)
	}

	return nil
}

// extractFramesAtTimePoints 在指定的时间点抽取帧
func extractFramesAtTimePoints(inputPath, outputDir string, timePoints []float64) error {
	for _, timePoint := range timePoints {
		outputPath := filepath.Join(outputDir, fmt.Sprintf("time-%.1f.jpg", timePoint))

		// 使用ffmpeg的-ss参数指定时间点
		err := ffmpeg.Input(inputPath, ffmpeg.KwArgs{"ss": timePoint}).
			Output(outputPath, ffmpeg.KwArgs{"vframes": 1, "q:v": 2}).
			OverWriteOutput().
			Run()

		if err != nil {
			return fmt.Errorf("在时间点%.1f抽帧失败: %v", timePoint, err)
		}

		fmt.Printf("成功抽取时间点%.1f的帧: %s\n", timePoint, outputPath)
	}

	return nil
}

// extractFramesByNumber 抽取指定帧号的帧
func extractFramesByNumber(inputPath, outputDir string, frameNumbers []int) error {
	for _, frameNum := range frameNumbers {
		outputPath := filepath.Join(outputDir, fmt.Sprintf("frame-%d.jpg", frameNum))

		// 使用select过滤器选择特定帧号
		buf := bytes.NewBuffer(nil)
		err := ffmpeg.Input(inputPath).
			Filter("select", ffmpeg.Args{fmt.Sprintf("eq(n,%d)", frameNum)}).
			Output("pipe:", ffmpeg.KwArgs{"vframes": 1, "format": "image2", "vcodec": "mjpeg"}).
			WithOutput(buf, nil).
			Run()

		if err != nil {
			return fmt.Errorf("抽取帧号%d失败: %v", frameNum, err)
		}

		// 解码并保存图像
		img, err := imaging.Decode(buf)
		if err != nil {
			return fmt.Errorf("解码帧号%d的图像失败: %v", frameNum, err)
		}

		err = imaging.Save(img, outputPath)
		if err != nil {
			return fmt.Errorf("保存帧号%d的图像失败: %v", frameNum, err)
		}

		fmt.Printf("成功抽取帧号%d的帧: %s\n", frameNum, outputPath)
	}

	return nil
}

常见问题

  1. FFmpeg 未安装或不在 PATH 中

    确保系统中已安装 FFmpeg 并添加到 PATH 环境变量中。可以通过运行 ffmpeg -version 命令来验证。

  2. 抽帧失败

    检查视频文件是否存在且格式正确。某些视频格式可能需要特定的解码器。

  3. 帧号超出范围

    确保指定的帧号不超过视频的总帧数。可以使用 ffmpeg.Probe() 函数获取视频信息。

  4. 内存使用过高

    处理大型视频文件时,可能需要优化内存使用。考虑分段处理或降低输出图像质量。

  5. Windows 路径问题

    在 Windows 系统上,注意使用正确的路径分隔符。建议使用 filepath.Join() 函数构建路径。