前言:对于从未接触过音视频编解码的同学来说,使用FFmpeg的学习曲线恐怕略显陡峭。本人由于工作需要,正好需要在项目中使用。因此特地将开发过程总结下来。只当提供给有兴趣的同学参考和学习。

由于FFmpeg是使用C语言开发,所有和函数调用都是面向过程的。以我目前的学习经验来说,通常我会把一个功能的代码全部放在main函数中实现。经过测试和修改认为功能正常,再以C++面向对象的方式逐步将代码分解和封装。因此在对本套指南中我也会采用先代码实现再功能封装的步骤。

一、开发前的准备工作

开发工具为VS2013+Qt5,目录结构:

  • bin:工作和测试目录
  • doc:开发文档目录
  • include:ffmpeg头文件配置目录
  • lib:ffmpeg静态库配置目录
  • src:源码目录

属性页配置:

  1. 常规-输出目录:..\..\bin
  2. 调试-工作目录:..\..\bin
  3. C/C++-常规-附加包含目录:..\..\include
  4. 链接器-常规-附加库目录:..\..\lib
  5. 链接器-系统-子系统:控制台 (/SUBSYSTEM:CONSOLE)

二、编解码基础知识

(1)封装格式

所谓封装格式是指音视频的组合格式,例如最常见的封装格式有mp4、mp3、flv等。简单来说,我们平时接触到的带有后缀的音视频文件都是一种封装格式。不同的封装格式遵循不同的协议标准。有兴趣的同学可以自行扩展,更深的东西我也不懂。

(2)编码格式

以mp4为例,通常应该包含有视频和音频。视频的编码格式为YUV420P,音频的编码格式为PCM。再以YUV420编码格式为例。我们知道通常图像的显示为RGB(红绿蓝三原色),在视频压缩的时候会首先将代表每一帧画面的RGB压缩为YUV,再按照关键帧(I帧),过渡帧(P帧或B帧)进行运算和编码。解码的过程正好相反,解码器会读到I帧,并根据I帧运算和解码P帧以及B帧。并最终根据视频文件预设的FPS还原每一帧画面的RGB数据。最后推送给显卡。所以通常我们说的编码过程就包括:画面采集、转码、编码再封装。

(3)视频解码和音频解码有什么区别

玩游戏的同学肯定对FPS不陌生,FPS太低画面会感觉闪烁不够连贯,FPS越高需要显卡性能越好。一些高速摄像机的采集速度能够达到11000帧/秒,那么在播放这类影片的时候我们是否也需要以11000帧/秒播放呢?当然不是,通常我们会按照25帧/秒或者60帧/秒设定图像的FPS值。但是由于视频存在关键帧和过渡帧的区别,关键帧保存了完整的画面而过渡帧只是保存了与前一帧画面的变化部分,需要通过关键帧计算获得。因此我们需要对每一帧都进行解码,即获取画面的YUV数据。同时只对我们真正需要显示的画面进行转码,即将YUV数据转换成RGB数据,包括计算画面的宽高等。

但是音频则不然,音频的播放必须和采集保持同步。提高或降低音频的播放速度都会让音质发生变化,这也是变声器的原理。因此在实际开发中为了保证播放的音视频同步,我们往往会按照音频的播放速度来控制视频的解码转码速度。

三、代码实现

(1)注册FFmpeg组件:注册和初始化FFmpeg封装器和网络设备

av_register_all();
avformat_network_init();
avdevice_register_all();

(2)打开文件和创建输入设备

AVFormatContext *pFormatCtx = NULL;
int errnum = avformat_open_input(&pFormatCtx, filename, NULL, NULL);
if (errnum < ) {
av_strerror(errnum, errbuf, sizeof(errbuf));
cout << errbuf << endl;
}

AVFormatContext 表示一个封装器,在读取多媒体文件的时候,它负责保存与封装和编解码有关的上下文信息。avformat_open_input函数可以根据文件后缀名来创建封装器。

(3)遍历流并初始化解码器

