Qt 局域网内的点对点语音通话

记录如何使用QUdpSocket、QAudioInput、QAudioOutput来实现点对点语音通话。
mainwindow.h

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>
#include <QUdpSocket>
#include <QAudioInput>
#include <QAudioOutput>
#include <QAudioDeviceInfo>
#include <QAudioFormat>
#include <QIODevice>
#include <QMessageBox>
#include <QString>
#include <QMap>

namespace Ui {
class MainWindow;
}

struct video{
    int lens = 0;
    char data[1024];
};

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    explicit MainWindow(QWidget *parent = nullptr);
    ~MainWindow();

signals:
    void signal_hangUp();

public slots:
    void slot_sendAudioData();

    void slot_callRequest();

    void slot_callCancel();

private slots:
    void on_pbtnJoinMulticast_clicked();

    void on_pbtnExitMulticast_clicked();

    void on_pbtnSend_clicked();

	void onSocketReadyRead(); // 读取socket传入的数据

    void handleStateChanged(QAudio::State newState);

    void on_buttonCall_clicked();

    void on_buttonHangUp_clicked();

private:
    void initAudioInput();

    void initMessageBox();

    void buttonIsAbled(bool callAble, bool hangUpAble);

private:
	QUdpSocket *udpSocket;			//用于与连接的客户端通讯的QUdpSocket
	QHostAddress groupAddress;		//组播地址

    QUdpSocket* m_callSocket = Q_NULLPTR;
    QAudioInput* m_audioInput = Q_NULLPTR; // 采集音频
    QAudioOutput* m_audioOutput = Q_NULLPTR; // 播放音频
    QIODevice *m_inputDevice = Q_NULLPTR;
    QIODevice* m_outputDevice = Q_NULLPTR;
    QMessageBox m_messageBox;
    QString m_curAddress{""};
    quint16 m_curPort = 0;
    QHostAddress m_targetIP;
    quint16 m_targetPort = 0;

    QMap<QString, quint16> m_friIPPort;

    bool flag = false;

private:
    Ui::MainWindow *ui;
};

#endif // MAINWINDOW_H

mainwindow.cpp

#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <cstring>
#include <QString>
#include <QNetworkInterface>
#include <QNetworkDatagram>
#include <QMap>
#include <QRandomGenerator>
#include <QNetworkProxy>
#include <QThread>

MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::MainWindow)
{
    ui->setupUi(this);
	udpSocket = new QUdpSocket(this); // 用于与连接的客户端通讯的QUdpSocket
									  // Multicast路由层次,1表示只在同一局域网内
									  // 组播TTL: 生存时间,每跨1个路由会减1,多播无法跨过大多数路由所以为1
									  // 默认值是1,表示数据包只能在本地的子网中传送。
	udpSocket->setSocketOption(QAbstractSocket::MulticastTtlOption, 1);
    connect(udpSocket, &QUdpSocket::readyRead, this, &MainWindow::onSocketReadyRead);

    m_callSocket = new QUdpSocket(this);
    connect(m_callSocket, &QUdpSocket::readyRead, this, &MainWindow::slot_callRequest);

	ui->cbxMultiIP->addItem("239.1.1.1");

    // 语音通话
    initAudioInput();
    initMessageBox();
    buttonIsAbled(false, false);
}

MainWindow::~MainWindow()
{
    if(udpSocket)
        udpSocket->close();
    if(m_callSocket)
        m_callSocket->close();
    if (m_inputDevice)
        m_inputDevice->close();
    if (m_outputDevice)
        m_outputDevice->close();
    if(m_audioInput)
        m_audioInput->stop();
    if(m_audioOutput)
        m_audioOutput->stop();
    delete ui;
}

void MainWindow::buttonIsAbled(bool callAble, bool hangUpAble)
{
    ui->buttonCall->setDisabled(!callAble);
    ui->buttonHangUp->setDisabled(!hangUpAble);
}

void MainWindow::initAudioInput()
{
    QAudioFormat format;
    format.setSampleRate(8000);
    format.setChannelCount(1);
    format.setSampleSize(16);
    format.setCodec("audio/pcm");
    format.setByteOrder(QAudioFormat::LittleEndian);
    format.setSampleType(QAudioFormat::SignedInt);

    QAudioDeviceInfo info = QAudioDeviceInfo::defaultInputDevice();
    if (!info.isFormatSupported(format)) {
      qWarning() << "Default format not supported, trying to use the nearest.";
      format = info.nearestFormat(format);
    }

    m_audioInput = new QAudioInput(format);
//    connect(m_audioInput, &QAudioInput::stateChanged, this, &MainWindow::handleStateChanged);

    m_audioOutput = new QAudioOutput(format);
    m_audioOutput->setBufferSize(10000000);
    m_outputDevice = m_audioOutput->start();
}

