上一章我用一个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):完善功能和基础封装的更多相关文章

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

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

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

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

  3. Qt与FFmpeg联合开发指南(一)——解码(1):功能实现

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

  4. 基于Asterisk的VoIP开发指南——(1)实现基本呼叫功能

    原文:基于Asterisk的VoIP开发指南--(1)实现基本呼叫功能 说明: 1.本文档探讨基于Asterisk如何实现VoIP的一些基本功能,包括基本呼叫功能的方案选取.主叫号码透传.如何编写As ...

  5. Qt版本中国象棋开发(四)

    内容:走法产生 中国象棋基础搜索AI, 极大值,极小值剪枝搜索, 静态估值函数 理论基础: (一)人机博弈走法产生: 先遍历某一方的所有棋子,再遍历整个棋盘,得到每个棋子的所有走棋情况(效率不高,可以 ...

  6. 【JavaWeb项目】一个众筹网站的开发(四)后台用户注册功能

    重点: 密码加密存储 使用jQuery插件做校验和错误提示等 密码不能明文存储,在数据库中是加密存储的 可逆加密:通过密文使用解密算法得到明文 DES AES 不可逆加密:通过密文,得不到明文 MD5 ...

  7. PHP全栈开发(四): HTML 学习(1.基础标签+表格标签)

    简单的学习一下HTML 学习HTML采用在www.runoob.com上学习的方法. 而且该网站还提供在线编辑器. 然后HTML编辑器使用Notepad++ 记得上Emmet的官网http://emm ...

  8. Jetty使用教程(四:21-22)—Jetty开发指南

    二十一.嵌入式开发 21.1 Jetty嵌入式开发HelloWorld 本章节将提供一些教程,通过Jetty API快速开发嵌入式代码 21.1.1 下载Jetty的jar包 Jetty目前已经把所有 ...

  9. ffmpeg开发指南

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

随机推荐

  1. 一个新人对HTML的理解

    首先 HTML里面包含的东西是什么? 在HTML里面   注释的表示方式是    <!--注释内容--> 注释 HTML初始默认包含了两大部分: 一部分是 <head>< ...

  2. map() 方法

    1. 方法概述 map() 方法返回一个由原数组中的每个元素调用一个指定方法后的返回值组成的新数组. 2. 例子 2.1 在字符串中使用map 在一个 String 上使用 map 方法获取字符串中每 ...

  3. Javascript--运算符判断成绩运算

    <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...

  4. nginx安装,反向代理配置

    1.centos 版本 下载最新稳定版 https://www.nginx.com/resources/wiki/start/topics/tutorials/install/# 2.执行语句: ./ ...

  5. VS2010/MFC编程入门之三十二(常用控件:标签控件Tab Control 上)

    前面两节鸡啄米讲了树形控件Tree Control,本节开始讲解标签控件Tab Control,也可以称为选项卡控件. 标签控件简介 标签控件也比较常见.它可以把多个页面集成到一个窗口中,每个页面对应 ...

  6. Solr安装中文分词器IK

    安装环境 jdk1.7 solr-4.10.3.tgz KAnalyzer2012FF_u1.jar tomcat7 VM虚拟机redhat6.5-x64:192.168.1.201 Xshell4 ...

  7. Python: re.compile()

    compile(pattern,flags=0) 1.编译一个正则表达式模式,返回一个模式对象 2.第二个参数flags是匹配模式,可以使用按位或‘|'表示同时生效,也可以在正则表达式字符串中指定 P ...

  8. 音响理论基础入门:Gain(增益)

    谈到放大器就必须先了解增益:一个小的信号Level(电平)经过放大电路成为大的信号Level ,也就是说由小变大之间的差异就叫增益,也叫放大率,反过来的叫衰减率.在音响系统内,一般以信号源的输入电平决 ...

  9. C/C++之全局、static对象/变量的初始化问题

    关于全局.static对象/变量的初始化问题 1. 全局变量.static变量的初始化时机:main()函数执行之前(或者说main中第一个用户语句执行之前). 2. 初始化顺序. 1)全局对象.外部 ...

  10. Python入门之实现简单的购物车功能

    Talk is cheap,Let's do this! product_list = [ ['Iphone7 Plus', 6500], ['Iphone8 ', 8200], ['MacBook ...