for (int i = ; i < pFormatCtx->nb_streams; ++i) {
AVCodecContext *pCodecCtx = pFormatCtx->streams[i]->codec; // 解码器上下文
if (pCodecCtx->codec_type == AVMEDIA_TYPE_VIDEO) { // 视频通道
int videoIndex = i; // 视频的宽,高
int srcWidth = pCodecCtx->width;
int srcHeight = pCodecCtx->height; // 创建视频解码器,打开解码器
AVCodec *codec = avcodec_find_decoder(pCodecCtx->codec_id);
if (!codec) {
// 无法创建对应的解码器
} errnum = avcodec_open2(pCodecCtx, codec, NULL);
if (errnum < ) {
av_strerror(errnum, errbuf, sizeof(errbuf));
cout << errbuf << endl;
}
cout << "video decoder open success!" << endl;
}
if (pCodecCtx->codec_type == AVMEDIA_TYPE_AUDIO) { // 音频通道
int audioIndex = i;
// 创建音频解码器,打开解码器
AVCodec *codec = avcodec_find_decoder(pCodecCtx->codec_id);
if (!codec) {
// 无法创建对应的解码器
} errnum = avcodec_open2(pCodecCtx, codec, NULL);
if (errnum < ) {
av_strerror(errnum, errbuf, sizeof(errbuf));
cout << errbuf << endl;
} int sampleRate = pCodecCtx->sample_rate; // 音频采样率
int channels = pCodecCtx->channels; // 声道数
AVSampleFormat fmt = pCodecCtx->sample_fmt; // 样本格式 cout << "audio decoder open success!" << endl;
}
}

封装器中保存了各种流媒体的通道,通常视频通道为0,音频通道为1。除此以外可能还包含字幕流通道等。

第2步和第3步基本就是打开多媒体文件的主要步骤,解码和转码的所有参数都可以在这里获取。接下来我们就需要循环进行读取、解码、转码直到播放完成。

(4)读取压缩数据:之所以称为压缩数据主要是为了区分AVPacketAVFrame两个结构体。AVPacket表示一幅经过了关键帧或过渡帧编码后的画面,AVFrame表示一个AVPacket经过解码后的完整YUV画面

AVPacket *pkt = NULL;
pkt = av_packet_alloc(); // 初始化AVPacket
// 读取一帧数据
errnum = av_read_frame(pFormatCtx, pkt);
if (errnum == AVERROR_EOF) {
// 已经读取到文件尾
av_strerror(errnum, errbuf, sizeof(errbuf));
cout << errbuf << endl;
}
if (errnum < ) {
av_strerror(errnum, errbuf, sizeof(errbuf));
cout << errbuf << endl;
}

(5)解码

errnum = avcodec_send_packet(pCodecCtx, pkt);
if (errnum < ) {
av_strerror(errnum, errbuf, sizeof(errbuf));
cout << errbuf << endl;
} AVFrame *yuv = av_frame_alloc();
AVFrame *pcm = av_frame_alloc();
if (pkt->stream_index == videoIndex) { // 判断当前解码帧为视频帧
errnum = avcodec_receive_frame(pCodecCtx, yuv); // 解码视频
if (errnum < ) {
av_strerror(errnum, errbuf, sizeof(errbuf));
cout << errbuf << endl;
}
}
if (pkt->stream_index == audioIndex) { // 判断当前解码帧为音频帧
errnum = avcodec_receive_frame(pCodecCtx, pcm); // 解码音频
if (errnum < ) {
av_strerror(errnum, errbuf, sizeof(errbuf));
cout << errbuf << endl;
}
}

(6)视频转码

// 720p输出标准
int outWidth = ;
int outHeight = ;
char *outData = new char[outWidth * outHeight * ] SwsContext *videoSwsCtx = NULL;
videoSwsCtx = sws_getCachedContext(videoSwsCtx, srcWidth, srcHeight, (AVPixelFormat)pixFmt, // 输入
outWidth, outHeight, AV_PIX_FMT_BGRA, // 输出
SWS_BICUBIC, // 算法
, , ); // 分配数据空间
uint8_t *dstData[AV_NUM_DATA_POINTERS] = { };
dstData[] = (uint8_t *)outData;
int dstStride[AV_NUM_DATA_POINTERS] = { };
dstStride[] = outWidth * ; int h = sws_scale(videoSwsCtx, yuv->data, yuv->linesize, , srcHeight, dstData, dstStride);
if (h != outHeight) {
// 转码失败
}