void MainWindow::initMessageBox()
{
    m_messageBox.setIcon(QMessageBox::Information);
    m_messageBox.setText(QString("您有一个来电,是否接听? "));
    m_messageBox.setWindowTitle(QString("通话 "));
    m_messageBox.addButton(QMessageBox::Yes);
    m_messageBox.addButton(QMessageBox::No);
}

void MainWindow::slot_callRequest()
{
    while(m_callSocket->hasPendingDatagrams()){
        video vp;
        memset(&vp, 0, sizeof(vp));
        QHostAddress peerAddr;
        quint16 peerPort;
        m_callSocket->readDatagram((char*)&vp, sizeof(video), &peerAddr, &peerPort);

        char tmp1[] = "#CALLREQUEST";
        if(strcmp(vp.data, tmp1) == 0){

            if (m_messageBox.exec() == -1)
                return;

            m_targetIP = peerAddr;
            m_targetPort = peerPort;

            QMessageBox::StandardButton button = m_messageBox.standardButton(m_messageBox.clickedButton());
            if (button == QMessageBox::Yes) {
                m_inputDevice = m_audioInput->start();
//                m_inputDevice->open(QIODevice::WriteOnly);
                connect(m_inputDevice, &QIODevice::readyRead, this, &MainWindow::slot_sendAudioData);
                m_callSocket->disconnectFromHost();
                m_callSocket->bind(QHostAddress(m_curAddress), m_curPort, QUdpSocket::ShareAddress | QUdpSocket::ReuseAddressHint);

                ui->plainTextEdit->appendPlainText(QString("通话中..."));
                buttonIsAbled(false, true);
                flag = true;
            }
            else if (button == QMessageBox::No) {
                m_messageBox.close();

                video vp;
                memset(&vp, 0, sizeof(vp));
                strcpy(vp.data, "#REFUSE");
                vp.lens = strlen(vp.data);
                m_callSocket->writeDatagram((const char*)&vp, sizeof(vp), m_targetIP, m_targetPort);
            }
            return;
        }

        char tmp2[] = "#HANGUP";
        if(strcmp(vp.data, tmp2) == 0){
            m_audioInput->stop();
            m_callSocket->disconnectFromHost();
            m_callSocket->bind(QHostAddress(m_curAddress), m_curPort, QUdpSocket::ShareAddress | QUdpSocket::ReuseAddressHint);

            ui->plainTextEdit->appendPlainText(QString::fromLocal8Bit("对方已挂断电话..."));
            flag = false;
            buttonIsAbled(true, false);
            return;
        }

        char tmp3[] = "#REFUSE";
        if(strcmp(vp.data, tmp3) == 0){
            m_audioInput->stop();
            m_callSocket->disconnectFromHost();
            m_callSocket->bind(QHostAddress(m_curAddress), m_curPort, QUdpSocket::ShareAddress | QUdpSocket::ReuseAddressHint);

            ui->plainTextEdit->appendPlainText(QString::fromLocal8Bit("对方拒绝接通电话..."));
            flag = false;
            buttonIsAbled(true, false);
            return;
        }

        m_outputDevice->write(vp.data, vp.lens);
//        ui->plainTextEdit->appendPlainText(QString::fromLocal8Bit(vp.data));
    }

}

void MainWindow::slot_sendAudioData()
{
    video vp;
    memset(&vp, 0, sizeof(vp));
    vp.lens = m_inputDevice->read(vp.data, 1024);
    int size = m_callSocket->writeDatagram((const char*)&vp, sizeof(vp), m_targetIP, m_targetPort);

    if(size <= 0){
        QAbstractSocket::SocketError error = m_callSocket->error();
        ui->plainTextEdit->appendPlainText(QString::fromLocal8Bit("拨打电话失败,错误:"));
        if(error == QAbstractSocket::NetworkError)
            ui->plainTextEdit->appendPlainText(QString::fromLocal8Bit("网络连接异常!"));
    }
//    auto byte = m_inputDevice->readAll();
//    m_callSocket->write(byte);
}

void MainWindow::slot_callCancel()
{
    m_messageBox.button(QMessageBox::No)->click();
}

void MainWindow::handleStateChanged(QAudio::State newState)
{
  switch (newState) {
      case QAudio::StoppedState:
          if (m_audioInput->error() != QAudio::NoError) {
              // Error handling
          } else {
              // Finished recording
          }
          break;

      case QAudio::ActiveState:
          // Started recording - read from IO device

          break;

      default:
          // ... other cases as appropriate
          break;
  }
}

