26

如何将ffmpeg封装golang/cgo库

 5 years ago
source link: https://studygolang.com/articles/17855?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

本文通过ffmpeg编程的例子来讲述如何封装cgo库

更多内容访问 我的博客

前言

继上一篇 ffmpeg音视频C编程入门 , 使用高性能的C语言进行音视频的处理,比较执行效率比较高,但是业务需求,快捷开发需要使用更方便的语言,比如 golang,本文介绍如何将 将视频转成GIF 的C语言方法封装成 golang 方法以便调用。(不明白的同学请点击上面链接多了解)

认识cgo的封装技巧

最简单的 cgo 封装例子看这篇cgo快速入门

我这里讲几个注意事项

  • CGO构建程序会自动构建当前目录下的C源文件,即是 go 会将当前目录下 .c 文件都编译成 .o目标文件,再链接汇编,这个特点衍生出几个注意事项:

    • go 代码以静态库或动态库方式引用 C 函数的话,需要将对应的C源文件移出 go源文件所在的目录
    • 如果想要将 C 函数编译到 go 程序,就需要将 C源文件与 go 文件放在同一目录下
  • 在C/C++混编下, go 中引用 C 函数,需要将 C 函数名置于全局,即 extern C

开始编程

第一步,处理例子中已经写好的 gen_gif 方法,修改 gen_gif.h 文件

#ifdef __cplusplus
extern "C" {
#endif

int gen_gif(const int gifSeconds, const int rotate, void* data, int data_size, void* outBuf, int outBufLen, int *outSize);

#ifdef __cplusplus
}
#endif

在处理 ffmpeg 的 av_log 日志的回调方法

/* log.c */
/* 
mpeg 的日志库用法

#include <libavutil/log.h>
// 设置日志级别
av_log_set_level(AV_LOG_DEBUG)
// 打印日志
av_log(NULL, AV_LOG_INFO,"...%s\n",op)

*/

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <libavutil/log.h>

// 定义输出日志的函数,留白给使用者实现
extern void Ffmpeglog(int , char*);

static void log_callback(void *avcl, int level, const char *fmt, va_list vl) 
{
    (void) avcl;
    char log[1024] = {0};
    // int vsnprintf(buffer,bufsize ,fmt, argptr) , va_list 是可变参数的指针,相关方法有: va_start(), type va_atg(va_list, type), va_end()
    int n = vsnprintf(log, 1024, fmt, vl);
    if (n > 0 && log[n - 1] == '\n')
        log[n - 1] = 0;
    if (strlen(log) == 0)
        return;
    Ffmpeglog(level, log);
}

void set_log_callback()
{
    // 给 av 解码器注册日志回调函数
    av_log_set_callback(log_callback);
}

第二步,编写 go 文件

package c

/*
#include <stdlib.h>
#include "gen_gif.h"
#include "log.h"
#cgo LDFLAGS: -lavcodec -lavformat -lswscale -lavutil -lavfilter -lm
*/
import "C"

import (
    "errors"
    "fmt"
    "unsafe"
)

func init() {
    C.set_log_callback()
}

var logger func(s string) = nil

func SetFfmpegLogger(f func(s string)) {
    logger = f
}

//export Ffmpeglog
func Ffmpeglog(l C.int, t *C.char) {
    if l <= 32 {
        if logger == nil {
            fmt.Printf("ffmpeg log:%s\n", C.GoString(t))
        } else {
            logger(fmt.Sprintf("ffmpeg log:%s\n", C.GoString(t)))
        }
    }
}

func GenGif(second, rotate int, input []byte) (err error, output []byte) {
    buf := make([]byte, 1<<20)
    var outsz C.int
    ret := C.gen_gif(C.int(second), C.int(rotate), unsafe.Pointer(&input[0]), C.int(len(input)), unsafe.Pointer(&buf[0]), C.int(len(buf)), &outsz)
    if ret != 0 {
        return errors.New(fmt.Sprintf("error, ret=%v", ret)), nil
    }
    output = make([]byte, outsz)
    copy(output, buf[:outsz])
    return nil, output
}
  • 使用动态链接的方式,调用 ffmpeg 的 libav* 的函数库,设置 #cgo LDFLAGS 的动态链接选项
  • 实现 log.c 中提供使用者实现的函数,使用 go 语言的方法打印日志
  • 处理调用 C 函数传参的指针、变量,通过 import "C" 提供的方法转化变量

