目录
1、FFMPEG简介
2、SDL简介
3、视频播放器原理
4、FFMPEG多媒体编解码库
4.1、FFMPEG库
4.2、数据类型
4.3、解码
4.3.1、接口函数
4.3.2、解码流程
4.4、存储(推送)
4.4.1、接口函数
4.4.2、存储流程
5、SDL库介绍
5.1、数据结构
5.2、接口函数
5.3、ffplay.c中的部分使用SDL的代码
6、实例代码
6.1、FFMPEG编译介绍
6.2 FFMPEG编译ffplay.c
7、演示工程
7.1、工程
7.2、TestFFLib的工程
7.2.1、FFplay.c 中的Main函数重新定义
7.2.2、TestFFLib接口函数
7.2.3、FFplay.c中增加文件输出操作
7.2.3.1、打开输出文件
7.2.3.2、输出一个AVPacket文件数据
7.2.3.3、关闭输出文件数据
7.3、演示demo
7.3.1、Lib接口函数引入
7.3.2、Demo中的部分代码:
7.3.3 DEMO APP截图
7.4、一个简单的获取RTSP流存储为MP4文件的例子
7.4.1、采用官方开发库,构建工程文件
7.4.2、直接调用FFMpeg Library代码
C++软件异常排查从入门到精通系列教程(专栏文章列表,欢迎订阅,持续更新...)https://blog.csdn.net/chenlycly/article/details/125529931C/C++实战进阶(已更新到380多篇,持续更新中...)https://blog.csdn.net/chenlycly/category_11931267.htmlVC++常用功能开发汇总(专栏文章列表,欢迎订阅,持续更新...)https://blog.csdn.net/chenlycly/article/details/124272585Windows C++ 软件开发从入门到精通(专栏文章,持续更新中...)https://blog.csdn.net/chenlycly/category_12695902.htmlC++软件分析工具从入门到精通案例集锦(专栏文章,持续更新中...)https://blog.csdn.net/chenlycly/article/details/131405795开源组件及数据库技术(专栏文章,持续更新中...)https://blog.csdn.net/chenlycly/category_12458859.html网络编程与网络问题分享(专栏文章,持续更新中...)https://blog.csdn.net/chenlycly/category_2276111.html
本文详细介绍一个基于基于开源FFmpeg和SDL2.0的音视频解码播放和存储系统的实现,希望能给大家提供一定的借鉴或参考。
1、FFMPEG简介
FFmpeg是一个开源的多媒体框架,广泛用于处理音视频处理领域。它提供了一套丰富的工具和库,支持广泛的多媒体格式的解码、编码、转码、等功能。支持多种格式的音视频编码和解码,以及音视频的转换、剪辑、过滤、流处理等功能。
FFmpeg主要特性如下:
1)音视频编解码
- FFmpeg包含了非常先进的音频/视频编解码库libavcodec,其中很多codec都是从头开发的,以保证高可移植性和编解码质量。
- 支持几乎所有的音视频格式和编解码器,包括常见的MP4、AVI、MOV、MKV、FLV、MP3、AAC等文件封装格式,以及H.264、H.265、AAC、VP9、AV1等音视频编解码器。
2)音视频处理
- 提供了丰富的音视频处理功能,如去噪、模糊视频、色彩转换、视频旋转、提取帧、缩放视频尺寸等。
- 可以通过其提供的滤镜库libavfilter对音视频数据进行各种处理,如缩放、裁剪、添加水印等。
3)流媒体处理
- FFmpeg支持将本地音视频文件转换为流媒体格式,并通过网络进行传输和播放。
- 完全支持使用HLS和MPEG-DASH打包视频,并可以配置为使用RTMP或其他协议来传输视频。
4)封装与解封装
- 提供了对各种音视频封装格式的读写支持,如MP4、FLV等。
- AVFormat模块封装了Protocol层和Demuxer、Muxer层,使得协议和格式对于开发者来说是透明的。
5)高效稳定
FFmpeg在音视频处理方面表现出色,具有高效且稳定的性能,能够处理大量的音视频数据。
6)开源FFmpeg采用LGPL或GPL许可证,其开源属性意味着任何人都能修改并使用它。
7)跨平台FFmpeg可以在多种操作系统上运行,包括但不限于Linux、Windows、macOS等,这使其具有非常广泛的适用性。
8)社区支持作为一个开源项目,FFmpeg拥有活跃的社区,用户可以从社区获得帮助和最新的开发动态。
几乎所有的视频播放器都使用到了FFmpeg的音视频解码功能,比如国内知名的暴风影音、QQ影音、腾讯视频、爱奇艺视频、优酷视频等。
FFmpeg内部支持多种音视频格式之间的相互转换,很多音视频转化软件都用到了FFmpeg的音视频转换功能,比如大家常用的格式工厂、暴风转码(暴风影音自带的工具)、QQ音影自带的视频格式转换工具、狸窝视频转换器、迅捷视频转换器等。
2、SDL简介
SDL(Simple DirectMedia Layer)是一套开源的跨平台多媒体开发库,使用 C 语言写成。它提供了绘制图像、播放声音、获取键盘输入等相关的 API,大大降低多媒体应用开发难度的同时,也让开发者只要用相同或是相似的代码就可以开发出跨多个平台(Linux、Windows、Mac OS X等)的应用软件。SDL广泛地应用于游戏开发、模拟器、媒体播放器、视频会议等多个应用领域中。
SDL 有两个常见版本:SDL1.2 和 SDL2.x。在不支持 OpenGL ES2 的嵌入式平台上,只能使用 SDL1.2,SDL2.x 依赖 OpengGL ES2。
在音视频开发领域,主要使用SDL去实现音视频的播放。SDL底层根据不同平台采用不同的渲染技术,比如在Windows平台上使用DirectX去渲染,在Linux平台上则依赖OpenGL去显示。
在Windows平台上我们可以去操作Windows平台专用的DirectX库中D3D9或D3D11去绘制视频图像,在Linux国产化桌面系统中则使用SDL库去进行视频图像的渲染与绘制(Linux平台上SDL底层使用的是OpenGL)。
3、视频播放器原理
播放视频文件的流程如下:
4、FFMPEG多媒体编解码库
FFmpeg软件包编译后会生成多个可执行文件和库文件,其中ffmpeg、ffserver、ffplay是三个主要的可执行文件。此外,还有libavcodec、libavformat、libavutil、libswscale、libpostproc等库文件,它们共同支持FFmpeg的音视频处理功能。
4.1、FFMPEG库
- libavformat:用于各种音视频封装格式的生成和解析,包括获取解码所需信息以生成解码上下文结构和读取音视频帧等功能。
- libavcodec:FFmpeg的核心模块,包含了大量的音视频编解码器,用于处理各种格式的音视频数据。几乎所有的视频播放器都用到该解码库。
- libavutil:一个通用的小型函数库,包含一些公共的工具函数,实现了CRC校验码的产生、128位整数数学、内存分配等功能,辅助多媒体编程等功能。
- libswscale:主要用于图像缩放、色彩空间转换等操作。
- libpostproc:用于后期效果处理。
- libswresample:主要用于音频重采样和格式转换。
此外,FFmpeg编译后会生成三个常用的exe工具:ffmpeg.exe、ffprobe.exe、ffplay.exe。
- ffmpeg.exe:主要用于对音视频进行处理,如剪切、抽取视频、抽取音频、增加贴纸水印等。
- ffprobe.exe:主要用于查看音视频文件格式,如将音视频文件的所有信息以JSON格式输出。
- ffplay.exe:主要用于播放视频,几乎支持所有的本地视频播放,还能支持流媒体等网络视频播放,甚至还能播放YUV视频。
4.2、数据类型
- AVFormatContext: 媒体流封装上下文
- AVInputFormat: 输入媒体流的封装格式
- AVStream: 视频流/音频流/字幕等
- AVCodecContext: 编解码上下文
- AVCodec: 编解码器
- AVPacket: 通过av_readframe得到待解码数据包
- AVFrame: 解码后帧
AVFormatContext,AVStream,AVCodecContext,AVPacket,AVFrame等,它们的关系解释如下:
一个AVFormatContext包含多个AVStream,每个码流包含了AVCodec和AVCodecContext,AVPicture是AVFrame的一个子集,他们都是数据流在编解过程中用来保存数据缓存的对像,从数据流读出的数据首先是保存在AVPacket里,也可以理解为一个AVPacket最多只包含一个AVFrame,而一个AVFrame可能包含好几个AVPacket,AVPacket是种数据流分包的概念。
4.3、解码
4.3.1、接口函数
- av_register_all(); // 初始化ffmpeg库
- avformat_network_init(); // 播放网络视频时调用
- avformat_open_input(); // 打开视频文件,获取媒体文件的封装格式和解复用的到音视频流
- avformat_find_stream_info(); // 查找音视频的流信息
- avcodec_find_decoder(); // 在库里面查找支持该格式的解码器
- avcodec_open2(); // 打开解码器
- av_read_frame(); // 读码流包
- avcodec_decode_video2(); // 视频解码
- avcodec_decode_audio4(); // 音频解码
4.3.2、解码流程
注意:
1)AVFormatContext需要经过avformat_open_input 和avformat_find_stream_info两个接口获取完整数据。
2)音频和视频的不同处,在解码处,视频读出来的AVPacket包含1帧信息,而音频的1个AvPacket可能包含多帧音频数据。
3)资源分配后都需要在合适的地方释放(AVPacket,AVFrame),以及uninit系统。
4)FFmpeg依赖SDL库输出音视频,目前是SDL2.05,如果不用SDL库,那么就要手动处理AVFrame数据实现音视频的播放和显示。
5)FFMpeg的lib库不仅支持本地文件播放,还支持各种网络协议的流媒体(RTSP/RTMP/HTTP/UDP/TCP/RTP)。
4.4、存储(推送)
4.4.1、接口函数
- avformat_alloc_output_context2(AVFormatContext **ctx, AVOutputFormat *oformat,const char *format_name, const char *filename) // 依据输入参数构造输出媒体封装文件上下文
- avcodec_copy_context() // 依据输入的媒体信息,构造输出的AVStream
- avio_open(AVIOContext **s, const char *url, int flags) // 打开输出文件
- avformat_write_header(AVFormatContext *s, AVDictionary **options) // 根据文件名的后缀写相应格式的文件头
- av_read_frame(AVFormatContext *s, AVPacket *pkt) // 从输入流中读取一个分包
- av_interleaved_write_frame(AVFormatContext *s, AVPacket *pkt) // 往输出流中写一个分包
- av_write_trailer(AVFormatContext *s) // 写输出流(文件)的文件尾
- avio_close // 关闭文件
4.4.2、存储流程
FFMpeg 保存数据流为文件或者为网络流流程图:
FFmpeg的存储概念很强大,存储即可以是一个简单的YUV文件,也可以是复杂的媒体格式,也可以是网络流的方式推送出去。
5、SDL库介绍
这里主要介绍用来图像显示的数据结构和基本操作接口,详细使用方法和实现逻辑具体可参考官网和相关source code。
5.1、数据结构
SDL显示视频涉及到下列结构体:
- SDL_Window:代表了窗口
- SDL_Renderer:代表了渲染器
- SDL_Texture:代表了纹理
- SDL_Rect:一个矩形框,用于确定纹理显示的位置。
结构体之间的关系如下图所示:
5.2、接口函数
初始化:
- SDL_Init():初始化SDL
- SDL_CreateWindow():创建窗口(Window)
- SDL_CreateWindowFrom():基于给定窗口句柄创建窗口
- SDL_CreateRenderer():基于窗口创建渲染器(Render)
- SDL_CreateTexture():基于渲染器创建纹理(Texture)
循环渲染数据::
- SDL_UpdateTexture():设置纹理的数据 (rgb) ,还有一个是UpdateYUVTexure
- SDL_RenderCopy():纹理复制给渲染器,memcpy
- SDL_RenderPresent():显示,类似windows下的BitBlt()
SDL显示视频的流程如下图所示:
5.3、ffplay.c中的部分使用SDL的代码
/* display the current picture, if any */
static void video_display(VideoState *is)
{
if (!window)
video_open(is, NULL);
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255);
SDL_RenderClear(renderer);
if (is->audio_st && is->show_mode != SHOW_MODE_VIDEO)
video_audio_display(is);
else if (is->video_st)
video_image_display(is);
SDL_RenderPresent(renderer);
}
其实在video_image_display中还是调用了SDL_UpdateTexure 和 SDL_RenderCopy(renderer, vp->bmp, NULL, &rect);来实现数据传递。
在这里,给大家重点推荐一下我的几个热门畅销专栏,欢迎订阅:(博客主页还有其他专栏,可以去查看)
专栏1:(该精品技术专栏的订阅量已达到480多个,专栏中包含大量项目实战分析案例,有很强的实战参考价值,广受好评!专栏文章持续更新中,预计更新到200篇以上!欢迎订阅!)
C++软件调试与异常排查从入门到精通系列文章汇总https://blog.csdn.net/chenlycly/article/details/125529931
本专栏根据多年C++软件异常排查的项目实践,系统地总结了引发C++软件异常的常见原因以及排查C++软件异常的常用思路与方法,详细讲述了C++软件的调试方法与手段,以图文并茂的方式给出具体的项目问题实战分析实例(很有实战参考价值),带领大家逐步掌握C++软件调试与异常排查的相关技术,适合基础进阶和想做技术提升的相关C++开发人员!
考察一个开发人员的水平,一是看其编码及设计能力,二是要看其软件调试能力!所以软件调试能力(排查软件异常的能力)很重要,必须重视起来!能解决一般人解决不了的问题,既能提升个人能力及价值,也能体现对团队及公司的贡献!
专栏中的文章都是通过项目实战总结出来的,包含大量项目问题实战分析案例,有很强的实战参考价值!专栏文章还在持续更新中,预计文章篇数能更新到200篇以上!
专栏2:
C++常用软件分析工具从入门到精通案例集锦汇总(专栏文章,持续更新中...)https://blog.csdn.net/chenlycly/article/details/131405795
常用的C++软件辅助分析工具有PE工具、Dependency Walker、Process Explorer、Process Monitor、API Monitor、Clumsy、Windbg、IDA Pro等,本专栏详细介绍如何使用这些工具去巧妙地分析和解决日常工作中遇到的问题,很有实战参考价值!
专栏3:(本专栏涵盖了多方面的内容,是当前重点打造的专栏,专栏文章已经更新到380多篇,持续更新中...)
C/C++基础与进阶(专栏文章,持续更新中...)https://blog.csdn.net/chenlycly/category_11931267.html
以多年的开发实战为基础,总结并讲解一些的C/C++基础与项目实战进阶内容,以图文并茂的方式对相关知识点进行详细地展开与阐述!专栏涉及了C/C++领域多个方面的内容,包括C++基础及编程要点(模版泛型编程、STL容器及算法函数的使用等)、C++11及以上新特性(不仅看开源代码会用到,日常编码中也会用到部分新特性,面试时也会涉及到)、常用C++开源库的介绍与使用、代码分享(调用系统API、使用开源库)、常用编程技术(动态库、多线程、多进程、数据库及网络编程等)、软件UI编程(Win32/duilib/QT/MFC)、C++软件调试技术(排查软件异常的手段与方法、分析C++软件异常的基础知识、常用软件分析工具使用、实战问题分析案例等)、设计模式、网络基础知识与网络问题分析进阶内容等。
专栏4:
VC++常用功能开发汇总(专栏文章,持续更新中...)https://blog.csdn.net/chenlycly/article/details/124272585
将10多年C++开发实践中常用的功能,以高质量的代码展现出来。这些常用的高质量规范代码,可以直接拿到项目中使用,能有效地解决软件开发过程中遇到的问题。
专栏5:
Windows C++ 软件开发从入门到精通(专栏文章,持续更新中...)https://blog.csdn.net/chenlycly/category_12695902.html
根据多年C++软件开发实践,详细地总结了Windows C++ 应用软件开发相关技术实现细节,分享了大量的实战案例,很有实战参考价值。
6、实例代码
6.1、FFMPEG编译介绍
有关ffmpeg在windows下编译的文章网络上很多, 这里再次说明一下我这里的经验。
如果在Linux或者Cygwin下编译ffmpeg其实很简单,只要在官网下载ffmpeg最新,并且安装yasm等基本的编译需要的支持组件;如果需要编译支持libx264和libx265,也可在ffmpeg官网找到下载地址。
很多时候我们想调试方便,都希望在VS工程下编译ffmpeg的工程。那么就有两种方法。一种是自己手动下载ffmepg的source code并在windows平台下通过相关工具编译出对应的dll,另一种是直接在官网下载现成的开发库和运行库。
6.2 FFMPEG编译ffplay.c
我们构建测试程序,可以有两种方式:
第一是直接利用ffmpeg提供的库函数来完成简单的操作,其次可以编译ffplay.c,ffmpeg工程中有这些文件,在cygwin或者linux下我们很容易编译出ffmpeg.exe 和 ffplay.exe, 但是如果你想在vs2010中编译ffplay.exe或者ffmpeg.exe就要费点功夫。
具体做法从ffmpeg工程下拷贝cmdutils.c cmdutils.h fplay.c到我们自己的工程目录下并加入这几个文件 , 同时指定头文件引用来自ffmpeg的 code目录,编译中会遇到一些问题如下:
1)AVRational操作
vs2010中至少不支持如下赋值方式:
AVRational t;
t = (AVRational){1, sample_rate};
我们针对这种操作要用如下方式代替:
{
AVRational t = {1, dec_ctx->sample_rate};
o_ctx->time_base = t;
}
大括号的目的是缩小定义变量的范围,同时避免编译条件的限制,同时解决赋值问题。在libavutil\avutil.h修改
#define AV_TIME_BASE_Q (AVRational){1, AV_TIME_BASE}
为:#define AV_TIME_BASE_Q {1, AV_TIME_BASE}
2)Options初始化
static const OptionDef options[] = {
#include "cmdutils_common_opts.h"
// { "x", HAS_ARG, { .func_arg = opt_width }, "force displayed width", "width" },如果你不希望采用ffplay带的options功能,你大可如上将其全部注释掉,但是个人不建议这样,因为ffplay的好处就是支持庞大的输入参数解析,这也是编译ffplay.c最直接的原因。
这里的关键问题是{ .func_arg = opt_width },的赋值,vs2010不支持这种识别,其实查看其数据结构,是一个union,所以直接将函数指针传给它就成,没必要非要写出类型,我想设计者写出类型的目的是为了更好的查看代码。
修改方式如下:
{ "x", HAS_ARG, { opt_width }, "force displayed width", "width" },
3)注释掉如下数据结构
在network.h
102行 struct sockaddr_storage
128行 struct addrinfo
在os_support.h
115行 struct pollfd
在libavutil\libm.h
注释掉static inline av_const double hypot(double x, double y)这个函数。
4)inline的未定义问题
在libavutil\avstring.h,libavutil\error.h的头部加入如下定义:
#if defined(WIN32) || defined(WIN64)
#define inline __inline
#endif只要是没有inline的问题,都这样操作。
5)未找到的头文件
\libavutil\common.h,34行,注释掉//#include <inttypes.h>
其实这个文件定义比较简单,可以自己伪造一个,简单的办法是注释掉,没有影响。相关地方都这样操作。
6)av_err2str的问题
用函数替换宏定义:
inline char *av_err2str(int errnum)
{
static char info[AV_ERROR_MAX_STRING_SIZE] = {0};
return av_make_error_string(info, AV_ERROR_MAX_STRING_SIZE, errnum);
}
如此之后编译ffplay.c就没有什么问题了。
简单办法是把ffplay.c的main接口改为自己需要的xxx(argc,argv),然后封装成一个lib,在新的工程中直接extern “C” ffplay.c中定义的接口函数,那么我们就无需在关心那些头文件的事情,一切问题都被ffplaylib已经解决。
7、演示工程
项目的目标是通过APP读写MP4多媒体数据文件,通过RTSP方式推送到接收端,接收端接收数据流之后进行解码播放并存储位MP4文件。
前面已经讨论了FFMPEG和SDL相关技术知识。本工程就是要利用open source的FFMPEG进行二次开发。由于FFmpeg程序已经有现成的工程ffmpeg.c实现了读取mp4文件进行码流推送的功能,我们这里不再重复实现这个功能,当然基于前面章节提到的技术,也可以简单写一个读取mp4文件并发送码流的程序。这里主要是依据FFmpeg提供的lib库和ffplay.c设计实现一个接收网络流数据并存储位MP4的演示程序。
采用VS2010构建了一个Solution,包含三个project,分别完成ffplay控制台执行程序,lib库的封装,可视界面的演示;码流的推送直接利用了官方的ffmpeg.exe,也可以自己编译出来。
7.1、工程
1)testFFplay 是vs2010下编译ffplay.c, 类似直接在cygwin和linux下编译ffmpeg工程,输出ffplay.exe一样。
2)testFFLib是针对ffplay.c进行封装的一个简单lib,这样测试程序只要关心这个lib输出的接口函数就好,它友好的继承了ffplay.c的对输入参数的解析以及播放控制。
3)testFFApp是利用playLib进行播放显示的MFC可执行程序。4)工程文件相互关系如下:
7.2、TestFFLib的工程
7.2.1、FFplay.c 中的Main函数重新定义
在ffplay.c中找到main函数,按照如下方式重新封装,目的是为了作为一个lib库输出时提供一个标准接口。
#ifdef FFLIB
int ff_play(int argc, char **argv)
#else
int main(int argc, char **argv)
#endif
依照上述原则,在help输出打印加入控制,lib时不需要一些控制台输出,同时要处理一下do_exit函数,不能因为异常直接quit应用程序。
7.2.2、TestFFLib接口函数
为了友好的给测试demo进行播放控制,testFFLib输出了如下几个接口函数:
int ff_set_windows_handle(void *hwd); // 设置窗口句柄
void ff_stop(); //停止播放
int ff_pause(); //暂停播放
int ff_play(int argc, char **argv); //开始播放输入文件
int ff_set_output_file(const char *filename); //设置输出文件名
首先,设置窗口句柄,指定视频显示窗口位置。其次,如果要输出文件,就要设置输出文件名称,之后调用play函数播放输入的媒体数据。
7.2.3、FFplay.c中增加文件输出操作
7.2.3.1、打开输出文件
static int open_output_file(VideoState *is)
{
AVStream *out_stream;
AVStream *in_stream;
AVCodecContext *dec_ctx, *o_ctx;
AVCodec *encoder;
int ret = 0;
unsigned int i;
if ('\0' == output_filename[0])
{
av_log(NULL, AV_LOG_WARNING, "The output file name is empty.\n");
return;
}
//初始化输出文件结构
o_fmt_ctx = NULL;
avformat_alloc_output_context2(&o_fmt_ctx, NULL, NULL, output_filename);
if (!o_fmt_ctx) {
av_log(NULL, AV_LOG_ERROR, "Could not create output context\n");
return -1;
}
for (i = 0; i < is->ic->nb_streams; i++) {
in_stream = is->ic->streams[i];
out_stream = avformat_new_stream(o_fmt_ctx, in_stream->codec->codec);
if (!out_stream) {
av_log(NULL, AV_LOG_ERROR, "Failed allocating output stream\n");
return -1;
}
if (avcodec_copy_context(out_stream->codec, in_stream->codec) < 0) {
av_log(NULL, AV_LOG_ERROR,
"Failed to copy context from input to output stream codec context\n");
break;
}
dec_ctx = in_stream->codec;
o_ctx = out_stream->codec;
out_stream->codec->codec_tag = 0;
//*o_ctx = *dec_ctx;
if (dec_ctx->codec_type == AVMEDIA_TYPE_VIDEO
|| dec_ctx->codec_type == AVMEDIA_TYPE_AUDIO) {
} else if (dec_ctx->codec_type == AVMEDIA_TYPE_UNKNOWN) {
av_log(NULL, AV_LOG_FATAL, "Elementary stream #%d is of unknown type, cannot proceed\n", i);
return -1;
} else {
/* if this stream must be remuxed */
ret = avcodec_copy_context(o_fmt_ctx->streams[i]->codec, is->ic->streams[i]->codec);
if (ret < 0) {
av_log(NULL, AV_LOG_ERROR, "Copying stream context failed\n");
return ret;
}
}
if (o_fmt_ctx->oformat->flags & AVFMT_GLOBALHEADER)
o_ctx->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
}
// av_dump_format(o_fmt_ctx, 0, filename, 1);
if (!(o_fmt_ctx->oformat->flags & AVFMT_NOFILE)) {
ret = avio_open(&o_fmt_ctx->pb, output_filename, AVIO_FLAG_WRITE);
if (ret < 0) {
av_log(NULL, AV_LOG_ERROR, "Could not open output file '%s'", output_filename);
return ret;
}
}
/* init muxer, write output file header */
ret = avformat_write_header(o_fmt_ctx, NULL);
if (ret < 0) {
char *err = av_err2str(ret);
av_log(NULL, AV_LOG_ERROR,
"Could not write header for output file #%s "
"(incorrect codec parameters ?): %s",
output_filename, av_err2str(ret));
return ret;
}
return 0;
}
依据输入媒体数据构建输出媒体文件的基础数据。
7.2.3.2、输出一个AVPacket文件数据
static int write_output_file(AVPacket *pkt)
{
int ret;
AVPacket opk;
av_init_packet(&opk);
opk.size = 0;
opk.data = NULL;
if (NULL == pkt || NULL == o_fmt_ctx)
{
av_log(NULL, AV_LOG_ERROR, "Input pkt or output ctx is NULL.\n");
return 1;
}
av_packet_ref(&opk, pkt);
//opk.pts = pts;
//opk.duration = duration;
ret = av_interleaved_write_frame(o_fmt_ctx, &opk);
//ret = av_write_frame(o_fmt_ctx, &opk);
av_packet_unref(&opk);
av_free_packet(&opk);
return ret;
}
在每次成功读取一个AVPacket之后,调用此接口进行文件存储。
7.2.3.3、关闭输出文件数据
static void close_output_file()
{
if (NULL != o_fmt_ctx)
{
av_write_trailer(o_fmt_ctx);
if (o_fmt_ctx && !(o_fmt_ctx->oformat->flags & AVFMT_NOFILE))
{
avio_close(o_fmt_ctx->pb);
}
avformat_free_context(o_fmt_ctx);
o_fmt_ctx = NULL;
}
}
在播放终止时调用此函数。写入文件结尾,关闭输出上下文以及输出文件,并释放相关资源。
7.3、演示demo
7.3.1、Lib接口函数引入
extern "C" {
int ff_set_windows_handle(void *hwd);
void ff_stop();
int ff_pause();
int ff_play(int argc, char **argv);
int ff_set_output_file(const char *filename);
}
7.3.2、Demo中的部分代码:
void CtestFFAppDlg::ChangeButStatus(int flags)
{
m_ff_status = flags;
CMenu *menu = this->GetMenu();
CMenu* submenu = menu->GetSubMenu(1);
switch (flags)
{
case FF_RUNNING:
submenu->EnableMenuItem(ID_ACTION_PLAY, MF_DISABLED | MF_GRAYED);
submenu->EnableMenuItem(ID_ACTION_PAUSE, MF_ENABLED );
submenu->EnableMenuItem(ID_ACTION_STOP, MF_ENABLED );
submenu->EnableMenuItem(ID_FILE_SAVEAS, MF_DISABLED | MF_GRAYED );
break;
case FF_STOPPED:
submenu->EnableMenuItem(ID_ACTION_PLAY, MF_ENABLED);
submenu->EnableMenuItem(ID_ACTION_PAUSE, MF_DISABLED | MF_GRAYED);
submenu->EnableMenuItem(ID_ACTION_STOP, MF_DISABLED | MF_GRAYED);
submenu->EnableMenuItem(ID_FILE_SAVEAS, MF_ENABLED);
default:
break;
}
}
void CtestFFAppDlg::OnBnClickedMenuOpen()
{
g_videoDlg->ShowWindow(TRUE);
}
// CVideoDlg message handlers
UINT Thread_Play(LPVOID lpParam)
{
CtestFFAppDlg *dlg=(CtestFFAppDlg *)lpParam;
int argc = 0;
static char *argv[100] = {0};
char *str = NULL;
CString fileInfo = dlg->GetFileInfo();
str = UnicodeToAnsi(fileInfo.GetBuffer(fileInfo.GetLength()));
SplitCString(str, ";", &argc, argv);
if (argc < 2)
{
AfxMessageBox(_T("Failed to serial lines input data."),MB_OK,0);
return 1;
}
//dlg->GetDlgItem(IDC_STATIC_PIC)->ShowWindow(TRUE);
dlg->ShowWindow(TRUE);
dlg->ChangeButStatus(FF_RUNNING);
if (0 != ff_play(argc, argv))
{
dlg->OnBnClickedMenuStop();
AfxMessageBox(_T("Failed to initialize VideoState!"),MB_OK,0);
}
FreeInputArgv(argv);
dlg->OnBnClickedMenuStop();
dlg->ShowWindow(TRUE);
return 0;
}
void CtestFFAppDlg::OnBnClickedMenuRun()
{
if (GetFFStatus() == FF_STOPPED)
{
AfxBeginThread(Thread_Play,this);// a thread for run ff_play
}
}
void CtestFFAppDlg::OnBnClickedMenuPause()
{
CMenu *menu = this->GetMenu();
CMenu* submenu = menu->GetSubMenu(1);
if (m_ff_status == FF_RUNNING)
{
ff_pause();
m_ff_status = FF_PAUSED;
}
else if (m_ff_status == FF_PAUSED)
{
ff_pause();
m_ff_status = FF_RUNNING;
}
}
void CtestFFAppDlg::OnBnClickedMenuStop()
{
if (FF_STOPPED != m_ff_status)
{
ff_stop();
ChangeButStatus(FF_STOPPED);
setWindowsPos(m_defaultRect.Width(), m_defaultRect.Height());
}
}
void CtestFFAppDlg::setWindowsPos(int w, int h)
{
CRect tmp;
int nScreenWidth = GetSystemMetrics(SM_CXSCREEN);
int nScreenHeight = GetSystemMetrics(SM_CYSCREEN);
int nLeft = (nScreenWidth - w) / 2;
int nTop = (nScreenHeight - h) / 2;
CRect rectWindows(nLeft, nTop, w, h);
SetWindowPos(NULL, rectWindows.left, rectWindows.top,
rectWindows.Width(), rectWindows.Height(), NULL);
}
void CtestFFAppDlg::SetInputFile(CString fileInfo)
{
m_inputFileInfo = fileInfo;
OnBnClickedMenuRun();
}
void CtestFFAppDlg::OnBnClickedMenuSaveas()
{
int flag = 0;
CFileDialog dlg(TRUE,NULL,NULL,NULL, _T("*.mp4|*.mp4||"));
if (m_ff_status == FF_STOPPED)
{
if (dlg.DoModal() == IDOK)
{
m_outFileName = dlg.GetPathName();
int len = m_outFileName.GetLength();
if (len == 0 || len < 4)
{
AfxMessageBox(_T("You do not select a right file name to save."),MB_OK,0);
return;
}
len = m_outFileName.Find(_T(".mp4"), m_outFileName.GetLength() - 4);
if (len < 0)
{
m_outFileName += ".mp4";
}
ff_set_output_file( UnicodeToAnsi(m_outFileName.GetBuffer(m_outFileName.GetLength())));
}
}
}
7.3.3 DEMO APP截图
7.4、一个简单的获取RTSP流存储为MP4文件的例子
7.4.1、采用官方开发库,构建工程文件
以vs2010为例,如果直接采用官方开发库,构建工程文件。
【LIB库引入】
首先在工程-属性-vc++路径中添加lib和include的引入路径;其次在工程-属性-linker中的依赖lib中添加如下几个文件:
avcodec.lib
avformat.lib
avutil.lib
avdevice.lib
avfilter.lib
swresample.lib
swscale.lib
SDL2.lib // 注意sdl2.0一定要是官网最新的
【接口头文件】
在需要使用ffmpeg库的cpp或者.h文件中添加库文件的引入方式:
extern "C" {
#include "libavformat/avformat.h"
#include "libswscale/swscale.h"
#include "libswresample/swresample.h"
#include "libavdevice/avdevice.h"
#include "libavutil/error.h"
#include "libavutil/audio_fifo.h"
#include "libavutil/pixfmt.h"
#include "libavfilter/avfilter.h"
#include "SDL.h"
#include "SDL_thread.h"
}
然后你就可以尝试在代码中调用av_register_all()来验证库文件是否被正常配置。
由于lib文件仅仅是个库的符号导出,所以在实际运行的时候,一定要将相关dll文件copy到可执行文件所在的目录。
7.4.2、直接调用FFMpeg Library代码
其实ffmpeg-3.2.4\doc\examples 中有具体如何使用常见接口的例子,大家可以参考。
#include <stdio.h>
#define __STDC_CONSTANT_MACROS
#ifdef _WIN32
//Windows
extern "C"
{
#include "libavformat/avformat.h"
#include "libavutil/mathematics.h"
#include "libavutil/time.h"
};
#else
//Linux...
#ifdef __cplusplus
extern "C"
{
#endif
#include <libavformat/avformat.h>
#include <libavutil/mathematics.h>
#include <libavutil/time.h>
#ifdef __cplusplus
};
#endif
#endif
//'1': Use H.264 Bitstream Filter
#define USE_H264BSF 0
int main(int argc, char* argv[])
{
AVOutputFormat *ofmt = NULL;
//Input AVFormatContext and Output AVFormatContext
AVFormatContext *ifmt_ctx = NULL, *ofmt_ctx = NULL;
AVPacket pkt;
const char *in_filename, *out_filename;
int ret, i;
int videoindex=-1;
int frame_index=0;
in_filename = "rtsp://localhost/test";
out_filename = "test.mp4";
av_register_all();
//Network
avformat_network_init();
//Input
if ((ret = avformat_open_input(&ifmt_ctx, in_filename, 0, 0)) < 0) {
printf( "Could not open input file.");
goto end;
}
if ((ret = avformat_find_stream_info(ifmt_ctx, 0)) < 0) {
printf( "Failed to retrieve input stream information");
goto end;
}
for(i=0; i<ifmt_ctx->nb_streams; i++)
if(ifmt_ctx->streams[i]->codec->codec_type==AVMEDIA_TYPE_VIDEO){
videoindex=i;
break;
}
av_dump_format(ifmt_ctx, 0, in_filename, 0);
//Output
avformat_alloc_output_context2(&ofmt_ctx, NULL, NULL, out_filename); //RTMP
if (!ofmt_ctx) {
printf( "Could not create output context\n");
ret = AVERROR_UNKNOWN;
goto end;
}
ofmt = ofmt_ctx->oformat;
for (i = 0; i < ifmt_ctx->nb_streams; i++) {
//Create output AVStream according to input AVStream
AVStream *in_stream = ifmt_ctx->streams[i];
AVStream *out_stream = avformat_new_stream(ofmt_ctx, in_stream->codec->codec);
if (!out_stream) {
printf( "Failed allocating output stream\n");
ret = AVERROR_UNKNOWN;
goto end;
}
//Copy the settings of AVCodecContext
ret = avcodec_copy_context(out_stream->codec, in_stream->codec);
if (ret < 0) {
printf( "Failed to copy context from input to output stream codec context\n");
goto end;
}
out_stream->codec->codec_tag = 0;
if (ofmt_ctx->oformat->flags & AVFMT_GLOBALHEADER)
out_stream->codec->flags |= CODEC_FLAG_GLOBAL_HEADER;
}
//Dump Format------------------
av_dump_format(ofmt_ctx, 0, out_filename, 1);
//Open output URL
if (!(ofmt->flags & AVFMT_NOFILE)) {
ret = avio_open(&ofmt_ctx->pb, out_filename, AVIO_FLAG_WRITE);
if (ret < 0) {
printf( "Could not open output URL '%s'", out_filename);
goto end;
}
}
//Write file header
ret = avformat_write_header(ofmt_ctx, NULL);
if (ret < 0) {
printf( "Error occurred when opening output URL\n");
goto end;
}
#if USE_H264BSF
AVBitStreamFilterContext* h264bsfc = av_bitstream_filter_init("h264_mp4toannexb");
#endif
while (1) {
AVStream *in_stream, *out_stream;
//Get an AVPacket
ret = av_read_frame(ifmt_ctx, &pkt);
if (ret < 0)
break;
in_stream = ifmt_ctx->streams[pkt.stream_index];
out_stream = ofmt_ctx->streams[pkt.stream_index];
/* copy packet */
//Convert PTS/DTS
pkt.pts = av_rescale_q_rnd(pkt.pts, in_stream->time_base, out_stream->time_base, (AVRounding)(AV_ROUND_NEAR_INF|AV_ROUND_PASS_MINMAX));
pkt.dts = av_rescale_q_rnd(pkt.dts, in_stream->time_base, out_stream->time_base, (AVRounding)(AV_ROUND_NEAR_INF|AV_ROUND_PASS_MINMAX));
pkt.duration = av_rescale_q(pkt.duration, in_stream->time_base, out_stream->time_base);
pkt.pos = -1;
//Print to Screen
if(pkt.stream_index==videoindex){
printf("Receive %8d video frames from input URL\n",frame_index);
frame_index++;
#if USE_H264BSF
av_bitstream_filter_filter(h264bsfc, in_stream->codec, NULL, &pkt.data, &pkt.size, pkt.data, pkt.size, 0);
#endif
}
//ret = av_write_frame(ofmt_ctx, &pkt);
ret = av_interleaved_write_frame(ofmt_ctx, &pkt);
if (ret < 0) {
printf( "Error muxing packet\n");
break;
}
av_free_packet(&pkt);
}
#if USE_H264BSF
av_bitstream_filter_close(h264bsfc);
#endif
//Write file trailer
av_write_trailer(ofmt_ctx);
end:
avformat_close_input(&ifmt_ctx);
/* close output */
if (ofmt_ctx && !(ofmt->flags & AVFMT_NOFILE))
avio_close(ofmt_ctx->pb);
avformat_free_context(ofmt_ctx);
if (ret < 0 && ret != AVERROR_EOF) {
printf( "Error occurred.\n");
return -1;
}
avformat_close_input(ifmt_ctx);
return 0;
}