如何將ffmpeg封裝golang/cgo庫

本文通過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快速入門

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章