这里需要解释一下outWidth * outHeight * 4计算理由:720p标准的视频画面包含720 * 480个像素点,每一个像素点包含了RGBA4类数据,每一类数据分别由1个byte即8个bit表示。因此一幅完整画面所占的大小为outWidth * outHeight * 4。

(7)音频转码

char *outData = new char[]; 输出指针

AVCodecContext *pCodecCtx = pFormatCtx->streams[audioIndex]->codec; // 获取音频解码器上下文
SwrContext *audioSwrCtx = NULL;
audioSwrCtx = swr_alloc();
audioSwrCtx = swr_alloc_set_opts(audioSwrCtx,
AV_CH_LAYOUT_STEREO, AV_SAMPLE_FMT_S16, , // 输出参数:双通道立体声 CD音质
pCodecCtx->channel_layout, pCodecCtx->sample_fmt, pCodecCtx->sample_rate, // 输入参数
, );
swr_init(audioSwrCtx); uint8_t *out[AV_NUM_DATA_POINTERS] = { };
out[] = (uint8_t *)outData;
// 计算输出空间
int dst_nb_samples = av_rescale_rnd(pcm->nb_samples, pCodecCtx->sample_rate, pCodecCtx->sample_rate, AV_ROUND_UP);
int len = swr_convert(audioSwrCtx,
out, dst_nb_samples,
(const uint8_t **)pcm->data, pcm->nb_samples); int channels = av_get_channel_layout_nb_channels(AV_CH_LAYOUT_STEREO); // AV_CH_LAYOUT_STEREO -> 2 根据声道类型得到声道数
// 实际音频数据长度
int dst_bufsize = av_samples_get_buffer_size(NULL,
channels, // 通道数
pcm->nb_samples,//
AV_SAMPLE_FMT_S16,
);
if (dst_bufsize < ) {
// 音频转码错误
}

至此我们已经基本完成了对一个多媒体文件的解码工作,不过离真正的播放还有一些工作没有完成。包括对代码的封装和界面设计我们都会放在下一篇博客中介绍。

完整的项目代码:https://gitee.com/learnhow/ffmpeg_studio/tree/master/_64bit/src/av_player

Qt与FFmpeg联合开发指南(一)——解码(1):功能实现的更多相关文章

  1. Qt与FFmpeg联合开发指南(二)——解码(2):封装和界面设计

    与解码相关的主要代码在上一篇博客中已经做了介绍,本篇我们会先讨论一下如何控制解码速度再提供一个我个人的封装思路.最后回归到界面设计环节重点看一下如何保证播放器界面在缩放和拖动的过程中保证视频画面的宽高 ...

  2. Qt与FFmpeg联合开发指南(三)——编码(1):代码流程演示

    前两讲演示了基本的解码流程和简单功能封装,今天我们开始学习编码.编码就是封装音视频流的过程,在整个编码教程中,我会首先在一个函数中演示完成的编码流程,再解释其中存在的问题.下一讲我们会将编码功能进行封 ...

  3. Qt与FFmpeg联合开发指南(四)——编码(2):完善功能和基础封装

    上一章我用一个demo函数演示了基于Qt的音视频采集到编码的完整流程,最后经过测试我们也发现了代码中存在的问题.本章我们就先处理几个遗留问题,再对代码进行完善,最后把编码功能做基础封装. 一.遗留问题 ...

  4. HarmonyOS三方件开发指南(14)-Glide组件功能介绍

    <HarmonyOS三方件开发指南>系列文章合集 引言 在实际应用开发中,会用到大量图片处理,如:网络图片.本地图片.应用资源.二进制流.Uri对象等,虽然官方提供了PixelMap进行图 ...

  5. ffmpeg开发指南

    FFmpeg是一个集录制.转换.音/视频编码解码功能为一体的完整的开源解决方案.FFmpeg的开发是基于Linux操作系统,但是可以在大多数操作系统中编译和使用.FFmpeg支持MPEG.DivX.M ...

  6. 详细介绍Qt,ffmpeg 和SDL开发

        Qt 与 ffmpeg 与 SDl 教程是本文要介绍的内容,从多个角度介绍本文,运用了qmake,先来看内容. 1.  注释 从“ #” 开始,到这一行结束. 2.  指定源文件 1.     ...

  7. ASP.NET Aries 开源开发框架:开发指南(一)

    前言: 上周开源了Aries开发框架后,好多朋友都Download了源码,在运行过程里,有一些共性的问题会问到. 所以本篇打算写一下简单的开发指南,照顾一下不是太看的懂源码的同学,同时也会讲解一下框架 ...

  8. 智捷公开课马上开始了-欢迎大家一起讨论学习-第一系列读《Swift开发指南(修订版) 》看Swift视频教程

    引用: 智捷课堂携手51CTO学院.图灵教育联合举办iOS线上培训就业班系列体验公开课. 分享移动开发.移动设计方向最新,最热,最抢眼技术热点以及设计经验.我们每周将最少举办一次公开课,同时会提前安排 ...

  9. 《iOS开发指南》正式出版-源码-样章-目录,欢迎大家提出宝贵意见

    智捷iOS课堂-关东升老师最新作品:<iOS开发指南-从0基础到AppStore上线>正式出版了 iOS架构设计.iOS性能优化.iOS测试驱动.iOS调试.iOS团队协作版本控制.... ...

