通过C++/CLI使用FFMPEG库进行视频解码[初步]

  所有源代码均为共有领域,您可以对他做任何事情。

         源代码:https://github.com/slayercat/FFMPEG_H264_VIDEO_PLAYER

 

其实我现在还不知道这么写是不是对的,因为有种想法告诉我FFmpeg是在特指FFmpeg这个应用程序,而我们使用的是他提供的编程接口。

 

使用的是C++/CLI,做出这个选择之前我看了一下例程,有大量的struct,如果使用C#的话,会有大量的转换工作要做,而且我对这个并不熟悉。使用C++/CLI的话,可以同时调用C++的原生本机代码和CLI的托管代码。

 

很多东西其实我也不知道,因为我只用C++/CLI做过一个很小的应用程序。不过没关系,有些东西写在这里或许会对某些同学有所帮助。

 

关于理解程序需要的C++/CLI的一些语法

 

对于托管库(如.net Freamwork)来说,所有的静态方法/命名空间里头引用类等等等等,只要不涉及到具体的成员,全部使用“::”运算符指定。例如:

 

System::Void

 

对于具体托管对象而言,全部需要使用“->”运算符,以指定对该变量的成员进行操作。例如:

 

textBox1->Text

 

所有在托管堆上进行的新建操作,全部需要使用gcnew关键字。该关键字相当于csharp里头的new,返回的是一个叫做“句柄”的东西——这是我从网上看到的,但是实际上这个和我们熟知的,windows内核的“句柄”不是一个东西。无论如何,就叫他某个对象的句柄吧,相当于某个对象的指针,但是指向的对象没有其他对象引用的时候会在适当的时候被垃圾回收——所以叫做托管堆。

 

句柄用“^”运算符声明,例如:

 

array<Byte>^ out_buf = gcnew array<Byte>(bmpheader.bfSize);

 

nullptr关键字用于表示托管堆上的空,任何托管堆句柄在没有gcnew之前都是nullptr,相当于csharp里头的null关键字

 

知道了这些,大致就可以用csharp的方法写了。

 

不过还有几个东西……对于我们的混合编程非常重要……那就是托管堆和非托管堆之间的复制。要不然两者之间是不能进行信息交换的。

 

一个类叫做IntPtr用于完成这个操作,这个类用于封装托管堆里头对非托管部分指针的调用。

 

IntPtr(&bmpinfo.bmiHeader)

 

单纯知道这个没啥用,但是一个拷贝函数就用到了这个:

 

Marshal::Copy函数~~用于将数据从非托管8位数组拷贝到托管8位数组。

 

大致是这样的:

 

