toy-Kapi_Rtc/02Kapi_Rtc_火山RTC替换实现方案.md
2026-01-20 16:55:17 +08:00

20 KiB
Raw Blame History

Kapi_Rtc项目火山RTC替换实现方案

1. 背景概述

当前Kapi_Rtc项目使用WebSocket协议进行音频数据传输和通信但由于业务需求变化需要替换为火山RTCByte RTC协议。本方案基于对Airhub_Rtc_h项目中火山RTC实现的分析结合Kapi_Rtc项目的现有架构提供详细的替换实现方案。

2. 技术分析

2.1 现有WebSocket实现

Kapi_Rtc项目当前使用WebsocketProtocol类实现通信,主要功能包括:

  • 建立和维护WebSocket连接
  • 发送和接收音频数据
  • 发送文本消息
  • 处理连接状态变化

主要接口:

void Start() override;
void SendAudio(const std::vector<uint8_t>& data) override;
bool OpenAudioChannel() override;
void CloseAudioChannel() override;
bool IsAudioChannelOpened() const override;
void SendText(const std::string& text) override;

2.2 火山RTC实现来自Airhub_Rtc_h

Airhub_Rtc_h项目提供了完整的火山RTC封装主要功能包括

  • RTC引擎初始化和销毁
  • 加入/离开房间
  • 发送音频/视频/消息数据
  • 处理RTC事件回调

主要接口:

volc_rtc_t volc_rtc_create(const char* appid, void* context, cJSON* p_config, volc_msg_cb message_callback, volc_data_cb data_callback);
void volc_rtc_destroy(volc_rtc_t handle);
int volc_rtc_start(volc_rtc_t rtc, const char* bot_id, volc_iot_info_t* iot_info);
int volc_rtc_stop(volc_rtc_t handle);
int volc_rtc_send(volc_rtc_t handle, const void* data, int size, volc_data_info_t* data_info);
int volc_rtc_interrupt(volc_rtc_t rtc);

2.3 架构对比

特性 WebSocket 火山RTC
连接管理 简单的TCP连接 复杂的RTC连接管理ICE/STUN/TURN
音频质量 依赖网络质量 自适应网络,支持丢包重传和抗抖动
延迟 相对较高 低延迟(适合实时通信)
功能丰富度 基础的消息和二进制数据传输 提供完整的RTC功能音频/视频/消息)
集成复杂度 中等

3. 替换方案

3.1 架构设计

保留Kapi_Rtc项目现有的Protocol抽象接口,创建新的VolcRtcProtocol类继承自Protocol,实现与WebsocketProtocol相同的接口,从而实现无缝替换。

┌─────────────────┐
│   Application   │
└────────┬────────┘
         │
┌────────▼────────┐
│    Protocol     │
└────────┬────────┘
         ├───────────────┐
┌────────▼────────┐ ┌─────▼──────────┐
│WebsocketProtocol│ │VolcRtcProtocol │
└─────────────────┘ └────────────────┘

3.2 实现步骤

3.2.1 引入火山RTC依赖

  1. 将Airhub_Rtc_h项目中的火山RTC相关文件复制到Kapi_Rtc项目

    • /components/volc_engine_rtc_lite/ - 火山RTC SDK
    • /components/common/src/volc_rtc.c - RTC封装实现
    • /components/common/include/volc_rtc.h - RTC封装头文件
  2. 更新CMakeLists.txt文件添加火山RTC组件

# 添加火山RTC组件
set(COMPONENTS ${COMPONENTS} volc_engine_rtc_lite)
set(COMPONENTS ${COMPONENTS} common)

3.2.2 SDK配置与鉴权

火山RTC需要以下关键配置项进行鉴权和连接

  1. Kconfig.projbuild中添加配置项:
config VOLC_INSTANCE_ID
    string "volcano instance id"
    default ""
    help
        Instance ID for Volc RTC service.

config VOLC_PRODUCT_KEY
    string "volcano product key"
    default ""
    help
        Product Key for Volc RTC service.