随机推荐

  1. !!【通达信】求教:如何对A股的所有股票按照某个选股指标的某个参数排序? - 理想论坛 中国人气最旺的股票论坛

    http://www.55188.com/thread-7152852-1-1.html .401进入指标排序,然后占右键把指标更改为MACD即可.(注意401前投资面有一个点!)

  2. 论文参考文献中J、M等是什么意思

    最近不务正业的写论文,记录下常见的文献标示 国家期刊出版格式要求在中图分类号的下面应标出文献标识码,规定如下: 作者可从下列A.B.C.D.E中选用一种标识码来揭示文章的性质: A—理论与应用研究学术 ...

  3. Python 读写文件中w与wt, r与rt的区别

    w和wt是一们的,r和rt是一样的,t是默认参数,可以省略的,help(open)就能看到open的参数的详细说明. w,r,wt,rt都是python里面文件操作的模式.w是写模式,r是读模式.t是 ...

  4. idea 上搭建 Mybatis 逆向工程

    网盘地址:https://pan.baidu.com/s/1VAILpdgQbFk9t89eEv_nWQ 提取码:xdyc

  5. linux常用命令:top 命令

    top命令是Linux下常用的性能分析工具,能够实时显示系统中各个进程的资源占用状况,类似于Windows的任务管理器.下面详细介绍它的使用方法.top是 一个动态显示过程,即可以通过用户按键来不断刷 ...

  6. Python3 异常: name 'basestring' is not defined

    Python3 异常: name 'basestring' is not defined 问题分析: python3 里已经没有basestring 类型,用str代替了basestring : 解决 ...

  7. Duilib Edit编辑框禁止输入中文的方法

    转载:http://www.myexception.cn/vc-mfc/300749.html 编辑框是供用户输入的,但有时候我们要限制用户输入的内容,比如我们不让用户输入中文,只能输入字符和数字,因 ...

  8. VirtualBox 安装XP虚拟机需要注意的问题

    1.首先要有xp系统的镜像文件 http://pan.baidu.com/s/1i4xrwdJ 2.新建虚拟机,并安装 http://www.pczhishi.cn/shipin/107.html 3 ...

  9. 使用Oracle执行计划分析SQL性能

    执行计划:一条查询语句在ORACLE中的执行过程或访问路径的描述.即就是对一个查询任务,做出一份怎样去完成任务的详细方案. 如果要分析某条SQL的性能问题,通常我们要先看SQL的执行计划,看看SQL的 ...

  10. Hadoop新增和删除节点

    #新增节点 1.安装lunix,和以前一样的版本 2.初始化系统环境 2.1.设置静态ip vi /etc/sysconfig/network-scripts/ifcfg-eth0 //增加 #Adv ...