Qt/C++编写音视频实时通话程序/画中画/设备热插拔/支持本地摄像头和桌面

发布于:2025-05-16 ⋅ 阅读:(16) ⋅ 点赞:(0)

一、前言

近期有客户提需求,需要在嵌入式板子上和电脑之间音视频通话,要求用Qt开发,可以用第三方的编解码组件,能少用就尽量少用,以便后期移植起来方便。如果换成5年前的知识储备,估计会采用纯网络通信收发图片和声音数据方式实现,比如用qcamera打开摄像头,转成图片,base64编码发送,接收到收到后base64解码成图片绘制,声音用qaudioinput采集后pcm数据直接tcp发送,收到后直接发给qaudiooutput设备播放即可。这种能不能实现效果呢,也是可以的,就是体验不大好友好,比如画面的流畅度要低不少,估计只能做到20fps,而且双方只能用私有协议通信,外部如果要取流比如在网页查看通话的视频,无法实现。

按照现在的知识储备,方案就很多了,最佳方案就是用推拉流,实时性极好,局域网可以做到0.2s内,而且音视频通话都是实时的,可以连续7*24小时运行,而且可以拓展成一对多通话,多一个人员加入只需要多一路拉流就行,非常的方便,悬浮画面还可以设置各种位置,使用下来的效果非常的棒。所以随着知识储备的增加,以前无法解决的疑难杂症,现在只需要分分钟就搞定。

二、效果图

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

三、相关地址

  1. 国内站点:https://gitee.com/feiyangqingyun
  2. 国际站点:https://github.com/feiyangqingyun
  3. 个人作品:https://blog.csdn.net/feiyangqingyun/article/details/97565652
  4. 文件地址:https://pan.baidu.com/s/1d7TH_GEYl5nOecuNlWJJ7g 提取码:01jf 文件名:bin_video_call。

四、功能特点

  1. 支持局域网和外网音视频实时通话,延迟极低,资源占用极低。
  2. 自动获取本地所有视音频输入设备,本地摄像头设备自动罗列所有支持的分辨率、帧率、采集格式等信息。
  3. 可以指定采集的视频设备和音频输入设备,自由组合,视频设备可以设置不同的分辨率、帧率、采集格式。
  4. 支持本地桌面屏幕作为视频设备采集,支持多个屏幕,自动识别屏幕分辨率。
  5. 可以选择不同的声卡设备播放声音。
  6. 内置自动重连机制,视音频设备支持热插拔。
  7. 支持固定画中画功能,可交换主画面和浮窗画面,可设置画面左右排列等布局方式。
  8. 可自定义悬浮画面位置,指定左上角、右上角、左下角、右下角、自定义位置和大小。
  9. 内置流媒体服务程序,程序启动后自动启动流媒体服务,自动推拉流。
  10. 视音频流数据支持rtsp/rtmp/http/webrtc等方式拉流,可以直接网页上打开视频画面。
  11. 实时显示本地音频振幅和远程音量振幅,可以分别对输入输出音量设置静音,方便测试。
  12. 支持自定义水印,包括文字和图片水印,支持多个水印,指定任意位置。
  13. 支持不同的视音频设备组合,比如本地摄像头加电脑麦克风而不是摄像头的麦克风,比如本地电脑桌面屏幕加摄像头的麦克风等。
  14. 纯Qt+ffmpeg编写,支持windows和linux以及macos等系统,支持所有Qt版本、所有系统、所有编译器。
  15. 支持嵌入式linux板子和树莓派香橙派等,以及国产linux系统。

五、相关代码

#include "frmconfig.h"
#include "frmmain.h"
#include "ui_frmmain.h"
#include "qthelper.h"
#include "apphelper.h"
#include "osdgraph.h"
#include "ffmpegthread.h"
#include "ffmpegthreadcheck.h"

frmMain::frmMain(QWidget *parent) : QWidget(parent), ui(new Ui::frmMain)
{
    ui->setupUi(this);
    this->initForm();
    this->installEventFilter(this);
    QMetaObject::invokeMethod(this, "formChanged", Qt::QueuedConnection);
}

frmMain::~frmMain()
{
    delete ui;
}

void frmMain::savePos()
{
    AppConfig::FormMax = this->isMaximized();
    if (!AppConfig::FormMax) {
        AppConfig::FormGeometry = this->geometry();
    }

    AppConfig::writeConfig();
}