config VOLC_PRODUCT_SECRET
    string "volcano product secret"
    default ""
    help
        Product Secret for Volc RTC service.

config VOLC_DEVICE_NAME
    string "volcano device name"
    default ""
    help
        Device Name for Volc RTC service.

config VOLC_BOT_ID
    string "volcano bot id"
    default ""
    help
        Bot ID for Volc RTC service.

config USE_VOLC_RTC
    bool "Use Volc RTC instead of WebSocket"
    default n
    help
        Select to use Volc RTC protocol instead of WebSocket.
  1. sdkconfig.defaults中设置默认值:
# Volc RTC Configuration
CONFIG_VOLC_INSTANCE_ID="your_instance_id"
CONFIG_VOLC_PRODUCT_KEY="your_product_key"
CONFIG_VOLC_PRODUCT_SECRET="your_product_secret"
CONFIG_VOLC_DEVICE_NAME="your_device_name"
CONFIG_VOLC_BOT_ID="your_bot_id"
CONFIG_USE_VOLC_RTC=y
  1. 创建配置JSON模板
// 对话AI配置格式
#define CONV_AI_CONFIG_FORMAT "{\"ver\": 1,\"iot\":{\"instance_id\":\"%s\",\"product_key\":\"%s\",\"product_secret\":\"%s\",\"device_name\":\"%s\"},\"rtc\":{\"log_level\":1,\"audio\":{\"publish\":true,\"subscribe\":true,\"codec\":4},\"video\":{\"publish\":false,\"subscribe\":false,\"codec\":1},\"params\":[\"{\\\"debug\\\":{\\\"log_to_console\\\":1}}\",\"{\\\"audio\\\":{\\\"codec\\\":{\\\"internal\\\":{\\\"enable\\\":1}}}}\",\"{\\\"rtc\\\":{\\\"access\\\":{\\\"concurrent_requests\\\":1}}}\",\"{\\\"rtc\\\":{\\\"ice\\\":{\\\"concurrent_agents\\\":1}}}\"]}}"

3.2.3 创建VolcRtcProtocol类

创建新的volc_rtc_protocol.hvolc_rtc_protocol.cc文件,实现Protocol接口:

// volc_rtc_protocol.h
#ifndef _VOLC_RTC_PROTOCOL_H_
#define _VOLC_RTC_PROTOCOL_H_

#include "protocol.h"
#include "volc_rtc.h"
#include <mutex>
#include <cJSON.h>

class VolcRtcProtocol : public Protocol {
public:
    VolcRtcProtocol();
    ~VolcRtcProtocol();

    void Start() override;
    void SendAudio(const std::vector<uint8_t>& data) override;
    bool OpenAudioChannel() override;
    void CloseAudioChannel() override;
    bool IsAudioChannelOpened() const override;

private:
    void SendText(const std::string& text) override;
    void OnRtcMessage(volc_msg_t* msg);
    void OnRtcData(const void* data, int data_len, volc_data_info_t* info);
    
    volc_rtc_t rtc_handle_ = nullptr;
    std::mutex rtc_mutex_;
    bool is_audio_channel_opened_ = false;
    cJSON* rtc_config_ = nullptr;
};

#endif
// volc_rtc_protocol.cc
#include "volc_rtc_protocol.h"
#include "board.h"
#include "system_info.h"
#include "application.h"
#include "assets/lang_config.h"

#include <cstring>
#include <cJSON.h>
#include <esp_log.h>

#define TAG "VolcRTC"

// RTC消息回调函数
static void rtc_message_callback(void* context, volc_msg_t* msg) {
    if (context) {
        ((VolcRtcProtocol*)context)->OnRtcMessage(msg);
    }
}

// RTC数据回调函数
static void rtc_data_callback(void* context, const void* data, int data_len, volc_data_info_t* info) {
    if (context) {
        ((VolcRtcProtocol*)context)->OnRtcData(data, data_len, info);
    }
}

