Voice & Video Relay Pipeline

08. 音视频通信链路

CallWindow 编排通话状态,VoiceCallManager 与 VideoCallManager 管理设备采集和播放,中心服务按 call_id 中继媒体数据。

7 个核心函数5 个重点文件基于当前工程源码

模块职责

CallWindow 编排通话状态,VoiceCallManager 与 VideoCallManager 管理设备采集和播放,中心服务按 call_id 中继媒体数据。

重点文件

调用链

  1. 1通话信令建立会话
  2. 2初始化本地设备
  3. 3采集媒体帧
  4. 4UDP 中继
  5. 5远端解码与播放
  6. 6挂断释放资源
通话界面编排

CallWindow::startVoiceSession

创建 VoiceCallManager、连接媒体信号并启动语音采集与播放。

callwindow.cpp · L278–L367
原型void CallWindow::startVoiceSession()

调用时机

双方进入可通话状态后调用。

返回说明

无返回值。

参数

无显式参数。

执行流程

  1. 校验会话状态
  2. 创建语音管理器
  3. 连接音频发送信号
  4. 连接远端音频入口
  5. 启动设备
  6. 更新界面状态

工程说明

界面层只编排生命周期,不处理音频设备细节。

关联接口

查看完整实现
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);
        }
    }
}
语音设备启动

VoiceCallManager::start

选择兼容音频格式,创建输入输出设备并启动麦克风读取。

voicecallmanager.cpp · L136–L211
原型bool VoiceCallManager::start()

调用时机

CallWindow 启动语音会话时调用。

返回说明

设备启动成功返回 true。

参数

无显式参数。

执行流程

  1. 获取默认音频设备
  2. 选择支持格式
  3. 创建 QAudioSource/QAudioSink
  4. 启动输入输出
  5. 连接 readyRead

工程说明

格式选择兼顾输入与输出设备兼容性。

关联接口

查看完整实现
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;
}
远端语音播放

VoiceCallManager::onRemoteAudioReceived

过滤非当前通话数据,将远端音频字节写入输出设备。

voicecallmanager.cpp · L295–L330
原型void VoiceCallManager::onRemoteAudioReceived(const QString& from, const QString& callId, qint64 seq, const QByteArray& audioBytes)

调用时机

ClientLogic 收到 media_audio 后调用。

返回说明

无返回值。

参数

参数说明
from媒体发送者
callId通话标识
audioBytes音频数据

执行流程

  1. 校验来源与 call_id
  2. 确认输出设备已启动
  3. 写入播放缓冲
  4. 处理写入异常

工程说明

媒体包按通话标识隔离。

关联接口

查看完整实现
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();
    }
}
视频会话编排

CallWindow::startVideoSession

在语音会话基础上创建 VideoCallManager,并将本地预览和远端画面接入 CallWindow。

callwindow.cpp · L369–L457
原型void CallWindow::startVideoSession()

调用时机

视频通话建立后调用。

返回说明

无返回值。

参数

无显式参数。

执行流程

  1. 确保语音会话可用
  2. 创建视频管理器
  3. 绑定本地预览
  4. 绑定视频发送
  5. 绑定远端画面
  6. 启动摄像头

工程说明

音频与视频在同一 CallWindow 中统一管理。

关联接口

查看完整实现
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("视频通话中");
}
摄像头采集启动

VideoCallManager::start

选择摄像头、创建 QCamera 与 QVideoSink,并开始帧采集。

videocallmanager.cpp · L29–L75
原型bool VideoCallManager::start()

调用时机

CallWindow 启动视频会话时调用。

返回说明

启动成功返回 true。

参数

无显式参数。

执行流程

  1. 查找视频输入设备
  2. 创建 QCamera
  3. 绑定 QVideoSink
  4. 连接 videoFrameChanged
  5. 启动摄像头

工程说明

摄像头开关由 setCameraEnabled 独立控制。

关联接口

查看完整实现
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;
}
远端视频重组

VideoCallManager::onRemoteVideoReceived

接收分片视频数据,按帧标识重组并更新远端画面。

videocallmanager.cpp · L210–L288
原型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分片数据

执行流程

  1. 校验通话来源
  2. 缓存分片
  3. 判断帧是否完整
  4. 合并图像数据
  5. 解码并更新远端画面

工程说明

不完整帧会由清理机制回收。

关联接口

查看完整实现
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();
}
服务端语音中继

BusinessHandler::handleMediaAudio

认证媒体发送者,定位通话对端并转发音频数据。

business_handler.cpp · L793–L828
原型void BusinessHandler::handleMediaAudio(const std::string& json_str, struct sockaddr_in client_addr)

调用时机

dispatchTask 识别 media_audio 时调用。

返回说明

无直接返回。

参数

参数说明
json_str媒体 JSON
client_addr来源地址

执行流程

  1. 解析媒体包
  2. 校验认证与 call_id
  3. 确认目标在线
  4. 转发原始媒体数据

工程说明

服务端不解码媒体内容,只负责会话级路由。

关联接口

查看完整实现
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";
    }
}