bool frmMain::eventFilter(QObject *watched, QEvent *event)
{
    //尺寸发生变化或者窗体移动位置记住窗体位置
    int type = event->type();
    if (type == QEvent::Resize || type == QEvent::Move) {
        QMetaObject::invokeMethod(this, "savePos", Qt::QueuedConnection);
    } else if (type == QEvent::Close) {
        audioInput->stop(false);
        audioOutput->stop(false);
        exit(0);
    }

    //尺寸发生变化重新调整小预览窗体的位置
    if (this->isVisible() && type == QEvent::Resize) {
        this->formChanged();
    }

    return QWidget::eventFilter(watched, event);
}

void frmMain::initForm()
{
    //初始化输入输出视频控件
    AppHelper::initVideoWidget(ui->videoInput);
    AppHelper::initVideoWidget(ui->videoOutput);

    //初始化输入输出音频线程
    audioInput = new FFmpegThread(this);
    audioOutput = new FFmpegThread(this);
    checkInput = new FFmpegThreadCheck(audioInput, this);
    AppHelper::initAudioThread(audioInput, ui->levelInput);
    AppHelper::initAudioThread(audioOutput, ui->levelOutput);

    //输入打开成功后立即推流
    connect(audioInput, SIGNAL(receivePlayStart(int)), this, SLOT(receivePlayStart(int)));
    connect(ui->videoInput, SIGNAL(sig_receivePlayStart(int)), this, SLOT(receivePlayStart(int)));

    ui->ckInput->setChecked(AppConfig::MuteInput ? Qt::Checked : Qt::Unchecked);
    ui->ckOutput->setChecked(AppConfig::MuteOutput ? Qt::Checked : Qt::Unchecked);
    if (AppConfig::StartServer) {
        on_btnStart_clicked();
    }
}

void frmMain::clearLevel()
{
    ui->levelInput->setLevel(0);
    ui->levelOutput->setLevel(0);
}

void frmMain::formChanged()
{
    AppHelper::changeWidget(ui->videoInput, ui->videoOutput, ui->gridLayout, NULL);
}

void frmMain::receivePlayStart(int time)
{
    QObject *obj = sender();
    if (obj == ui->videoInput) {
#ifdef betaversion
        OsdGraph::testOsd(ui->videoInput);
#endif
        ui->videoInput->recordStart(AppConfig::VideoPush);
    } else if (obj == audioInput) {
        audioInput->recordStart(AppConfig::AudioPush);
    }
}

void frmMain::on_btnStart_clicked()
{
    if (ui->btnStart->text() == "启动服务") {
        if (AppConfig::VideoUrl == "video=" || AppConfig::AudioUrl == "audio=") {
            QtHelper::showMessageBoxError("请先打开系统设置, 选择对应的视音频设备");
            //return;
        }

        ui->videoInput->open(AppConfig::VideoUrl);
        ui->videoOutput->open(AppConfig::VideoPull);
        audioInput->setMediaUrl(AppConfig::AudioUrl);
        audioOutput->setMediaUrl(AppConfig::AudioPull);
        audioInput->play();
        audioOutput->play();
        checkInput->start();
        ui->btnStart->setText("停止服务");
    } else {
        ui->videoInput->stop();
        ui->videoOutput->stop();
        audioInput->stop();
        audioOutput->stop();
        checkInput->stop();
        ui->btnStart->setText("启动服务");
        QMetaObject::invokeMethod(this, "clearLevel", Qt::QueuedConnection);
    }

    AppConfig::StartServer = (ui->btnStart->text() == "停止服务");
    AppConfig::writeConfig();
}

void frmMain::on_btnConfig_clicked()
{
    static frmConfig *config = NULL;
    if (!config) {
        config = new frmConfig;
        connect(config, SIGNAL(formChanged()), this, SLOT(formChanged()));
    }

    config->show();
    config->activateWindow();
}

void frmMain::on_ckInput_stateChanged(int arg1)
{
    bool muted = (arg1 != 0);
    audioInput->setMuted(muted);
    AppConfig::MuteInput = muted;
    AppConfig::writeConfig();
}

void frmMain::on_ckOutput_stateChanged(int arg1)
{
    bool muted = (arg1 != 0);
    audioOutput->setMuted(muted);
    AppConfig::MuteOutput = muted;
    AppConfig::writeConfig();
}

网站公告

今日签到

点亮在社区的每一天
去签到