VolcRtcProtocol::VolcRtcProtocol() {
    // 创建完整的火山RTC配置
    char config_buf[1024] = {0};
    
    // 使用配置模板构建完整配置
    snprintf(config_buf, sizeof(config_buf), CONV_AI_CONFIG_FORMAT,
             CONFIG_VOLC_INSTANCE_ID,
             CONFIG_VOLC_PRODUCT_KEY,
             CONFIG_VOLC_PRODUCT_SECRET,
             CONFIG_VOLC_DEVICE_NAME);
    
    // 解析配置JSON
    rtc_config_ = cJSON_Parse(config_buf);
    if (!rtc_config_) {
        ESP_LOGE(TAG, "Failed to parse RTC config");
        rtc_config_ = cJSON_CreateObject();
        
        // 设置默认配置
        cJSON_AddNumberToObject(rtc_config_, "audio.codec", AUDIO_CODEC_TYPE_OPUS);
        cJSON_AddNumberToObject(rtc_config_, "video.codec", VIDEO_CODEC_TYPE_NONE);
        cJSON_AddBoolToObject(rtc_config_, "audio.publish", true);
        cJSON_AddBoolToObject(rtc_config_, "video.publish", false);
        cJSON_AddBoolToObject(rtc_config_, "audio.subscribe", true);
        cJSON_AddBoolToObject(rtc_config_, "video.subscribe", false);
        cJSON_AddNumberToObject(rtc_config_, "log_level", BYTE_RTC_LOG_LEVEL_INFO);
    }
}

VolcRtcProtocol::~VolcRtcProtocol() {
    CloseAudioChannel();
    
    if (rtc_config_) {
        cJSON_Delete(rtc_config_);
    }
}

void VolcRtcProtocol::Start() {
    // RTC协议在OpenAudioChannel时启动这里不需要额外操作
}

bool VolcRtcProtocol::OpenAudioChannel() {
    std::lock_guard<std::mutex> lock(rtc_mutex_);
    
    if (rtc_handle_ != nullptr) {
        ESP_LOGW(TAG, "RTC handle already exists");
        return true;
    }
    
    // 创建RTC实例 - 传入完整配置,包括鉴权信息
    const char* app_id = CONFIG_VOLC_INSTANCE_ID; // 使用实例ID作为app_id
    rtc_handle_ = volc_rtc_create(app_id, this, rtc_config_, rtc_message_callback, rtc_data_callback);
    
    if (rtc_handle_ == nullptr) {
        ESP_LOGE(TAG, "Failed to create RTC instance");
        SetError(Lang::Strings::SERVER_ERROR);
        return false;
    }
    
    // 构造IoT信息
    volc_iot_info_t iot_info = {0};
    
    // 设置设备ID
    strncpy(iot_info.device_id, SystemInfo::GetMacAddress().c_str(), sizeof(iot_info.device_id));
    
    // 设置产品信息用于鉴权
    strncpy(iot_info.product_key, CONFIG_VOLC_PRODUCT_KEY, sizeof(iot_info.product_key));
    strncpy(iot_info.product_secret, CONFIG_VOLC_PRODUCT_SECRET, sizeof(iot_info.product_secret));
    strncpy(iot_info.device_name, CONFIG_VOLC_DEVICE_NAME, sizeof(iot_info.device_name));
    
    // 设置房间信息
    const char* bot_id = CONFIG_VOLC_BOT_ID;
    
    // 启动RTC并加入房间 - 内部会处理token生成和鉴权
    int ret = volc_rtc_start(rtc_handle_, bot_id, &iot_info);
    if (ret != 0) {
        ESP_LOGE(TAG, "Failed to start RTC: %d", ret);
        volc_rtc_destroy(rtc_handle_);
        rtc_handle_ = nullptr;
        SetError(Lang::Strings::SERVER_ERROR);
        return false;
    }
    
    is_audio_channel_opened_ = true;
    
    if (on_audio_channel_opened_ != nullptr) {
        on_audio_channel_opened_();
    }
    
    return true;
}

