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 ...
随机推荐
- EF6添加mysql的edmx实体时报错:无法生成模型:“System.Data.StrongTypingException: 表“TableDetails”中列“IsPrimaryKey”的值为 DBNull
EF6.1.3 ,使用mysql5.7的实体数据模型时,提示: 由于出现以下异常,无法生成模型:“System.Data.StrongTypingException: 表“TableDetails”中 ...
- C# Bulk Operations(转)
转自http://blog.csdn.net/winnyrain/article/details/51240684 Overcome SqlBulkCopy Limitations with C# B ...
- CMPXCHG指令
一.CMPXCHG汇编指令详解. 这条指令将al\ax\eax\rax中的值与首操作数比较: 1.如果相等,第2操作数的直装载到首操作数,zf置1.(相当于相减为0,所以0标志位置位) 2.如果不等, ...
- 结合ajax 的表单验证
浪费了我两天的时间 我也是醉了 html 结构 <!-- 密码修改 --> <div class="modal fade" id="operatePa ...
- Linux服务器---基础设置
Centos分辨率 virtualbox里新安装的Centos 7 的分辨率默认的应该是800*600. 如果是‘最小化安装’的Centos7 进入的就是命令模式 .如果安装的是带有GUI的 ...
- javashop组件开发指南
javashop组件开发指南 1. 概念解释 组件:可以理解为是插件,功能点的一个集合. 插件:是指具体的某个功能. 插件桩:是负责调用插件. 事件:是要决定什么时候执行插件 一个组件是由多 ...
- Servlet过滤器和监听器配置范例
1,Servlet过滤器 <filter> <filter-name>charset</filter-name> <filter-class>org.g ...
- 04: 打开tornado源码剖析处理过程
目录:Tornado其他篇 01: tornado基础篇 02: tornado进阶篇 03: 自定义异步非阻塞tornado框架 04: 打开tornado源码剖析处理过程 目录: 1.1 torn ...
- 01: html常用标签
目录: 1.1 web开发的三把利器介绍 1.2 网页头部head标签中几个常用标签 1.3 html常用标签归类 1.4 input系列标签 1.5 HTML其他标签 1.1 web开发的三把利器介 ...
- JAVA学习调查问卷——20145101
1.你对自己的未来有什么规划?做了哪些准备? 我希望在未来不管自己是否从事机要工作,都要做一个有能力,对社会能有所贡献的人.所以在现阶段我应该努力学习基础知识,夯实基本功,具备成为合格机要人的素质. ...