void MainWindow::on_pbtnJoinMulticast_clicked()
{
	QString IP = ui->cbxMultiIP->currentText();
	groupAddress = QHostAddress(IP); // QHostAddress::AnyIPv4 与此地址绑定的socket将仅侦听IPv4交互
	QString groupPort = ui->lePort->text();// groupPort,多播组统一的一个端口
	quint16 portValue = groupPort.toUShort();

	// QUdpSocket::ShareAddress 允许其他服务绑定到相同的地址和端口
	// QUdpSocket::ReuseAddressHint 向QAbstractSocket提供提示,提示它应尝试重新绑定服务,即使地址和端口已被另一个套接字绑定。在Windows和Unix上,这相当于SO_REUSEADDR套接字选项。
	// QUdpSocket::ShareAddress | QUdpSocket::ReuseAddressHint 组合使用才能在本机同时启动多个程序绑定相同端口,适合没有局域网只有一台电脑的本地测试使用
    if (udpSocket->bind(QHostAddress::AnyIPv4, portValue, QUdpSocket::ShareAddress | QUdpSocket::ReuseAddressHint)) //先绑定端口
	{
        // 加入到组播
		udpSocket->joinMulticastGroup(groupAddress); //加入IP地址为groupAddress的多播组,绑定端口groupPort进行通信
		ui->plainTextEdit->appendPlainText(QString::fromLocal8Bit("加入组播成功"));
		ui->plainTextEdit->appendPlainText(QString::fromLocal8Bit("组播地址IP:") + IP);
		ui->plainTextEdit->appendPlainText(QString::fromLocal8Bit("绑定端口:") + QString::number(portValue));
		ui->pbtnJoinMulticast->setEnabled(false);
		ui->pbtnExitMulticast->setEnabled(true);
		ui->cbxMultiIP->setEnabled(false);
        connect(udpSocket, &QUdpSocket::readyRead, this, &MainWindow::onSocketReadyRead);

        // 语音通话
        auto ipList = QNetworkInterface::allAddresses();
        QHostAddress curIP;
        for(auto& address : ipList){
            auto nProtocol = address.protocol();
            if(nProtocol == QAbstractSocket::IPv4Protocol){
                curIP = address;
                break;
            }
        }
        m_curAddress = curIP.toString();
        ui->leMyID->setText(m_curAddress);

        m_curPort = QRandomGenerator::global()->bounded(2001, 9999);
//        ui->plainTextEdit->appendPlainText(QString::number(m_curPort));

        QString groupPort = ui->lePort->text();
        quint16 portValue = groupPort.toUShort();
        QString msg = "A" + m_curAddress + " " + QString::number(m_curPort);
        QByteArray datagram = msg.toUtf8();
        udpSocket->writeDatagram(datagram, groupAddress, portValue);

        m_callSocket->bind(curIP, m_curPort, QUdpSocket::ShareAddress | QUdpSocket::ReuseAddressHint);
        connect(m_callSocket, &QUdpSocket::readyRead, this, &MainWindow::slot_callRequest);

        buttonIsAbled(true, false);
	}
	else
		ui->plainTextEdit->appendPlainText(QString::fromLocal8Bit("**绑定端口失败"));
}

void MainWindow::on_pbtnExitMulticast_clicked()
{
    if(flag){
        m_messageBox.setText(QString("您还在通话中,请先挂断电话再进行此操作"));
        m_messageBox.setWindowTitle(QString("警告 "));
        m_messageBox.button(QMessageBox::No)->hide();
        m_messageBox.exec();
        return;
    }

    // 通讯ID中移除此id
    QString groupPort = ui->lePort->text();
    quint16 portValue = groupPort.toUShort();
    QByteArray datagram = QString("D" + ui->leMyID->text()).toUtf8();
    udpSocket->writeDatagram(datagram, groupAddress, portValue);

	udpSocket->leaveMulticastGroup(groupAddress);// 退出组播
	udpSocket->abort(); // 中止当前连接并重置套接字。与disconnectFromHost()不同,此函数会立即关闭套接字,丢弃写入缓冲区中的所有挂起数据。
    ui->pbtnJoinMulticast->setEnabled(true);
	ui->pbtnExitMulticast->setEnabled(false);
	ui->cbxMultiIP->setEnabled(true);
	ui->plainTextEdit->appendPlainText(QString::fromLocal8Bit("**已退出组播,解除端口绑定"));

    m_callSocket->abort();
    ui->cbFriID->clear();
    buttonIsAbled(false, false);
}

void MainWindow::on_pbtnSend_clicked()
{
	QString groupPort = ui->lePort->text();// groupPort,多播组统一的一个端口
	quint16 portValue = groupPort.toUShort();
	QString msg = ui->leSendData->text();
	QByteArray datagram = msg.toUtf8();

	udpSocket->writeDatagram(datagram, groupAddress, portValue);
	ui->plainTextEdit->appendPlainText("[multicst] " + msg);
	ui->leSendData->clear();
    ui->leSendData->setFocus();
}