void VolcRtcProtocol::CloseAudioChannel() {
    std::lock_guard<std::mutex> lock(rtc_mutex_);
    
    if (rtc_handle_ != nullptr) {
        // 停止RTC
        volc_rtc_stop(rtc_handle_);
        
        // 销毁RTC实例
        volc_rtc_destroy(rtc_handle_);
        rtc_handle_ = nullptr;
    }
    
    is_audio_channel_opened_ = false;
    
    if (on_audio_channel_closed_ != nullptr) {
        on_audio_channel_closed_();
    }
}

bool VolcRtcProtocol::IsAudioChannelOpened() const {
    return is_audio_channel_opened_;
}

void VolcRtcProtocol::SendAudio(const std::vector<uint8_t>& data) {
    std::lock_guard<std::mutex> lock(rtc_mutex_);
    
    if (!is_audio_channel_opened_ || rtc_handle_ == nullptr) {
        ESP_LOGD(TAG, "RTC not connected, dropping audio data");
        return;
    }
    
    // 构造音频数据信息
    volc_data_info_t data_info = {0};
    data_info.type = VOLC_DATA_TYPE_AUDIO;
    data_info.info.audio.data_type = VOLC_AUDIO_DATA_TYPE_OPUS;
    
    // 发送音频数据
    int ret = volc_rtc_send(rtc_handle_, data.data(), data.size(), &data_info);
    if (ret != 0) {
        ESP_LOGE(TAG, "Failed to send audio data: %d", ret);
    }
}

void VolcRtcProtocol::SendText(const std::string& text) {
    std::lock_guard<std::mutex> lock(rtc_mutex_);
    
    if (!is_audio_channel_opened_ || rtc_handle_ == nullptr) {
        ESP_LOGD(TAG, "RTC not connected, dropping text message");
        return;
    }
    
    // 构造消息数据信息
    volc_data_info_t data_info = {0};
    data_info.type = VOLC_DATA_TYPE_MESSAGE;
    data_info.info.message.is_binary = false;
    
    // 发送文本消息
    int ret = volc_rtc_send(rtc_handle_, text.c_str(), text.size(), &data_info);
    if (ret != 0) {
        ESP_LOGE(TAG, "Failed to send text message: %d", ret);
    }
}

void VolcRtcProtocol::OnRtcMessage(volc_msg_t* msg) {
    if (!msg) {
        return;
    }
    
    switch (msg->code) {
        case VOLC_MSG_CONNECTED:
            ESP_LOGI(TAG, "RTC connected");
            break;
        case VOLC_MSG_DISCONNECTED:
            ESP_LOGI(TAG, "RTC disconnected");
            CloseAudioChannel();
            break;
        case VOLC_MSG_USER_JOINED:
            ESP_LOGI(TAG, "Remote user joined");
            break;
        case VOLC_MSG_USER_OFFLINE:
            ESP_LOGI(TAG, "Remote user offline");
            break;
        case VOLC_MSG_CONV_STATUS:
            ESP_LOGI(TAG, "Conversation status: %d", msg->data.conv_status);
            break;
        default:
            ESP_LOGD(TAG, "Unhandled RTC message: %d", msg->code);
            break;
    }
}

void VolcRtcProtocol::OnRtcData(const void* data, int data_len, volc_data_info_t* info) {
    if (!data || !info) {
        return;
    }
    
    switch (info->type) {
        case VOLC_DATA_TYPE_AUDIO:
            if (on_incoming_audio_ != nullptr) {
                on_incoming_audio_(std::vector<uint8_t>((uint8_t*)data, (uint8_t*)data + data_len));
            }
            break;
        case VOLC_DATA_TYPE_MESSAGE:
            if (info->info.message.is_binary) {
                // 处理二进制消息
                ESP_LOGD(TAG, "Received binary message, length: %d", data_len);
            } else {
                // 处理文本消息
                std::string text((char*)data, data_len);
                auto root = cJSON_Parse(text.c_str());
                if (root != nullptr) {
                    if (on_incoming_json_ != nullptr) {
                        on_incoming_json_(root);
                    }
                    cJSON_Delete(root);
                }
            }
            break;
        default:
            ESP_LOGD(TAG, "Unhandled RTC data type: %d", info->type);
            break;
    }
    
    last_incoming_time_ = std::chrono::steady_clock::now();
}

