1、新增HTTPS故事播放功能(SendStoryRequest通过蓝牙MAC请求故事API,支持intro+body两段式无缝播放);
2、新增HttpsPlaybackFromUrl通用HTTPS音频下载播放方法,obtain_story同时支持HTTPS URL和WebSocket两种方式; 3、新增RTC↔HTTPS双向音频切换三标志位状态机(opus_playback_active_/https_playback_active_/https_playback_abort_),HTTPS播放期间静默丢弃RTC PCM包,OnAudioOutput捕获is_opus_frame防止残留Opus帧杂音; 4、新增AbortHttpsPlayback中止方法,使用独立高优先级任务(priority=10)执行DMA flush; 5、协议层新增OnBotMessage回调,Bot下行消息立即中止HTTPS播放;volc_rtc_protocol移除is_binary依赖改为直接前缀检测,新增info前缀和subv跳过逻辑; 6、新增subtitle字幕消息解析,通过bot_前缀区分USER/AI,用户说话时立即中止HTTPS播放; 7、AbortSpeaking新增HTTPS中止信号和DMA缓冲区flush; 8、Kconfig新增STORY_API_URL故事播放API地址配置; Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
d494a1025f
commit
b9bbcc456c
@ -12,6 +12,12 @@ config DEVICE_STATUS_REPORT_URL
|
|||||||
help
|
help
|
||||||
URL for reporting device status to server
|
URL for reporting device status to server
|
||||||
|
|
||||||
|
config STORY_API_URL
|
||||||
|
string "Story API URL"
|
||||||
|
default "http://192.168.124.8:8000/api/v1/devices/stories/"
|
||||||
|
help
|
||||||
|
故事播放API接口地址,设备会附加 ?mac_address=XX:XX:XX:XX:XX:XX 参数请求
|
||||||
|
|
||||||
choice
|
choice
|
||||||
prompt "语言选择"
|
prompt "语言选择"
|
||||||
default LANGUAGE_ZH_CN
|
default LANGUAGE_ZH_CN
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -70,7 +70,9 @@ public:
|
|||||||
void Alert(const char* status, const char* message, const char* emotion = "", const std::string_view& sound = "");// 警报管理 状态、消息、情感、声音
|
void Alert(const char* status, const char* message, const char* emotion = "", const std::string_view& sound = "");// 警报管理 状态、消息、情感、声音
|
||||||
void DismissAlert();// 关闭警报
|
void DismissAlert();// 关闭警报
|
||||||
void AbortSpeaking(AbortReason reason);// 打断语音播报
|
void AbortSpeaking(AbortReason reason);// 打断语音播报
|
||||||
void SendStoryRequest(); // 发送讲故事 请求
|
void AbortHttpsPlayback(const char* reason);// 中止HTTPS音频播放并清空DMA
|
||||||
|
void SendStoryRequest(); // 发送讲故事 请求(WebSocket方式)
|
||||||
|
void HttpsPlaybackFromUrl(const std::string& url); // 通过HTTPS下载JSON并播放音频(故事/歌曲等)
|
||||||
void ToggleChatState();// 切换聊天状态
|
void ToggleChatState();// 切换聊天状态
|
||||||
void ToggleListeningState();// 切换监听状态
|
void ToggleListeningState();// 切换监听状态
|
||||||
void StartListening();// 开始监听
|
void StartListening();// 开始监听
|
||||||
@ -145,7 +147,9 @@ private:
|
|||||||
bool realtime_chat_enabled_ = false;
|
bool realtime_chat_enabled_ = false;
|
||||||
#endif
|
#endif
|
||||||
std::atomic<bool> ws_downlink_enabled_{true};// 🌐 WebSocket下行通道是否启用
|
std::atomic<bool> ws_downlink_enabled_{true};// 🌐 WebSocket下行通道是否启用
|
||||||
std::atomic<bool> ws_playback_active_{false};// 🌐 WebSocket下行播放活跃标志
|
std::atomic<bool> opus_playback_active_{false};// 🌐 Opus解码播放活跃标志(WS/HTTPS共用)
|
||||||
|
std::atomic<bool> https_playback_active_{false};// 🌐 HTTPS音频播放进行中标志
|
||||||
|
std::atomic<bool> https_playback_abort_{false};// 🌐 HTTPS音频播放中止标志
|
||||||
bool aborted_ = false;
|
bool aborted_ = false;
|
||||||
bool voice_detected_ = false;
|
bool voice_detected_ = false;
|
||||||
bool audio_paused_ = false; // 音频暂停状态标志
|
bool audio_paused_ = false; // 音频暂停状态标志
|
||||||
|
|||||||
@ -24,6 +24,10 @@ void Protocol::OnNetworkError(std::function<void(const std::string& message)> ca
|
|||||||
on_network_error_ = callback;
|
on_network_error_ = callback;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Protocol::OnBotMessage(std::function<void()> callback) {
|
||||||
|
on_bot_message_ = callback;
|
||||||
|
}
|
||||||
|
|
||||||
void Protocol::SetError(const std::string& message) {
|
void Protocol::SetError(const std::string& message) {
|
||||||
error_occurred_ = true;
|
error_occurred_ = true;
|
||||||
if (on_network_error_ != nullptr) {
|
if (on_network_error_ != nullptr) {
|
||||||
|
|||||||
@ -49,6 +49,7 @@ public:
|
|||||||
void OnAudioChannelOpened(std::function<void()> callback);
|
void OnAudioChannelOpened(std::function<void()> callback);
|
||||||
void OnAudioChannelClosed(std::function<void()> callback);
|
void OnAudioChannelClosed(std::function<void()> callback);
|
||||||
void OnNetworkError(std::function<void(const std::string& message)> callback);
|
void OnNetworkError(std::function<void(const std::string& message)> callback);
|
||||||
|
void OnBotMessage(std::function<void()> callback);
|
||||||
|
|
||||||
virtual void Start() = 0;
|
virtual void Start() = 0;
|
||||||
virtual bool OpenAudioChannel() = 0;
|
virtual bool OpenAudioChannel() = 0;
|
||||||
@ -76,6 +77,7 @@ protected:
|
|||||||
std::function<void()> on_audio_channel_opened_;
|
std::function<void()> on_audio_channel_opened_;
|
||||||
std::function<void()> on_audio_channel_closed_;
|
std::function<void()> on_audio_channel_closed_;
|
||||||
std::function<void(const std::string& message)> on_network_error_;
|
std::function<void(const std::string& message)> on_network_error_;
|
||||||
|
std::function<void()> on_bot_message_;
|
||||||
|
|
||||||
int server_sample_rate_ = 24000;
|
int server_sample_rate_ = 24000;
|
||||||
int server_frame_duration_ = 60;
|
int server_frame_duration_ = 60;
|
||||||
|
|||||||
@ -579,16 +579,23 @@ void VolcRtcProtocol::DataCallback(void* context, const void* data, size_t len,
|
|||||||
if (data && len > 0) {
|
if (data && len > 0) {
|
||||||
const uint8_t* buf = static_cast<const uint8_t*>(data);
|
const uint8_t* buf = static_cast<const uint8_t*>(data);
|
||||||
std::string json_text;
|
std::string json_text;
|
||||||
if (info->info.message.is_binary && len >= 8) {
|
// 检测二进制前缀格式: [prefix(4字节)] + [json_len(4字节大端)] + [JSON]
|
||||||
|
// 注意: SDK DataCallback中 is_binary 始终为false,不能依赖此字段
|
||||||
|
bool is_subv = false;
|
||||||
|
if (len >= 8) {
|
||||||
bool is_ctrl = (memcmp(buf, "ctrl", 4) == 0);
|
bool is_ctrl = (memcmp(buf, "ctrl", 4) == 0);
|
||||||
bool is_conv = (memcmp(buf, "conv", 4) == 0);
|
bool is_conv = (memcmp(buf, "conv", 4) == 0);
|
||||||
bool is_tool = (memcmp(buf, "tool", 4) == 0);
|
bool is_tool = (memcmp(buf, "tool", 4) == 0);
|
||||||
if (is_ctrl || is_conv || is_tool) {
|
is_subv = (memcmp(buf, "subv", 4) == 0);
|
||||||
|
bool is_info = (memcmp(buf, "info", 4) == 0);
|
||||||
|
if (is_ctrl || is_conv || is_tool || is_subv || is_info) {
|
||||||
uint32_t json_len = (uint32_t)((buf[4] << 24) | (buf[5] << 16) | (buf[6] << 8) | (buf[7]));
|
uint32_t json_len = (uint32_t)((buf[4] << 24) | (buf[5] << 16) | (buf[6] << 8) | (buf[7]));
|
||||||
if (json_len > 0 && (size_t)(8 + json_len) <= len) {
|
if (json_len > 0 && (size_t)(8 + json_len) <= len) {
|
||||||
json_text.assign(reinterpret_cast<const char*>(buf + 8), json_len);
|
json_text.assign(reinterpret_cast<const char*>(buf + 8), json_len);
|
||||||
if (!protocol->suppress_incoming_message_log_) {
|
// 字幕消息不打印内容(频率高)
|
||||||
ESP_LOGI(TAG, "接收下行二进制消息(%s): %.*s", is_ctrl ? "ctrl" : (is_conv ? "conv" : "tool"), (int)json_text.size(), json_text.c_str());
|
if (!is_subv && !protocol->suppress_incoming_message_log_) {
|
||||||
|
const char* prefix = is_ctrl ? "ctrl" : (is_conv ? "conv" : (is_tool ? "tool" : "info"));
|
||||||
|
ESP_LOGI(TAG, "接收下行消息(%s): %.*s", prefix, (int)json_text.size(), json_text.c_str());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -599,48 +606,52 @@ void VolcRtcProtocol::DataCallback(void* context, const void* data, size_t len,
|
|||||||
ESP_LOGI(TAG, "接收下行消息: %.*s", (int)json_text.size(), json_text.c_str());
|
ESP_LOGI(TAG, "接收下行消息: %.*s", (int)json_text.size(), json_text.c_str());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
cJSON* root = cJSON_Parse(json_text.c_str());
|
|
||||||
if (root) {
|
// 非subv消息立即通知应用层中止HTTPS播放(尽早触发,不等JSON解析)
|
||||||
const char* sid_keys[] = {"sessionId", "session_id", "sid"};
|
// subv字幕消息由应用层subtitle handler处理(可区分USER/AI)
|
||||||
cJSON* sid = nullptr;
|
if (!is_subv && protocol->on_bot_message_) {
|
||||||
for (size_t i = 0; i < sizeof(sid_keys) / sizeof(sid_keys[0]); ++i) {
|
protocol->on_bot_message_();
|
||||||
sid = cJSON_GetObjectItem(root, sid_keys[i]);
|
|
||||||
if (sid && cJSON_IsString(sid) && sid->valuestring && sid->valuestring[0] != '\0') {
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
sid = nullptr;
|
|
||||||
}
|
cJSON* root = cJSON_Parse(json_text.c_str());
|
||||||
if (!sid) {
|
if (root) {
|
||||||
const char* containers[] = {"data", "payload", "context", "session"};
|
// 提取 Session ID(支持多种字段名和嵌套位置)
|
||||||
for (size_t i = 0; i < sizeof(containers) / sizeof(containers[0]); ++i) {
|
const char* sid_keys[] = {"sessionId", "session_id", "sid"};
|
||||||
cJSON* obj = cJSON_GetObjectItem(root, containers[i]);
|
cJSON* sid = nullptr;
|
||||||
if (obj && cJSON_IsObject(obj)) {
|
for (size_t i = 0; i < sizeof(sid_keys) / sizeof(sid_keys[0]); ++i) {
|
||||||
for (size_t j = 0; j < sizeof(sid_keys) / sizeof(sid_keys[0]); ++j) {
|
sid = cJSON_GetObjectItem(root, sid_keys[i]);
|
||||||
sid = cJSON_GetObjectItem(obj, sid_keys[j]);
|
if (sid && cJSON_IsString(sid) && sid->valuestring && sid->valuestring[0] != '\0') break;
|
||||||
if (sid && cJSON_IsString(sid) && sid->valuestring && sid->valuestring[0] != '\0') {
|
sid = nullptr;
|
||||||
break;
|
}
|
||||||
|
if (!sid) {
|
||||||
|
const char* containers[] = {"data", "payload", "context", "session"};
|
||||||
|
for (size_t i = 0; i < sizeof(containers) / sizeof(containers[0]); ++i) {
|
||||||
|
cJSON* obj = cJSON_GetObjectItem(root, containers[i]);
|
||||||
|
if (obj && cJSON_IsObject(obj)) {
|
||||||
|
for (size_t j = 0; j < sizeof(sid_keys) / sizeof(sid_keys[0]); ++j) {
|
||||||
|
sid = cJSON_GetObjectItem(obj, sid_keys[j]);
|
||||||
|
if (sid && cJSON_IsString(sid) && sid->valuestring && sid->valuestring[0] != '\0') break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
if (sid) break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (sid) break;
|
if (sid && cJSON_IsString(sid) && sid->valuestring && sid->valuestring[0] != '\0') {
|
||||||
|
protocol->session_id_ = sid->valuestring;
|
||||||
|
ESP_LOGI(TAG, "Session ID set: %s", protocol->session_id_.c_str());
|
||||||
|
if (protocol->is_audio_channel_opened_ && protocol->start_listening_pending_) {
|
||||||
|
ListeningMode m = protocol->pending_listening_mode_;
|
||||||
|
protocol->start_listening_pending_ = false;
|
||||||
|
protocol->SendStartListening(m);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (protocol->on_incoming_json_) {
|
||||||
|
protocol->on_incoming_json_(root);
|
||||||
|
}
|
||||||
|
cJSON_Delete(root);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (sid && cJSON_IsString(sid) && sid->valuestring && sid->valuestring[0] != '\0') {
|
|
||||||
protocol->session_id_ = sid->valuestring;
|
|
||||||
ESP_LOGI(TAG, "Session ID set: %s", protocol->session_id_.c_str());
|
|
||||||
if (protocol->is_audio_channel_opened_ && protocol->start_listening_pending_) {
|
|
||||||
ListeningMode m = protocol->pending_listening_mode_;
|
|
||||||
protocol->start_listening_pending_ = false;
|
|
||||||
protocol->SendStartListening(m);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (protocol->on_incoming_json_) {
|
|
||||||
protocol->on_incoming_json_(root);
|
|
||||||
}
|
|
||||||
cJSON_Delete(root);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user