项目介绍
本项目是为了实现一个window环境的音视频直播推流或者说录制软件,本篇博客主要介绍使用qt的库,采集视频yuv数据,并实现预览,参数设置。可以截图。 采集麦克风数据,以及实时监测音频活动量。
效果图如下:
File菜单的开始按钮启动采集的start,可以启动yuv的写文件。后期就是是启动编码,封装,推流。万里长征第一步。
开发环境
- VS 版本: Visual Studio Professional 2022 (64 位)
- QT 版本: 5.12.0
- c++ 语言
代码仓库
代码结构截图
程序环境描述
程序使用qt环境开发,要添加qt的以下模块
vs 里面如何继承qt环境 参考我的博客 # vs studio 集成qt环境
主要代码介绍
- main.cpp 主程序启动
- MediaPushWindow.ui 主界面的ui文件
- imagesettings.ui 视频采集的设置ui文件
- MediaPushWindow.cpp 主ui的控制程序,以及负责主要逻辑的集成
- CBaseCameraSurface.cpp 视频采集yuv的抽象类实现
- AudioCapture.cpp 音频采集的实现文件
核心代码介绍
1. UI界面
1. MediaPushWindow.ui 是主程序界面,如下
本界面是参考官方example,File 菜单里面,关于启动和停止,设置。
- VDevices ADevices里面列出系统可用的摄像头和麦克风列表。
- 右边的focus是对焦按钮。
- Image/Video是在摄像头支持的情况下,采集图片模式和视频模式(CaptureStillImage和CaptureVideo)。我的目前只支持CaptureStillImage,所以另外一种模式的代码,目前是删除的。
- Capture Photo 按钮是截图,保存地址,会显示在状态栏里面。
- Exposure Compensation 是曝光补偿。有效与否,看摄像头支持。
2. imagesettings.ui 设置界面
负责采集预览的分辨率,截图的图片格式。视频质量的设置,确定后实时生效,在start后,不能中途设置。
2. 初始化
MediaPushWindow 的构造函数是初始化过程
MediaPushWindow::MediaPushWindow(QWidget *parent)
: QMainWindow(parent)
{
ui.setupUi(this);
//添加摄像头设备菜单
QActionGroup* videoDevicesGroup = new QActionGroup(this);
videoDevicesGroup->setExclusive(true);
const QList<QCameraInfo> availableCameras = QCameraInfo::availableCameras();
for (const QCameraInfo& cameraInfo : availableCameras) {
QAction* videoDeviceAction = new QAction(cameraInfo.description(), videoDevicesGroup);
videoDeviceAction->setCheckable(true);
videoDeviceAction->setData(QVariant::fromValue(cameraInfo));
if (cameraInfo == QCameraInfo::defaultCamera())
videoDeviceAction->setChecked(true);
ui.menuVDevices->addAction(videoDeviceAction);
}
//监听选择摄像头
connect(videoDevicesGroup, &QActionGroup::triggered, this, &MediaPushWindow::updateCameraDevice);
//添加麦克风设备菜单
QActionGroup* audioDevicesGroup = new QActionGroup(this);
audioDevicesGroup->setExclusive(true);
QList<QAudioDeviceInfo> availableMics =QAudioDeviceInfo::availableDevices(QAudio::Mode::AudioInput);
for (const QAudioDeviceInfo& audioInfo : availableMics) {
QAction* audioDeviceAction = new QAction(audioInfo.deviceName(), audioDevicesGroup);
audioDeviceAction->setCheckable(true);
audioDeviceAction->setData(QVariant::fromValue(audioInfo));
if (audioInfo == QAudioDeviceInfo::defaultInputDevice());
audioDeviceAction->setChecked(true);
ui.menuADevices->addAction(audioDeviceAction);
}
//监听麦克风设备
connect(audioDevicesGroup, &QActionGroup::triggered, this, &MediaPushWindow::updateMicDevice);
//视频的模式切换
connect(ui.captureWidget, &QTabWidget::currentChanged, this, &MediaPushWindow::updateCaptureMode);
//设置摄像头
setCamera(QCameraInfo::defaultCamera());
//设置麦克风
setMic(QAudioDeviceInfo::defaultInputDevice());
ui.actionStart->setEnabled(true);
ui.actionStop->setEnabled(false);
ui.actionSettings->setEnabled(true);
renderArea = new RenderArea(this);
renderArea->setLevel(0.01);
//renderArea->resize(600, 30);
ui.statusbar->addWidget(renderArea);
ui.statusbar->setStyleSheet("background-color: white;");
}
RenderArea 是用于实时展示麦克风活动量的状态。就是底部红色条。可以查看自己使用的麦克风是否有效数据。
- 初始化的时候,是没有启动写文件的,所以file/start,file/setings 按钮是可用的。
- updateCaptureMode 是右边的那个tab的切换的,会更具是支持,如果不支持,就不能切换。比如我的这里video是不支持的。
- updateCameraDevice,updateMicDevice 就是触发不同mic,camera的设备切换时候触发的。里面调用的也就是setCamera,setMic发方法
//设置摄像头
setCamera(QCameraInfo::defaultCamera());
//设置麦克风
setMic(QAudioDeviceInfo::defaultInputDevice());
- 启动默认设备。setCamera,setMic 也是实现的核心启动逻辑
3. 启动摄像头
void MediaPushWindow::setCamera(const QCameraInfo& cameraInfo)
{
m_camera.reset(new QCamera(cameraInfo));
connect(m_camera.data(), &QCamera::stateChanged, this, &MediaPushWindow::updateCameraState);
connect(m_camera.data(), QOverload<QCamera::Error>::of(&QCamera::error), this, &MediaPushWindow::displayCameraError);
m_imageCapture.reset(new QCameraImageCapture(m_camera.data()));
connect(ui.exposureCompensation, &QAbstractSlider::valueChanged, this, &MediaPushWindow::setExposureCompensation);
if (showUserViewFinder)
{
m_camera->setViewfinder(ui.viewfinder);
}
else
{
//创建yuv采集器
if (!m_pCameraInterface)
{
m_pCameraInterface.reset(new CBaseCameraSurface(this));
}
m_camera->setViewfinder(m_pCameraInterface.data());
displayCapturedImage();
connect(m_pCameraInterface.data(), SIGNAL(frameAvailable(QVideoFrame&)), this, SLOT(recvVFrame(QVideoFrame&)));
}
updateCameraState(m_camera->state());
updateLockStatus(m_camera->lockStatus(), QCamera::UserRequest);
connect(m_imageCapture.data(), &QCameraImageCapture::readyForCaptureChanged, this, &MediaPushWindow::readyForCapture);
connect(m_imageCapture.data(), &QCameraImageCapture::imageCaptured, this, &MediaPushWindow::processCapturedImage);
connect(m_imageCapture.data(), &QCameraImageCapture::imageSaved, this, &MediaPushWindow::imageSaved);
connect(m_imageCapture.data(), QOverload<int, QCameraImageCapture::Error, const QString&>::of(&QCameraImageCapture::error),
this, &MediaPushWindow::displayCaptureError);
connect(m_camera.data(), QOverload<QCamera::LockStatus, QCamera::LockChangeReason>::of(&QCamera::lockStatusChanged),
this, &MediaPushWindow::updateLockStatus);
QList<QVideoFrame::PixelFormat> list = m_camera->supportedViewfinderPixelFormats();
ui.captureWidget->setTabEnabled(0, (m_camera->isCaptureModeSupported(QCamera::CaptureStillImage)));
ui.captureWidget->setTabEnabled(1, (m_camera->isCaptureModeSupported(QCamera::CaptureVideo)));
updateCaptureMode();
m_camera->start();
}
m_camera 是QT的QCamera类型,每次切换一个摄像头的时候,重新创建一个
updateCameraState方法里面更具摄像头启动状态。去设置一个ui的可用与否。
displayCameraError 显示一些camera的异常错误
setExposureCompensation 是监听设置的曝光补偿的值,然后设置
m_imageCapture 是QCameraImageCapture类型,主要用于截图的一些监听,以下就是它的几个监听方法,主要是准备截图,截图保存的处理
connect(m_imageCapture.data(), &QCameraImageCapture::readyForCaptureChanged, this, &MediaPushWindow::readyForCapture);
connect(m_imageCapture.data(), &QCameraImageCapture::imageCaptured, this, &MediaPushWindow::processCapturedImage);
connect(m_imageCapture.data(), &QCameraImageCapture::imageSaved, this, &MediaPushWindow::imageSaved);
connect(m_imageCapture.data(), QOverload<int, QCameraImageCapture::Error, const QString&>::of(&QCameraImageCapture::error),
this, &MediaPushWindow::displayCaptureError);
updateLockStatus 是设置对焦状态,也是控制focus按钮的状态。
接下来就是设置图片模式和视频模式的状态
if (showUserViewFinder)
{
m_camera->setViewfinder(ui.viewfinder);
}
else
{
//创建yuv采集器
if (!m_pCameraInterface)
{
m_pCameraInterface.reset(new CBaseCameraSurface(this));
}
m_camera->setViewfinder(m_pCameraInterface.data());
displayCapturedImage();
connect(m_pCameraInterface.data(), SIGNAL(frameAvailable(QVideoFrame&)), this, SLOT(recvVFrame(QVideoFrame&)));
}
以上是核心,showUserViewFinder是我自己加的变量。如果只是预览,那么可以使用一个 qt自带的QCameraViewfinder的类型,ui.viewfinder 。 但是无法采集到yuv数据。当为false状态的时候。m_pCameraInterface是一个根据QAbstractVideoSurface抽象类实现的采集器器。里面的实现后面介绍。连接它的信号 frameAvailable可以在recvVFrame的槽函数里面收到QVideoFrame方法。在recvVFrame方法里面,我们可以把视频帧渲染到widget里面。
最后调用camera的start方法,就启动了预览采集。
4. 启动麦克风
void MediaPushWindow::setMic(const QAudioDeviceInfo& cameraInfo)
{
m_mic.reset(new AudioCapture());
connect(m_mic.data(), SIGNAL(aframeAvailable(const char* data, qint64 len)), this, SLOT(recvAFrame(const char* data, qint64 len)));
//监听麦克风的level
connect(m_mic.data(), &AudioCapture::updateLevel, [this]() {
renderArea->setLevel(m_mic->level());
});
m_mic->Start(cameraInfo);
}
每次切换一个麦克风,重新创建一个 AudioCapture。
updateLevel是音频采集活动量的信号,设置到renderArea里面,用来判断是否有声音。
后面启动麦克风。关于这个AudioCaptrue单独介绍
5. 接收处理视频数据
recvVFrame 是接收处理数据的地方
void MediaPushWindow::recvVFrame(QVideoFrame& frame) {
frame.map(QAbstractVideoBuffer::ReadOnly);
// 获取 YUV 编码的原始数据
QVideoFrame::PixelFormat pixelFormat = frame.pixelFormat();
//需要写文件的时候才去转换数据
if (pixelFormat == QVideoFrame::Format_RGB32 && yuv_out_file)
{
int width = frame.width();
int height = frame.height();
if (dst_yuv_420 == nullptr)
{
dst_yuv_420 = new uchar[width * height * 3 / 2];
memset(dst_yuv_420, 128, width * height * 3 / 2);
}
int planeCount = frame.planeCount();
int mapBytes = frame.mappedBytes();
int rgb32BytesPerLine = frame.bytesPerLine();
const uchar* data = frame.bits();
if (data == NULL)
{
return;
}
QVideoFrame::FieldType fileType = frame.fieldType();
int idx = 0;
int idxu = 0;
int idxv = 0;
for (int i = height-1;i >=0 ;i--)
{
for (int j = 0;j < width;j++)
{
uchar b = data[(width*i+j) * 4];
uchar g = data[(width * i + j) * 4 + 1];
uchar r = data[(width * i + j) * 4 + 2];
uchar a = data[(width * i + j) * 4 + 3];
if (rgb_out_file) {
//fwrite(&b, 1, 1, rgb_out_file);
//fwrite(&g, 1, 1, rgb_out_file);
//fwrite(&r, 1, 1, rgb_out_file);
//fwrite(&a, 1, 1, rgb_out_file);
}
uchar y = RGB2Y(r, g, b);
uchar u = RGB2U(r, g, b);
uchar v = RGB2V(r, g, b);
dst_yuv_420[idx++] = clip_value(y,0,255);
if (j % 2 == 0 && i % 2 == 0) {
dst_yuv_420[width*height + idxu++] = clip_value(u,0,255);
dst_yuv_420[width*height*5/4 + idxv++] = clip_value(v,0,255);
}
}
}
if (yuv_out_file) {
fwrite(dst_yuv_420, 1, width * height * 3 / 2, yuv_out_file);
}
}
if (!showUserViewFinder)
{
QImage::Format qImageFormat = QVideoFrame::imageFormatFromPixelFormat(frame.pixelFormat());
QImage videoImg(frame.bits(),
frame.width(),
frame.height(),
qImageFormat);
videoImg = videoImg.convertToFormat(qImageFormat).mirrored(false, true);
ui.lastImagePreviewLabel->setPixmap(QPixmap::fromImage(videoImg));
}
}
frame.map(QAbstractVideoBuffer::ReadOnly); 这一行比较重要,这会把数据注入frame。
frame.pixelFormat(); 可以获取frame的piexlformat格式,我这里获得是 Format_RGB32。我这里加了个判断,yuv_out_file 没创建,也就是没有启动的时候,我这里不处理。
接下来就是根据 宽高 分配 dst_yuv_420空间。
int planeCount = frame.planeCount();
int mapBytes = frame.mappedBytes();
int rgb32BytesPerLine = frame.bytesPerLine();
planeCount 是有多少个平面,因为是rgb32的,所以是只有一个平面。 mapBytes是frame里面总共的rgb数据量。rgb32BytesPerLine 返回一行数据量。比如这里就是 width*4。 因为每个像素点占用 r,g,b,a四个字节。
接下来,我读出了frame里面的r,g,b,a值
注意这里虽然叫rgb32,但数据的排列顺序是b,g,r,a。 而且是从底部往上扫描的。所以i的起点是height-1。不然,你一会转换出来的yuv就是上下颠倒的。
rgb24 又不一样,它是b,g,r。但是它是从上往下扫描的,自己可以去调整,研究这个rgb的格式
#define RGB2Y(r,g,b) \
((unsigned char)((66 * r + 129 * g + 25 * b + 128) >> 8)+16)
#define RGB2U(r,g,b) \
((unsigned char)((-38 * r - 74 * g + 112 * b + 128) >> 8) + 128)
#define RGB2V(r,g,b) \
((unsigned char)((112 * r - 94 * g - 18 * b + 128) >> 8) + 128)
这三个宏方法,可以转换rgb数据为yuv,注意 uv分量只有y分量的4分之一。
当 showUserViewFinder 为false的时候,也就是用我自己写的采集器预览的时候,我们把QVideoFrame转换为QImag。
注意上下镜像反转一下。
然后 通过 setPixmap方法,把它绘画到一个label组件里面,就可以达到预览的效果。
这里写pcm文件的地方,后面就会改成编码,和推流。
6. 接收处理音数据
void MediaPushWindow::recvAFrame(const char* data, qint64 len)
{
if (pcm_out_file) {
fwrite(data, 1, len, pcm_out_file);
}
}
比较简单,写文件就是
7. 视频采集器详细介绍
CBaseCameraSurface 是实现的视频采集器,它是QAbstractVideoSurface抽象类的实现。
virtual bool isFormatSupported(const QVideoSurfaceFormat& format) const override;
virtual bool present(const QVideoFrame& frame) override;
virtual bool start(const QVideoSurfaceFormat& format) override;
virtual void stop() override;
virtual QList<QVideoFrame::PixelFormat> supportedPixelFormats(QAbstractVideoBuffer::HandleType type = QAbstractVideoBuffer::NoHandle) const override;
它的以上几个方法是需要被实现的。 isFormatSupported 是判断某种格式是否支持的
bool CBaseCameraSurface::isFormatSupported(const QVideoSurfaceFormat& format) const {
return QVideoFrame::imageFormatFromPixelFormat(format.pixelFormat()) != QImage::Format_Invalid;
}
supportedPixelFormats 是返回支持的格式列表,可以更具平台实际定义
QList<QVideoFrame::PixelFormat> CBaseCameraSurface::supportedPixelFormats(QAbstractVideoBuffer::HandleType handleType) const
{
if (handleType == QAbstractVideoBuffer::NoHandle) {
return QList<QVideoFrame::PixelFormat>() << QVideoFrame::Format_RGB32
<< QVideoFrame::Format_ARGB32
<< QVideoFrame::Format_ARGB32_Premultiplied
<< QVideoFrame::Format_RGB565
<< QVideoFrame::Format_RGB555;
//这里添加更多的类型,因为有很多摄像头返回的数据类型是yuv格式的
}
else {
return QList<QVideoFrame::PixelFormat>();
}
}
start方法,就是判断了一下条件,然后调用父类start
bool CBaseCameraSurface::start(const QVideoSurfaceFormat& format) {
//要通过QCamera::start()启动
if (QVideoFrame::imageFormatFromPixelFormat(format.pixelFormat()) != QImage::Format_Invalid
&& !format.frameSize().isEmpty()) {
QAbstractVideoSurface::start(format);
return true;
}
return false;
}
stop直接调用父类的就可以
present 方法里面就是获取视频帧了,这里拷贝一下,并且发出信号,调用到外层的recvVFrame
bool CBaseCameraSurface::present(const QVideoFrame& frame) {
//QCamera::start()启动之后才会触发
//qDebug()<<"camera present";
if (!frame.isValid()) {
qDebug() << "get invalid frame";
stop();
return false;
}
QVideoFrame cloneFrame(frame);
emit frameAvailable(cloneFrame);
return true;
}
8. 音频采集器详细介绍
音频采集器依然是使用 QIODevice 和 QAudioInput结合实现。QIODevice是一个输入输出管道
void AudioCapture::Start(const QAudioDeviceInfo& micInfo)
{
format = micInfo.preferredFormat();
switch (format.sampleSize()) {
case 8:
switch (format.sampleType()) {
case QAudioFormat::UnSignedInt:
m_maxAmplitude = 255;
break;
case QAudioFormat::SignedInt:
m_maxAmplitude = 127;
break;
default:
break;
}
break;
case 16:
switch (format.sampleType()) {
case QAudioFormat::UnSignedInt:
m_maxAmplitude = 65535;
break;
case QAudioFormat::SignedInt:
m_maxAmplitude = 32767;
break;
default:
break;
}
break;
case 32:
switch (format.sampleType()) {
case QAudioFormat::UnSignedInt:
m_maxAmplitude = 0xffffffff;
break;
case QAudioFormat::SignedInt:
m_maxAmplitude = 0x7fffffff;
break;
case QAudioFormat::Float:
m_maxAmplitude = 0x7fffffff; // Kind of
default:
break;
}
break;
default:
break;
}
if (!micInfo.isFormatSupported(format)) {
QMessageBox::warning(nullptr, tr("Audio Capture Error"), "format is not support");
return;
}
qDebug() << "default devicdName: " << micInfo.deviceName() << " : ";
qDebug() << "sameple: " << format.sampleRate() << " : ";
qDebug() << "channel: " << format.channelCount() << " : ";
qDebug() << "fmt: " << format.sampleSize() << " : ";
audioInput = new QAudioInput(micInfo, format);
// 连接errorOccurred信号
//QObject::connect(audioInput, &this::error,this,&AudioCapture::OnError);
audioInput->start(this);
this->open(QIODevice::WriteOnly);
// QMessageBox::warning(this, tr("Image Capture Error"), errorString);
}
在start方法里面,根据外边选择的设备,preferred 返回设备首选的音频采集format,里面就包含了采样率,声道,fmt等信息。
QAudioInput 注入设备信息,和format信息。然后就start了。
AudioCapture 是集成QIODevice的。启动后。数据就会通过重写的方法 writeData输出采集的pcm数据。
qint64 AudioCapture::writeData(const char* data, qint64 len)
{
// 在这里处理音频数据,例如保存到文件、进行处理等
qDebug() << "Received audio data. Size:" << len;
CaculateLevel(data,len);
emit aframeAvailable(data,len);
return len;
}
采集到数据后,我们先将数据传入 CaculateLevel 来前置计算活动量。这个算法,来自于qt的官方例子。可以作为储备程序。
RenderArea是一个手绘类,在其他窗口创建。CaculateLevel加速那出level值(0-1),发送型号到外面就可以绘画活动量到RernderArea里面。
writeData方法里面,然后发送信号aframeAvailable,在主窗口,就可以接收音频pcm数据,做自己的处理了。
9. 视频设置
ImageSettings 是一个QDialog弹出框。在弹出的时候,传入,当前的设置,执行完之后,返回设置参数,设置到m_imageCapture里面
void MediaPushWindow::configureImageSettings()
{
ImageSettings settingsDialog(m_imageCapture.data());
settingsDialog.setWindowFlags(settingsDialog.windowFlags() & ~Qt::WindowContextHelpButtonHint);
settingsDialog.setImageSettings(m_imageSettings);
if (settingsDialog.exec()) {
m_imageSettings = settingsDialog.imageSettings();
m_imageCapture->setEncodingSettings(m_imageSettings);
}
}
QCameraImageCapture::supportedImageCodecs 是返回支持的图片格式
QCameraImageCapture::supportedResolutions 是支持的分辨率
QMultimedia::VeryHighQuality 是质量的支持,是个枚举类型
这里虽然没有和CBaseCameraSurface直接联系,但是它改变了摄像头参数,所以实际使用的时候,它修改了截图的分辨率参数,同时也改动了视频yuv采集yuv的格式。虽然它看起来和自定的采集yuv没什么关系,所以说这些设置,都是改到摄像头原始位置的。
结束语言
有了这个采集端,和之前写的播放端,后期,我们就可以做出很多应用来,因为视频无非就是输入,输出。首先我们会加入编码,然后加入录制,推流,或者入会,等各种功能。
难点注意,
在阅读和调试的时候,大家需要对yuv,和pcm有所了解,大家可以先了解相关知识。在这个项目里面,还涉及rgb格式。
7. 其他:
-
仓库: git主页
-
讲解视频地址:视频讲解地址
-
联系我:
- 邮箱: gu19860621@163.com
- 微信: p13071210551