3.2.4 更新Protocol工厂

修改创建协议实例的代码根据配置选择使用WebSocket或火山RTC

// 在application.cc或相关文件中
#include "websocket_protocol.h"
#include "volc_rtc_protocol.h"

// 创建协议实例
#ifdef CONFIG_USE_VOLC_RTC
    protocol_ = new VolcRtcProtocol();
#else
    protocol_ = new WebsocketProtocol();
#endif

3.2.5 添加语音打断支持

集成火山RTC的语音打断功能

// 在volc_rtc_protocol.h中添加
int SendInterrupt();

// 在volc_rtc_protocol.cc中实现
int VolcRtcProtocol::SendInterrupt() {
    std::lock_guard<std::mutex> lock(rtc_mutex_);
    
    if (!is_audio_channel_opened_ || rtc_handle_ == nullptr) {
        ESP_LOGD(TAG, "RTC not connected, cannot send interrupt");
        return -1;
    }
    
    return volc_rtc_interrupt(rtc_handle_);
}

Application类中添加打断功能:

// 在application.h中添加
void SendInterrupt();

// 在application.cc中实现
void Application::SendInterrupt() {
    if (!protocol_) {
        ESP_LOGE(TAG, "Protocol not initialized");
        return;
    }
    
#ifdef CONFIG_USE_VOLC_RTC
    auto* rtc_protocol = dynamic_cast<VolcRtcProtocol*>(protocol_);
    if (rtc_protocol) {
        rtc_protocol->SendInterrupt();
    }
#endif
}

4. 业务功能实现

4.1 完整RTC连接通讯实现链路

4.1.1 初始化与鉴权流程

  1. 配置加载从Kconfig和sdkconfig加载火山RTC配置INSTANCE_ID, PRODUCT_KEY, PRODUCT_SECRET, DEVICE_NAME, BOT_ID
  2. 时间同步初始化SNTP服务确保设备时间与服务器同步RTC连接需要准确时间戳
  3. 配置构建使用配置模板构建完整的JSON配置字符串包含鉴权信息
  4. 引擎初始化:调用volc_rtc_create()创建RTC实例传入配置信息
  5. IoT信息准备构建包含设备ID、产品信息的IoT结构体
  6. 加入房间:调用volc_rtc_start()加入RTC房间内部自动处理token生成和鉴权

4.1.2 启动引擎对话

  1. 用户触发:用户通过唤醒词或按钮触发对话
  2. 开始监听Application类调用protocol_->SendStartListening()
  3. 指令发送通过火山RTC发送开始监听指令给AI引擎
  4. 音频采集:启动麦克风采集音频数据
  5. 音频发送:将处理后的音频数据通过protocol_->SendAudio()发送给火山RTC
  6. AI处理火山RTC将音频数据转发给AI引擎进行处理
  7. 结果返回AI引擎处理结果通过火山RTC返回给设备
  8. 音频播放:接收到的音频数据通过on_incoming_audio_回调传递给Application类播放

4.1.3 关闭引擎

  1. 对话结束AI引擎返回对话结束信号或用户手动触发关闭
  2. 离开房间Application类调用protocol_->CloseAudioChannel()
  3. 停止RTC:调用volc_rtc_stop()停止RTC服务
  4. 销毁实例:调用volc_rtc_destroy()销毁RTC实例释放资源

4.2 鉴权机制详解