void MainWindow::onSocketReadyRead()
{
	while (udpSocket->hasPendingDatagrams())
	{
		QByteArray datagram;
		datagram.resize(udpSocket->pendingDatagramSize());
		QHostAddress peerAddr;
		quint16 peerPort;
		udpSocket->readDatagram(datagram.data(), datagram.size(), &peerAddr, &peerPort);
		QString str = datagram.data();
        QString address{""};
        if(str.length() > 0){
            address = str.mid(1);
        }
//        QRegExp reg("[0-9]+");
        if(address != "" && (str.at(0) == 'A' || str.at(0) == 'D' || str.at(0) == 'E')){
            int len = address.length();
            QString add{""};
            int index = 0;
            for(int i = 0; i < len; i++){
                if(address.at(i) == ' '){
                    index = i + 1;
                    break;
                }
                add += address.at(i);
            }

            quint16 port;
            if(index != 0)
                port = address.mid(index).toUShort();

            if(add == m_curAddress)
                continue;

            if(str.at(0) == 'A'){
                m_friIPPort.insert(add, port);
                ui->cbFriID->addItem(add);

                QString groupPort = ui->lePort->text();
                quint16 portValue = groupPort.toUShort();
                QString msg = "E" + m_curAddress + " " + QString::number(m_curPort);
                QByteArray datagram = msg.toUtf8();
                udpSocket->writeDatagram(datagram, groupAddress, portValue);
            }
            else if(str.at(0) == 'D'){
                int index = 0;
                for(int i = 0; i < ui->cbFriID->count(); i++){
                    auto tmp = ui->cbFriID->itemText(i);
                    if(tmp == address){
                        index = i;
                        break;
                    }
                }
                ui->cbFriID->removeItem(index);
                m_friIPPort.remove(address);
            }
            else if(str.at(0) == 'E'){
                bool exit = false;
                for(int i = 0; i < ui->cbFriID->count(); i++){
                    auto tmp = ui->cbFriID->itemText(i);
                    if(tmp == add){
                        exit = true;
                        break;
                    }
                }
                if(!exit){
                    ui->cbFriID->addItem(add);
                    m_friIPPort.insert(add, port);
                }
            }
        }
        else{
            QString peer = "[From " + peerAddr.toString() + ":" + QString::number(peerPort) + "] ";
            ui->plainTextEdit->appendPlainText(peer + str);
        }
	}
}

void MainWindow::on_buttonCall_clicked()
{
    auto targetIP = ui->cbFriID->currentText();

    m_targetIP = QHostAddress(targetIP);
    m_targetPort = m_friIPPort.value(targetIP);

    video vp;
    memset(&vp, 0, sizeof(vp));
    strcpy(vp.data, "#CALLREQUEST");
    vp.lens = strlen(vp.data);
    int size = m_callSocket->writeDatagram((const char*)&vp, sizeof(vp), m_targetIP, m_targetPort);

    if(size <= 0){
        QAbstractSocket::SocketError error = m_callSocket->error();
        qDebug() << QString::fromLocal8Bit("拨打电话失败,错误:") << error;
        if(error == QAbstractSocket::NetworkError)
            qDebug() << QString::fromLocal8Bit("网络连接异常!");
    }
    else{
        m_inputDevice = m_audioInput->start();
        connect(m_inputDevice, &QIODevice::readyRead, this, &MainWindow::slot_sendAudioData);
        flag = true;
        ui->plainTextEdit->appendPlainText(QString::fromLocal8Bit("通话中..."));
        buttonIsAbled(false, true);
    }
}

void MainWindow::on_buttonHangUp_clicked()
{
    m_audioInput->stop();
    m_callSocket->disconnectFromHost();
    m_callSocket->bind(QHostAddress(m_curAddress), m_curPort, QUdpSocket::ShareAddress | QUdpSocket::ReuseAddressHint);

    flag = false;

    video vp;
    memset(&vp, 0, sizeof(vp));
    strcpy(vp.data, "#HANGUP");
    vp.lens = strlen(vp.data);
    int size = m_callSocket->writeDatagram((const char*)&vp, sizeof(vp), m_targetIP, m_targetPort);
    if(size <= 0){
        QAbstractSocket::SocketError error = m_callSocket->error();
        ui->plainTextEdit->appendPlainText(QString::fromLocal8Bit("拨打电话失败,错误:"));
        if(error == QAbstractSocket::NetworkError)
            ui->plainTextEdit->appendPlainText(QString::fromLocal8Bit("网络连接异常!"));
    }

    ui->plainTextEdit->appendPlainText(QString::fromLocal8Bit("已挂断电话。"));
    buttonIsAbled(true, false);
}

  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值