原型void CallWindow::startVoiceSession()
执行流程
- 校验会话状态
- 创建语音管理器
- 连接音频发送信号
- 连接远端音频入口
- 启动设备
- 更新界面状态
工程说明
界面层只编排生命周期,不处理音频设备细节。
关联接口
VoiceCallManager::startClientLogic::sendMediaAudioCallWindow::stopVoiceSession
查看完整实现
callwindow.cpp
void CallWindow::startVoiceSession() {
if (!m_logic) {
setStatus("通话模块初始化失败");
return;
}
if (m_peer.isEmpty() || m_callId.isEmpty()) {
setStatus("通话信息不完整,请重新发起");
return;
}
if (m_voiceManager && m_voiceManager->isRunning()) {
setStatus("通话中");
return;
}
if (m_voiceManager) {
m_voiceManager->stop();
m_voiceManager->deleteLater();
m_voiceManager = nullptr;
}
m_voiceManager = new VoiceCallManager(m_logic, m_peer, m_callId, this);
connect(m_voiceManager, &VoiceCallManager::sigLog,
this,
[=](const QString& msg) {
qDebug() << "[Voice]" << msg;
if (msg.contains("已选择音频格式")) {
setStatus("音频设备已就绪,正在通话");
} else if (msg.contains("语音通话已启动")) {
setStatus("通话中");
} else if (msg.contains("已静音麦克风")) {
setStatus("通话中 · 已静音");
} else if (msg.contains("已取消静音")) {
setStatus("通话中");
} else if (msg.contains("语音通话已停止")) {
setStatus("已结束");
}
});
connect(m_voiceManager, &VoiceCallManager::sigError,
this,
[=](const QString& msg) {
qDebug() << "[VoiceError]" << msg;
setStatus("语音模块异常:" + msg);
});
connect(m_voiceManager, &VoiceCallManager::sigStateChanged,
this,
[=](bool running) {
muteBtn->setEnabled(running);
hangupBtn->setEnabled(running);
if (running) {
setStatus("通话中");
setMutedUi(false);
} else {
muteBtn->setEnabled(false);
hangupBtn->setEnabled(false);
setMutedUi(false);
}
});
if (!m_voiceManager->start()) {
m_voiceManager->deleteLater();
m_voiceManager = nullptr;
muteBtn->setEnabled(false);
hangupBtn->setEnabled(false);
setMutedUi(false);
setStatus("语音设备不可用,请检查麦克风或扬声器");
return;
}
muteBtn->setEnabled(true);
hangupBtn->setEnabled(true);
setMutedUi(false);
setStatus("通话中");
if (m_callStartMs == 0) {
m_callStartMs = QDateTime::currentMSecsSinceEpoch();
if (durationTimer) {
durationTimer->start(1000);
}
}
}
原型bool VoiceCallManager::start()
调用时机
CallWindow 启动语音会话时调用。
执行流程
- 获取默认音频设备
- 选择支持格式
- 创建 QAudioSource/QAudioSink
- 启动输入输出
- 连接 readyRead
关联接口
VoiceCallManager::handleMicReadyReadVoiceCallManager::onRemoteAudioReceivedVoiceCallManager::stop
查看完整实现
voicecallmanager.cpp
bool VoiceCallManager::start() {
if (m_running) {
emit sigLog("ℹ️ 语音通话已经在运行中。");
return true;
}
if (!m_logic) {
emit sigError("VoiceCallManager 启动失败:ClientLogic 为空。");
return false;
}
if (m_peer.isEmpty() || m_callId.isEmpty()) {
emit sigError("VoiceCallManager 启动失败:peer 或 call_id 为空。");
return false;
}
QAudioDevice inputDevice = QMediaDevices::defaultAudioInput();
if (inputDevice.isNull()) {
emit sigError("未找到可用麦克风。");
return false;
}
QAudioDevice outputDevice = QMediaDevices::defaultAudioOutput();
if (outputDevice.isNull()) {
emit sigError("未找到可用扬声器。");
return false;
}
m_format = selectSupportedFormat(inputDevice, outputDevice);
if (!m_format.isValid()) {
emit sigError("没有找到麦克风和扬声器同时支持的音频格式。请检查系统音频设备,或后续加入重采样。");
return false;
}
m_frameBytes = calcFrameBytes(m_format);
if (m_frameBytes <= 0) {
emit sigError("音频帧大小计算失败。");
return false;
}
emit sigLog(QString("🎧 已选择音频格式:%1,每包 %2 字节")
.arg(formatToString(m_format))
.arg(m_frameBytes));
m_audioSink = new QAudioSink(outputDevice, m_format, this);
m_audioSink->setBufferSize(m_frameBytes * 30);
m_outputDevice = m_audioSink->start();
if (!m_outputDevice) {
cleanupDevices();
emit sigError("扬声器启动失败。");
return false;
}
m_audioSource = new QAudioSource(inputDevice, m_format, this);
m_audioSource->setBufferSize(m_frameBytes * 10);
m_inputDevice = m_audioSource->start();
if (!m_inputDevice) {
cleanupDevices();
emit sigError("麦克风启动失败。");
return false;
}
connect(m_inputDevice, &QIODevice::readyRead,
this, &VoiceCallManager::handleMicReadyRead);
m_seq = 0;
m_pendingMicBytes.clear();
m_running = true;
emit sigStateChanged(true);
emit sigLog(QString("🎙️ 语音通话已启动:%1").arg(m_peer));
return true;
}
原型void VoiceCallManager::onRemoteAudioReceived(const QString& from, const QString& callId, qint64 seq, const QByteArray& audioBytes)
调用时机
ClientLogic 收到 media_audio 后调用。
参数
| 参数 | 说明 |
|---|
from | 媒体发送者 |
callId | 通话标识 |
audioBytes | 音频数据 |
执行流程
- 校验来源与 call_id
- 确认输出设备已启动
- 写入播放缓冲
- 处理写入异常
关联接口
BusinessHandler::handleMediaAudioVoiceCallManager::start
查看完整实现
voicecallmanager.cpp
void VoiceCallManager::onRemoteAudioReceived(const QString& from,
const QString& callId,
qint64 seq,
const QByteArray& audioBytes) {
if (!m_running || !m_outputDevice) {
return;
}
if (from != m_peer) {
return;
}
if (callId != m_callId) {
return;
}
if (audioBytes.isEmpty()) {
return;
}
qint64 written = m_outputDevice->write(audioBytes);
if (written < 0) {
emit sigLog("⚠️ 写入扬声器失败。");
return;
}
if (seq % 50 == 0) {
qDebug() << "[Voice] played remote audio packet"
<< "from=" << from
<< "call_id=" << callId
<< "seq=" << seq
<< "size=" << audioBytes.size();
}
}
原型void CallWindow::startVideoSession()
执行流程
- 确保语音会话可用
- 创建视频管理器
- 绑定本地预览
- 绑定视频发送
- 绑定远端画面
- 启动摄像头
工程说明
音频与视频在同一 CallWindow 中统一管理。
关联接口
VideoCallManager::startClientLogic::sendMediaVideoCallWindow::stopVideoSession
查看完整实现
callwindow.cpp
void CallWindow::startVideoSession() {
if (m_videoManager && m_videoManager->isRunning()) {
return;
}
if (m_peer.isEmpty() || m_callId.isEmpty()) {
setStatus("视频通话信息不完整,请重新发起");
return;
}
if (m_videoManager) {
m_videoManager->stop();
m_videoManager->deleteLater();
m_videoManager = nullptr;
}
m_videoManager = new VideoCallManager(
m_logic,
m_peer,
m_callId,
localPreviewLabel,
remoteVideoLabel,
this
);
connect(m_videoManager, &VideoCallManager::sigLog,
this,
[=](const QString& msg) {
qDebug() << "[Video]" << msg;
});
connect(m_videoManager, &VideoCallManager::sigError,
this,
[=](const QString& msg) {
qDebug() << "[VideoError]" << msg;
if (msg.contains("camera", Qt::CaseInsensitive) ||
msg.contains("摄像头") ||
msg.contains("占用")) {
setStatus("摄像头不可用,仅接收远端视频");
if (localPreviewLabel) {
localPreviewLabel->setText("摄像头不可用");
}
} else {
setStatus("视频模块异常,请稍后重试");
}
});
connect(m_videoManager, &VideoCallManager::sigStateChanged,
this,
[=](bool running) {
if (running) {
setStatus("视频通话中");
}
});
connect(m_videoManager, &VideoCallManager::sigStatsChanged,
this,
[=](const QString& stats) {
if (videoStatsLabel) {
videoStatsLabel->setText(stats);
}
});
if (!m_videoManager->start()) {
if (cameraBtn) {
cameraBtn->setEnabled(false);
cameraBtn->setText("📷 摄像头不可用");
}
if (localPreviewLabel) {
localPreviewLabel->clear();
localPreviewLabel->setText("摄像头不可用");
}
setStatus("摄像头不可用,仅接收远端视频");
return;
}
if (cameraBtn) {
cameraBtn->setEnabled(true);
cameraBtn->setText("📷 关闭摄像头");
}
setStatus("视频通话中");
}
原型bool VideoCallManager::start()
调用时机
CallWindow 启动视频会话时调用。
执行流程
- 查找视频输入设备
- 创建 QCamera
- 绑定 QVideoSink
- 连接 videoFrameChanged
- 启动摄像头
工程说明
摄像头开关由 setCameraEnabled 独立控制。
关联接口
VideoCallManager::handleVideoFrameVideoCallManager::stop
查看完整实现
videocallmanager.cpp
bool VideoCallManager::start() {
if (m_running) {
return true;
}
if (!m_logic) {
emit sigError("VideoCallManager 启动失败:ClientLogic 为空");
return false;
}
if (m_peer.isEmpty() || m_callId.isEmpty()) {
emit sigError("VideoCallManager 启动失败:peer 或 call_id 为空");
return false;
}
QCameraDevice cameraDevice = QMediaDevices::defaultVideoInput();
if (cameraDevice.isNull()) {
emit sigError("未找到可用摄像头");
return false;
}
m_captureSession = new QMediaCaptureSession(this);
m_camera = new QCamera(cameraDevice, this);
m_videoSink = new QVideoSink(this);
m_captureSession->setCamera(m_camera);
m_captureSession->setVideoSink(m_videoSink);
connect(m_videoSink, &QVideoSink::videoFrameChanged,
this, &VideoCallManager::handleVideoFrame);
m_sentFrameCount = 0;
m_receivedFrameCount = 0;
m_displayedFrameCount = 0;
m_droppedFrameCount = 0;
m_cameraEnabled = true;
m_sendTimer.start();
m_camera->start();
m_running = true;
emit sigStateChanged(true);
emit sigLog("🎥 视频采集已启动");
return true;
}
原型void VideoCallManager::onRemoteVideoReceived(const QString& from, const QString& callId, qint64 frameId, int chunkIndex, int chunkCount, const QByteArray& chunkBytes)
调用时机
客户端收到 media_video 分片时调用。
参数
| 参数 | 说明 |
|---|
from | 媒体发送者 |
callId | 通话标识 |
frameId | 帧标识 |
chunkIndex | 分片序号 |
chunkCount | 分片总数 |
chunkData | 分片数据 |
执行流程
- 校验通话来源
- 缓存分片
- 判断帧是否完整
- 合并图像数据
- 解码并更新远端画面
关联接口
BusinessHandler::handleMediaVideoVideoCallManager::cleanupOldFrames
查看完整实现
videocallmanager.cpp
void VideoCallManager::onRemoteVideoReceived(const QString& from,
const QString& callId,
qint64 frameId,
int chunkIndex,
int chunkCount,
const QByteArray& chunkBytes) {
if (!m_running) {
return;
}
if (from != m_peer || callId != m_callId) {
return;
}
if (frameId <= 0 || chunkIndex < 0 || chunkCount <= 0 || chunkIndex >= chunkCount) {
return;
}
if (frameId <= m_lastDisplayedFrameId) {
m_droppedFrameCount++;
return;
}
if (chunkBytes.isEmpty()) {
return;
}
qint64 now = QDateTime::currentMSecsSinceEpoch();
PendingFrame& pending = m_pendingFrames[frameId];
if (pending.chunks.isEmpty()) {
pending.chunkCount = chunkCount;
pending.chunks.resize(chunkCount);
pending.receivedCount = 0;
}
m_receivedFrameCount++;
if (pending.chunkCount != chunkCount) {
m_pendingFrames.remove(frameId);
return;
}
pending.lastUpdateMs = now;
if (pending.chunks[chunkIndex].isEmpty()) {
pending.chunks[chunkIndex] = chunkBytes;
pending.receivedCount++;
}
if (pending.receivedCount == pending.chunkCount) {
QByteArray jpegBytes;
for (int i = 0; i < pending.chunkCount; ++i) {
jpegBytes.append(pending.chunks[i]);
}
QImage image;
if (image.loadFromData(jpegBytes, "JPG")) {
if (frameId > m_lastDisplayedFrameId) {
m_lastDisplayedFrameId = frameId;
updateRemoteVideo(image);
m_displayedFrameCount++;
if (m_displayedFrameCount % 25 == 0) {
qDebug() << "[Video] displayed remote frame"
<< "from=" << from
<< "call_id=" << callId
<< "frame_id=" << frameId
<< "displayed_frames=" << m_displayedFrameCount
<< "pending_frames=" << m_pendingFrames.size()
<< "dropped=" << m_droppedFrameCount;
}
}
}
m_pendingFrames.remove(frameId);
}
cleanupOldFrames();
}
原型void BusinessHandler::handleMediaAudio(const std::string& json_str, struct sockaddr_in client_addr)
调用时机
dispatchTask 识别 media_audio 时调用。
参数
| 参数 | 说明 |
|---|
json_str | 媒体 JSON |
client_addr | 来源地址 |
执行流程
- 解析媒体包
- 校验认证与 call_id
- 确认目标在线
- 转发原始媒体数据
关联接口
ClientLogic::sendMediaAudioVoiceCallManager::onRemoteAudioReceived
查看完整实现
business_handler.cpp
void BusinessHandler::handleMediaAudio(const std::string& json_str, struct sockaddr_in client_addr) {
try {
json j = json::parse(json_str);
std::string from;
if (!checkAuth(j, client_addr, from)) {
return;
}
if (!j.contains("target") || !j.contains("call_id") || !j.contains("payload")) {
std::cerr << "⚠️ [语音中继] 缺少 target/call_id/payload 字段\n";
return;
}
std::string target = j["target"];
Session target_session;
if (!net_server->getSession(target, target_session)) {
return;
}
j["from"] = from;
struct sockaddr_in target_addr{};
target_addr.sin_family = AF_INET;
inet_pton(AF_INET, target_session.ip.c_str(), &target_addr.sin_addr);
target_addr.sin_port = htons(target_session.port);
net_server->sendData(j.dump(), target_addr);
} catch (std::exception& e) {
std::cerr << "语音媒体中继异常: " << e.what() << "\n";
}
}