火山RTC使用基于产品密钥的鉴权机制

  1. 配置信息设备需要提前配置INSTANCE_ID, PRODUCT_KEY, PRODUCT_SECRET, DEVICE_NAME
  2. Token生成RTC SDK内部使用产品密钥生成临时token
  3. Token使用:在byte_rtc_join_room()调用时传入token进行身份验证
  4. Token过期处理SDK监听on_token_privilege_will_expire事件在token即将过期时自动更新

4.3 音频数据流程

  1. 音频输入:麦克风采集的音频数据经过AudioProcessor处理后,通过protocol_->SendAudio()发送给火山RTC
  2. 音频传输火山RTC使用优化的RTC协议传输音频数据支持丢包重传和抗抖动
  3. AI处理火山引擎接收音频数据进行语音识别和AI处理
  4. 结果返回AI处理结果音频和文本通过火山RTC返回给设备
  5. 音频输出:接收到的音频数据通过on_incoming_audio_回调传递给Application类,然后播放出来

4.4 消息处理

  1. 文本消息:通过protocol_->SendText()发送文本消息
  2. JSON消息接收到的JSON消息通过on_incoming_json_回调传递给Application类进行处理
  3. 二进制消息:支持发送和接收二进制消息,用于特殊功能扩展

5. 测试和验证

5.1 功能测试

  1. 连接测试验证设备能否成功连接到火山RTC服务器
  2. 音频传输测试:验证音频数据能否正常发送和接收
  3. 语音打断测试:验证语音打断功能能否正常工作
  4. 异常处理测试:验证网络异常或服务器异常时的处理逻辑

5.2 性能测试

  1. 延迟测试:测量音频从采集到播放的延迟
  2. 稳定性测试:长时间运行测试,验证系统稳定性
  3. 资源占用测试监控CPU和内存占用情况

5.3 兼容性测试

  1. 设备兼容性:测试在不同硬件设备上的运行情况
  2. 网络兼容性:测试在不同网络环境下的运行情况

6. 注意事项

  1. 配置管理

    • 确保火山RTC的INSTANCE_ID, PRODUCT_KEY, PRODUCT_SECRET, DEVICE_NAME和BOT_ID等配置正确
    • 避免将敏感信息如PRODUCT_SECRET硬编码到代码中应通过配置文件管理
    • 定期更新密钥,确保安全性
  2. 时间同步

    • RTC连接需要准确的设备时间确保SNTP服务正常工作
    • 在网络不稳定时考虑使用本地RTC作为备用时间源
  3. 错误处理

    • 完善异常处理逻辑,特别是网络异常和服务器异常情况
    • 实现token过期自动更新机制
    • 添加连接重试逻辑,提高系统稳定性
  4. 资源管理

    • 合理管理RTC资源避免内存泄漏
    • 在不需要RTC服务时及时关闭连接
    • 监控系统资源占用情况,避免资源耗尽
  5. 日志管理

    • 添加详细的日志,便于调试和问题定位
    • 分类管理日志级别,在生产环境中降低日志级别以减少性能开销
  6. 版本兼容性

    • 确保火山RTC SDK版本与项目兼容
    • 定期更新SDK获取最新的功能和安全修复
  7. 网络环境

    • 确保设备处于稳定的网络环境中
    • 考虑网络切换场景如WiFi/4G切换的处理逻辑

7. 结论

本方案通过创建新的VolcRtcProtocol类实现了与WebsocketProtocol相同的接口从而实现了无缝替换。火山RTC提供了更丰富的功能和更好的音频质量能够满足实时通信的需求。同时本方案保持了Kapi_Rtc项目的现有架构不变降低了迁移风险和成本。

8. 后续支持

如果您需要进一步的帮助,我可以:

  1. 协助实现文档中的方案,包括代码编写和配置
  2. 回答关于RTC连接鉴权和SDK配置的具体问题
  3. 提供完整的实时AI对话业务实现链路的技术支持
  4. 帮助进行测试和调试确保RTC连接和音频传输正常工作
  5. 优化系统性能,提高实时对话的稳定性和响应速度

请随时告诉我您的需求。