Qt与FFmpeg联合开发指南(四)——编码(2):完善功能和基础封装
上一章我用一个demo函数演示了基于Qt的音视频采集到编码的完整流程,最后经过测试我们也发现了代码中存在的问题。本章我们就先处理几个遗留问题,再对代码进行完善,最后把编码功能做基础封装。
一、遗留问题和解决方法
(1)如何让音视频的录制保持同步?
在我们的演示代码中之所以发现音视频录制不同步的主要原因是音频帧和视频帧不应该简单的按照1:1的比例进行编码。那么到底应该以什么样的比例控制呢?首先建议大家回顾一下之前写过的解码过程。如果我们把解码音视频的过程输出到控制台,我们会注意到大致每解码一帧画面应该解码2~4帧声音。按照这个思路我们先尝试修改一下demo中的编码步骤,人为控制视频和音频的编码比例为1:3。修改以后的代码如下:
// 音频编码
for (int i = ; i < ; ++i) {
// 固定写法:配置一帧音频的数据结构
const char *pcm = aa->getPCM(); /* 此处省略的代码请参考上一章的内容或查看源码 */ delete pcm;
}
然后再尝试录制,我们发现音频似乎可以正常播放,但是画面和音频并没有同步。另外,如果仔细一些的同学可能还会发现。在上一篇博客的最后一张截图中,音频的比特率显示为35kbps。
让我们先了解一下视频帧率和音频帧率的概念:通常fps10代表1秒显示10幅画面,这个比较容易理解。不太容易理解的是音频,以CD音质为例44100Hz的采样率,假设一帧音频包含1024个采样数据,那么1秒钟的音频大约有43帧。在编码阶段无论是视频还是音频我们都需要提供一个基础的pts作为参考。代表视频的vpts每次自增1即可,而代表音频的apts需要每次自增1024。
FFmpeg提供了一个比较函数 av_compare_ts(int64_t ts_a, AVRational tb_a, int64_t ts_b, AVRational tb_b) 来帮助开发人员计算音视频pts同步。
while (true) {
// 音频编码
const char *pcm = aa->getPCM();
if (pcm) {
... apkt->pts = apts;
apkt->dts = apkt->pts;
apts += av_rescale_q(aframe->nb_samples, { , pAudioCodecCtx->sample_rate }, pAudioCodecCtx->time_base); //
errnum = av_interleaved_write_frame(pFormatCtx, apkt); ...
delete pcm;
av_packet_free(&apkt);
} // 比较音视频pts,大于0表示视频帧在前,音频需要连续编码。小于0表示,音频帧在前,应该至少编码一帧视频
int ts = av_compare_ts(vpts, pVideoCodecCtx->time_base, apts, pAudioCodecCtx->time_base);
if (ts > ) {
continue;
} // 视频编码
const uchar *rgb = va->getRGB();
if (rgb) {
... vframe->pts = vpts++; ...
errnum = av_interleaved_write_frame(pFormatCtx, vpkt); ...
delete rgb;
av_packet_free(&vpkt);
}
}
这样音视频同步的部分就基本完成。
(2)如何正确析构QImage
通过memcpy函数将QImage中的数据拷贝一份
QPixmap pix = screen->grabWindow(wid);
uchar *rgb = new uchar[width * height * ]; // 申请图像存储空间
memcpy(rgb, pix.toImage().bits(), width * height * ); // 拷贝数据到新的内存区域
这样外部的调用者正常对rgb数据析构就不会有任何问题了。
(3)有关Qt截屏的效率讨论*
Qt提供的截屏方案虽然简单,但是时间开销有点大。如果我们希望录制fps25以上的画面时可能不尽如人意。因此如果是在Windows环境下,我推荐通过DirectX做截屏操作。有兴趣的同学可以参考我的源码,这里就不做过多讨论了。
二、功能封装
首先说明一下我们的封装目标。由于主线程需要留给界面和事件循环,因此音视频采集以及编码都各自运行在独立的线程中。音视频的采集可以和编码分离,通过队列暂存数据。
(1)界面设计(略)
这个部分不是本文的重点
(2)视频捕获线程(VideoAcquisitionThread)
const uchar* VideoAcquisitionThread::getRGB()
{
mtx.lock();
if (rgbs.size() > ) {
uchar *rgb = rgbs.front();
rgbs.pop_front();
mtx.unlock();
return rgb;
}
mtx.unlock();
return NULL;
} void VideoAcquisitionThread::run()
{
int interval = / fps;
QTime rt;
while (!isThreadQuit) {
if (rgbs.size() < listSize) {
rt.restart();
mtx.lock();
QPixmap pix = screen->grabWindow(wid);
uchar *rgb = new uchar[width * height * ]; // 申请图像存储空间
memcpy(rgb, pix.toImage().bits(), width * height * ); // 拷贝数据到新的内存区域
rgbs.push_back(rgb);
cout << ".";
mtx.unlock();
int el = rt.restart();
if (interval > el) {
msleep(interval - el);
}
}
}
}
(3)音频捕获线程(AudioAcquishtionThread)
const char* AudioAcquishtionThread::getPCM()
{
mtx.lock();
if (pcms.size() > ) {
char *pcm = pcms.front();
pcms.pop_front();
mtx.unlock();
return pcm;
}
mtx.unlock();
return NULL;
} void AudioAcquishtionThread::run()
{ while (!isThreadQuit) {
mtx.lock();
if (pcms.size() < listSize) {
int readOnceSize = ; // 每次从音频设备中读取的数据大小
int offset = ; // 当前已经读到的数据大小,作为pcm的偏移量
int pcmSize = * * ;
char *pcm = new char[pcmSize];
while (audioInput) {
int remains = pcmSize - offset; // 剩余空间
int ready = audioInput->bytesReady(); // 音频采集设备目前已经准备好的数据大小
if (ready < readOnceSize) { // 当前音频设备中的数据不足
QThread::msleep();
continue;
}
if (remains < readOnceSize) { // 当帧存储(pcmSize)的剩余空间(remainSize)小于单次读取数据预设(readSizeOnce)时
device->read(pcm + offset, remains); // 从设备中读取剩余空间大小的数据
// 读满一帧数据退出
break;
}
int len = device->read(pcm + offset, readOnceSize);
offset += len;
}
pcms.push_back(pcm);
}
mtx.unlock();
}
}
(4)初始化封装器,音视频流和音视频转码器
bool EncoderThread::init(QString filename, int fps)
{
close();
mtx.lock();
at = new AudioAcquishtionThread();
vt = new VideoAcquisitionThread();
// 启动音视频采集线程
vt->start(fps);
at->start();
this->filename = filename;
errnum = avformat_alloc_output_context2(&pFormatCtx, NULL, NULL, filename.toLocal8Bit().data());
if (errnum < ) {
av_strerror(errnum, errbuf, sizeof(errbuf));
mtx.unlock();
return false;
}
// 创建视频编码器
const AVCodec *vcodec = avcodec_find_encoder(AVCodecID::AV_CODEC_ID_H264);
if (!vcodec) {
mtx.unlock();
return false;
} pVideoCodecCtx = avcodec_alloc_context3(vcodec);
if (!pVideoCodecCtx) {
mtx.unlock();
return false;
} // 比特率、宽度、高度
pVideoCodecCtx->bit_rate = ;
pVideoCodecCtx->width = vt->getWidth();
pVideoCodecCtx->height = vt->getHeight();
// 时间基数、帧率
pVideoCodecCtx->time_base = { , fps };
pVideoCodecCtx->framerate = { fps, };
// 关键帧间隔
pVideoCodecCtx->gop_size = ;
// 不使用b帧
pVideoCodecCtx->max_b_frames = ;
// 帧、编码格式
pVideoCodecCtx->pix_fmt = AVPixelFormat::AV_PIX_FMT_YUV420P;
pVideoCodecCtx->codec_id = AVCodecID::AV_CODEC_ID_H264;
// 预设:快速
av_opt_set(pVideoCodecCtx->priv_data, "preset", "superfast", );
// 全局头
pVideoCodecCtx->flags |= AV_CODEC_FLAG_GLOBAL_HEADER; // 打开编码器
errnum = avcodec_open2(pVideoCodecCtx, vcodec, NULL);
if (errnum < ) {
av_strerror(errnum, errbuf, sizeof(errbuf));
mtx.unlock();
return false;
} // 创建音频编码器
const AVCodec *acodec = avcodec_find_encoder(AVCodecID::AV_CODEC_ID_AAC);
if (!acodec) {
mtx.unlock();
return false;
} // 根据编码器创建编码器上下文
pAudioCodecCtx = avcodec_alloc_context3(acodec);
if (!pAudioCodecCtx) {
mtx.unlock();
return false;
} // 比特率、采样率、采样类型、音频通道、文件格式
pAudioCodecCtx->bit_rate = ;
pAudioCodecCtx->sample_rate = ;
pAudioCodecCtx->sample_fmt = AVSampleFormat::AV_SAMPLE_FMT_FLTP;
pAudioCodecCtx->channels = ;
pAudioCodecCtx->channel_layout = av_get_default_channel_layout(); // 根据音频通道数自动选择输出类型(默认为立体声)
pAudioCodecCtx->flags |= AV_CODEC_FLAG_GLOBAL_HEADER; // 打开编码器
errnum = avcodec_open2(pAudioCodecCtx, acodec, NULL);
if (errnum < ) {
av_strerror(errnum, errbuf, sizeof(errbuf));
mtx.unlock();
return false;
} // 初始化视频转码器
swsCtx = sws_getContext(
vt->getWidth(), vt->getHeight(), AVPixelFormat::AV_PIX_FMT_BGRA,
vt->getWidth(), vt->getHeight(), AVPixelFormat::AV_PIX_FMT_YUV420P,
SWS_BICUBIC,
, , );
if (!swsCtx) {
mtx.unlock();
return false;
} // 初始化音频转码器
swrCtx = swr_alloc_set_opts(swrCtx,
av_get_default_channel_layout(), AVSampleFormat::AV_SAMPLE_FMT_FLTP, , // 输出
av_get_default_channel_layout(), AVSampleFormat::AV_SAMPLE_FMT_S16, , // 输入
, );
errnum = swr_init(swrCtx);
if (errnum < ) {
mtx.unlock();
return false;
} mtx.unlock();
return true;
}
(5)添加视频流
bool EncoderThread::addVideoStream()
{
mtx.lock();
// 为封装器创建视频流
pVideoStream = avformat_new_stream(pFormatCtx, NULL);
if (!pVideoStream) {
mtx.unlock();
return false;
} // 配置视频流的编码参数
errnum = avcodec_parameters_from_context(pVideoStream->codecpar, pVideoCodecCtx);
if (errnum < ) {
av_strerror(errnum, errbuf, sizeof(errbuf));
mtx.unlock();
return false;
} pVideoStream->codec->codec_tag = ;
pVideoStream->codecpar->codec_tag = ; mtx.unlock();
return true;
}
(6)添加音频流
bool EncoderThread::addAudioStream()
{
mtx.lock();
// 添加音频流
pAudioStream = avformat_new_stream(pFormatCtx, NULL);
if (!pAudioStream) {
mtx.unlock();
return false;
}
// 配置音频流的编码器参数
errnum = avcodec_parameters_from_context(pAudioStream->codecpar, pAudioCodecCtx);
if (errnum < ) {
av_strerror(errnum, errbuf, sizeof(errbuf));
mtx.unlock();
return false;
}
pAudioStream->codec->codec_tag = ;
pAudioStream->codecpar->codec_tag = ;
mtx.unlock();
return true;
}
(7)重写线程启动方法(代理模式)
void EncoderThread::start()
{ mtx.lock();
// 打开输出流
errnum = avio_open(&pFormatCtx->pb, filename.toLocal8Bit().data(), AVIO_FLAG_WRITE); // 打开AVIO流
if (errnum < ) {
av_strerror(errnum, errbuf, sizeof(errbuf));
avio_closep(&pFormatCtx->pb);
mtx.unlock();
return;
}
// 写文件头
errnum = avformat_write_header(pFormatCtx, NULL);
if (errnum < ) {
av_strerror(errnum, errbuf, sizeof(errbuf));
mtx.unlock();
return;
} quitFlag = false;
mtx.unlock();
QThread::start();
}
(8)编码线程
void EncoderThread::run()
{
// 初始化视频帧
AVFrame *vframe = av_frame_alloc();
vframe->format = AVPixelFormat::AV_PIX_FMT_YUV420P;
vframe->width = vt->getWidth();
vframe->height = vt->getHeight();
vframe->pts = ;
// 为视频帧分配空间
errnum = av_frame_get_buffer(vframe, );
if (errnum < ) {
av_strerror(errnum, errbuf, sizeof(errbuf));
return;
} // 初始化音频帧
AVFrame *aframe = av_frame_alloc();
aframe->format = AVSampleFormat::AV_SAMPLE_FMT_FLTP;
aframe->channels = ;
aframe->channel_layout = av_get_default_channel_layout();
aframe->nb_samples = ;
// 为音频帧分配空间
errnum = av_frame_get_buffer(aframe, );
if (errnum < ) {
av_strerror(errnum, errbuf, sizeof(errbuf));
return;
} int vpts = ;
int apts = ; while (!quitFlag) {
// 音频编码
const char *pcm = at->getPCM();
if (pcm) {
const uint8_t *in[AV_NUM_DATA_POINTERS] = { };
in[] = (uint8_t *)pcm; int len = swr_convert(swrCtx,
aframe->data, aframe->nb_samples, // 输出
in, aframe->nb_samples); // 输入
if (len < ) {
continue;
}
// 音频编码
errnum = avcodec_send_frame(pAudioCodecCtx, aframe);
if (errnum < ) {
av_strerror(errnum, errbuf, sizeof(errbuf));
continue;
}
AVPacket *apkt = av_packet_alloc();
errnum = avcodec_receive_packet(pAudioCodecCtx, apkt);
if (errnum < ) {
av_strerror(errnum, errbuf, sizeof(errbuf));
av_packet_free(&apkt);
continue;
}
apkt->stream_index = pAudioStream->index; apkt->pts = apts;
apkt->dts = apkt->pts;
apts += av_rescale_q(aframe->nb_samples, { , pAudioCodecCtx->sample_rate }, pAudioCodecCtx->time_base);
errnum = av_interleaved_write_frame(pFormatCtx, apkt);
if (errnum < ) {
av_strerror(errnum, errbuf, sizeof(errbuf));
continue;
}
delete pcm;
av_packet_free(&apkt);
cout << ".";
} int ts = av_compare_ts(vpts, pVideoCodecCtx->time_base, apts, pAudioCodecCtx->time_base);
if (ts > ) {
continue;
} // 视频编码
const uchar *rgb = vt->getRGB();
if (rgb) {
// 固定写法:配置1帧原始视频画面的数据结构通常为RGBA的形式
uint8_t *srcSlice[AV_NUM_DATA_POINTERS] = { };
srcSlice[] = (uint8_t *)rgb;
int srcStride[AV_NUM_DATA_POINTERS] = { };
srcStride[] = vt->getWidth() * ;
// 转换
int h = sws_scale(swsCtx, srcSlice, srcStride, , vt->getHeight(), vframe->data, vframe->linesize);
if (h < ) {
continue;
} vframe->pts = vpts++;
errnum = avcodec_send_frame(pVideoCodecCtx, vframe);
if (errnum < ) {
av_strerror(errnum, errbuf, sizeof(errbuf));
continue;
}
AVPacket *vpkt = av_packet_alloc(); errnum = avcodec_receive_packet(pVideoCodecCtx, vpkt);
if (errnum < || vpkt->size <= ) {
av_packet_free(&vpkt);
av_strerror(errnum, errbuf, sizeof(errbuf));
continue;
}
// 转换pts
av_packet_rescale_ts(vpkt, pVideoCodecCtx->time_base, pVideoStream->time_base);
vpkt->stream_index = pVideoStream->index;
// 向封装器中写入压缩报文,该函数会自动释放pkt空间,不需要调用者手动释放
errnum = av_interleaved_write_frame(pFormatCtx, vpkt);
if (errnum < ) {
av_strerror(errnum, errbuf, sizeof(errbuf));
continue;
}
delete rgb;
av_packet_free(&vpkt);
cout << "*";
}
} errnum = av_write_trailer(pFormatCtx);
if (errnum != ) {
return;
}
errnum = avio_closep(&pFormatCtx->pb); // 关闭AVIO流
if (errnum != ) {
return;
} // 清理音视频帧
if (vframe) {
av_frame_free(&vframe);
}
if (aframe) {
av_frame_free(&aframe);
}
}
(9)关闭与成员变量析构
void EncoderThread::close()
{
mtx.lock();
quitFlag = true;
wait();
if (pFormatCtx) {
avformat_close_input(&pFormatCtx); // 关闭封装上下文
}
// 关闭编码器和清理上下文的所有空间
if (pVideoCodecCtx) {
avcodec_close(pVideoCodecCtx);
avcodec_free_context(&pVideoCodecCtx);
}
if (pAudioCodecCtx) {
avcodec_close(pAudioCodecCtx);
avcodec_free_context(&pAudioCodecCtx);
}
// 音视频转换上下文
if (swsCtx) {
sws_freeContext(swsCtx);
swsCtx = NULL;
}
if (swrCtx) {
swr_free(&swrCtx);
}
mtx.unlock(); }
这个部分都是对代码的封装处理,这里就不做什么解释了。最后附上完整的源码地址,仅供参考。
Qt与FFmpeg联合开发指南(四)——编码(2):完善功能和基础封装的更多相关文章
- Qt与FFmpeg联合开发指南(三)——编码(1):代码流程演示
前两讲演示了基本的解码流程和简单功能封装,今天我们开始学习编码.编码就是封装音视频流的过程,在整个编码教程中,我会首先在一个函数中演示完成的编码流程,再解释其中存在的问题.下一讲我们会将编码功能进行封 ...
- Qt与FFmpeg联合开发指南(二)——解码(2):封装和界面设计
与解码相关的主要代码在上一篇博客中已经做了介绍,本篇我们会先讨论一下如何控制解码速度再提供一个我个人的封装思路.最后回归到界面设计环节重点看一下如何保证播放器界面在缩放和拖动的过程中保证视频画面的宽高 ...
- Qt与FFmpeg联合开发指南(一)——解码(1):功能实现
前言:对于从未接触过音视频编解码的同学来说,使用FFmpeg的学习曲线恐怕略显陡峭.本人由于工作需要,正好需要在项目中使用.因此特地将开发过程总结下来.只当提供给有兴趣的同学参考和学习. 由于FFmp ...
- 基于Asterisk的VoIP开发指南——(1)实现基本呼叫功能
原文:基于Asterisk的VoIP开发指南--(1)实现基本呼叫功能 说明: 1.本文档探讨基于Asterisk如何实现VoIP的一些基本功能,包括基本呼叫功能的方案选取.主叫号码透传.如何编写As ...
- Qt版本中国象棋开发(四)
内容:走法产生 中国象棋基础搜索AI, 极大值,极小值剪枝搜索, 静态估值函数 理论基础: (一)人机博弈走法产生: 先遍历某一方的所有棋子,再遍历整个棋盘,得到每个棋子的所有走棋情况(效率不高,可以 ...
- 【JavaWeb项目】一个众筹网站的开发(四)后台用户注册功能
重点: 密码加密存储 使用jQuery插件做校验和错误提示等 密码不能明文存储,在数据库中是加密存储的 可逆加密:通过密文使用解密算法得到明文 DES AES 不可逆加密:通过密文,得不到明文 MD5 ...
- PHP全栈开发(四): HTML 学习(1.基础标签+表格标签)
简单的学习一下HTML 学习HTML采用在www.runoob.com上学习的方法. 而且该网站还提供在线编辑器. 然后HTML编辑器使用Notepad++ 记得上Emmet的官网http://emm ...
- Jetty使用教程(四:21-22)—Jetty开发指南
二十一.嵌入式开发 21.1 Jetty嵌入式开发HelloWorld 本章节将提供一些教程,通过Jetty API快速开发嵌入式代码 21.1.1 下载Jetty的jar包 Jetty目前已经把所有 ...
- ffmpeg开发指南
FFmpeg是一个集录制.转换.音/视频编码解码功能为一体的完整的开源解决方案.FFmpeg的开发是基于Linux操作系统,但是可以在大多数操作系统中编译和使用.FFmpeg支持MPEG.DivX.M ...
随机推荐
- js数组之可变函数
在js的数组中有两个方法为数组添加元素:1.push();2.unshift(),push函数是将元素添加到数组的末尾,现在不用说大家估计也能猜出来,unshift这个函数就是把元素添加到数组的开头的 ...
- 54. Spiral Matrix(剑指offer--19)
Given a matrix of m x n elements (m rows, n columns), return all elements of the matrix in spiral or ...
- linux常用命令:cal 命令
cal命令可以用来显示公历(阳历)日历.公历是现在国际通用的历法,又称格列历,通称阳历.“阳历”又名“太阳历”,系以地球绕行太阳一周为一年,为西方各国所通用,故又名“西历”. 1.命令格式: cal ...
- nohup 命令(设置后台进程): appending output to ‘nohup.out’ 问题
一.Linux 下使用 nohup Unix/Linux下一般比如想让某个程序在后台运行,很多都是使用 & 在程序结尾来让程序自动运行. 比如我们要运行weblogic在后台:./startW ...
- pyDay6
内容来自廖雪峰的官方网站 1.在Python中,代码不是越多越好,而是越少越好.代码不是越复杂越好,而是越简单越好,1行代码能实现的功能,决不写5行代码.请始终牢记,代码越少,开发效率越高. 2.取指 ...
- Python3.x:抓取百事糗科段子
Python3.x:抓取百事糗科段子 实现代码: #Python3.6 获取糗事百科的段子 import urllib.request #导入各类要用到的包 import urllib import ...
- web应用下的安全问题以及tomcat/nginx对应解决方法(持续更新、亲测可解决问题)
最近一券商那边扫描反馈了下面几个非业务型安全漏洞,要求解决,如下: XSS 自己写个脚本response的时候对特殊字符进行了处理,或者网上搜下一堆(不要忘了回车.换行). HTML form wit ...
- Android 深入理解Activity 页面Intent跳转
- PHP安装包TS和NTS的区别
原文链接:http://blog.csdn.net/zhuifengshenku/article/details/38796555 TS指Thread Safety,即线程安全,一般在IIS以ISAP ...
- cron表达式增加一段时间变为新的表达式
cron表达式是使用任务调度经常使用的表达式了.对于通常的简单任务,我们只需要一条cron表达式就能满足.但是有的时候任务也可以很复杂. 最近我遇到了一个问题,一条任务在开始的时候要触发A方法,在结束 ...