Qt与FFmpeg联合开发指南——编码:完善功能和基础封装
Posted 思踌之路
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Qt与FFmpeg联合开发指南——编码:完善功能和基础封装相关的知识,希望对你有一定的参考价值。
上一章我用一个demo函数演示了基于Qt的音视频采集到编码的完整流程,最后经过测试我们也发现了代码中存在的问题。本章我们就先处理几个遗留问题,再对代码进行完善,最后把编码功能做基础封装。
一、遗留问题和解决方法
(1)如何让音视频的录制保持同步?
在我们的演示代码中之所以发现音视频录制不同步的主要原因是音频帧和视频帧不应该简单的按照1:1的比例进行编码。那么到底应该以什么样的比例控制呢?首先建议大家回顾一下之前写过的解码过程。如果我们把解码音视频的过程输出到控制台,我们会注意到大致每解码一帧画面应该解码2~4帧声音。按照这个思路我们先尝试修改一下demo中的编码步骤,人为控制视频和音频的编码比例为1:3。修改以后的代码如下:
// 音频编码 for (int i = 0; i < 3; ++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, { 1, pAudioCodecCtx->sample_rate }, pAudioCodecCtx->time_base); // 1024 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 > 0) { 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 * 4]; // 申请图像存储空间 memcpy(rgb, pix.toImage().bits(), width * height * 4); // 拷贝数据到新的内存区域
这样外部的调用者正常对rgb数据析构就不会有任何问题了。
(3)有关Qt截屏的效率讨论*
Qt提供的截屏方案虽然简单,但是时间开销有点大。如果我们希望录制fps25以上的画面时可能不尽如人意。因此如果是在Windows环境下,我推荐通过DirectX做截屏操作。有兴趣的同学可以参考我的源码,这里就不做过多讨论了。
二、功能封装
首先说明一下我们的封装目标。由于主线程需要留给界面和事件循环,因此音视频采集以及编码都各自运行在独立的线程中。音视频的采集可以和编码分离,通过队列暂存数据。
(1)界面设计(略)
这个部分不是本文的重点
(2)视频捕获线程(VideoAcquisitionThread)
const uchar* VideoAcquisitionThread::getRGB() { mtx.lock(); if (rgbs.size() > 0) { uchar *rgb = rgbs.front(); rgbs.pop_front(); mtx.unlock(); return rgb; } mtx.unlock(); return NULL; } void VideoAcquisitionThread::run() { int interval = 1000 / fps; QTime rt; while (!isThreadQuit) { if (rgbs.size() < listSize) { rt.restart(); mtx.lock(); QPixmap pix = screen->grabWindow(wid); uchar *rgb = new uchar[width * height * 4]; // 申请图像存储空间 memcpy(rgb, pix.toImage().bits(), width * height * 4); // 拷贝数据到新的内存区域 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() > 0) { 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 = 1024; // 每次从音频设备中读取的数据大小 int offset = 0; // 当前已经读到的数据大小,作为pcm的偏移量 int pcmSize = 1024 * 2 * 2; char *pcm = new char[pcmSize]; while (audioInput) { int remains = pcmSize - offset; // 剩余空间 int ready = audioInput->bytesReady(); // 音频采集设备目前已经准备好的数据大小 if (ready < readOnceSize) { // 当前音频设备中的数据不足 QThread::msleep(1); 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 < 0) { 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 = 4000000; pVideoCodecCtx->width = vt->getWidth(); pVideoCodecCtx->height = vt->getHeight(); // 时间基数、帧率 pVideoCodecCtx->time_base = { 1, fps }; pVideoCodecCtx->framerate = { fps, 1 }; // 关键帧间隔 pVideoCodecCtx->gop_size = 10; // 不使用b帧 pVideoCodecCtx->max_b_frames = 0; // 帧、编码格式 pVideoCodecCtx->pix_fmt = AVPixelFormat::AV_PIX_FMT_YUV420P; pVideoCodecCtx->codec_id = AVCodecID::AV_CODEC_ID_H264; // 预设:快速 av_opt_set(pVideoCodecCtx->priv_data, "preset", "superfast", 0); // 全局头 pVideoCodecCtx->flags |= AV_CODEC_FLAG_GLOBAL_HEADER; // 打开编码器 errnum = avcodec_open2(pVideoCodecCtx, vcodec, NULL); if (errnum < 0) { 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 = 64000; pAudioCodecCtx->sample_rate = 44100; pAudioCodecCtx->sample_fmt = AVSampleFormat::AV_SAMPLE_FMT_FLTP; pAudioCodecCtx->channels = 2; pAudioCodecCtx->channel_layout = av_get_default_channel_layout(2); // 根据音频通道数自动选择输出类型(默认为立体声) pAudioCodecCtx->flags |= AV_CODEC_FLAG_GLOBAL_HEADER; // 打开编码器 errnum = avcodec_open2(pAudioCodecCtx, acodec, NULL); if (errnum < 0) { 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, 0, 0, 0); if (!swsCtx) { mtx.unlock(); return false; } // 初始化音频转码器 swrCtx = swr_alloc_set_opts(swrCtx, av_get_default_channel_layout(2), AVSampleFormat::AV_SAMPLE_FMT_FLTP, 44100, // 输出 av_get_default_channel_layout(2), AVSampleFormat::AV_SAMPLE_FMT_S16, 44100, // 输入 0, 0); errnum = swr_init(swrCtx); if (errnum < 0) { 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 < 0) { av_strerror(errnum, errbuf, sizeof(errbuf)); mtx.unlock(); return false; } pVideoStream->codec->codec_tag = 0; pVideoStream->codecpar->codec_tag = 0; 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 < 0) { av_strerror(errnum, errbuf, sizeof(errbuf)); mtx.unlock(); return false; } pAudioStream->codec->codec_tag = 0; pAudioStream->codecpar->codec_tag = 0; mtx.unlock(); return true; }
(7)重写线程启动方法(代理模式)
void EncoderThread::start() { mtx.lock(); // 打开输出流 errnum = avio_open(&pFormatCtx->pb, filename.toLocal8Bit().data(), AVIO_FLAG_WRITE); // 打开AVIO流 if (errnum < 0) { av_strerror(errnum, errbuf, sizeof(errbuf)); avio_closep(&pFormatCtx->pb); mtx.unlock(); return; } // 写文件头 errnum = avformat_write_header(pFormatCtx, NULL); if (errnum < 0) { 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 = 0; // 为视频帧分配空间 errnum = av_frame_get_buffer(vframe, 32); if (errnum < 0) { av_strerror(errnum, errbuf, sizeof(errbuf)); return; } // 初始化音频帧 AVFrame *aframe = av_frame_alloc(); aframe->format = AVSampleFormat::AV_SAMPLE_FMT_FLTP; aframe->channels = 2; aframe->channel_layout = av_get_default_channel_layout(2); aframe->nb_samples = 1024; // 为音频帧分配空间 errnum = av_frame_get_buffer(aframe, 0); if (errnum < 0) { av_strerror(errnum, errbuf, sizeof(errbuf)); return; } int vpts = 0; int apts = 0; while (!quitFlag) { // 音频编码 const char *pcm = at->getPCM(); if (pcm) { const uint8_t *in[AV_NUM_DATA_POINTERS] = { 0 }; in[0] = (uint8_t *)pcm; int len = swr_convert(swrCtx, aframe->data, aframe->nb_samples, // 输出 in, aframe->nb_samples); // 输入 if (len < 0) { continue; } // 音频编码 errnum = avcodec_send_frame(pAudioCodecCtx, aframe); if (errnum < 0) { av_strerror(errnum, errbuf, sizeof(errbuf)); continue; } AVPacket *apkt = av_packet_alloc(); errnum = avcodec_receive_packet(pAudioCodecCtx, apkt); if (errnum < 0) { 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, { 1, pAudioCodecCtx->sample_rate }, pAudioCodecCtx->time_base); errnum = av_interleaved_write_frame(pFormatCtx, apkt); if (errnum < 0) { 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 > 0) { continue; } // 视频编码 const uchar *rgb = vt->getRGB(); if (rgb) { // 固定写法:配置1帧原始视频画面的数据结构通常为RGBA的形式 uint8_t *srcSlice[AV_NUM_DATA_POINTERS] = { 0 }; srcSlice[0] = (uint8_t *)rgb; int srcStride[AV_NUM_DATA_POINTERS] = { 0 }; srcStride[0] = vt->getWidth() * 4; // 转换 int h = sws_scale(swsCtx, srcSlice, srcStride, 0, vt->getHeight(), vframe->data, vframe->linesize); if (h < 0) { continue; } vframe->pts = vpts++; errnum = avcodec_send_frame(pVideoCodecCtx, vframe); if (errnum < 0) { av_strerror(errnum, errbuf, sizeof(errbuf)); continue; } AVPacket *vpkt = av_packet_alloc(); errnum = avcodec_receive_packet(pVideoCodecCtx, vpkt); if (errnum < 0 || vpkt->size <= 0) { 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 < 0) { av_strerror(errnum, errbuf, sizeof(errbuf)); continue; } delete rgb; av_packet_free(&vpkt); cout << "*"; } } errnum = av_write_trailer(pFormatCtx); if (errnum != 0) { return; } errnum = avio_closep(&pFormatCtx->pb); // 关闭AVIO流 if (errnum != 0) { 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联合开发指南——编码:完善功能和基础封装的主要内容,如果未能解决你的问题,请参考以下文章
基于 FFMPEG 的视频编码(libavcodec ,致敬雷霄骅)
基于 FFMPEG 的视频编码(libavcodec ,致敬雷霄骅)
基于 FFMPEG 的视频编码(libavcodec ,致敬雷霄骅)
Qt音视频开发29-ffmpeg中x264/x265编码库支持