最近正在基于机器学习搭建一个多媒体分析平台,一方面鉴于组内成员多则有近两年的Go使用经验,少则也有半年的Go使用经验,
另一方面由于Go的格式统一、工程系统能力强大,所以选择Go为主要的开发语言。而对于多媒体分析,第一步就是图片视频的编解码,图片好说,而视频就比较难了。普通的编解码可以使用 exec
调用 ffmpeg
,但要获取视频每帧的数据内容,就需要使用 ffmpeg
的API了。通过 github
,我们找到了 go-libav
这个库,相比其他的 go binding of ffmpeg libraries
,这个库有以下几个优点:
ffmpeg 3
,也支持 ffmpeg 2
,但已废弃 ffmpeg
API 的简单封装,而是以更加go的形式进行封装
其中第二点和第三点是我最欣赏这个库的主要原因,相比与其他 ffmpeg
库的直接封装, go-libav
库加入了更多的语言易用性的思考。但是,目前这个库还在持续的开发中,还存在下面几个问题:
avcodec avfilter avformat avutil
这四个库的一些基础API ffmpeg API
的经验,上手较难
我们近期已经为 avutil
扩展了一些功能,正在添加examples和单元测试,后续会提 Merge Request
反馈到这个库。在使用这个库的过程中,我们踩了一些 cgo
的坑,在这里总结一下 cgo
的使用方法和注意问题。
cgo
可以在 go
中调用 C
,也可以在 C
中调用 go
。但因为 go
和 C
垃圾回收以及使用方式的不同,建议尽量避免使用 cgo
。
使用 cgo
的方法比较 怪异
,在 go
的源代码中把 C
代码作为注释来写,并标明依赖的库文件和路径,最后使用 import "C"
即可。比如要使用 C
中 stdlib.h
中的 random
函数,可以这么写:
package main /* #include <stdlib.h> */ import "C" import "fmt" func main() { rand := int(C.random()) fmt.Println("get random value from C", rand) }
注意,一般使用 import
会把所有要使用的包放在一起,比如:
import ( "fmt" "os" )
但使用 cgo
是个例外,必须给 import "C"
单独一行,且必须放在注释的 C
代码后面一行。
下面就是 cgo
和 go
对应类型的转换了。进行类型转换的目的很简单,就是为了在 C
中使用C的类型,在go中使用go的类型。
go的标准类型转换为C的标准类型比较简单,直接使用 C.char
, C.schar (signed char)
, C.uchar (unsigned char)
, C.short
, C.ushort (unsigned short)
, C.int
, C.uint (unsigned int)
, C.long
, C.ulong (unsigned long)
, C.longlong (long long)
, C.ulonglong (unsigned long long)
, C.float
, C.double
, C.complexfloat (complex float)
以及 C.complexdouble (complex double)
,这些类型已经可以满足基本的数值运算了。
例子:要调用一个参数类型为int的C函数,这个函数返回一个int值,在go中需要将返回值做类型转换才可以使用:
var goInt int ret := int(C.cfunc(C.int(goInt))) ...
go
字符串转换为 C
字符串: C.CString(gostr string)
,返回的是 C
中的 *char
,这里返回的 *char
不会被 go
的垃圾回收清理,所以需要自行释放调,可以这么使用 defer C.free(unsafe.Pointer(cstr))
。 C
字符串转换为 go
字符串: C.GoString(cstr string)
,返回的是 go
的 string
。还有一个类似的函数,通过设置长度,可以取一段子字符串, C.GoString(cstr *C.char, length C.int)
。 C.struct_xxx
。比如, C_struct_AVOption
struct
类似,可以使用 C.union_xx
和 C.enum_xx
,比如, C.enum_AVPictureType
。
这样使用起来确实有些别扭,但是封装C的代码时,但遵循一定的方法,也可以让封装库的内部和外部调用都 go-style
。其实方法很简单,想想如果用go写 struct
和 enum
时,是怎么写的?
对于C的 struct
,我们可以新建一个go的 struct
,把 C.struct_xx
作为其中的一个元素,比如:
type PixelFormatDescriptor struct { CAVPixFmtDescriptor *C.AVPixFmtDescriptor } func NewPixelFormatDescriptorFromC(cCtx unsafe.Pointer) *PixelFormatDescriptor { return &PixelFormatDescriptor{CAVPixFmtDescriptor: (*C.AVPixFmtDescriptor)(cCtx)} } func FindPixelFormatDescriptorByPixelFormat(pixelFormat PixelFormat) *PixelFormatDescriptor { cDescriptor := C.av_pix_fmt_desc_get(C.enum_AVPixelFormat(pixelFormat)) if cDescriptor == nil { return nil } return NewPixelFormatDescriptorFromC(unsafe.Pointer(cDescriptor)) } func (d *PixelFormatDescriptor) Name() string { return C.GoString(d.CAVPixFmtDescriptor.name) } func (d *PixelFormatDescriptor) ComponentCount() int { return int(d.CAVPixFmtDescriptor.nb_components) }
这样看起来是不是有了 go-style
?可以使用 NewPixelFormatDescriptorFromC
和 FindPixelFormatDescriptorByPixelFormat
这两个方法创建go的结构体 PixelFormatDescriptor
,后面的调用方法就非常简单明了了。
注意,这里用到了 unsafe.Pointer
这个类型,你可以把它的作用简单的理解为C中的 void*
,从上面的例子也可以看出,主要用来做类型转换的。
在上面的例子中,还有这样一个类型 PixelFormat
,它的定义是
type PixelFormat C.enum_AVPixelFormat
这样,在后续的传参和调用时,使用 PixelFormat
会更加简单些。
同时,我们也可以看到,调用C的结构体中的元素时,也很简单:
CAVPixFmtDescriptor.nb_components
直接加点就可以访问其成员。
有了上面的知识,做一些简单的封装应该没有问题,要注意的地方就是类型转换,尤其是涉及到指针时,更要小心谨慎。如果觉得难以处理,就可以使用自定义函数的方法,把复杂的类型转换拆解为简单的函数调用,这时只要注意C代码的编写规范就可以了。
以上是自己这段时间使用cgo和阅读源码的一些总结,网上有人会说使用cgo很难,其实只是cgo的用法与go有差异,一旦涉及到C,可能就会让人望而却步。其实不然,用好cgo有以下几个方面:
unsafe.Pointer