Copy(源的指针的intptr封装,目标Array的句柄,要写到目标Array的开始点,8bit数组的个数。

 

一个栗子:

 

Marshal::Copy(IntPtr(&bmpinfo.bmiHeader), out_buf, currectPoint, sizeof(BITMAPINFOHEADER));

 

有了这些C++/CLI的东西,大致就够了,要是还有不明确的,可能就需要自己google一下了。

 

接下来继续扯没有扯完的题目问题

 

关于FFMPEG和libavcodec

 

FFMPEG官网上的描述表明,FFMPEG是一个开源的解决方案,用于记录,转换和流处理音频与视频。包括一个叫做libavcodec的库~~这个库里头是音频和视频的编解码器。

 

下载windows版本的库

 

可以从官网down到源代码,可惜这个东西对我们用处不大,我们需要一些别的东西……

 

注意到download里头的这句话:

 

FFmpeg Windows Builds are available at Zeranoe FFmpeg Builds.

 

OK,这里头有下载,我们需要的是Builds (Shared) 和Builds (Dev)。

 

前者里头有一堆的dll,后者有一堆的lib和头文件。

 

下32bit还是64bit就仁者见仁智者见智了,但是我刚开始本能的下载了个x64的build,结果MS的编译器给我报告了一堆的链接错误,没有明白是怎么回事,以为是名称改编的问题,改了大概一个小时的设置,然后扔到一边干别的去了,两天后才发现自己的工程是32bit的。改为32bit的库直接ok。

 

使用64bit的童鞋需要注意这一点。要么用32bit的库,要么将项目先调整为64bit的。

 

用惯了csharp里头没有32bit和64bit的区别,这个是需要特别注意的。

 

下载好了之后,放到项目根目录下头,然后修改include的路径。

 

修改路径及lib配置

 

 

放个图

 

位置在:配置属性——>C/C++——>常规——>附加包含目录。

 

这个地方建议改一下,否则就需要使用对项目根目录的绝对路径进行include,感觉上是比较麻烦的。

 

然后是链接的问题,解压出的那堆lib放在项目的根目录下的某个文件夹里头。

 

然后,我们看链接器

 

位置在:配置属性——>链接器——>附加库目录

 

这里配置找lib文件的目录。然后看输入。

 

这个里头有个附加依赖项,将你下载到的所有lib的文件名(全名),写在这里头。每个一行。

 

 

确认配置是否成功

 

试着在stdafx.h里头加如下几行并编译:

 

    #include "libavcodec/avcodec.h"

    #include "libavformat/avformat.h"

#include "libswscale/swscale.h"

 

如果没有报告错误,说明附加包含目录改对了。

 

但是有一点问题……编译出来的lib文件,可以用相关工具看到,全是用的c调用格式。因此,在include语句前后,应当用extern “C”括起来,即:

 

extern "C" {

    #include "libavcodec/avcodec.h"

    #include "libavformat/avformat.h"

    #include "libswscale/swscale.h"

}

 

现在,在你项目的main里头,写上这一句并编译:

 

av_register_all();

 

如果没有问题,那么恭喜你,编译连接成功了。

 

这个时候如果你运行,会提示缺少文件,将你的shared库里头的所有dll都拷贝到生成的可执行文件目录下就可以了。

 

如果不成功怎么办?

 

如果include提示找不到,用相对于项目根目录的绝对路径试一试。

 

例如:我的项目建立在D:\temp_ffmpeg\FFMPEG_H264_PLAYER\FFMPEG_H264_PLAYER下,FFMPEG头文件在FFMPEG_INCLUDE里头。如果配置成功了,就不用写FFMPEG_INCLUDE,但是如果配置错误,那么或许你可以试试将#include "libavformat/avformat.h"改为#include "FFMPEG_INCLUDE/libavformat/avformat.h"。

 

另外,使用引号而不是尖括号,因为尖括号代表相对于系统库路径,这个行为可能是根据编译器而定的,但使用引号是一定没有错的。

 

关于链接问题,第一是必须使用c的调用约定,必须用extern "C" {}将所有的include语句包括进去。如果这个不行,将你所有引用的头文件的最前头和最后头都用extern "C" {}包括起来试试。

 

要是还不行,而且你又下载的是x64的库,那么你应该慎重的考虑是不是x32和x64的链接问题,在C++/CLI下头,他给我报告的错误莫名其妙,加了extern ”C”,但报告的错误似乎还是在寻找C++的命名约定,里头有“@”符号的那种。

 

其他的就只能自行google了。

 

关于编译问题,有个问题是没有头文件,有个头文件缺失了。这或许是linux下的标准头文件,但似乎不是win下的标准头文件~~解决方法,google这个头文件,并且下载下来。

 

另一个编译问题和vc2010/vc2012的版本有关,有个叫做stdint.h的文件,定义了一堆类似于typedef   int16_t         int_fast16_t;的东西,但这条语句产生了一个问题,我从网上google到的结果是,从VS2010开始,int_fast16_t就内置了一个数据类型,导致编译器报告一个重定义错误……

 

解决方法:将int_fast16_t和uint_fast16_t的typedef用#if _MSC_VER < 1600{}围起来。

 

我只遇到这两个和FFMPEG有关的编译问题。

 

另外,和FFMPEG无关的编译问题,倒是和C++/CLI有关的问题,有几个可能大家会遇到:

 

在头文件里头定义函数,函数没有错,IntelliSense也没有发现任何错误,但是编译提示找不到xxxx之类的东西……而这些东西明明应该有的。

 

解决方法是,.h里头只写定义,函数的实现在.cpp里头写。

 

另一个是关于类找不到的问题,包括新建的窗体和自己的托管类,实际上很简单,就是没有include头文件。但是C++/CLI继承了一个东西,就是这些头文件得写在stdafx.h里头。

 

FFMPEG是一个更新速度非常快的东西,官方提供了一个教程,实际上非常经典,很多网上的资源都是从这个里头出来的。但是有一点——对于我下载的版本来说,它已经过时了。

 

教程:An ffmpeg and SDL Tutorial,位置http://dranger.com/ffmpeg/

 

过时的函数会报告编译错误,明显的,我下载的这个版本,已经去掉了一些旧的,而且是旧的里头非常重要的函数。但大部分函数都只是换了个名字,你可以google错误信息,或者在函数名之后加deprecated。

 

唯一一个会给我们带来麻烦的东西是转换机制的改变,img_convert这个函数已经彻底的没有了。取而代之的是sws转换环境。

 

首先得取得一个转换环境,使用sws_getContext函数。并使用sws_scale和取得的转换环境进行转换,转换的结果会保存在一个frame里头。

 

我这里这两句代码:

 

SwsContext* img_convert_ctx = sws_getContext(pCodecCtx->width, pCodecCtx->height, pCodecCtx->pix_fmt, pCodecCtx->width, pCodecCtx->height, PIX_FMT_BGR24, SWS_BICUBIC, NULL, NULL, NULL);

 

sws_scale(img_convert_ctx,pFrame->data, pFrame->linesize, 0, pCodecCtx->height,pFrameRGB->data, pFrameRGB->linesize);

 

需要注意的是,转换环境是一个很重要的东西,因为他定义了转换后的样子……对于官方文档,他保存了五个帧,使用.ppm格式进行保存,使用的是RGB格式,对应的,使用的是PIX_FMT_RGB24,而我们需要的BMP格式,对应的是PIX_FMT_BGR24。如果不处理这点,结果的色彩会不正确,有些像红绿色盲的感觉——如果我没有记错的话。

 

在构造BMP文件的时候,使用了windows的两个结构:BITMAPFILEHEADER和BITMAPINFO。一个在前一个在后。BITMAPFILEHEADER里头没什么东西,就说了整个头的大小,而BITMAPINFO则有一定的重要性了,包括了宽度、高度等一系列格式信息。

 

除了构造这两个之外,就依次将BGR格式的数据写进去就可以了。使用提到过的Marshal::Copy函数。

碰到的一个死锁问题

程序用一个线程处理界面,另一个线程处理播放的解码等东西。解码用while(b_IsThreadPlayRunning)控制,若b_IsThreadPlayRunning被控制线程设置为false,则播放结束。而FFMPEG用av_read_frame(pFormatCtx, &packet)>=0来控制是否结束,若返回值大于等于0,则继续读取,小于0则结束。

 

由于b_IsThreadPlayRunning可能被多个进程访问,为了避免同步问题,必须用volatile关键字进行修饰。

 

而且,不能使用System::Boolean,该类型相当于bool的包装类,但是是托管的成员,使用这个给程序造成了同步问题——对它的操作不是原子的。

posted @ 2012-09-11 06:01  荣耀属于跪拜猫  Views(7523)  Comments(0Edit  收藏  举报