记录如何使用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);
}