图1 S3C6410视频编解码软件架构 具体操作方法如下: ① 通过open函数打开编解码器设备文件; ② 使用mmap方法在用户空间和驱动空间之间映射输入/输出缓存空间,这样做的好处是可以快速进行数据输入/输出; ③ 通过ioctl设备编解码参数,初始化编解码器; ④ 输入数据,通过ioctl执行编解码过程,输出数据; ⑤ 通过close方法关闭编解码器设备文件。 值得注意的是,无论编码还是解码,处理的数据都是以一帧帧的形式操作的,所以第4步是一个不断循环的过程,直到所有数据处理完成。另外,虽然编解码器以设备文件的形式使用,但是它不能使用标准的文件读写操作,查看编解码的设备驱动可以发现,其文件读写函数是空的,这一点三星公司的开发文档并没有说明。 3 H.264硬件编解码实现 FFmpeg的H.264硬件编解码[5]实现就是自定义一个视频编解码器,加入到FFmpeg库中。这个视频编解码器使用S3C6410处理视频硬件编解码功能来实现H.264的视频编码和解码过程,这样使用FFmpeg库的多媒体程序可以用访问FFmpeg其他编解码器一样的方法使用这个自定义的编解码器。添加自定义编解码器的关键是根据FFmpeg中对编解码的描述定义编解码器,并实现定义中的相关函数。 在libavcodec/avcodec.h中的AVCodec结构体是定义FFmpeg编解码器的关键结构体,包括编解码器的名字、类型(声音/视频)、编解码器的识别号(CodecID)、支持格式和一些用于初始化、编码、解码和关闭的函数指针。 typedef struct AVCodec { const char *name; enum CodecType type; enum CodecID id; int priv_data_size; int (*init)(AVCodecContext *); int (*encode)(AVCodecContext *,uint8_t *buf,int buf_size,void *data); int (*close)(AVCodecContext *); int (*decode)(AVCodecContext *,void *outdata,int *outdata_size, uint8_t *buf,int buf_size); int capabilities; struct AVCodec *next; void (*flush)(AVCodecContext *); const AVRational *supported_framerates; const enum PixelFormat *pix_fmts; } AVCodec; H.264硬件编解码器定义如下: AVCodec s3cx264_encoder = { .name="s3cx264", .type=AVMEDIA_TYPE_VIDEO, .id=CODEC_ID_H264, .init=X264_init, .encode=X264_frame, .decode=X264_decode, .close=X264_close, … }; 解码器的名字为s3cx264,类型为视频。CodecID为H264,表示这个解码器用于H.264视频编解码。初始化、编码、解码和关闭函数指针分别指向X264_init、X264_frame、X264_decodec和X264_close函数。 添加s3cx264编解码器到编解器链中,关键是通过修改libavcodec/allcodecs.c文件实现,修改如下: REGISTER_ENCDEC (ASV1,asv1); REGISTER_ENCDEC (S3CX264,s3cx264); //添加s3cx264编解码器 REGISTER_ENCDEC (ASV2,asv2); 这样,在程序运行时调用av_register_all(void)函数后,就可以把自定义的编解码器s3cx264添加到FFmpeg存放在内存中的解编码器链中。值得提出的是,对同一个视频格式FFmpeg有多个编解码器与之相对应。如H.264格式的视频,FFmpeg本身就带有对应的软解码器,现在添加了硬解码器,为了避免不确定是哪一个解码器在执行,可以把自定义的硬件编解码器在注册时放在注册过程的最前面,这样编解码器在添加到解编器链中时就会放在靠前的位置,查找时就可以优于软件解码器找到硬解码器。 把硬件编解码器s3cx264注册到编解码器链后,还要完成X264_init、X264_frame、X264_decodec和X264_close函数,编解码器才能正常工作。以下结合前面对S3C6410视频编解码过程的分析,以编码为例详细阐述实现过程。 定义X264Context结构体,保存设备文件描述符、编码参数和输入/输出地址等信息,用于FFmpeg模块间数据的传递: typedef struct X264Context { int dev_fd; uint8_t *addr; s3c_mfc_enc_init_arg_t enc_init; s3c_mfc_enc_exe_arg_t enc_exe; s3c_mfc_get_buf_addr_arg_t get_buf_addr; uint8_t *in_buf,*out_buf; AVFrame out_pic; } X264Context; X264_init实现的是编码器初始化过程, 用于编码器设备文件的打开、内存空间的映射、编码参数设置和获取编解码数据输入/输出地址。 static av_cold int X264_init(AVCodecContext *avctx){ X264Context *x4 = avctx>priv_data; //打开编码器设备文件 x4>dev_fd = open(MFC_DEV_NAME,O_RDWR|O_NDELAY); //内存空间映射 x4>addr = (uint8_t *) mmap(0,BUF_SIZE,PROT_READ |PROT_WRITE,MAP_SHARED,x4>dev_fd,0); //编码参数设置 ioctl(x4>dev_fd,S3C_MFC_IOCTL_MFC_H264_ENC_INIT,&x4>enc_init); //获取输入/输出地址 x4>get_buf_addr.in_usr_data = (int)x4>addr; ioctl(x4>dev_fd,S3C_MFC_IOCTL_MFC_GET_YUV_BUF_ADDR,&x4>get_buf_addr); x4>in_buf = (uint8_t *)x4>get_buf_addr.out_buf_addr; x4>get_buf_addr.in_usr_data = (int)x4>addr; ioctl(x4>dev_fd,S3C_MFC_IOCTL_MFC_GET_LINE_BUF_ADDR,&x4>get_buf_addr); x4>out_buf = (uint8_t *)x4>get_buf_addr.out_buf_addr; return 0; } ioctl的参数为S3C_MFC_IOCTL_MFC_H264_ENC_INIT,表示使用H.264编码。 X264_frame函数执行编码过程。需要注意的是data参数保存了需要编码的数据,是一个四维的数组,要把它转换成一维数组用于S3C6410编码器输入。另外,编码数据存在空的情况,也就是空帧。这是需要处理的,方法是返回“0”,表示没有输出数据,否则程序运行时会出现段错误。 static int X264_frame(AVCodecContext *ctx,uint8_t *buf,int bufsize,void *data){ …… //空间转换 if(frame){ memcpy(x4>in_buf,frame>data[0],ctx>width*ctx>height); memcpy(x4>in_buf+ctx>width*ctx>height,frame>data[1],ctx>width*ctx>height/4); memcpy(x4>in_buf+ctx>width*ctx>height+ctx>width*ctx>height/4,frame>data[2], ctx>width*ctx>height/4); } else return 0;//空帧,返回 //执行编码过程 ioctl(x4>dev_fd,S3C_MFC_IOCTL_MFC_H264_ENC_EXE,&x4>enc_exe); //编码数据输出 bufsize = x4>enc_exe.out_encoded_size; memcpy(buf,x4>out_buf,bufsize); …… return bufsize; } X264_close关闭函数用于编码结束后的资源释放,包括取消空间映射和关闭设备文件。 static av_cold int X264_close(AVCodecContext *avctx){ … //取消空间映射 munmap(x4>addr,BUF_SIZE); //关闭设备文件 close(x4>dev_fd); return 0; } 解码函数的实现过程类似于编码函数,包括空间转换、执行解码和解码数据输出。初始化时使用S3C_MFC_IOCTL_MFC_H264_DEC_INIT参数,执行时使用S3C_MFC_IOCTL_MFC_H264_ENC_EXE参数。 4 运行测试 s3cx264编解码器添加到FFmpeg后,可以通过以下方式测试: ① 用如下命令编译FFmpeg。 ./configure enablecrosscompile arch=armv6 cpu=armv6 targetos=linux crossprefix =/usr/local/arm/4.3.2/bin/ armlinux ② 运行 ./ffmpeg codecs查看可以找到s3cx264编解码器,如图2所示。
图2 FFmpeg显示s3cx264编解码器信息 ③ 结合USB摄像头测试s3cx264编码。运行 ./ffmpeg s 320x240 r 50 f video4linux2 i /dev/video2 vcodec s3cx264 test.mp4 可以看到FFmpegg正使用s3cx264编码器将USB摄像头采集的数据编码压缩成test.mp4文件。test.mp4能够正常播放显示。 以上测试说明已经成功地将s3cx264硬件视频编码器添加到了FFmpeg中,能够编码视频数据,可以运用到其他使用FFmpeg库的多媒体程序中。 结语 对于多媒体开发来说,编解码时使用FFmpeg多媒体库是一个不错的选择,支持较多的音视频编解码,编程接口简单易用。了解FFmpeg编解码过程,熟悉FFmpeg硬件编解码器添加方法,对多媒体开发,尤其是资源有限的嵌入式多媒体开发有很大帮助。本文通过分析FFmpeg视频编解码过程和三星S3C6410处理器视频硬件编解码方法,在FFmpeg库中成功添加S3C6410硬件编解码器,使FFmpeg库具有H.264视频格式的硬件编解码能力,可运用于游戏设备、监控设备、视频会议设备和数字网络电视等嵌入式系统中,同时也为其他嵌入式设备添加别的视频格式的编解码器到FFmpeg多媒体库提供了参考。 参考文献 [1] http://www.ffmpeg.org/ . [2] Samsung.S3C6410 Datasheet,2010. [3] 李少春.基于FFMPEG的嵌入式视频监控系统[J].电子技术,2007(3):3437. [4] API Document S3C6400/6410 MultiFormat Codec,2008. [5] FFmpeg codec HOWTO[EB/OL].2010[201101].http://wiki.multimedia.cx/index.php?title=FFmpeg_codec_HOWTO/. 刘建敏(硕士生)、杨斌(教授),主要研究方向为单片机与嵌入式系统及应用。