Qt与FFmpeg联合开发指南(一)——解码(1):功能实现
前言:对于从未接触过音视频编解码的同学来说,使用FFmpeg的学习曲线恐怕略显陡峭。本人由于工作需要,正好需要在项目中使用。因此特地将开发过程总结下来。只当提供给有兴趣的同学参考和学习。
由于FFmpeg是使用C语言开发,所有和函数调用都是面向过程的。以我目前的学习经验来说,通常我会把一个功能的代码全部放在main函数中实现。经过测试和修改认为功能正常,再以C++面向对象的方式逐步将代码分解和封装。因此在对本套指南中我也会采用先代码实现再功能封装的步骤。
一、开发前的准备工作
开发工具为VS2013+Qt5,目录结构:
- bin:工作和测试目录
- doc:开发文档目录
- include:ffmpeg头文件配置目录
- lib:ffmpeg静态库配置目录
- src:源码目录
属性页配置:
- 常规-输出目录:..\..\bin
- 调试-工作目录:..\..\bin
- C/C++-常规-附加包含目录:..\..\include
- 链接器-常规-附加库目录:..\..\lib
- 链接器-系统-子系统:控制台 (/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)读取压缩数据:之所以称为压缩数据主要是为了区分AVPacket和AVFrame两个结构体。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):功能实现的更多相关文章
- Qt与FFmpeg联合开发指南(二)——解码(2):封装和界面设计
与解码相关的主要代码在上一篇博客中已经做了介绍,本篇我们会先讨论一下如何控制解码速度再提供一个我个人的封装思路.最后回归到界面设计环节重点看一下如何保证播放器界面在缩放和拖动的过程中保证视频画面的宽高 ...
- Qt与FFmpeg联合开发指南(三)——编码(1):代码流程演示
前两讲演示了基本的解码流程和简单功能封装,今天我们开始学习编码.编码就是封装音视频流的过程,在整个编码教程中,我会首先在一个函数中演示完成的编码流程,再解释其中存在的问题.下一讲我们会将编码功能进行封 ...
- Qt与FFmpeg联合开发指南(四)——编码(2):完善功能和基础封装
上一章我用一个demo函数演示了基于Qt的音视频采集到编码的完整流程,最后经过测试我们也发现了代码中存在的问题.本章我们就先处理几个遗留问题,再对代码进行完善,最后把编码功能做基础封装. 一.遗留问题 ...
- HarmonyOS三方件开发指南(14)-Glide组件功能介绍
<HarmonyOS三方件开发指南>系列文章合集 引言 在实际应用开发中,会用到大量图片处理,如:网络图片.本地图片.应用资源.二进制流.Uri对象等,虽然官方提供了PixelMap进行图 ...
- ffmpeg开发指南
FFmpeg是一个集录制.转换.音/视频编码解码功能为一体的完整的开源解决方案.FFmpeg的开发是基于Linux操作系统,但是可以在大多数操作系统中编译和使用.FFmpeg支持MPEG.DivX.M ...
- 详细介绍Qt,ffmpeg 和SDL开发
Qt 与 ffmpeg 与 SDl 教程是本文要介绍的内容,从多个角度介绍本文,运用了qmake,先来看内容. 1. 注释 从“ #” 开始,到这一行结束. 2. 指定源文件 1. ...
- ASP.NET Aries 开源开发框架:开发指南(一)
前言: 上周开源了Aries开发框架后,好多朋友都Download了源码,在运行过程里,有一些共性的问题会问到. 所以本篇打算写一下简单的开发指南,照顾一下不是太看的懂源码的同学,同时也会讲解一下框架 ...
- 智捷公开课马上开始了-欢迎大家一起讨论学习-第一系列读《Swift开发指南(修订版) 》看Swift视频教程
引用: 智捷课堂携手51CTO学院.图灵教育联合举办iOS线上培训就业班系列体验公开课. 分享移动开发.移动设计方向最新,最热,最抢眼技术热点以及设计经验.我们每周将最少举办一次公开课,同时会提前安排 ...
- 《iOS开发指南》正式出版-源码-样章-目录,欢迎大家提出宝贵意见
智捷iOS课堂-关东升老师最新作品:<iOS开发指南-从0基础到AppStore上线>正式出版了 iOS架构设计.iOS性能优化.iOS测试驱动.iOS调试.iOS团队协作版本控制.... ...
随机推荐
- unp第七章补充之TCP半开连接与半闭连接
半打开(Half-Open)连接和半关闭(Half-Close)连接.TCP是一个全双工(Full-Duplex)协议,因此这里的半连接"半"字就是相对于全双工的"全&q ...
- cxf-webservice完整示例
最近一段时间研究webservice,一般来说,开发java的Webservice经常使用axis2和cxf这两个比较流行的框架 先使用cxf,开发一个完整示例,方便对webservice有一个整体的 ...
- 完整table
.table-bordered{ border:1px solid #cccccc; } .table { border-spacing: 0;/*设置或检索当表格边框独立时(即border-coll ...
- linux复制指定目录下的全部文件到另一个目录中,linux cp 文件夹
linux复制指定目录下的全部文件到另一个目录中复制指定目录下的全部文件到另一个目录中文件及目录的复制是经常要用到的.linux下进行复制的命令为cp.假设复制源目录 为 dir1 ,目标目录为dir ...
- SpringBoot之统一异常处理
异常,不仅仅是程序运行状态的描述,还可以使得代码编写更加的规范 1.自定义异常:FieldValueInvalidException package com.geniuses.sewage_zer ...
- MATERIALIZED VIEW-物化视图
Oracle的实体化视图提供了强大的功能,可以用在不同的环境中,实体化视图和表一样可以直接进行查询.实体化视图可以基于分区表,实体化视图本身也可以分区. 主要用于预先计算并保存表连接或聚集等耗时较多 ...
- SaltStack安装及配置
1.简介SaltStack是一个服务器基础架构集中化管理平台,具备配置管理.远程执行.监控等功能,一般可以理解为简化版的puppet和加强版的func.SaltStack基于Python语言实现,结合 ...
- ACM题目———— 一种排序(STL之set)
描述 输入 第一行有一个整数 0<n<10000,表示接下来有n组测试数据:每一组第一行有一个整数 0<m<1000,表示有m个长方形:接下来的m行,每一行有三个数 ,第一个数 ...
- (五)使用GitHub的前期准备
创建账户 创建账号成功后将得到一个个人的公开页面URL:https://github.com/xkfx. 设置头像 设置SSH Key SSH 为 Secure Shell 的缩写. from bai ...
- Python Web学习笔记之Python多线程基础
多线程理解 多线程是多个任务同时运行的一种方式.比如一个循环中,每个循环看做一个任务,我们希望第一次循环运行还没结束时,就可以开始第二次循环,用这种方式来节省时间. python中这种同时运行的目的是 ...