上一章我用一个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. 下载mysql的源码包

  2. Azkaban学习笔记(一)

    1. 任务调度概述 一个完整的数据分析系统通常都是由大量任务单元组成: shell脚本程序,java程序,mapreduce程序.hive脚本等 各任务单元之间存在时间先后及前后依赖关系 现成的开源调 ...

  3. bzoj1689 / P1589 [Usaco2005 Open] Muddy roads 泥泞的路

    P1589 [Usaco2005 Open] Muddy roads 泥泞的路 简单的模拟题. 给水坑排个序,蓝后贪心放板子. 注意边界细节. #include<iostream> #in ...

  4. Linux服务器上Tomcat的Web工程部署

    Linux服务器上Tomcat的Web工程部署 部署Web应用到Tomcat服务器就是将开放好的JavaWeb应用打包成war包,然后发布到tomcat服务器的webapps目录下: 步骤1,先进入t ...

  5. 安装使用composer基本流程

    composer工作原理: 这里经过几个步骤:1.composer读取composer.json(这个文件手动建立,官网有格式),这个json是在当前执行composer目录的,如果目录下没有这个js ...

  6. Python3基础 str find+index 是否存在指定字符串,有则返回第一个索引值

             Python : 3.7.0          OS : Ubuntu 18.04.1 LTS         IDE : PyCharm 2018.2.4       Conda ...

  7. 逃离迷宫(BFS)题解

    Problem Description 给定一个m × n (m行, n列)的迷宫,迷宫中有两个位置,gloria想从迷宫的一个位置走到另外一个位置,当然迷宫中有些地方是空地,gloria可以穿越,有 ...

  8. RHEL7使用NAT方式上网

    1.首先,Windows7无法设置网络共享VMNet8的问题,是因为禁用了Firewall服务,设置为自动,启用即可:且需要启动VMWare的DHCP和NAT两个服务,这两个服务在我的机器上是关闭的, ...

  9. springBoot 打包 dubbo jar包

    参看链接:http://blog.csdn.net/cqdz_dj/article/details/51942681       http://blog.csdn.net/u014695188/art ...

  10. C#学习笔记(十八):数据结构和泛型

    数据结构 只有这四种 a.集合:数据之间没有特定的关系 b.线性结构:数据之间有一对一的前后联系 c.树形结构:数据之间有一对多的关系,一个父节点有多个子节点,一个子节点只能有一个父节点 d.图状结构 ...