测试代码

将上文编写好的代码,提交到代码仓库,或者在本地的 GOPATH 中建好相应的目录,如笔者的 github.com/lightfish-zhang/mpegUtil/c 路径。

接下来,可以在业务需要的地方使用 GenGif() 了,让我们来测试一下

package main

import (
    "fmt"
    "io/ioutil"
    "os"

    mpegUtil "github.com/lightfish-zhang/mpegUtil/c"
)

func main() {
    if len(os.Args) < 3 {
        fmt.Printf("Usage: %s <input file> <output file>\n", os.Args[0])
        os.Exit(1)
    }
    inFile, err := os.Open(os.Args[1])
    if err != nil {
        fmt.Printf("open file fail, path=%v, err=%v", os.Args[1], err)
        os.Exit(1)
    }
    outFile, err := os.Create(os.Args[2])
    if err != nil {
        fmt.Printf("create file fail, path=%v, err=%v", os.Args[2], err)
        os.Exit(1)
    }
    input, err := ioutil.ReadAll(inFile)
    if err != nil {
        fmt.Printf("read file fail, err=%v", err)
        os.Exit(1)
    }

    err, output := mpegUtil.GenGif(5, 90, input)
    if err != nil {
        fmt.Printf("generate gif fail, err=%v", err)
        os.Exit(1)
    }
    _, err = outFile.Write(output)
    if err != nil {
        fmt.Printf("write file fail, err=%v", err)
        os.Exit(1)
    }
}

我在本地执行,对某一个 mp4 文件进行转码 GIF,如

./genGif test.mp4 test.gif

运行成功,在我的电脑上可以看到完美的 GIF 图片!

动态链接或者静态链接

golang 语言的优势是打包出一个静态的执行文件,可以在相同平台下运行。但是,我们例子是动态链接了 ffmpeglibav*.so ,在编译或者部署时,都需要在机器上安装好 ffmpeg 库。

有没有便利的方法进行编译或部署呢?

打包动态链接库

一个使用 LD_LIBRARY_PATH 指定动态链接目录的小技巧,不过这个技巧需要 编译机器与部署机器的运行环境差不多,除了 libav*.so 可以没有预先安装在部署机器。为了保持 libc.so 的函数可用,最好机器之间的 linux 版本一样。

使用 ldd genGif 查看编译出来的执行文件,需要动态链接哪些 *.so 文件,将关键的文件拷贝出来,比如 libav* 的 so 文件是必须的,其他比如 libm.so , libz.so 视部署机器有没有而定。

部署时候,需要把以上找到的 *.so 一起拷贝到目标机器上,同时在运行 golang 程序时,设置好全局变量 LD_LIBRARY_PATH ,指向 *.so 文件目录。

静态链接编译

这个方法也需要机器的 linux 最好保持一致,ffmpeg 依赖的 libc.so 的函数好像会随着 linux 版本不同而有所差异。

准备好各种依赖库的静态库,参照如下命令

  • 编译C例子
g++ -o gen_gif -I./ -I./ffmpeg/include -I/usr/local/include main.o gen_gif.a ./ffmpeg/lib/libavdevice.a ./ffmpeg/lib/libavfilter.a ./ffmpeg/lib/libavformat.a ./ffmpeg/lib/libavcodec.a  ./ffmpeg/lib/libavutil.a  ./ffmpeg/lib/libswreample.a  ./ffmpeg/lib/libswcale.a  ./lib/liblzma.a ./lib/libm.a ./lib/libz.a ./lib/libbz2.a -lpthread
  • 编译 golang 例子,修改 #cgo 命令
/*
#cgo CFLAGS: -I./ffmpeg/include -I/usr/local/include
#cgo LDFLAGS: -L./lib -L./ffmpeg/lib
*/

Reference

cgo快速入门


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK