chore: checkpoint 第一版
ci / verify (push) Canceled after 0s

This commit is contained in:
Codex Local
2026-09-06 17:04:11 +08:00
parent a8cdec98d1
commit 0c2151ace9
659 changed files with 185855 additions and 554 deletions
@@ -0,0 +1,51 @@
// Copyright (2025) Beijing Volcano Engine Technology Ltd.
// SPDX-License-Identifier: Apache-2.0
#ifndef __CONV_AI_BASE_VOLC_BASE_H__
#define __CONV_AI_BASE_VOLC_BASE_H__
#include <stdbool.h>
#ifdef __cplusplus
extern "C" {
#endif
#include <stdint.h>
#include <stddef.h>
#include "util/volc_list.h"
#include "volc_conv_ai.h"
// TODO: internal or external
typedef enum {
VOLC_MSG_CONNECTED = 0, // 成功连接
VOLC_MSG_DISCONNECTED, // 断开连接
VOLC_MSG_USER_JOINED, // 用户加入
VOLC_MSG_USER_OFFLINE, // 用户离开
VOLC_MSG_APP_ID_INVALID,
VOLC_MSG_TOKEN_INVALID,
VOLC_MSG_TOKEN_EXPIRED,
VOLC_MSG_LICENSE_EXPIRED,
VOLC_MSG_KEY_FRAME_REQ, // 关键帧请求
VOLC_MSG_TARGET_BITRATE_CHANGED, // 目标码率变化
VOLC_MSG_CONV_STATUS, // 会话状态
VOLC_MSG_QUOTA_EXCEEDED, // 资源耗尽
} volc_msg_e;
typedef struct {
volc_msg_e code;
union {
uint32_t target_bitrate;
uint32_t conv_status;
char* msg;
} data;
} volc_msg_t;
typedef void(*volc_msg_cb)(void* context, volc_msg_t *msg);
typedef void(*volc_data_cb)(void* context, const void* buffer, size_t len, volc_data_info_t *info);
#ifdef __cplusplus
}
#endif
#endif /* __CONV_AI_BASE_VOLC_BASE_H__ */
@@ -0,0 +1,211 @@
// Copyright (2025) Beijing Volcano Engine Technology Ltd.
// SPDX-License-Identifier: Apache-2.0
#include "volc_device_manager.h"
#include <inttypes.h>
#include <stdlib.h>
#include <string.h>
#include "cJSON.h"
#include "volc_osal.h"
#include "util/volc_http.h"
#include "util/volc_json.h"
#include "util/volc_base64.h"
#include "util/volc_auth.h"
#include "util/volc_list.h"
#include "util/volc_log.h"
#include "volc_conv_ai.h"
#define VOLC_IOT_HOST "https://iot-cn-shanghai.iot.volces.com"
#define VOLC_DYNAMIC_REGISTER_PATH "/2021-12-14/DynamicRegister"
#define VOLC_API_VERSION "2021-12-14"
#define VOLC_API_VERSION_QUERY_PARAM "Version=2021-12-14"
#define VOLC_API_ACTION_DYNAMIC_REGISTER "Action=DynamicRegister"
char* volc_generate_signature(const char* secret_key, const char* product_key, const char* device_name, int rnd, uint64_t timestamp, int auth_type)
{
char input_str[256] = {0};
uint8_t hmac_result[32] = {0};
int hmac_result_len = sizeof(hmac_result);
int base64_encoded_len = 0;
size_t olen = 0;
snprintf(input_str, sizeof(input_str), "auth_type=%d&device_name=%s&random_num=%d&product_key=%s&timestamp=%" PRIu64, auth_type, device_name, rnd, product_key, timestamp);
volc_sha256_hmac((const unsigned char*)secret_key, strlen(secret_key), (const unsigned char*)input_str, strlen(input_str), hmac_result, &hmac_result_len);
base64_encoded_len = volc_base64_encoded_length(hmac_result_len);
unsigned char* base64_encoded = (unsigned char*)volc_osal_malloc(base64_encoded_len);
if (!base64_encoded) {
LOGE("Failed to allocate memory for base64 encoded string");
return NULL;
}
volc_base64_encode(base64_encoded, base64_encoded_len, &olen, (const unsigned char*)hmac_result, sizeof(hmac_result));
return (char*)base64_encoded;
}
char* volc_generate_signature_ws(const char* secret_key, const char* product_key, const char* device_name, const char* instance_id, int rnd, uint64_t timestamp, int auth_type) {
char input_str[256] = {0};
uint8_t hmac_result[32] = {0};
int hmac_result_len = sizeof(hmac_result);
int base64_encoded_len = 0;
size_t olen = 0;
snprintf(input_str, sizeof(input_str), "auth_type=%d&device_name=%s&random_num=%d&product_key=%s&timestamp=%" PRIu64 "&instance_id=%s", auth_type, device_name, rnd, product_key, timestamp, instance_id);
volc_sha256_hmac((const unsigned char*)secret_key, strlen(secret_key), (const unsigned char*)input_str, strlen(input_str), hmac_result, &hmac_result_len);
base64_encoded_len = volc_base64_encoded_length(hmac_result_len);
unsigned char* base64_encoded = (unsigned char*)volc_osal_malloc(base64_encoded_len);
if (!base64_encoded) {
LOGE("Failed to allocate memory for base64 encoded string");
return NULL;
}
volc_base64_encode(base64_encoded, base64_encoded_len, &olen, (const unsigned char*)hmac_result, sizeof(hmac_result));
return (char*)base64_encoded;
}
volc_error_code_e volc_inter_err_2_ext_err(int code) {
switch (code) {
case ERROR_LICENSE_EXHAUSTED:
return VOLC_ERR_LICENSE_EXHAUSTED;
case ERROR_LICENSE_EXPIRED:
return VOLC_ERR_LICENSE_EXPIRED;
default:
return VOLC_ERR_FAILED;
}
}
int volc_device_register(volc_iot_info_t* info, char** output)
{
int ret = 0;
uint64_t current_time = volc_osal_get_time_ms();
int32_t random_num = (int32_t)current_time;
char url[256] = {0};
char* payload = NULL;
char* signature = volc_generate_signature(info->product_secret, info->product_key, info->device_name, random_num, current_time, 1);
cJSON* response_json = NULL;
cJSON* root = cJSON_CreateObject();
cJSON_AddStringToObject(root, "InstanceID", info->instance_id);
cJSON_AddStringToObject(root, "product_key", info->product_key);
cJSON_AddStringToObject(root, "device_name", info->device_name);
cJSON_AddNumberToObject(root, "random_num", random_num);
cJSON_AddNumberToObject(root, "timestamp", (double)current_time);
cJSON_AddNumberToObject(root, "auth_type", 1);
cJSON_AddStringToObject(root, "signature", signature);
char* json_str = cJSON_PrintUnformatted(root);
snprintf(url, sizeof(url), "%s%s?%s&%s", VOLC_IOT_HOST, VOLC_DYNAMIC_REGISTER_PATH, VOLC_API_ACTION_DYNAMIC_REGISTER, VOLC_API_VERSION_QUERY_PARAM);
LOGD("url: %s, body: %s", url, json_str);
char* response = volc_http_post(url, json_str, strlen(json_str));
if (response == NULL) {
LOGE("Failed to get response from server");
ret = -1;
goto err_out_label;
}
response_json = cJSON_Parse(response);
if (response_json == NULL) {
LOGE("Failed to parse response JSON");
ret = -1;
goto err_out_label;
}
int code = 0;
ret = volc_json_read_int(response_json, "ResponseMetadata.Error.CodeN", &code);
if (0 == ret) {
ret = volc_inter_err_2_ext_err(code);
LOGE("register device failed, ret: %d, code: %d", ret, code);
goto err_out_label;
}
ret = volc_json_read_string(response_json, "Result.payload", &payload);
if (ret != 0) {
LOGE("Failed to read payload from response JSON");
ret = -1;
goto err_out_label;
}
ret = volc_json_read_string(response_json, "Result.RTCAppID", &info->rtc_app_id);
if (ret != 0) {
LOGE("Failed to read rtc app id from response JSON");
ret = -1;
goto err_out_label;
}
LOGD("rtc app id: %s", info->rtc_app_id);
// TODO: device secret, should be freed by caller
*output = volc_aes_decode(info->product_secret, payload, true);
err_out_label:
HAL_SAFE_FREE(payload);
HAL_SAFE_FREE(response);
HAL_SAFE_FREE(root);
HAL_SAFE_FREE(response_json);
HAL_SAFE_FREE(signature);
HAL_SAFE_FREE(json_str);
return ret;
}
#define VOLC_GET_RTC_CONFIG_PATH "/2021-12-14/GetRTCConfig"
#define VOLC_API_ACTION_GET_RTC_CONFIG "Action=GetRTCConfig"
int volc_get_rtc_config(volc_iot_info_t* info, int audio_codec, const char* bot_id, const char* task_id, volc_room_info_t* room_info, const char* params) {
int ret = 0;
uint64_t current_time = volc_osal_get_time_ms();
int32_t random_num = (int32_t)current_time;
char url[256] = {0};
char* signature = volc_generate_signature(info->device_secret, info->product_key, info->device_name, random_num, current_time, 0);
cJSON* response_json = NULL;
cJSON* root = cJSON_CreateObject();
cJSON_AddStringToObject(root, "InstanceID", info->instance_id);
cJSON_AddStringToObject(root, "product_key", info->product_key);
cJSON_AddStringToObject(root, "device_name", info->device_name);
cJSON_AddNumberToObject(root, "random_num", random_num);
cJSON_AddNumberToObject(root, "timestamp", (double)current_time);
cJSON_AddStringToObject(root, "signature", signature);
cJSON_AddStringToObject(root, "bot_id", bot_id);
cJSON_AddNumberToObject(root, "audio_codec", audio_codec);
cJSON_AddStringToObject(root, "task_id", task_id);
if(params){
cJSON* params_config = cJSON_Parse(params);
cJSON* agent_config = cJSON_CreateObject();
volc_json_read_object(params_config,"AgentConfig",&agent_config);
cJSON_AddItemToObject(root, "agent_config", agent_config);
cJSON* config = cJSON_CreateObject();
volc_json_read_object(params_config,"Config",&config);
cJSON_AddItemToObject(root, "config", config);
}
char* json_str = cJSON_PrintUnformatted(root);
snprintf(url, sizeof(url), "%s%s?%s&%s", VOLC_IOT_HOST, VOLC_GET_RTC_CONFIG_PATH, VOLC_API_ACTION_GET_RTC_CONFIG, VOLC_API_VERSION_QUERY_PARAM);
LOGI("url: %s, body: %s", url, json_str);
char* response = volc_http_post(url, json_str, strlen(json_str));
if (response == NULL) {
LOGE("Failed to get response from server");
ret = -1;
goto err_out_label;
}
response_json = cJSON_Parse(response);
volc_json_read_string(response_json, "Result.RoomID", &room_info->rtc_opt.p_channel_name);
volc_json_read_string(response_json, "Result.UserID", &room_info->rtc_opt.p_uid);
volc_json_read_string(response_json, "Result.Token", &room_info->rtc_opt.p_token);
volc_json_read_string(response_json, "Result.TaskID", &room_info->task_id);
if (room_info->rtc_opt.p_channel_name == NULL || room_info->rtc_opt.p_uid == NULL || room_info->rtc_opt.p_token == NULL || room_info->task_id == NULL) {
LOGE("Failed to get RTC config from server: %s", response);
ret = -1;
goto err_out_label;
}
err_out_label:
if (root) {
cJSON_Delete(root);
}
if (response_json) {
cJSON_Delete(response_json);
}
HAL_SAFE_FREE(signature);
HAL_SAFE_FREE(json_str);
HAL_SAFE_FREE(response);
return ret;
}
@@ -0,0 +1,46 @@
// Copyright (2025) Beijing Volcano Engine Technology Ltd.
// SPDX-License-Identifier: Apache-2.0
#ifndef __CONV_AI_SRC_VOLC_DEVICE_MANAGER__
#define __CONV_AI_SRC_VOLC_DEVICE_MANAGER__
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
#define HARDWARE_ID "a2:c8:2c:89:6e:46"
#define ERROR_LICENSE_EXHAUSTED 12000130
#define ERROR_LICENSE_EXPIRED 12000140
typedef struct {
char* instance_id;
char* product_key;
char* product_secret;
char* device_name;
char* device_secret;
char* rtc_app_id;
} volc_iot_info_t;
typedef struct {
char *p_channel_name;
char *p_uid;
char *p_token;
} volc_rtc_option_t;
typedef struct {
volc_rtc_option_t rtc_opt;
char* task_id; // Task ID for the room
} volc_room_info_t;
int volc_device_register(volc_iot_info_t* info, char** output);
int volc_get_rtc_config(volc_iot_info_t* info, int audio_codec, const char* bot_id, const char* task_id, volc_room_info_t* room_info, const char* params);
char* volc_generate_signature(const char* secret_key, const char* product_key, const char* device_name, int rnd, uint64_t timestamp, int auth_type);
char* volc_generate_signature_ws(const char* secret_key, const char* product_key, const char* device_name, const char* instance_id, int rnd, uint64_t timestamp, int auth_type);
#ifdef __cplusplus
}
#endif
#endif /* __CONV_AI_SRC_VOLC_DEVICE_MANAGER__ */
@@ -0,0 +1,43 @@
// Copyright (2025) Beijing Volcano Engine Technology Ltd.
// SPDX-License-Identifier: Apache-2.0
#ifndef __CONV_AI_NODE_VOLC_RTC_H__
#define __CONV_AI_NODE_VOLC_RTC_H__
#if defined(ENABLE_RTC_MODE)
#include <stdbool.h>
#include "cJSON.h"
#include "VolcEngineRTCLite.h"
#include "base/volc_base.h"
#include "base/volc_device_manager.h"
#ifdef __cplusplus
extern "C" {
#endif
typedef void* volc_rtc_t;
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 rtc);
int volc_rtc_start(volc_rtc_t rtc, const char* bot_id, volc_iot_info_t* iot_info, const char* params);
int volc_rtc_stop(volc_rtc_t rtc);
int volc_rtc_send(volc_rtc_t rtc, const void* data, int size, volc_data_info_t* data_info);
int volc_rtc_interrupt(volc_rtc_t rtc);
int volc_rtc_send_jpg(volc_rtc_t rtc, void* data, int size);
int volc_rtc_send_text_to_agent(volc_rtc_t rtc, const char* text, volc_agent_type_e type, int interrupt_mode);
#endif
#ifdef __cplusplus
}
#endif
#endif /* __CONV_AI_NODE_VOLC_RTC_H__ */
@@ -0,0 +1,643 @@
// Copyright (2025) Beijing Volcano Engine Technology Ltd.
// SPDX-License-Identifier: Apache-2.0
#include "volc_rtc.h"
#include <string.h>
#include <unistd.h>
#include <inttypes.h>
#include "volc_osal.h"
#include "util/volc_list.h"
#include "util/volc_log.h"
#include "util/volc_json.h"
#define MAGIC_CONTROL "ctrl"
#define MAGIC_CONV "conv"
#define MAGIC_LENGTH 4
#define MAGIC_OFFSET 8
const char* interrupt_str = "{\"Command\":\"interrupt\"}";
typedef struct {
bool b_pipeline_started;
bool b_user_joined;
bool b_channel_joined;
bool b_first_keyframe_received;
bool b_fini;
bool b_audio_publish;
bool b_video_publish;
bool b_audio_subscribe;
bool b_video_subscribe;
char* p_appid;
char* p_channel_name;
char* p_user_id;
char* p_remote_user_id;
char* p_token;
void* context;
int audio_codec;
volc_room_info_t info;
volc_msg_cb message_callback;
volc_data_cb data_callback;
byte_rtc_engine_t rtc;
byte_rtc_event_handler_t event_handler;
} rtc_impl_t;
static bool __is_first_keyframe_not_received(rtc_impl_t* rtc, int is_key_frame)
{
return (!rtc->b_first_keyframe_received && !is_key_frame);
}
static int _build_binary_message(const char* magic, const char* message,
uint8_t** out_buf, size_t* out_len) {
size_t magic_len = strlen(magic);
size_t msg_len = strlen(message);
// 分配内存:魔术字 + 4字节长度 + 消息内容
*out_len = magic_len + 4 + msg_len;
*out_buf = (uint8_t*)volc_osal_malloc(*out_len);
if (!*out_buf) {
LOGE("volc_osal_malloc failed");
return -1;
}
// 填充魔术字
memcpy(*out_buf, magic, magic_len);
// 以大端序填充长度
uint8_t* len_ptr = *out_buf + magic_len;
len_ptr[0] = (uint8_t)((msg_len >> 24) & 0xFF); // 最高位字节
len_ptr[1] = (uint8_t)((msg_len >> 16) & 0xFF);
len_ptr[2] = (uint8_t)((msg_len >> 8) & 0xFF);
len_ptr[3] = (uint8_t)(msg_len & 0xFF); // 最低位字节
// 填充消息内容
memcpy(*out_buf + magic_len + 4, message, msg_len);
return 0;
}
static int __rtc_start(rtc_impl_t* rtc, volc_rtc_option_t* option)
{
volc_opt_t* p_opt = NULL;
byte_rtc_room_options_t room_opt = {0};
if (!rtc || !option) {
LOGE("rtc or option is NULL");
return -1;
}
room_opt.auto_publish_audio = rtc->b_audio_publish;
room_opt.auto_publish_video = rtc->b_video_publish;
room_opt.auto_subscribe_audio = rtc->b_audio_subscribe;
room_opt.auto_subscribe_video = rtc->b_video_subscribe;
LOGI("Joining channel: %s, uid: %s, token: %s, vpub: %d, vsub: %d, apub: %d, asub: %d", option->p_channel_name, option->p_uid, option->p_token, (int)room_opt.auto_publish_video, (int)room_opt.auto_subscribe_video, (int)room_opt.auto_publish_audio, (int)room_opt.auto_subscribe_audio);
int ret = byte_rtc_join_room(rtc->rtc, option->p_channel_name, option->p_uid, option->p_token, &room_opt);
if (ret != 0) {
LOGE("Failed to join room: %d", ret);
return ret;
}
rtc->b_pipeline_started = true;
rtc->p_channel_name = strdup(option->p_channel_name);
rtc->p_user_id = strdup(option->p_uid);
return 0;
}
static void __rtc_stop(rtc_impl_t* rtc)
{
if (!rtc) {
LOGE("rtc instance is NULL");
return;
}
if (!rtc->b_pipeline_started) {
LOGI("pipeline not started");
return;
}
int ret = byte_rtc_leave_room(rtc->rtc, rtc->p_channel_name);
if (ret != 0) {
LOGE("Failed to leave room: %d", ret);
return;
}
rtc->b_pipeline_started = false;
HAL_SAFE_FREE(rtc->p_channel_name);
return;
}
static void __send_data_2_user(rtc_impl_t* rtc, const void* data, int data_len, volc_data_info_t* info) {
if (rtc->data_callback) {
rtc->data_callback(rtc->context, data, data_len, info);
}
}
static void _register_message_router(rtc_impl_t* rtc, volc_msg_cb callback)
{
rtc->message_callback = callback;
}
static void _send_message_2_user(rtc_impl_t* rtc, volc_msg_t* msg)
{
if (rtc->message_callback) {
rtc->message_callback(rtc->context, msg);
}
}
static bool _is_target_message(const uint8_t* message, const char* target) {
if (message == NULL || target == NULL) {
return false;
}
// Check if the first 4 bytes match the magic number for "subv"
if (*(const uint32_t*)message != *(const uint32_t*)target) {
return false;
}
return true;
}
static int _on_conversion_status_message_parsed(uint8_t* message, rtc_impl_t* rtc) {
int c = -1;
char* error_reason = NULL;
volc_msg_t msg = { 0 };
cJSON *root = cJSON_Parse((const char*)message);
if (root == NULL) {
return c;
}
volc_json_read_int(root, "Stage.Code", &c);
volc_json_read_string(root, "ErrorInfo.Reason", &error_reason);
if(error_reason != NULL) {
LOGW("ErrorInfo.Reason %s",error_reason);
if(strcmp(error_reason, "preParamCheck:AI license duration quota exceeded") == 0) {
msg.code = VOLC_MSG_QUOTA_EXCEEDED;
msg.data.conv_status = 0;
_send_message_2_user(rtc, &msg);
}
HAL_SAFE_FREE(error_reason);
return -1;
}
if(root != NULL) cJSON_Delete(root);
return c;
}
static void _on_join_channel_success(byte_rtc_engine_t engine, const char* channel, int elapsed_ms, bool rejoin)
{
volc_msg_t msg = {0};
LOGI("join channel success %s elapsed %d ms\n", channel, elapsed_ms);
rtc_impl_t* rtc = (rtc_impl_t*) byte_rtc_get_user_data(engine);
rtc->b_first_keyframe_received = false;
rtc->b_channel_joined = true;
msg.code = VOLC_MSG_CONNECTED;
_send_message_2_user(rtc, &msg);
};
static void _on_user_joined(byte_rtc_engine_t engine, const char* channel, const char* user_name, int elapsed_ms)
{
rtc_impl_t* rtc = (rtc_impl_t*) byte_rtc_get_user_data(engine);
volc_msg_t msg = {0};
LOGI("remote user joined %s:%s elapsed %d ms\n", channel, user_name, elapsed_ms);
rtc->b_user_joined = true;
rtc->p_remote_user_id = strdup(user_name);
msg.code = VOLC_MSG_USER_JOINED;
_send_message_2_user(rtc, &msg);
};
static void _on_user_offline(byte_rtc_engine_t engine, const char* channel, const char* user_name, int reason)
{
rtc_impl_t* rtc = (rtc_impl_t*) byte_rtc_get_user_data(engine);
volc_msg_t msg = {0};
LOGI("remote user offline %s:%s reason %d\n", channel, user_name, reason);
rtc->b_user_joined = false;
msg.code = VOLC_MSG_USER_OFFLINE;
_send_message_2_user(rtc, &msg);
};
static void _on_user_mute_audio(byte_rtc_engine_t engine, const char* channel, const char* user_name, int muted)
{
LOGD("remote user mute audio %s:%s %d\n", channel, user_name, muted);
};
static void _on_user_mute_video(byte_rtc_engine_t engine, const char* channel, const char* user_name, int muted)
{
LOGD("remote user mute video %s:%s %d\n", channel, user_name, muted);
};
static void _on_audio_data(byte_rtc_engine_t engine, const char* channel, const char* user_name, uint16_t sent_ts, audio_data_type_e data_type,
const void* data_ptr, size_t data_len, const uint8_t* extra_info, size_t extra_info_size)
{
volc_data_info_t info = {0};
rtc_impl_t* rtc = (rtc_impl_t*)byte_rtc_get_user_data(engine);
if (NULL == rtc || !rtc->b_pipeline_started || !rtc->b_channel_joined || !rtc->b_user_joined) {
LOGE("pipeline not started or channel not joined or user not joined");
return;
}
info.type = VOLC_DATA_TYPE_AUDIO;
info.info.audio.data_type = (volc_audio_data_type_e) data_type;
// info.info.audio.sent_ts = sent_ts;
__send_data_2_user(rtc, data_ptr, data_len, &info);
};
static void _on_video_data(byte_rtc_engine_t engine, const char* channel, const char* user_name, uint16_t sent_ts, video_data_type_e codec,
int is_key_frame, const void* data_ptr, size_t data_len)
{
volc_data_info_t info = {0};
rtc_impl_t* rtc = (rtc_impl_t*) byte_rtc_get_user_data(engine);
if (NULL == rtc || !rtc->b_pipeline_started || !rtc->b_channel_joined || !rtc->b_user_joined) {
LOGD("pipeline not started or channel not joined or user not joined");
return;
}
if (__is_first_keyframe_not_received(rtc, is_key_frame)) {
LOGD("first keyframe not received, request key frame");
byte_rtc_request_video_key_frame(rtc->rtc, channel, user_name);
return;
}
rtc->b_first_keyframe_received = true;
info.type = VOLC_DATA_TYPE_VIDEO;
info.info.video.data_type = (volc_video_data_type_e) codec;
__send_data_2_user(rtc, data_ptr, data_len, &info);
};
static void _on_channel_error(byte_rtc_engine_t engine, const char* channel, int code, const char* msg)
{
volc_msg_t msg_data = {0};
rtc_impl_t* rtc = (rtc_impl_t*) byte_rtc_get_user_data(engine);
LOGE("channel error %s:%d %s\n", channel, code, msg ? msg : "");
msg_data.code = code;
msg_data.data.msg = (char *)msg;
_send_message_2_user(rtc, &msg_data);
};
static void _on_global_error(byte_rtc_engine_t engine, int code, const char* message)
{
volc_msg_t msg_data = {0};
rtc_impl_t* rtc = (rtc_impl_t*) byte_rtc_get_user_data(engine);
rtc->b_channel_joined = false;
rtc->b_first_keyframe_received = false;
LOGI("global error %d %s\n", code, message ? message : "");
msg_data.code = VOLC_MSG_DISCONNECTED;
_send_message_2_user(rtc, &msg_data);
};
static void _on_key_frame_gen_req(byte_rtc_engine_t engine, const char* channel, const char* user_name)
{
LOGI("remote req key frame %s:%s\n", channel, user_name);
rtc_impl_t* rtc = (rtc_impl_t*) byte_rtc_get_user_data(engine);
volc_msg_t msg_data = {0};
msg_data.code = VOLC_MSG_KEY_FRAME_REQ;
_send_message_2_user(rtc, &msg_data);
};
static void _on_target_bitrate_changed(byte_rtc_engine_t engine, const char* channel, uint32_t target_bps)
{
rtc_impl_t* rtc = (rtc_impl_t*) byte_rtc_get_user_data(engine);
volc_msg_t msg_data = {0};
LOGD("target bitrate changed %s %d bps\n", channel, (int)target_bps);
// TODO: do not send it to user
msg_data.code = VOLC_MSG_TARGET_BITRATE_CHANGED;
msg_data.data.target_bitrate = target_bps;
_send_message_2_user(rtc, &msg_data);
};
static void _on_token_privilege_will_expire(byte_rtc_engine_t engine, const char* token)
{
LOGI("\ntoken privilege will expire %s", token ? token : "");
rtc_impl_t* rtc = (rtc_impl_t*) byte_rtc_get_user_data(engine);
volc_msg_t msg_data = {0};
msg_data.code = VOLC_MSG_TOKEN_EXPIRED;
// TODO: token?
_send_message_2_user(rtc, &msg_data);
};
static void _on_message_received(byte_rtc_engine_t engine, const char* channel_name, const char* src, const uint8_t* message, int size, bool binary)
{
int ret = 0;
volc_msg_t msg = { 0 };
rtc_impl_t* rtc = (rtc_impl_t*) byte_rtc_get_user_data(engine);
volc_data_info_t info = {0};
info.type = VOLC_DATA_TYPE_MESSAGE;
info.info.message.is_binary = binary;
if (NULL == rtc || !rtc->b_pipeline_started || !rtc->b_channel_joined || !rtc->b_user_joined) {
LOGE("pipeline not started or channel not joined or user not joined");
return;
}
if (_is_target_message(message, MAGIC_CONV)) {
ret = _on_conversion_status_message_parsed((uint8_t *)message + MAGIC_OFFSET,rtc);
if(ret != -1){
msg.code = VOLC_MSG_CONV_STATUS;
msg.data.conv_status = ret;
_send_message_2_user(rtc, &msg);
}
return;
}
__send_data_2_user(rtc, message, size, &info);
};
static void _on_message_send_result(byte_rtc_engine_t engine, const char* channel_name, int64_t msgid, int error, const char* extencontent)
{
LOGD("----------------------->MessageSendResult msg id %" PRId64 ", error %d, extencontent %s \n", msgid, error, extencontent ? extencontent : "");
};
static void _on_license_will_expire(byte_rtc_engine_t engine, int daysleft)
{
LOGI("----------------------->license will expire in %d days \n", daysleft);
volc_msg_t msg_data = {0};
rtc_impl_t* rtc = (rtc_impl_t*) byte_rtc_get_user_data(engine);
msg_data.code = VOLC_MSG_LICENSE_EXPIRED;
_send_message_2_user(rtc, &msg_data);
};
static void _on_fini_notify(byte_rtc_engine_t engine)
{
rtc_impl_t* rtc = (rtc_impl_t*) byte_rtc_get_user_data(engine);
rtc->b_fini = true;
}
/*
VOLC_AUDIO_CODEC_TYPE_UNKNOWN = 0,
VOLC_AUDIO_CODEC_TYPE_OPUS = 1,
VOLC_AUDIO_CODEC_TYPE_G722 = 2,
VOLC_AUDIO_CODEC_TYPE_G711A = 3,
VOLC_AUDIO_CODEC_TYPE_PCM = 4,
VOLC_AUDIO_CODEC_TYPE_G711U = 5,
*/
static int __volc_to_rtc_audio_codec(int volc_codec)
{
switch (volc_codec) {
case VOLC_AUDIO_CODEC_TYPE_OPUS:
return AUDIO_CODEC_TYPE_OPUS;
case VOLC_AUDIO_CODEC_TYPE_G722:
return AUDIO_CODEC_TYPE_G722;
case VOLC_AUDIO_CODEC_TYPE_G711A:
return AUDIO_CODEC_TYPE_G711A;
case VOLC_AUDIO_CODEC_TYPE_G711U:
return AUDIO_CODEC_TYPE_G711U;
default:
LOGW("volc audio codec %d not supported, use opus instead", volc_codec);
return AUDIO_CODEC_TYPE_OPUS;
}
}
static int __rtc_init(rtc_impl_t* engine, cJSON* p_config)
{
int ret = 0;
int video_codec = 0;
int log_level = 0;
int params_cnt = 0;
cJSON* p_params = NULL;
byte_rtc_event_handler_t rtc_event_handler = {.on_global_error = _on_global_error,
.on_join_room_success = _on_join_channel_success,
.on_room_error = _on_channel_error,
.on_user_joined = _on_user_joined,
.on_user_offline = _on_user_offline,
.on_user_mute_audio = _on_user_mute_audio,
.on_user_mute_video = _on_user_mute_video,
.on_audio_data = _on_audio_data,
.on_video_data = _on_video_data,
.on_key_frame_gen_req = _on_key_frame_gen_req,
.on_target_bitrate_changed = _on_target_bitrate_changed,
.on_token_privilege_will_expire = _on_token_privilege_will_expire,
.on_message_received = _on_message_received,
.on_message_send_result = _on_message_send_result,
.on_license_expire_warning = _on_license_will_expire,
.on_fini_notify = _on_fini_notify};
ret = volc_json_read_int(p_config, "audio.codec", &engine->audio_codec);
if (ret != 0 || engine->audio_codec < 0 || engine->audio_codec > AUDIO_CODEC_TYPE_G711U) {
LOGE("volc_rtc_create: read audio_codec failed");
return -1;
}
ret = volc_json_read_int(p_config, "video.codec", &video_codec);
if (ret != 0 || video_codec < 0 || video_codec > VIDEO_DATA_TYPE_MJPEG) {
LOGE("volc_rtc_create: read video_codec failed");
return -1;
}
ret = volc_json_read_int(p_config, "log_level", &log_level);
if (ret != 0) {
log_level = BYTE_RTC_LOG_LEVEL_WARN; // default log level
}
ret = volc_json_read_bool(p_config, "audio.publish", &engine->b_audio_publish);
if (ret != 0) {
engine->b_audio_publish = true; // default to publish audio
}
ret = volc_json_read_bool(p_config, "video.publish", &engine->b_video_publish);
if (ret != 0) {
engine->b_video_publish = false; // default to no publish video
}
ret = volc_json_read_bool(p_config, "audio.subscribe", &engine->b_audio_subscribe);
if (ret != 0) {
engine->b_audio_subscribe = true; // default to subscribe audio
}
ret = volc_json_read_bool(p_config, "video.subscribe", &engine->b_video_subscribe);
if (ret != 0) {
engine->b_video_subscribe = false; // default to no subscribe video
}
engine->rtc = byte_rtc_create(engine->p_appid, &rtc_event_handler);
byte_rtc_set_user_data(engine->rtc, (void*) engine);
byte_rtc_set_log_level(engine->rtc, log_level);
ret = volc_json_read_object(p_config, "params", &p_params);
if (0 == ret) {
params_cnt = cJSON_GetArraySize(p_params);
if (params_cnt > 0) {
for (int i = 0; i < params_cnt; i++) {
cJSON* p_param = cJSON_GetArrayItem(p_params, i);
if (p_param && cJSON_IsString(p_param)) {
const char* param_str = cJSON_GetStringValue(p_param);
if (param_str && strlen(param_str) > 0) {
byte_rtc_set_params(engine->rtc, param_str);
LOGI("volc_rtc_create: set param[%d]: %s", i, param_str);
}
}
}
}
cJSON_Delete(p_params);
}
byte_rtc_init(engine->rtc);
byte_rtc_set_audio_codec(engine->rtc, __volc_to_rtc_audio_codec(engine->audio_codec));
byte_rtc_set_video_codec(engine->rtc, video_codec - 1); // -1 for default codec
return 0;
}
volc_rtc_t volc_rtc_create(const char* appid, void* context, cJSON* p_config, volc_msg_cb message_callback, volc_data_cb data_callback)
{
rtc_impl_t* rtc = (rtc_impl_t*) volc_osal_calloc(1, sizeof(rtc_impl_t));
if (!rtc) {
LOGE("volc_rtc_create: malloc rtc failed");
return NULL;
}
rtc->message_callback = message_callback;
rtc->data_callback = data_callback;
rtc->context = context;
rtc->p_appid = strdup(appid);
if (NULL == rtc->p_appid) {
LOGE("malloc appid memory failed");
goto err_out_label;
}
if (__rtc_init(rtc, p_config) != 0) {
LOGE("volc_rtc_create: rtc init failed");
goto err_out_label;
}
LOGD("rtc create success");
return (volc_rtc_t)rtc;
err_out_label:
HAL_SAFE_FREE(rtc->p_appid);
HAL_SAFE_FREE(rtc);
return NULL;
}
void volc_rtc_destroy(volc_rtc_t handle)
{
rtc_impl_t* rtc = (rtc_impl_t*)handle;
if (!rtc) {
LOGE("rtc instance is NULL");
return;
}
__rtc_stop(rtc);
byte_rtc_fini(rtc->rtc);
while (!rtc->b_fini) {
usleep(1000 * 10);
}
byte_rtc_destroy(rtc->rtc);
HAL_SAFE_FREE(rtc->p_channel_name);
HAL_SAFE_FREE(rtc->p_remote_user_id);
HAL_SAFE_FREE(rtc->p_token);
HAL_SAFE_FREE(rtc);
LOGD("rtc destroy success");
}
int volc_rtc_start(volc_rtc_t rtc, const char* bot_id, volc_iot_info_t* iot_info, const char* params) {
int ret = 0;
volc_rtc_option_t *opt = NULL;
rtc_impl_t* rtc_impl = (rtc_impl_t*) rtc;
if (!rtc_impl) {
LOGE("rtc instance is NULL");
return -1;
}
char* task_id = "test";
if (volc_get_rtc_config(iot_info, __volc_to_rtc_audio_codec(rtc_impl->audio_codec), bot_id, task_id, &rtc_impl->info, params)) {
LOGE("get rtc config failed");
return -1;
}
opt = &rtc_impl->info.rtc_opt;
return __rtc_start(rtc_impl, opt);
}
int volc_rtc_stop(volc_rtc_t handle) {
rtc_impl_t* rtc = (rtc_impl_t *)handle;
if (!rtc) {
LOGE("rtc instance is NULL");
return -1;
}
__rtc_stop(rtc);
return 0;
}
int volc_rtc_send(volc_rtc_t handle, const void* data, int size, volc_data_info_t* data_info) {
rtc_impl_t* rtc = (rtc_impl_t *)handle;
audio_frame_info_t audio_info = {0};
video_frame_info_t video_info = {0};
if (NULL == rtc || NULL == data || NULL == data_info) {
LOGE("input args is invalid, rtc(%p), data(%p), data_info(%p)", rtc, data, data_info);
return -1;
}
switch (data_info->type) {
case VOLC_DATA_TYPE_AUDIO: {
audio_info.data_type = (audio_data_type_e) data_info->info.audio.data_type;
byte_rtc_send_audio_data(rtc->rtc, rtc->p_channel_name, data, size, &audio_info);
break;
}
case VOLC_DATA_TYPE_VIDEO: {
video_info.data_type = (video_data_type_e) data_info->info.video.data_type;
video_info.stream_type = VIDEO_STREAM_HIGH;
video_info.frame_type = (video_info.data_type == VIDEO_DATA_TYPE_MJPEG) ? VIDEO_FRAME_KEY : VIDEO_FRAME_AUTO_DETECT;
LOGD("Sending video to channel: %s, data length: %d, data type: %d", rtc->p_channel_name, size, video_info.data_type);
byte_rtc_send_video_data(rtc->rtc, rtc->p_channel_name, data, size, &video_info);
break;
}
case VOLC_DATA_TYPE_MESSAGE: {
LOGD("Sending message to channel: %s, remote user: %s, data length: %d, is_binary: %d",
rtc->p_channel_name, rtc->p_remote_user_id, size, data_info->info.message.is_binary);
byte_rtc_rts_send_message(rtc->rtc, rtc->p_channel_name, rtc->p_remote_user_id, data, size, data_info->info.message.is_binary,
RTS_MESSAGE_RELIABLE);
break;
}
default:
LOGW("unsupported data type: %d", data_info->type);
return -1;
}
return 0;
}
int volc_rtc_interrupt(volc_rtc_t rtc) {
int ret = 0;
uint8_t* msg_ctrl = NULL;
size_t msg_len = 0;
rtc_impl_t* rtc_impl = (rtc_impl_t*) rtc;
volc_data_info_t data_info = {0};
if (!rtc_impl) {
LOGE("rtc instance is NULL");
return -1;
}
if (_build_binary_message(MAGIC_CONTROL, interrupt_str, &msg_ctrl, &msg_len) != 0) {
LOGE("build control message failed");
return -1;
}
data_info.type = VOLC_DATA_TYPE_MESSAGE;
data_info.info.message.is_binary = true;
if ((ret = volc_rtc_send(rtc, msg_ctrl, msg_len, &data_info)) != 0) {
LOGE("send interrupt message failed");
}
HAL_SAFE_FREE(msg_ctrl);
return ret;
}
int volc_rtc_send_text_to_agent(volc_rtc_t rtc, const char* text, volc_agent_type_e type, int interrupt_mode){
int ret = 0;
cJSON * pJsn = cJSON_CreateObject();
uint8_t* msg_ctrl = NULL;
size_t msg_len = 0;
rtc_impl_t* rtc_impl = (rtc_impl_t*) rtc;
volc_data_info_t data_info = {0};
if (!rtc_impl) {
LOGE("rtc instance is NULL");
return -1;
}
if(type == VOLC_AGENT_TYPE_TTS){
cJSON_AddStringToObject(pJsn,"Command","ExternalTextToSpeech");
} else if(type == VOLC_AGENT_TYPE_LLM){
cJSON_AddStringToObject(pJsn,"Command","ExternalTextToLLM");
}
cJSON_AddStringToObject(pJsn,"Message",text);
cJSON_AddNumberToObject(pJsn,"InterruptMode",interrupt_mode);
char* text_str = cJSON_Print(pJsn);
if (_build_binary_message(MAGIC_CONTROL, text_str, &msg_ctrl, &msg_len) != 0) {
LOGE("build control message failed");
return -1;
}
data_info.type = VOLC_DATA_TYPE_MESSAGE;
data_info.info.message.is_binary = true;
if ((ret = volc_rtc_send(rtc, msg_ctrl, msg_len, &data_info)) != 0) {
LOGE("send interrupt message failed");
}
HAL_SAFE_FREE(msg_ctrl);
cJSON_Delete(pJsn);
return ret;
}
int volc_rtc_send_jpg(volc_rtc_t rtc, void* data, int size) {
return 0;
}
@@ -0,0 +1,35 @@
// Copyright (2025) Beijing Volcano Engine Technology Ltd.
// SPDX-License-Identifier: Apache-2.0
#ifndef __CONV_AI_NODE_VOLC_WS_H__
#define __CONV_AI_NODE_VOLC_WS_H__
#include <stdbool.h>
#include "cJSON.h"
#include "base/volc_base.h"
#include "base/volc_device_manager.h"
#ifdef __cplusplus
extern "C" {
#endif
typedef void* volc_ws_t;
volc_ws_t volc_ws_create(void* context, cJSON* p_config, volc_msg_cb message_callback, volc_data_cb data_callback);
void volc_ws_destroy(volc_ws_t ws);
int volc_ws_start(volc_ws_t ws, const char* bot_id, volc_iot_info_t* iot_info, const char* params);
int volc_ws_send(volc_ws_t ws, const void* data, int size, volc_data_info_t* data_info);
int volc_ws_stop(volc_ws_t ws);
int volc_ws_interrupt(volc_ws_t ws);
#ifdef __cplusplus
}
#endif
#endif /* __CONV_AI_NODE_VOLC_WS_H__ */
@@ -0,0 +1,688 @@
// Copyright (2025) Beijing Volcano Engine Technology Ltd.
// SPDX-License-Identifier: Apache-2.0
#include "volc_ws.h"
#include "volc_osal.h"
#include "base/volc_base.h"
#include "util/volc_list.h"
#include "util/volc_log.h"
#include "util/volc_json.h"
#include "util/volc_base64.h"
#include "websocket.h"
#define WS_AIGC_URI "wss://ai-gateway.vei.volces.com"
#define WS_AIGC_PATH "/v1/realtime"
const char* ws_interrupt_str = "{\"type\": \"response.cancel\"}";
typedef struct {
uint8_t opcode;
uint8_t* buffer;
int size;
int capacity;
int in_progress;
} ws_assembler_t;
typedef struct ws_params {
volc_audio_codec_type_e audio_codec_type;
char* session_update_json;
} ws_params_t;
typedef struct {
bool b_pipeline_started;
bool b_connected;
bool b_interrupted;
char* p_data_buf;
char* p_bot_id;
char* p_last_response_id;
char headers[1024];
char uri[256];
char event_id[64];
int event_id_index;
size_t data_buf_size;
void* context;
volatile volc_conv_status_e conv_status;
volc_msg_cb message_callback;
volc_data_cb data_callback;
char hardware_id[32];
ws_params_t params;
ws_assembler_t assembler;
volc_ws_client_t* client;
} ws_impl_t;
static int __ws_init(ws_impl_t* ws, cJSON* p_config)
{
int ret = volc_json_read_int(p_config, "audio.codec", (int*)&ws->params.audio_codec_type);
if (ret != 0) {
ws->params.audio_codec_type = VOLC_AUDIO_CODEC_TYPE_PCM;
}
return 0;
}
static bool __ws_wait_for_session_update(ws_impl_t* ws) {
(void)ws;
return true;
}
static int __build_ws_message(const char* msg, uint8_t** out_buf, size_t* out_len) {
size_t msg_len = strlen(msg) + 1;
*out_len = msg_len - 1;
*out_buf = (uint8_t*)volc_osal_malloc(msg_len);
if (!*out_buf) {
LOGE("volc_osal_malloc failed");
return -1;
}
memcpy(*out_buf, msg, msg_len);
(*out_buf)[msg_len - 1] = 0;
return 0;
}
static void __send_message_2_user(ws_impl_t* ws, volc_msg_t* msg)
{
if (ws->message_callback) {
ws->message_callback(ws->context, msg);
}
}
static void __send_data_2_user(ws_impl_t* ws, const char* data, int data_len, volc_data_info_t* info) {
if (ws->data_callback) {
ws->data_callback(ws->context, (const void*)data, data_len, info);
}
}
static bool __ws_drop_for_interrupted(ws_impl_t* ws, const char* p_response_id) {
if (ws->b_interrupted && p_response_id) {
ws->b_interrupted = false;
if (ws->p_last_response_id) {
volc_osal_free(ws->p_last_response_id);
ws->p_last_response_id = NULL;
}
ws->p_last_response_id = strdup(p_response_id);
LOGI("drop for interrupted, response_id: %s", p_response_id);
}
if (ws->p_last_response_id && p_response_id && strcmp(ws->p_last_response_id, p_response_id) == 0) {
return true;
}
return false;
}
static void __ws_recv_data(ws_impl_t* ws, const char* data, int data_len)
{
cJSON* p_json = NULL;
char* p_type = NULL;
char* p_delta = NULL;
char* p_status = NULL;
void* p_data = NULL;
char* p_response_id = NULL;
size_t len = 0;
volc_data_info_t info = { 0 };
volc_msg_t msg = { 0 };
if (NULL == data || 0 == data_len) {
return;
}
p_json = cJSON_ParseWithLength(data, data_len);
if (!p_json) {
LOGE("Failed to parse json, data_len: %d, data: %s", data_len, data);
return;
}
// LOGI("json: %s", data);
volc_json_read_string(p_json, "type", &p_type);
volc_json_read_string(p_json, "delta", &p_delta);
volc_json_read_string(p_json, "response.status", &p_status);
if (strcmp(p_type, "response.audio.delta") == 0 && p_delta) {
if (strlen(p_delta) == 0) {
LOGE("delta is empty, data: %s", data);
goto err_out_label;
}
if (NULL == ws || !ws->b_pipeline_started) {
LOGD("pipeline not started");
goto err_out_label;
}
volc_json_read_string(p_json, "response_id", &p_response_id);
if (__ws_drop_for_interrupted(ws, p_response_id)) {
goto err_out_label;
}
len = volc_base64_decoded_length((const uint8_t*)p_delta, strlen(p_delta));
p_data = volc_osal_malloc(len + 1);
if (NULL == p_data) {
LOGE("Failed to alloc memory");
goto err_out_label;
}
if (ws->conv_status == VOLC_CONV_STATUS_THINKING) {
ws->conv_status = VOLC_CONV_STATUS_ANSWERING;
msg.code = VOLC_MSG_CONV_STATUS;
msg.data.conv_status = ws->conv_status;
__send_message_2_user(ws, &msg);
}
volc_base64_decode((unsigned char *)p_data, len, &len, (const unsigned char *)p_delta, strlen(p_delta));
info.type = VOLC_DATA_TYPE_AUDIO;
info.info.audio.data_type = VOLC_AUDIO_DATA_TYPE_PCM;
// info.info.audio.sent_ts = volc_get_time(); // TODO
__send_data_2_user(ws, p_data, len, &info);
HAL_SAFE_FREE(p_data);
} else if (strcmp(p_type, "input_audio_buffer.speech_started") == 0) {
ws->conv_status = VOLC_CONV_STATUS_LISTENING;
msg.code = VOLC_MSG_CONV_STATUS;
msg.data.conv_status = VOLC_CONV_STATUS_LISTENING;
__send_message_2_user(ws, &msg);
} else if (strcmp(p_type, "input_audio_buffer.speech_stopped") == 0) {
ws->conv_status = VOLC_CONV_STATUS_THINKING;
msg.code = VOLC_MSG_CONV_STATUS;
msg.data.conv_status = VOLC_CONV_STATUS_THINKING;
__send_message_2_user(ws, &msg);
} else if (strcmp(p_type, "response.done") == 0 && p_status) {
msg.code = VOLC_MSG_CONV_STATUS;
if (strcmp(p_status, "completed") == 0) {
ws->conv_status = VOLC_CONV_STATUS_ANSWER_FINISH;
msg.data.conv_status = VOLC_CONV_STATUS_ANSWER_FINISH;
} else if (strcmp(p_status, "cancelled") == 0) {
ws->conv_status = VOLC_CONV_STATUS_INTERRUPTED;
msg.data.conv_status = VOLC_CONV_STATUS_INTERRUPTED;
}
LOGI("data: %s", data);
__send_message_2_user(ws, &msg);
} else {
if (strcmp(p_type, "session.created") == 0) {
LOGI("%s", data);
} else if (strcmp(p_type, "response.audio_transcript.delta") == 0 || strcmp(p_type, "response.audio_transcript.done") == 0 || strcmp(p_type, "response.audio.done") == 0) {
volc_json_read_string(p_json, "response_id", &p_response_id);
if (__ws_drop_for_interrupted(ws, p_response_id)) {
goto err_out_label;
}
}
info.type = VOLC_DATA_TYPE_MESSAGE;
info.info.message.is_binary = false;
__send_data_2_user(ws, data, data_len, &info);
}
err_out_label:
HAL_SAFE_FREE(p_type);
HAL_SAFE_FREE(p_delta);
HAL_SAFE_FREE(p_status);
HAL_SAFE_FREE(p_response_id);
cJSON_Delete(p_json);
}
static void __ws_assembler_free(ws_assembler_t* a) {
HAL_SAFE_FREE(a->buffer);
a->size = 0;
a->capacity = 0;
a->in_progress = 0;
}
static int __ws_generate_event_id(ws_impl_t* ws) {
if (!ws) {
LOGE("ws instance is NULL");
return -1;
}
memset(ws->event_id, 0, sizeof(ws->event_id));
snprintf(ws->event_id, sizeof(ws->event_id), "event_id_%llu", ws->event_id_index++);
return 0;
}
static char* __ws_audio_codec_to_string(ws_impl_t* ws) {
switch (ws->params.audio_codec_type) {
case VOLC_AUDIO_CODEC_TYPE_OPUS:
return "opus16";
case VOLC_AUDIO_CODEC_TYPE_G722:
return "g722";
case VOLC_AUDIO_CODEC_TYPE_G711A:
return "g711_alaw";
case VOLC_AUDIO_CODEC_TYPE_G711U:
return "g711u_ulaw";
default:
return "pcm16";
}
}
static char* __ws_generate_session_update(ws_impl_t* ws) {
char* json = NULL;
cJSON* root = NULL;
cJSON* session = NULL;
if (!ws) {
LOGE("ws instance is NULL");
return NULL;
}
if (ws->params.session_update_json != NULL) {
return strdup(ws->params.session_update_json);
}
__ws_generate_event_id(ws);
root = cJSON_CreateObject();
cJSON_AddStringToObject(root, "event_id", ws->event_id);
cJSON_AddStringToObject(root, "type", "session.update");
session = cJSON_CreateObject();
cJSON_AddItemToObject(root, "session", session);
cJSON_AddStringToObject(session, "object", "realtime.session");
cJSON_AddStringToObject(session, "model", "");
cJSON_AddStringToObject(session, "input_audio_format", __ws_audio_codec_to_string(ws));
json = cJSON_PrintUnformatted(root);
cJSON_Delete(root);
return json;
}
static void __ws_append_data(ws_impl_t* ws, volc_ws_event_data_t* data) {
int new_capacity = 0;
uint8_t* new_buffer = NULL;
if (data->op_code == VOLC_WS_OPCODES_TEXT || data->op_code == VOLC_WS_OPCODES_BINARY) {
ws->assembler.in_progress = 1;
ws->assembler.opcode = data->op_code;
} else if (data->op_code == VOLC_WS_OPCODES_CONT) {
if (!ws->assembler.in_progress) {
LOGW("append data, but assembler not in progress");
return;
}
} else {
LOGD("append data, skip opcode: %d", data->op_code);
return;
}
if (ws->assembler.in_progress) {
new_capacity = ws->assembler.size + data->data_len + 1;
if (new_capacity > ws->assembler.capacity) {
LOGI("append data, new_capacity: %d", new_capacity);
new_capacity = new_capacity < 1024 ? 1024 : new_capacity * 2;
new_buffer = volc_osal_realloc(ws->assembler.buffer, new_capacity);
if (new_buffer == NULL) {
LOGE("Failed to alloc memory");
__ws_assembler_free(&ws->assembler);
return;
}
ws->assembler.buffer = new_buffer;
ws->assembler.capacity = new_capacity;
}
memcpy(ws->assembler.buffer + ws->assembler.size, data->data_ptr, data->data_len);
ws->assembler.size += data->data_len;
if (data->fin && (data->payload_len == data->payload_offset + data->data_len)) {
LOGD("append data, fin, len: %d", ws->assembler.size);
ws->assembler.buffer[ws->assembler.size] = 0;
__ws_recv_data(ws, (const char*)ws->assembler.buffer, ws->assembler.size);
memset(ws->assembler.buffer, 0, ws->assembler.capacity);
ws->assembler.size = 0;
ws->assembler.in_progress = 0;
}
}
}
static void __ws_stop(ws_impl_t* ws);
static void __ws_event_handler(void* context, int32_t event_id, void* event_data) {
ws_impl_t* ws = (ws_impl_t*)context;
volc_msg_t msg = { 0 };
volc_ws_event_data_t *data = (volc_ws_event_data_t *)event_data;
if (NULL == ws) {
LOGW("ws is NULL");
return;
}
switch (event_id) {
case VOLC_WS_EVENT_CONNECTED:
ws->b_connected = true;
msg.code = VOLC_MSG_CONNECTED;
__send_message_2_user(ws, &msg);
break;
case VOLC_WS_EVENT_DISCONNECTED:
ws->b_connected = false;
msg.code = VOLC_MSG_DISCONNECTED;
__send_message_2_user(ws, &msg);
break;
case VOLC_WS_EVENT_DATA:
__ws_append_data(ws, data);
break;
case VOLC_WS_EVENT_CLOSED:
LOGW("receive close event");
ws->b_connected = false;
msg.code = VOLC_MSG_DISCONNECTED;
__send_message_2_user(ws, &msg);
/* The owner joins this worker; never join from its own callback. */
break;
default:
break;
}
}
static int __ws_send_message(ws_impl_t* ws, const void* data_ptr, size_t data_len) {
if (!ws || !data_ptr || !data_len) {
LOGE("ws or data or info is NULL");
return -1;
}
return volc_ws_client_send_text(ws->client, (const char*)data_ptr, data_len, 1000);
}
static int __ws_start(ws_impl_t* ws, volc_iot_info_t* iot_info)
{
/* The standard-protocol signature requires Unix seconds, not milliseconds. */
uint64_t current_time = volc_osal_get_time_ms() / 1000;
bool b_wait_for_session_update = __ws_wait_for_session_update(ws);
int random_num = (int)current_time;//volc_get_random_num();
char time_str[32] = { 0 };
char num_str[32] = { 0 };
char platform_info[16] = { 0 };
char user_agent[64] = { 0 };
if (!ws || !iot_info) {
LOGE("ws or option is NULL");
return -1;
}
/* This instance is reused across conversations; readiness is per socket. */
__ws_stop(ws);
ws->b_connected = false;
ws->b_interrupted = false;
ws->conv_status = VOLC_CONV_STATUS_ANSWER_FINISH;
HAL_SAFE_FREE(ws->p_last_response_id);
__ws_assembler_free(&ws->assembler);
volc_osal_get_platform_info(platform_info, sizeof(platform_info));
snprintf(user_agent, sizeof(user_agent), "%s(%s)", volc_get_version(), platform_info);
snprintf(time_str, sizeof(time_str), "%llu", current_time);
snprintf(num_str, sizeof(num_str), "%d", random_num);
char* signature = volc_generate_signature_ws(iot_info->device_secret, iot_info->product_key, iot_info->device_name, iot_info->instance_id, random_num, current_time, 1);
volc_ws_config_t ws_cfg = { 0 };
snprintf(ws->uri, sizeof(ws->uri), "%s%s?bot=%s&wait_for_session_update=%s", WS_AIGC_URI, WS_AIGC_PATH, ws->p_bot_id, b_wait_for_session_update ? "true" : "false");
ws_cfg.uri = ws->uri;
ws_cfg.user_agent = user_agent;
snprintf(ws->headers, sizeof(ws->headers),
"X-Signature: %s\r\n"
"X-Auth-Type: 1\r\n"
"X-Device-Name: %s\r\n"
"X-Product-Key: %s\r\n"
"X-Random-Num: %s\r\n"
"X-Timestamp: %s\r\n"
"X-Hardware-Id: %s\r\n"
"X-Instance-Id: %s\r\n", signature, iot_info->device_name, iot_info->product_key, num_str, time_str, ws->hardware_id, iot_info->instance_id);
ws_cfg.headers = ws->headers;
LOGI("WS URL: %s", ws_cfg.uri);
ws_cfg.user_context = ws;
ws_cfg.buffer_size = 1024 * 5;
ws_cfg.ws_event_handler = __ws_event_handler;
ws->client = volc_ws_client_init(&ws_cfg);
if(volc_ws_client_start(ws->client)) {
LOGE("Failed to start websocket client");
__ws_stop(ws);
return -1;
}
if (b_wait_for_session_update) {
uint64_t connect_deadline = volc_osal_get_time_ms() + 15000;
while (!ws->b_connected) {
if (ws->client->exit || volc_osal_get_time_ms() >= connect_deadline) {
LOGE("WebSocket connection did not become ready");
__ws_stop(ws);
return -1;
}
volc_osal_thread_sleep(10);
}
char* session_update = __ws_generate_session_update(ws);
int sent = session_update ? __ws_send_message(ws, session_update, strlen(session_update)) : -1;
bool sent_complete = session_update && sent == (int)strlen(session_update);
HAL_SAFE_FREE(session_update);
if (!sent_complete) {
LOGE("Initial session.update failed; audio remains disabled");
__ws_stop(ws);
return -1;
}
LOGI("Initial session.update sent after WebSocket connected");
}
ws->b_pipeline_started = true;
return 0;
}
static void __ws_stop(ws_impl_t* ws)
{
if (!ws) {
LOGE("ws instance is NULL");
return;
}
ws->b_connected = false;
ws->b_pipeline_started = false;
if (ws->client) volc_ws_client_destroy(ws->client);
ws->client = NULL;
ws->b_pipeline_started = false;
}
static int __ws_input_audio_buffer_append(ws_impl_t* ws, const char* data_ptr, size_t data_len) {
int ret = 0;
cJSON* root = NULL;
if (!ws || !data_ptr || data_len == 0) {
LOGE("ws or data or data_len is NULL");
return -1;
}
root = cJSON_CreateObject();
cJSON_AddStringToObject(root, "type", "input_audio_buffer.append");
cJSON_AddStringToObject(root, "audio", data_ptr);
char* json = cJSON_PrintUnformatted(root);
cJSON_Delete(root);
ret = volc_ws_client_send_text(ws->client, json, strlen(json), 1000);
if (ret >= 0) {
ret = 0;
} else {
LOGW("failed to send audio buffer");
}
HAL_SAFE_FREE(json);
return ret;
}
static int __ws_input_audio_buffer_commit(ws_impl_t* ws) {
int ret = 0;
cJSON* root = NULL;
if (!ws) {
LOGE("ws instance is NULL");
return -1;
}
root = cJSON_CreateObject();
cJSON_AddStringToObject(root, "type", "input_audio_buffer.commit");
char* json = cJSON_PrintUnformatted(root);
LOGD("json: %s", json);
cJSON_Delete(root);
ret = volc_ws_client_send_text(ws->client, json, strlen(json), 1000);
if (ret >= 0) {
ret = 0;
}
HAL_SAFE_FREE(json);
return ret;
}
static int __ws_response_create(ws_impl_t* ws) {
int ret = 0;
cJSON* root = NULL;
if (!ws) {
LOGE("ws instance is NULL");
return -1;
}
root = cJSON_CreateObject();
cJSON_AddStringToObject(root, "type", "response.create");
// 创建 "response" 对象
cJSON* response_obj = cJSON_CreateObject();
cJSON_AddItemToObject(root, "response", response_obj);
// 添加 “modalities” 字段
cJSON* modalities_array = cJSON_CreateArray();
cJSON_AddItemToArray(modalities_array, cJSON_CreateString("text"));
cJSON_AddItemToArray(modalities_array, cJSON_CreateString("audio"));
cJSON_AddItemToObject(response_obj, "modalities", modalities_array);
// 生成 JSON 字符串
const char* json = cJSON_PrintUnformatted(root);
LOGD("json: %s", json);
// 释放内存
cJSON_Delete(root);
ret = volc_ws_client_send_text(ws->client, json, strlen(json), 1000);
HAL_SAFE_FREE(json);
return ret;
}
static int __ws_send_audio(ws_impl_t* ws, const void* data_ptr, size_t data_len, bool commit) {
int ret = 0;
size_t len = 0;
if (!ws || !data_ptr || !data_len) {
LOGE("ws or data or info is NULL");
return -1;
}
len = volc_base64_encoded_length(data_len);
if (ws->data_buf_size < len) {
HAL_SAFE_FREE(ws->p_data_buf);
ws->p_data_buf = (char*)volc_osal_malloc(len);
if (!ws->p_data_buf) {
LOGE("failed to malloc data buf");
ws->data_buf_size = 0;
return -1;
}
ws->data_buf_size = len;
}
volc_base64_encode((unsigned char*) ws->p_data_buf, len, &len, (const unsigned char*) data_ptr, data_len);
if (ret != 0) {
LOGE("failed to encode audio");
return -1;
}
ret = __ws_input_audio_buffer_append(ws, ws->p_data_buf, len);
if (ret != 0) {
LOGE("failed to append audio buffer");
return -1;
}
if (commit) {
ret = __ws_input_audio_buffer_commit(ws);
if (ret != 0) {
LOGE("failed to commit audio buffer");
return -1;
}
ret = __ws_response_create(ws);
}
return ret;
}
static void __ws_parse_params(const char* params, ws_params_t* out_params) {
cJSON* p_json = NULL;
if (!params || !out_params) {
LOGW("params or out_params is NULL");
return;
}
p_json = cJSON_Parse(params);
if (!p_json) {
LOGW("Failed to parse json params: %s", params);
return;
}
volc_json_read_int(p_json, "audio.codec", (int*)&out_params->audio_codec_type);
cJSON* session_update = cJSON_GetObjectItem(p_json, "session_update");
if (cJSON_IsObject(session_update)) {
HAL_SAFE_FREE(out_params->session_update_json);
out_params->session_update_json = cJSON_PrintUnformatted(session_update);
}
cJSON_Delete(p_json);
}
volc_ws_t volc_ws_create(void* context, cJSON* p_config, volc_msg_cb message_callback, volc_data_cb data_callback) {
ws_impl_t* ws = (ws_impl_t*)volc_osal_calloc(1, sizeof(ws_impl_t));
if (!ws) {
LOGE("volc_ws_create: malloc ws failed");
return NULL;
}
ws->message_callback = message_callback;
ws->data_callback = data_callback;
ws->context = context;
ws->conv_status = VOLC_CONV_STATUS_ANSWER_FINISH;
ws->params.audio_codec_type = VOLC_AUDIO_CODEC_TYPE_PCM;
volc_osal_get_uuid(ws->hardware_id, sizeof(ws->hardware_id));
if (__ws_init(ws, p_config) != 0) {
HAL_SAFE_FREE(ws);
LOGE("volc_ws_create: ws init failed");
return NULL;
}
LOGI("ws create success, hardware id: %s", ws->hardware_id);
return (volc_ws_t) ws;
}
void volc_ws_destroy(volc_ws_t ws) {
ws_impl_t* ws_impl = (ws_impl_t*) ws;
if (!ws_impl) {
LOGE("ws instance is NULL");
return;
}
__ws_stop(ws_impl);
HAL_SAFE_FREE(ws_impl->p_data_buf);
HAL_SAFE_FREE(ws_impl->p_bot_id);
HAL_SAFE_FREE(ws_impl->p_last_response_id);
HAL_SAFE_FREE(ws_impl->params.session_update_json);
HAL_SAFE_FREE(ws_impl);
}
int volc_ws_start(volc_ws_t ws, const char* bot_id, volc_iot_info_t* iot_info, const char* params) {
ws_impl_t* ws_impl = (ws_impl_t*) ws;
if (!ws_impl) {
LOGE("ws instance is NULL");
return -1;
}
HAL_SAFE_FREE(ws_impl->p_bot_id);
ws_impl->p_bot_id = strdup(bot_id);
__ws_parse_params(params, &ws_impl->params);
return __ws_start(ws_impl, iot_info);
}
int volc_ws_send(volc_ws_t ws, const void* data, int size, volc_data_info_t* data_info) {
ws_impl_t* ws_impl = (ws_impl_t*) ws;
if (!ws_impl) {
LOGE("ws instance is NULL");
return -1;
}
if (!ws_impl->b_pipeline_started || !ws_impl->b_connected) {
LOGD("pipeline started[%d], connected[%d], cannot process data", (int) ws_impl->b_pipeline_started, (int) ws_impl->b_connected);
return -1;
}
switch (data_info->type) {
case VOLC_DATA_TYPE_AUDIO: {
return __ws_send_audio(ws_impl, data, size, data_info->info.audio.commit);
}
case VOLC_DATA_TYPE_VIDEO: {
LOGW("video data not supported");
break;
}
case VOLC_DATA_TYPE_MESSAGE: {
return __ws_send_message(ws, data, size);
}
default:
LOGW("unsupported data type: %d", data_info->type);
return -1;
}
return 0;
}
int volc_ws_stop(volc_ws_t ws) {
ws_impl_t* ws_impl = (ws_impl_t*) ws;
if (!ws_impl) {
LOGE("ws instance is NULL");
return -1;
}
__ws_stop(ws_impl);
return 0;
}
int volc_ws_interrupt(volc_ws_t ws) {
int ret = 0;
uint8_t* msg = NULL;
size_t msg_len = 0;
ws_impl_t* ws_impl = (ws_impl_t*) ws;
if (!ws_impl) {
LOGE("ws instance is NULL");
return -1;
}
if (ws_impl->conv_status != VOLC_CONV_STATUS_LISTENING && ws_impl->conv_status != VOLC_CONV_STATUS_THINKING &&
ws_impl->conv_status != VOLC_CONV_STATUS_ANSWERING) {
LOGW("interrupt failed, conv status is not listening or thinking");
return -1;
}
if (__build_ws_message(ws_interrupt_str, &msg, &msg_len) != 0) {
LOGE("build control message failed");
return -1;
}
if (ws_impl->conv_status == VOLC_CONV_STATUS_ANSWERING) {
ws_impl->b_interrupted = true;
}
if ((__ws_send_message(ws_impl, msg, msg_len)) <= 0) {
LOGE("send control message failed");
ret = -1;
}
HAL_SAFE_FREE(msg);
return ret;
}
@@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
@@ -0,0 +1,152 @@
#ifndef __WEBSOCKET_H__
#define __WEBSOCKET_H__
#include <stdbool.h>
#ifdef __cplusplus
extern "C" {
#endif
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "volc_osal.h"
#include "tls_client.h"
#define CONFIG_WEBSOCKET_TLS
typedef enum {
VOLC_WS_STATE_ERROR = -1,
VOLC_WS_STATE_UNKNOW = 0,
VOLC_WS_STATE_INIT,
VOLC_WS_STATE_CONNECTED,
VOLC_WS_STATE_WAIT_TIMEOUT,
VOLC_WS_STATE_CLOSING,
} volc_ws_state_e;
typedef enum {
VOLC_WS_OPCODES_CONT = 0x00, // 附加数据帧
VOLC_WS_OPCODES_TEXT = 0x01, // 文本数据帧
VOLC_WS_OPCODES_BINARY = 0x02, // 二进制数据帧
VOLC_WS_OPCODES_CLOSE = 0x08, // 连接关闭
VOLC_WS_OPCODES_PING = 0x09, // 心跳
VOLC_WS_OPCODES_PONG = 0x0a,
VOLC_WS_OPCODES_FIN = 0x80,
VOLC_WS_OPCODES_NONE = 0x100,
} volc_ws_opcode_e;
typedef enum {
VOLC_WS_EVENT_ANY = -1,
VOLC_WS_EVENT_ERROR = 0,
VOLC_WS_EVENT_CONNECTED,
VOLC_WS_EVENT_DISCONNECTED,
VOLC_WS_EVENT_DATA,
VOLC_WS_EVENT_CLOSED,
VOLC_WS_EVENT_MAX
} volc_ws_event_id_e;
typedef struct {
uint8_t opcode;
bool fin;
char mask_key[4];
int payload_len;
int bytes_remaining;
bool header_received;
} volc_ws_frame_state_t;
typedef struct {
char* path;
char* buffer;
char* sub_protocol;
char* user_agent;
char* headers;
bool propagate_control_frames;
volc_ws_frame_state_t frame_state;
} transport_ws_t;
typedef struct {
uint64_t last_print_ms;
int sent;
int received;
} ws_stats_t;
typedef void (*volc_ws_event_handler_t)(void* user_context, int32_t event_id, void* event_data);
typedef struct {
char* host;
char* path;
char* scheme;
char* signature;
int port;
volc_ws_state_e state;
// uint64_t keepalive_tick_ms;
uint64_t reconnect_tick_ms;
uint64_t ping_tick_ms;
uint64_t pingpong_tick_ms;
int auto_reconnect;
volatile bool run;
volatile bool exit;
bool wait_for_pong_resp;
char* rx_buffer;
char* tx_buffer;
int buffer_size;
int rx_retry;
bool last_fin;
volc_ws_opcode_e last_opcode;
int payload_len;
int payload_offset;
transport_ws_t* ws_transport;
int sockfd;
int is_tls;
#if defined(CONFIG_WEBSOCKET_TLS)
MbedTLSSession* ssl;
#endif
void* user_context;
volc_ws_event_handler_t ws_event_handler;
volc_osal_mutex_t mutex;
volc_osal_mutex_t send_mutex;
volc_osal_tid_t tid;
ws_stats_t stats;
} volc_ws_client_t;
typedef struct {
const char* uri;
// bool disable_auto_reconnect;
int rx_retry;
void* user_context;
int buffer_size;
const char* subprotocol;
const char* user_agent;
const char* headers;
// bool disable_pingpong_discon;
volc_ws_event_handler_t ws_event_handler;
} volc_ws_config_t;
/**
* @brief Websocket event data
*/
typedef struct {
char* data_ptr;
int data_len;
bool fin;
uint8_t op_code;
int payload_len;
int payload_offset;
} volc_ws_event_data_t;
volc_ws_client_t* volc_ws_client_init(const volc_ws_config_t* input);
int volc_ws_client_destroy(volc_ws_client_t* client);
#if defined(PLATFORM_MACOS)
void* volc_ws_client_task(void* thread_param);
#else
void volc_ws_client_task(void* thread_param);
#endif
int volc_ws_client_start(volc_ws_client_t* client);
int volc_ws_client_stop(volc_ws_client_t* client);
int volc_ws_client_send_text(volc_ws_client_t* client, const char* data, int len, int timeout);
#ifdef __cplusplus
}
#endif
#endif /* __WEBSOCKET_H__ */
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,293 @@
/*
Copyright (c) 2009-2017 Dave Gamble and cJSON contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
#ifndef cJSON__h
#define cJSON__h
#ifdef __cplusplus
extern "C"
{
#endif
#if !defined(__WINDOWS__) && (defined(WIN32) || defined(WIN64) || defined(_MSC_VER) || defined(_WIN32))
#define __WINDOWS__
#endif
#ifdef __WINDOWS__
/* When compiling for windows, we specify a specific calling convention to avoid issues where we are being called from a project with a different default calling convention. For windows you have 3 define options:
CJSON_HIDE_SYMBOLS - Define this in the case where you don't want to ever dllexport symbols
CJSON_EXPORT_SYMBOLS - Define this on library build when you want to dllexport symbols (default)
CJSON_IMPORT_SYMBOLS - Define this if you want to dllimport symbol
For *nix builds that support visibility attribute, you can define similar behavior by
setting default visibility to hidden by adding
-fvisibility=hidden (for gcc)
or
-xldscope=hidden (for sun cc)
to CFLAGS
then using the CJSON_API_VISIBILITY flag to "export" the same symbols the way CJSON_EXPORT_SYMBOLS does
*/
#define CJSON_CDECL __cdecl
#define CJSON_STDCALL __stdcall
/* export symbols by default, this is necessary for copy pasting the C and header file */
#if !defined(CJSON_HIDE_SYMBOLS) && !defined(CJSON_IMPORT_SYMBOLS) && !defined(CJSON_EXPORT_SYMBOLS)
#define CJSON_EXPORT_SYMBOLS
#endif
#if defined(CJSON_HIDE_SYMBOLS)
#define CJSON_PUBLIC(type) type CJSON_STDCALL
#elif defined(CJSON_EXPORT_SYMBOLS)
#define CJSON_PUBLIC(type) __declspec(dllexport) type CJSON_STDCALL
#elif defined(CJSON_IMPORT_SYMBOLS)
#define CJSON_PUBLIC(type) __declspec(dllimport) type CJSON_STDCALL
#endif
#else /* !__WINDOWS__ */
#define CJSON_CDECL
#define CJSON_STDCALL
#if (defined(__GNUC__) || defined(__SUNPRO_CC) || defined (__SUNPRO_C)) && defined(CJSON_API_VISIBILITY)
#define CJSON_PUBLIC(type) __attribute__((visibility("default"))) type
#else
#define CJSON_PUBLIC(type) type
#endif
#endif
/* project version */
#define CJSON_VERSION_MAJOR 1
#define CJSON_VERSION_MINOR 7
#define CJSON_VERSION_PATCH 13
#include <stddef.h>
/* cJSON Types: */
#define cJSON_Invalid (0)
#define cJSON_False (1 << 0)
#define cJSON_True (1 << 1)
#define cJSON_NULL (1 << 2)
#define cJSON_Number (1 << 3)
#define cJSON_String (1 << 4)
#define cJSON_Array (1 << 5)
#define cJSON_Object (1 << 6)
#define cJSON_Raw (1 << 7) /* raw json */
#define cJSON_IsReference 256
#define cJSON_StringIsConst 512
/* The cJSON structure: */
typedef struct cJSON
{
/* next/prev allow you to walk array/object chains. Alternatively, use GetArraySize/GetArrayItem/GetObjectItem */
struct cJSON *next;
struct cJSON *prev;
/* An array or object item will have a child pointer pointing to a chain of the items in the array/object. */
struct cJSON *child;
/* The type of the item, as above. */
int type;
/* The item's string, if type==cJSON_String and type == cJSON_Raw */
char *valuestring;
/* writing to valueint is DEPRECATED, use cJSON_SetNumberValue instead */
int valueint;
/* The item's number, if type==cJSON_Number */
double valuedouble;
/* The item's name string, if this item is the child of, or is in the list of subitems of an object. */
char *string;
} cJSON;
typedef struct cJSON_Hooks
{
/* malloc/free are CDECL on Windows regardless of the default calling convention of the compiler, so ensure the hooks allow passing those functions directly. */
void *(CJSON_CDECL *malloc_fn)(size_t sz);
void (CJSON_CDECL *free_fn)(void *ptr);
} cJSON_Hooks;
typedef int cJSON_bool;
/* Limits how deeply nested arrays/objects can be before cJSON rejects to parse them.
* This is to prevent stack overflows. */
#ifndef CJSON_NESTING_LIMIT
#define CJSON_NESTING_LIMIT 1000
#endif
/* returns the version of cJSON as a string */
CJSON_PUBLIC(const char*) cJSON_Version(void);
/* Supply malloc, realloc and free functions to cJSON */
CJSON_PUBLIC(void) cJSON_InitHooks(cJSON_Hooks* hooks);
/* Memory Management: the caller is always responsible to free the results from all variants of cJSON_Parse (with cJSON_Delete) and cJSON_Print (with stdlib free, cJSON_Hooks.free_fn, or cJSON_free as appropriate). The exception is cJSON_PrintPreallocated, where the caller has full responsibility of the buffer. */
/* Supply a block of JSON, and this returns a cJSON object you can interrogate. */
CJSON_PUBLIC(cJSON *) cJSON_Parse(const char *value);
CJSON_PUBLIC(cJSON *) cJSON_ParseWithLength(const char *value, size_t buffer_length);
/* ParseWithOpts allows you to require (and check) that the JSON is null terminated, and to retrieve the pointer to the final byte parsed. */
/* If you supply a ptr in return_parse_end and parsing fails, then return_parse_end will contain a pointer to the error so will match cJSON_GetErrorPtr(). */
CJSON_PUBLIC(cJSON *) cJSON_ParseWithOpts(const char *value, const char **return_parse_end, cJSON_bool require_null_terminated);
CJSON_PUBLIC(cJSON *) cJSON_ParseWithLengthOpts(const char *value, size_t buffer_length, const char **return_parse_end, cJSON_bool require_null_terminated);
/* Render a cJSON entity to text for transfer/storage. */
CJSON_PUBLIC(char *) cJSON_Print(const cJSON *item);
/* Render a cJSON entity to text for transfer/storage without any formatting. */
CJSON_PUBLIC(char *) cJSON_PrintUnformatted(const cJSON *item);
/* Render a cJSON entity to text using a buffered strategy. prebuffer is a guess at the final size. guessing well reduces reallocation. fmt=0 gives unformatted, =1 gives formatted */
CJSON_PUBLIC(char *) cJSON_PrintBuffered(const cJSON *item, int prebuffer, cJSON_bool fmt);
/* Render a cJSON entity to text using a buffer already allocated in memory with given length. Returns 1 on success and 0 on failure. */
/* NOTE: cJSON is not always 100% accurate in estimating how much memory it will use, so to be safe allocate 5 bytes more than you actually need */
CJSON_PUBLIC(cJSON_bool) cJSON_PrintPreallocated(cJSON *item, char *buffer, const int length, const cJSON_bool format);
/* Delete a cJSON entity and all subentities. */
CJSON_PUBLIC(void) cJSON_Delete(cJSON *item);
/* Returns the number of items in an array (or object). */
CJSON_PUBLIC(int) cJSON_GetArraySize(const cJSON *array);
/* Retrieve item number "index" from array "array". Returns NULL if unsuccessful. */
CJSON_PUBLIC(cJSON *) cJSON_GetArrayItem(const cJSON *array, int index);
/* Get item "string" from object. Case insensitive. */
CJSON_PUBLIC(cJSON *) cJSON_GetObjectItem(const cJSON * const object, const char * const string);
CJSON_PUBLIC(cJSON *) cJSON_GetObjectItemCaseSensitive(const cJSON * const object, const char * const string);
CJSON_PUBLIC(cJSON_bool) cJSON_HasObjectItem(const cJSON *object, const char *string);
/* For analysing failed parses. This returns a pointer to the parse error. You'll probably need to look a few chars back to make sense of it. Defined when cJSON_Parse() returns 0. 0 when cJSON_Parse() succeeds. */
CJSON_PUBLIC(const char *) cJSON_GetErrorPtr(void);
/* Check item type and return its value */
CJSON_PUBLIC(char *) cJSON_GetStringValue(cJSON *item);
CJSON_PUBLIC(double) cJSON_GetNumberValue(cJSON *item);
/* These functions check the type of an item */
CJSON_PUBLIC(cJSON_bool) cJSON_IsInvalid(const cJSON * const item);
CJSON_PUBLIC(cJSON_bool) cJSON_IsFalse(const cJSON * const item);
CJSON_PUBLIC(cJSON_bool) cJSON_IsTrue(const cJSON * const item);
CJSON_PUBLIC(cJSON_bool) cJSON_IsBool(const cJSON * const item);
CJSON_PUBLIC(cJSON_bool) cJSON_IsNull(const cJSON * const item);
CJSON_PUBLIC(cJSON_bool) cJSON_IsNumber(const cJSON * const item);
CJSON_PUBLIC(cJSON_bool) cJSON_IsString(const cJSON * const item);
CJSON_PUBLIC(cJSON_bool) cJSON_IsArray(const cJSON * const item);
CJSON_PUBLIC(cJSON_bool) cJSON_IsObject(const cJSON * const item);
CJSON_PUBLIC(cJSON_bool) cJSON_IsRaw(const cJSON * const item);
/* These calls create a cJSON item of the appropriate type. */
CJSON_PUBLIC(cJSON *) cJSON_CreateNull(void);
CJSON_PUBLIC(cJSON *) cJSON_CreateTrue(void);
CJSON_PUBLIC(cJSON *) cJSON_CreateFalse(void);
CJSON_PUBLIC(cJSON *) cJSON_CreateBool(cJSON_bool boolean);
CJSON_PUBLIC(cJSON *) cJSON_CreateNumber(double num);
CJSON_PUBLIC(cJSON *) cJSON_CreateString(const char *string);
/* raw json */
CJSON_PUBLIC(cJSON *) cJSON_CreateRaw(const char *raw);
CJSON_PUBLIC(cJSON *) cJSON_CreateArray(void);
CJSON_PUBLIC(cJSON *) cJSON_CreateObject(void);
/* Create a string where valuestring references a string so
* it will not be freed by cJSON_Delete */
CJSON_PUBLIC(cJSON *) cJSON_CreateStringReference(const char *string);
/* Create an object/array that only references it's elements so
* they will not be freed by cJSON_Delete */
CJSON_PUBLIC(cJSON *) cJSON_CreateObjectReference(const cJSON *child);
CJSON_PUBLIC(cJSON *) cJSON_CreateArrayReference(const cJSON *child);
/* These utilities create an Array of count items.
* The parameter count cannot be greater than the number of elements in the number array, otherwise array access will be out of bounds.*/
CJSON_PUBLIC(cJSON *) cJSON_CreateIntArray(const int *numbers, int count);
CJSON_PUBLIC(cJSON *) cJSON_CreateFloatArray(const float *numbers, int count);
CJSON_PUBLIC(cJSON *) cJSON_CreateDoubleArray(const double *numbers, int count);
CJSON_PUBLIC(cJSON *) cJSON_CreateStringArray(const char *const *strings, int count);
/* Append item to the specified array/object. */
CJSON_PUBLIC(cJSON_bool) cJSON_AddItemToArray(cJSON *array, cJSON *item);
CJSON_PUBLIC(cJSON_bool) cJSON_AddItemToObject(cJSON *object, const char *string, cJSON *item);
/* Use this when string is definitely const (i.e. a literal, or as good as), and will definitely survive the cJSON object.
* WARNING: When this function was used, make sure to always check that (item->type & cJSON_StringIsConst) is zero before
* writing to `item->string` */
CJSON_PUBLIC(cJSON_bool) cJSON_AddItemToObjectCS(cJSON *object, const char *string, cJSON *item);
/* Append reference to item to the specified array/object. Use this when you want to add an existing cJSON to a new cJSON, but don't want to corrupt your existing cJSON. */
CJSON_PUBLIC(cJSON_bool) cJSON_AddItemReferenceToArray(cJSON *array, cJSON *item);
CJSON_PUBLIC(cJSON_bool) cJSON_AddItemReferenceToObject(cJSON *object, const char *string, cJSON *item);
/* Remove/Detach items from Arrays/Objects. */
CJSON_PUBLIC(cJSON *) cJSON_DetachItemViaPointer(cJSON *parent, cJSON * const item);
CJSON_PUBLIC(cJSON *) cJSON_DetachItemFromArray(cJSON *array, int which);
CJSON_PUBLIC(void) cJSON_DeleteItemFromArray(cJSON *array, int which);
CJSON_PUBLIC(cJSON *) cJSON_DetachItemFromObject(cJSON *object, const char *string);
CJSON_PUBLIC(cJSON *) cJSON_DetachItemFromObjectCaseSensitive(cJSON *object, const char *string);
CJSON_PUBLIC(void) cJSON_DeleteItemFromObject(cJSON *object, const char *string);
CJSON_PUBLIC(void) cJSON_DeleteItemFromObjectCaseSensitive(cJSON *object, const char *string);
/* Update array items. */
CJSON_PUBLIC(cJSON_bool) cJSON_InsertItemInArray(cJSON *array, int which, cJSON *newitem); /* Shifts pre-existing items to the right. */
CJSON_PUBLIC(cJSON_bool) cJSON_ReplaceItemViaPointer(cJSON * const parent, cJSON * const item, cJSON * replacement);
CJSON_PUBLIC(cJSON_bool) cJSON_ReplaceItemInArray(cJSON *array, int which, cJSON *newitem);
CJSON_PUBLIC(cJSON_bool) cJSON_ReplaceItemInObject(cJSON *object,const char *string,cJSON *newitem);
CJSON_PUBLIC(cJSON_bool) cJSON_ReplaceItemInObjectCaseSensitive(cJSON *object,const char *string,cJSON *newitem);
/* Duplicate a cJSON item */
CJSON_PUBLIC(cJSON *) cJSON_Duplicate(const cJSON *item, cJSON_bool recurse);
/* Duplicate will create a new, identical cJSON item to the one you pass, in new memory that will
* need to be released. With recurse!=0, it will duplicate any children connected to the item.
* The item->next and ->prev pointers are always zero on return from Duplicate. */
/* Recursively compare two cJSON items for equality. If either a or b is NULL or invalid, they will be considered unequal.
* case_sensitive determines if object keys are treated case sensitive (1) or case insensitive (0) */
CJSON_PUBLIC(cJSON_bool) cJSON_Compare(const cJSON * const a, const cJSON * const b, const cJSON_bool case_sensitive);
/* Minify a strings, remove blank characters(such as ' ', '\t', '\r', '\n') from strings.
* The input pointer json cannot point to a read-only address area, such as a string constant,
* but should point to a readable and writable adress area. */
CJSON_PUBLIC(void) cJSON_Minify(char *json);
/* Helper functions for creating and adding items to an object at the same time.
* They return the added item or NULL on failure. */
CJSON_PUBLIC(cJSON*) cJSON_AddNullToObject(cJSON * const object, const char * const name);
CJSON_PUBLIC(cJSON*) cJSON_AddTrueToObject(cJSON * const object, const char * const name);
CJSON_PUBLIC(cJSON*) cJSON_AddFalseToObject(cJSON * const object, const char * const name);
CJSON_PUBLIC(cJSON*) cJSON_AddBoolToObject(cJSON * const object, const char * const name, const cJSON_bool boolean);
CJSON_PUBLIC(cJSON*) cJSON_AddNumberToObject(cJSON * const object, const char * const name, const double number);
CJSON_PUBLIC(cJSON*) cJSON_AddStringToObject(cJSON * const object, const char * const name, const char * const string);
CJSON_PUBLIC(cJSON*) cJSON_AddRawToObject(cJSON * const object, const char * const name, const char * const raw);
CJSON_PUBLIC(cJSON*) cJSON_AddObjectToObject(cJSON * const object, const char * const name);
CJSON_PUBLIC(cJSON*) cJSON_AddArrayToObject(cJSON * const object, const char * const name);
/* When assigning an integer value, it needs to be propagated to valuedouble too. */
#define cJSON_SetIntValue(object, number) ((object) ? (object)->valueint = (object)->valuedouble = (number) : (number))
/* helper for the cJSON_SetNumberValue macro */
CJSON_PUBLIC(double) cJSON_SetNumberHelper(cJSON *object, double number);
#define cJSON_SetNumberValue(object, number) ((object != NULL) ? cJSON_SetNumberHelper(object, (double)number) : (number))
/* Change the valuestring of a cJSON_String object, only takes effect when type of object is cJSON_String */
CJSON_PUBLIC(char*) cJSON_SetValuestring(cJSON *object, const char *valuestring);
/* Macro for iterating over an array or object */
#define cJSON_ArrayForEach(element, array) for(element = (array != NULL) ? (array)->child : NULL; element != NULL; element = element->next)
/* malloc/free objects using the malloc/free functions that have been set with cJSON_InitHooks */
CJSON_PUBLIC(void *) cJSON_malloc(size_t size);
CJSON_PUBLIC(void) cJSON_free(void *object);
#ifdef __cplusplus
}
#endif
#endif
@@ -0,0 +1,118 @@
// Copyright (2025) Beijing Volcano Engine Technology Ltd.
// SPDX-License-Identifier: Apache-2.0
#include "util/volc_auth.h"
#include <stdlib.h>
#include <string.h>
#include <mbedtls/ssl.h>
#include <mbedtls/entropy.h>
#include <mbedtls/ctr_drbg.h>
#include <mbedtls/error.h>
#include <mbedtls/sha256.h>
#include <mbedtls/md5.h>
#include <mbedtls/base64.h>
#include "volc_osal.h"
#include "util/volc_base64.h"
#include "util/volc_list.h"
#include "util/volc_log.h"
typedef bool(predicate_func)(unsigned char c);
static bool _url_trim_pred(unsigned char ch) {
if (ch < 32) {
return true;
}
switch (ch) {
case 0x20: /* ' ' - space */
case 0x09: /* '\t' - horizontal tab */
case 0x0A: /* '\n' - line feed */
case 0x0B: /* '\v' - vertical tab */
case 0x0C: /* '\f' - form feed */
case 0x0D: /* '\r' - carriage return */
case '\a': /* '\r' - carriage return */
return true;
default:
return false;
}
}
static int _right_trim_pred(unsigned char* str, predicate_func* pred) {
int len = strlen((const char*)str);
while(len > 0 && pred(str[len - 1])) {
--len;
}
str[len] = '\0';
return len;
}
static int __aes_cbc(bool encrypt, const unsigned char* key, unsigned char* iv, const unsigned char* input, uint32_t ilen, int key_bits, unsigned char* output) {
int mode = encrypt ? MBEDTLS_AES_ENCRYPT : MBEDTLS_AES_DECRYPT;
mbedtls_aes_context aes_ctx;
mbedtls_aes_init(&aes_ctx);
mbedtls_aes_setkey_dec(&aes_ctx, key, key_bits);
mbedtls_aes_crypt_cbc(&aes_ctx, mode, ilen, iv, (const unsigned char *)input, output);
mbedtls_aes_free(&aes_ctx);
return 0;
}
#define AES_KEY_SIZE 16 // AES key size in bytes
#define AES_KEY_SIZE_24 24
char* volc_aes_decode(const char* secret, const char* input, const bool partial_secret) {
size_t decoded_len = 0;
size_t secret_len = 0;
int key_bits = 0;
char* key = NULL;
unsigned char iv[16] = {0};
unsigned char* decoded_payload = NULL;
if (input == NULL || strlen(input) == 0) {
LOGE("Input is NULL or empty");
return NULL;
}
if (partial_secret) {
// If partial secret is used, we only use the first 8 bytes of the key and iv
key = (char *)volc_osal_calloc(AES_KEY_SIZE, 1);
memcpy(key, secret, AES_KEY_SIZE);
key_bits = 128; // AES-128
} else {
secret_len = strlen(secret);
if (secret_len < AES_KEY_SIZE_24) {
LOGE("Secret length is less than 24, secret: %s", secret);
return NULL;
}
key = (char *)volc_osal_calloc(AES_KEY_SIZE_24, 1); // +1 for null terminator
memcpy(key, secret, AES_KEY_SIZE_24);
key_bits = 192; // AES-192
}
decoded_len = volc_base64_decoded_length((const uint8_t*)input, strlen(input));
decoded_payload = (unsigned char *)volc_osal_calloc(decoded_len + 1, 1);
if (!decoded_payload) {
LOGE("Failed to allocate memory for decoded payload");
return NULL;
}
volc_base64_decode((unsigned char *)decoded_payload, decoded_len, &decoded_len, (const unsigned char *)input, strlen(input));
LOGV("decoded payload: %s, len: %zu", decoded_payload, decoded_len);
uint8_t *output = volc_osal_calloc(decoded_len + 16, 1); // +1 for null terminator
memcpy(iv, secret, AES_KEY_SIZE); // Use the same secret for IV
__aes_cbc(false, (const unsigned char *)key, (unsigned char *)iv, (const unsigned char *)decoded_payload, decoded_len, key_bits, output);
_right_trim_pred(output, _url_trim_pred);
LOGV("trimmed decrypted payload: %s, len: %d", output, (int)strlen((const char*)output));
HAL_SAFE_FREE(decoded_payload);
HAL_SAFE_FREE(key);
return (char *)output;
}
void volc_sha256_hmac(const unsigned char* key, int keylen, const unsigned char* input, int ilen, unsigned char* output, int* plen) {
mbedtls_md_hmac(mbedtls_md_info_from_type(MBEDTLS_MD_SHA256), key, keylen, input, ilen, output);
*(plen) = mbedtls_md_get_size(mbedtls_md_info_from_type(MBEDTLS_MD_SHA256));
}
@@ -0,0 +1,19 @@
// Copyright (2025) Beijing Volcano Engine Technology Ltd.
// SPDX-License-Identifier: Apache-2.0
#ifndef __CONV_AI_SRC_UTIL_VOLC_AUTH_H__
#define __CONV_AI_SRC_UTIL_VOLC_AUTH_H__
#include <stdbool.h>
#ifdef __cplusplus
extern "C" {
#endif
char* volc_aes_decode(const char* secret, const char* input, const bool partial_secret);
void volc_sha256_hmac(const unsigned char* key, int keylen, const unsigned char* input, int ilen, unsigned char* output, int* plen);
#ifdef __cplusplus
}
#endif
#endif /* __CONV_AI_SRC_UTIL_VOLC_AUTH_H__ */
@@ -0,0 +1,34 @@
// Copyright (2025) Beijing Volcano Engine Technology Ltd.
// SPDX-License-Identifier: Apache-2.0
#include "util/volc_base64.h"
#include <mbedtls/base64.h>
int volc_base64_encoded_length(int len) {
return (len + 2) / 3 * 4 + 1; // +1 for null terminator
}
int volc_base64_decoded_length(const uint8_t* to_decode, int len) {
int padding = 0;
if (len >= 2 && to_decode[len - 1] == '=' && to_decode[len - 2] == '=') { /*last two chars are = */
padding = 2;
} else if (to_decode[len - 1] == '=') { /*last char is = */
padding = 1;
}
return (len * 3) / 4 - padding; // Calculate the decoded length
}
void volc_base64_encode(unsigned char *dst, size_t dlen, size_t* olen, const unsigned char *src, size_t slen) {
int ret = mbedtls_base64_encode(dst, dlen, olen, src, slen);
if (ret != 0) {
*olen = 0; // If encoding fails, set output length to 0
}
}
void volc_base64_decode(unsigned char *dst, size_t dlen, size_t* olen, const unsigned char *src, size_t slen) {
int ret = mbedtls_base64_decode(dst, dlen, olen, src, slen);
if (ret != 0) {
*olen = 0; // If decoding fails, set output length to 0
}
}
@@ -0,0 +1,24 @@
// Copyright (2025) Beijing Volcano Engine Technology Ltd.
// SPDX-License-Identifier: Apache-2.0
#ifndef __CONV_AI_SRC_UTIL_VOLC_BASE64_H__
#define __CONV_AI_SRC_UTIL_VOLC_BASE64_H__
#include <stdint.h>
#include <stddef.h>
#ifdef __cplusplus
extern "C" {
#endif
int volc_base64_encoded_length(int len);
int volc_base64_decoded_length(const uint8_t* to_decode, int len);
void volc_base64_encode(unsigned char *dst, size_t dlen, size_t* olen, const unsigned char *src, size_t slen);
void volc_base64_decode(unsigned char *dst, size_t dlen, size_t* olen, const unsigned char *src, size_t slen);
#ifdef __cplusplus
}
#endif
#endif /* __CONV_AI_SRC_UTIL_VOLC_BASE64_H__ */
@@ -0,0 +1,40 @@
// Copyright (2025) Beijing Volcano Engine Technology Ltd.
// SPDX-License-Identifier: Apache-2.0
#include "volc_http.h"
#include "webclient.h"
#include "tls_certificate.h"
#include "util/volc_list.h"
#include "util/volc_log.h"
char* volc_http_post(const char* uri, const char* post_data, int data_len)
{
struct webclient_session* session = NULL;
char* buffer = NULL;
int resp_status;
size_t res_len = 0;
/* create webclient session and set header response size */
session = webclient_session_create(2048, GLOBAL_ROOT_CERT, GLOBAL_ROOT_CERT_LEN);
if (session == NULL) {
goto err_out_label;
}
webclient_header_fields_add(session, "Content-Type: application/json\r\n");
webclient_header_fields_add(session, "Content-Length: %d\r\n", strlen(post_data));
/* send POST request by default header */
if ((resp_status = webclient_post(session, uri, post_data, data_len)) != 200) {
LOGE("webclient POST request failed, response(%d) error.\n", resp_status);
}
webclient_response(session, (void**) &buffer, &res_len);
LOGD("url: %s, request: %s, response: %s", uri, post_data, buffer);
err_out_label:
if (session) {
webclient_close(session);
}
return buffer;
}
@@ -0,0 +1,16 @@
// Copyright (2025) Beijing Volcano Engine Technology Ltd.
// SPDX-License-Identifier: Apache-2.0
#ifndef __CONV_AI_SRC_UTIL_VOLC_HTTP_H__
#define __CONV_AI_SRC_UTIL_VOLC_HTTP_H__
#ifdef __cplusplus
extern "C" {
#endif
char* volc_http_post(const char* uri, const char* post_data, int data_len);
#ifdef __cplusplus
}
#endif
#endif /* __CONV_AI_SRC_UTIL_VOLC_HTTP_H__ */
@@ -0,0 +1,208 @@
// Copyright (2025) Beijing Volcano Engine Technology Ltd.
// SPDX-License-Identifier: Apache-2.0
#include "volc_json.h"
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
#include "volc_osal.h"
#include "util/volc_list.h"
#include "util/volc_log.h"
static int _get_array_index(char *fmt)
{
char *start, *end;
int num;
start = strchr(fmt, '[');
if (NULL != start) {
end = strchr(fmt, ']');
if (NULL != end) {
*start = *end = '\0';
start++;
num = atoi(start);
if (num >= 0) {
return num;
}
}
}
return -1;
}
static cJSON *_read_item_object(cJSON *root, const char *fmt)
{
cJSON *parent = root;
cJSON *child = NULL;
int fmt_size = strlen(fmt) + 1;
char *buf = (char *)volc_osal_malloc(fmt_size);
char *buf_p = buf;
int offset = 0;
int arr_index;
int i = 0;
if (NULL == buf) {
LOGE("malloc failed !");
return NULL;
}
/* 1. split */
memcpy(buf, fmt, fmt_size);
for (; i < fmt_size; i++) {
if ('.' == buf[i]) {
buf[i] = '\0';
}
}
/* 2. parse */
while (fmt_size > 0) {
offset = strlen(buf) + 1;
arr_index = _get_array_index(buf);
child = cJSON_GetObjectItem(parent, buf);
if (NULL == child) {
goto L_ERROR;
}
if (arr_index >= 0) {
child = cJSON_GetArrayItem(child, arr_index);
if (NULL == child) {
goto L_ERROR;
}
}
parent = child;
buf = buf + offset;
fmt_size -= offset;
}
L_ERROR:
HAL_SAFE_FREE(buf_p);
return child;
}
static int _read_item_int(cJSON *root, const char *fmt, int *dst)
{
if (NULL == root || NULL == fmt) {
LOGW("invalid input root %p fmt %p", root, fmt);
return -1;
}
cJSON *obj = _read_item_object(root, fmt);
if (!cJSON_IsNumber(obj)) {
LOGD("the value of the key(%s) is not the INT type", fmt);
return -1;
}
if (NULL != dst) {
*dst = (int)cJSON_GetNumberValue(obj);
}
return 0;
}
static int _read_item_double(cJSON *root, const char *fmt, double *dst)
{
if (NULL == root || NULL == fmt) {
LOGW("invalid input root %p fmt %p", root, fmt);
return -1;
}
cJSON *obj = _read_item_object(root, fmt);
if (!cJSON_IsNumber(obj)) {
LOGD("the value of the key(%s) is not the DOUBLE type", fmt);
return -1;
}
if (NULL != dst) {
*dst = obj->valuedouble;
}
return 0;
}
static int _read_item_string(cJSON *root, const char *fmt, char **dst)
{
if (NULL == root || NULL == fmt) {
LOGW("invalid input root %p fmt %p", root, fmt);
return -1;
}
cJSON *obj = _read_item_object(root, fmt);
if (!cJSON_IsString(obj)) {
LOGD("the value of the key(%s) is not the STRING type", fmt);
return -1;
}
if (NULL != dst) {
*dst = (char *)volc_osal_malloc(strlen(obj->valuestring) + 1);
if (NULL == *dst) {
LOGE("memory alloc failed");
return -1;
}
strncpy(*dst, obj->valuestring, strlen(obj->valuestring));
(*dst)[strlen(obj->valuestring)] = '\0';
}
return 0;
}
static int _read_item_bool(cJSON *root, const char *fmt, bool *dst)
{
if (!root || !fmt) {
LOGW("invalid arguments, root: %p, fmt: %p", root, fmt);
return -1;
}
cJSON *obj = _read_item_object(root, fmt);
if (!cJSON_IsBool(obj)) {
LOGW("the value of the key(%s) is not the BOOL type", fmt);
return -1;
}
if (NULL != dst) {
*dst = cJSON_IsTrue(obj) ? true: false;
}
return 0;
}
int volc_json_read_int(cJSON *root, const char *fmt, int *dst) {
return _read_item_int(root, fmt, dst);
}
int volc_json_read_double(cJSON *root, const char *fmt, double *dst) {
return _read_item_double(root, fmt, dst);
}
int volc_json_read_string(cJSON *root, const char *fmt, char **dst) {
return _read_item_string(root, fmt, dst);
}
int volc_json_read_bool(cJSON *root, const char *fmt, bool *dst)
{
return _read_item_bool(root, fmt, dst);
}
int volc_json_read_object(cJSON *root, const char *fmt, cJSON **dst) {
cJSON *obj;
if (NULL == root || NULL == fmt) {
LOGW("invalid input root %p fmt %p", root, fmt);
return -1;
}
obj = _read_item_object(root, fmt);
if (NULL == obj) {
LOGD("parse error, fmt=%s", fmt);
return -1;
}
if (NULL != dst) {
*dst = cJSON_Duplicate(obj, 1);
if (NULL == *dst) {
LOGE("memory alloc failed");
return -1;
}
}
return 0;
}
int volc_json_check_int(cJSON *root, const char *fmt)
{
return _read_item_int(root, fmt, NULL);
}
int volc_json_check_double(cJSON *root, const char *fmt)
{
return _read_item_double(root, fmt, NULL);
}
int volc_json_check_string(cJSON *root, const char *fmt)
{
return _read_item_string(root, fmt, NULL);
}
int volc_json_check_bool(cJSON *root, const char *fmt)
{
return _read_item_bool(root, fmt, NULL);
}
@@ -0,0 +1,48 @@
// Copyright (2025) Beijing Volcano Engine Technology Ltd.
// SPDX-License-Identifier: Apache-2.0
#ifndef __CONV_AI_SRC_UTIL_VOLC_JSON_H__
#define __CONV_AI_SRC_UTIL_VOLC_JSON_H__
#ifdef __cplusplus
extern "C" {
#endif
#include <stdbool.h>
#include "cJSON.h"
#define JSON_KEY_LEN_MAX (64)
/**
* @brief
*
* @param root
* @param fmt the string of the key, support the multi-level keys, such as: [key1.key2.key3]
* @param dst
* @return 0: success.
* -1: failure.
*/
int volc_json_read_int(cJSON *root, const char *fmt, int *dst);
int volc_json_read_double(cJSON *root, const char *fmt, double *dst);
int volc_json_read_string(cJSON *root, const char *fmt, char **dst);
int volc_json_read_object(cJSON *root, const char *fmt, cJSON **dst);
int volc_json_read_bool(cJSON *root, const char *fmt, bool *dst);
/**
* @brief
*
* @param root
* @param fmt
* @return 0: the type of the item is same as the suffix of the function name.
* -1: different.
*/
int volc_json_check_int(cJSON *root, const char *fmt);
int volc_json_check_double(cJSON *root, const char *fmt);
int volc_json_check_string(cJSON *root, const char *fmt);
int volc_json_check_bool(cJSON *root, const char *fmt);
#ifdef __cplusplus
}
#endif
#endif // __CONV_AI_SRC_UTIL_VOLC_JSON_H__
@@ -0,0 +1,79 @@
// Copyright (2025) Beijing Volcano Engine Technology Ltd.
// SPDX-License-Identifier: Apache-2.0
#ifndef __CONV_AI_SRC_UTIL_VOLC_LIST_H__
#define __CONV_AI_SRC_UTIL_VOLC_LIST_H__
#include <stdio.h>
#ifdef __cplusplus
extern "C"
{
#endif
typedef struct volc_list_head_t
{
struct volc_list_head_t *next, *prev;
} volc_list_head_t;
#define __LIST_ADD(entry, before, after) \
{ \
volc_list_head_t *new_ = (entry), *prev = (before), *next = (after); \
(next)->prev = (new_); \
(new_)->next = (next); \
(new_)->prev = (prev); \
(prev)->next = (new_); \
}
#define volc_list_init(entry) \
do \
{ \
(entry)->next = (entry); \
(entry)->prev = (entry); \
} while (0)
#define volc_list_add(entry, base) \
do \
{ \
__LIST_ADD((entry), (base), (base)->next); \
} while (0)
#define volc_list_add_after(entry, base) \
do \
{ \
__LIST_ADD((entry), (base), (base)->next); \
} while (0)
#define volc_list_add_before(entry, base) \
do \
{ \
__LIST_ADD((entry), (base)->prev, (base)); \
} while (0)
#define volc_list_add_head(entry, head) volc_list_add_after(entry, head)
#define volc_list_add_tail(entry, head) volc_list_add_before(entry, head)
#define volc_list_del(entry) \
do \
{ \
(entry)->prev->next = (entry)->next; \
(entry)->next->prev = (entry)->prev; \
(entry)->next = (entry)->prev = (entry); \
} while (0)
#define volc_list_empty(head) ((head)->next == (head))
#define volc_list_get_head(head) (volc_list_empty(head) ? (volc_list_head_t *)NULL : (head)->next)
#define volc_list_get_tail(head) (volc_list_empty(head) ? (volc_list_head_t *)NULL : (head)->prev)
#define volc_list_is_head(entry, head) ((entry)->prev == head)
#define volc_list_is_tail(entry, head) ((entry)->next == head)
#define volc_list_entry(ptr, type, member) ((type *)((char *)(ptr) - (unsigned long)(&((type *)0)->member)))
#define volc_list_for_each(h, head) for (h = (head)->next; h != (head); h = h->next)
#define volc_list_for_each_safe(h, n, head) for (h = (head)->next, n = h->next; h != (head); h = n, n = h->next)
#define volc_list_for_each_entry(p, head, type, member) \
for (p = volc_list_entry((head)->next, type, member); &p->member != (head); \
p = volc_list_entry(p->member.next, type, member))
#define volc_list_for_each_entry_safe(p, t, head, type, member) \
for (p = volc_list_entry((head)->next, type, member), t = volc_list_entry(p->member.next, type, member); \
&p->member != (head); p = t, t = volc_list_entry(t->member.next, type, member))
#define volc_list_get_head_entry(head, type, member) \
(volc_list_empty(head) ? (type *)NULL : volc_list_entry(((head)->next), type, member))
#define volc_list_get_tail_entry(head, type, member) \
(volc_list_empty(head) ? (type *)NULL : volc_list_entry(((head)->prev), type, member))
#ifdef __cplusplus
}
#endif
#endif /* #define __CONV_AI_SRC_UTIL_VOLC_LIST_H__ */
@@ -0,0 +1,58 @@
// Copyright (2025) Beijing Volcano Engine Technology Ltd.
// SPDX-License-Identifier: Apache-2.0
#ifndef __CONV_AI_SRC_UTIL_VOLC_LOG_H__
#define __CONV_AI_SRC_UTIL_VOLC_LOG_H__
#include <stdio.h>
#ifdef __cplusplus
extern "C"
{
#endif
typedef enum
{
VOLC_LOG_LEVEL_ERROR,
VOLC_LOG_LEVEL_WARN,
VOLC_LOG_LEVEL_INFO,
VOLC_LOG_LEVEL_DEBUG,
VOLC_LOG_LEVEL_VERBOSE,
VOLC_LOG_LEVEL_NONE, // 不输出日志
} volc_log_level_e;
#define VOLC_LOG_LEVEL_DEFAULT VOLC_LOG_LEVEL_INFO
// 日志级别颜色定义
#define LOG_COLOR_RESET "\033[0m"
#define LOG_COLOR_RED "\033[31m"
#define LOG_COLOR_GREEN "\033[32m"
#define LOG_COLOR_YELLOW "\033[33m"
#define LOG_COLOR_BLUE "\033[34m"
#define LOG_COLOR_PRUPLE "\033[35m"
#ifndef __FILENAME__
#define __FILENAME__ (__builtin_strrchr(__FILE__, '/') ? __builtin_strrchr(__FILE__, '/') + 1 : __FILE__)
#endif
// 通用日志宏
#define LOG(level, tag, color, format, ...) \
do \
{ \
if (level <= VOLC_LOG_LEVEL_DEFAULT) \
{ \
printf(color "[%s|%s:%d]" format LOG_COLOR_RESET "\n", tag, __FILENAME__, __LINE__, ##__VA_ARGS__); \
fflush(stdout); \
} \
} while (0)
// 分级日志实现
#define LOGV(format, ...) LOG(VOLC_LOG_LEVEL_VERBOSE, "VRB", LOG_COLOR_PRUPLE, format, ##__VA_ARGS__)
#define LOGD(format, ...) LOG(VOLC_LOG_LEVEL_DEBUG, "DBG", LOG_COLOR_BLUE, format, ##__VA_ARGS__)
#define LOGI(format, ...) LOG(VOLC_LOG_LEVEL_INFO, "INF", LOG_COLOR_GREEN, format, ##__VA_ARGS__)
#define LOGW(format, ...) LOG(VOLC_LOG_LEVEL_WARN, "WRN", LOG_COLOR_YELLOW, format, ##__VA_ARGS__)
#define LOGE(format, ...) LOG(VOLC_LOG_LEVEL_ERROR, "ERR", LOG_COLOR_RED, format, ##__VA_ARGS__)
#ifdef __cplusplus
}
#endif
#endif /* __CONV_AI_SRC_UTIL_VOLC_LOG_H__ */
@@ -0,0 +1,604 @@
// Copyright (2025) Beijing Volcano Engine Technology Ltd.
// SPDX-License-Identifier: Apache-2.0
#include "volc_conv_ai.h"
#include <string.h>
#include <inttypes.h>
#include "volc_osal.h"
#include "util/volc_json.h"
#include "util/volc_log.h"
#include "base/volc_device_manager.h"
#include "base/volc_base.h"
#if defined(ENABLE_RTC_MODE)
#include "VolcEngineRTCLite.h"
#include "volc_rtc.h"
#elif defined(ENABLE_WS_MODE)
#include "volc_ws.h"
#endif
#include "cJSON.h"
#define MAGIC_CONTROL "ctrl"
#define MAGIC_CONV "conv"
#define MAGIC_LENGTH 4
#define MAGIC_OFFSET 8
typedef enum {
VOLC_RT_STATE_NONE = 0, // Initial state
VOLC_RT_STATE_CREATED, // engine created
VOLC_RT_STATE_STARTED, // engine started
// VOLC_RT_STATE_STARTED, // Joined a channel
VOLC_RT_STATE_UNJOINED, // Connection lost
VOLC_RT_STATE_STOPPED, // engine stopped
VOLC_RT_STATE_DESTROYED, // engine destroyed
VOLC_RT_STATE_ERROR // Error state
} volc_rt_state_e;
typedef struct {
int bytes;
uint64_t last_time_ms;
} media_stats_t;
typedef struct {
volatile size_t status;
bool b_key_frame_request;
int target_kbps;
void* user_data;
volc_mode_e mode;
#if defined(ENABLE_WS_MODE)
volc_ws_t ws;
#endif
#if defined(ENABLE_RTC_MODE)
volc_rtc_t rtc;
#endif
media_stats_t audio_recv_stats;
media_stats_t audio_send_stats;
volc_iot_info_t info;
// volc_room_info_t room_info;
volc_event_handler_t event_handler;
} volc_engine_impl_t;
static void _iot_info_free(volc_iot_info_t* info) {
if (info) {
HAL_SAFE_FREE(info->device_name);
HAL_SAFE_FREE(info->device_secret);
HAL_SAFE_FREE(info->instance_id);
HAL_SAFE_FREE(info->product_key);
HAL_SAFE_FREE(info->product_secret);
}
}
static void __realtime_conv_status_to_user(volc_engine_impl_t* impl, volc_conv_status_e status) {
if (impl->event_handler.on_volc_conversation_status) {
impl->event_handler.on_volc_conversation_status(impl, status, impl->user_data);
}
}
static void __realtime_event_2_user_event(void* context, volc_msg_t* msg) {
volc_event_t event = { 0 };
if (context == NULL || msg == NULL) {
LOGE("context or message is NULL");
return;
}
volc_engine_impl_t* impl = (volc_engine_impl_t*)context;
switch (msg->code) {
case VOLC_MSG_CONNECTED:
event.code = VOLC_EV_CONNECTED;
break;
case VOLC_MSG_DISCONNECTED:
event.code = VOLC_EV_DISCONNECTED;
break;
case VOLC_MSG_USER_JOINED:
case VOLC_MSG_USER_OFFLINE:
break;
case VOLC_MSG_TOKEN_EXPIRED:
break;
case VOLC_MSG_KEY_FRAME_REQ:
impl->b_key_frame_request = true;
return;
case VOLC_MSG_TARGET_BITRATE_CHANGED:
impl->target_kbps = msg->data.target_bitrate / 1000;
return;
case VOLC_MSG_CONV_STATUS:
__realtime_conv_status_to_user(impl, msg->data.conv_status);
return;
case VOLC_MSG_QUOTA_EXCEEDED:
event.code = VOLC_EV_QUOTA_EXCEEDED;
break;
default:
LOGW("Unknown message type: %d", msg->code);
return; // Ignore unknown messages
}
if (impl->event_handler.on_volc_event) {
impl->event_handler.on_volc_event(impl, &event, impl->user_data);
}
}
static void __realtime_user_event_router(void* context, volc_msg_t* msg) {
__realtime_event_2_user_event(context, msg);
}
static void __record_media_bytes(media_stats_t* stats, size_t len, const char* tag) {
#if defined(ENABLE_WS_MODE)
uint64_t now_ms = 0;
stats->bytes += len;
now_ms = volc_osal_get_time_ms();
if (stats->last_time_ms == 0) {
stats->last_time_ms = now_ms;
}
if (now_ms - stats->last_time_ms >= 2000) {
LOGI("%s bps: %d", tag, (int)(stats->bytes * 8 / (now_ms - stats->last_time_ms)));
stats->last_time_ms = now_ms;
stats->bytes = 0;
}
#else
(void)stats;
(void)len;
(void)tag;
#endif
}
static void __realtime_audio_router(volc_engine_impl_t* impl, const void* data, size_t len, volc_data_info_t* info) {
if (impl->event_handler.on_volc_audio_data) {
impl->event_handler.on_volc_audio_data(impl, data, len, &info->info.audio, impl->user_data);
}
__record_media_bytes(&impl->audio_recv_stats, len, "audio recv");
}
static void __realtime_video_router(volc_engine_impl_t* impl, const void* data, size_t len, volc_data_info_t* info) {
if (impl->event_handler.on_volc_video_data) {
impl->event_handler.on_volc_video_data(impl, data, len, &info->info.video, impl->user_data);
}
}
static void __realtime_message_router(volc_engine_impl_t* impl, const void* data, size_t len, volc_data_info_t* info) {
int ret = 0;
if (impl->event_handler.on_volc_message_data) {
impl->event_handler.on_volc_message_data(impl, data, len, &info->info.message, impl->user_data);
}
}
static void __realtime_data_router(void* context, const void* data, size_t len, volc_data_info_t* info) {
volc_engine_impl_t* impl = (volc_engine_impl_t*)context;
if (info) {
switch(info->type) {
case VOLC_DATA_TYPE_AUDIO:
__realtime_audio_router(impl, data, len, info);
break;
case VOLC_DATA_TYPE_VIDEO:
__realtime_video_router(impl, data, len, info);
break;
case VOLC_DATA_TYPE_MESSAGE:
__realtime_message_router(impl, data, len, info);
break;
default:
break;
}
}
}
static int __config_iot_parse(cJSON* iot_config, volc_engine_impl_t* engine) {
int ret = 0;
ret |= volc_json_read_string(iot_config, "instance_id", &engine->info.instance_id);
ret |= volc_json_read_string(iot_config, "product_key", &engine->info.product_key);
ret |= volc_json_read_string(iot_config, "device_name", &engine->info.device_name);
/*
* Production devices receive a per-device Device Secret from our SaaS.
* Product Secret is deliberately not accepted by the firmware path.
*/
ret |= volc_json_read_string(iot_config, "device_secret", &engine->info.device_secret);
return ret == 0 ? 0 : -1;
}
const char* volc_get_version(void) {
static char version[16] = {0};
snprintf(version, sizeof(version), "%d.%d.%d", VOLC_VERSION_MAJOR, VOLC_VERSION_MINOR, VOLC_VERSION_PATCH);
return version;
}
const char* volc_err_2_str(int err_code) {
switch(err_code) {
case VOLC_ERR_NO_ERROR:
return "Success";
case VOLC_ERR_FAILED:
return "Failed";
case VOLC_ERR_LICENSE_EXHAUSTED:
return "License exhausted";
case VOLC_ERR_LICENSE_EXPIRED:
return "License expired";
default:
break;
}
return "Unknown error";
}
int volc_create(volc_engine_t* handle, const char* config_json, volc_event_handler_t* event_handler, void* user_data) {
int ret = 0;
volc_engine_impl_t* engine = NULL;
cJSON* config = NULL;
if (config_json == NULL || event_handler == NULL || handle == NULL) {
LOGE("Configuration JSON is NULL");
return VOLC_ERR_FAILED;
}
engine = (volc_engine_impl_t*)volc_osal_malloc(sizeof(volc_engine_impl_t));
if (engine == NULL) {
LOGE("Failed to allocate memory for engine");
return VOLC_ERR_FAILED;
}
memset(engine, 0, sizeof(volc_engine_impl_t));
engine->event_handler = *event_handler;
engine->user_data = user_data;
engine->mode = VOLC_MODE_UNKNOWN;
config = cJSON_Parse(config_json);
cJSON* iot_config = cJSON_GetObjectItem(config, "iot");
if (iot_config) {
ret = __config_iot_parse(iot_config, engine);
if (ret != 0) {
LOGE("Failed to parse IoT configuration");
goto err_out_label;
}
if (engine->info.device_secret == NULL || strlen(engine->info.device_secret) < 16) {
LOGE("Device Secret is missing or invalid");
ret = VOLC_ERR_FAILED;
goto err_out_label;
}
} else {
LOGE("IoT configuration is NULL");
goto err_out_label;
}
cJSON* rtc_cfg = cJSON_GetObjectItem(config, "rtc");
if (rtc_cfg) {
#if defined(ENABLE_RTC_MODE)
engine->rtc = volc_rtc_create(engine->info.rtc_app_id, engine, rtc_cfg, __realtime_user_event_router, __realtime_data_router);
#else
LOGW("RTC mode is not enabled");
#endif
} else {
LOGI("RTC configuration is NULL");
}
#if defined(ENABLE_WS_MODE)
cJSON* ws_cfg = cJSON_GetObjectItem(config, "ws");
engine->ws = volc_ws_create(engine, ws_cfg, __realtime_user_event_router, __realtime_data_router);
#else
LOGW("WS mode is not enabled");
#endif
engine->status = VOLC_RT_STATE_CREATED;
*handle = (volc_engine_t)engine;
cJSON_Delete(config);
LOGI("Engine created successfully at: %llu ms", volc_osal_get_time_ms());
return 0;
err_out_label:
_iot_info_free(&engine->info);
HAL_SAFE_FREE(engine);
cJSON_Delete(config);
return ret;
}
void volc_destroy(volc_engine_t handle) {
volc_engine_impl_t* engine = (volc_engine_impl_t *)handle;
if (NULL == engine) {
return;
}
// if (engine->status == VOLC_RT_STATE_STARTED) {
// volc_stop(handle);
// }
switch(engine->mode) {
case VOLC_MODE_WS:
#if defined(ENABLE_WS_MODE)
volc_ws_destroy(engine->ws);
#else
LOGW("WS mode is not enabled");
#endif
break;
case VOLC_MODE_RTC:
#if defined(ENABLE_RTC_MODE)
volc_rtc_destroy(engine->rtc);
#else
LOGW("RTC mode is not enabled");
#endif
break;
default:
break;
}
_iot_info_free(&engine->info);
HAL_SAFE_FREE(engine);
}
int volc_start(volc_engine_t handle, volc_opt_t* opt) {
int ret = 0;
volc_engine_impl_t* engine = (volc_engine_impl_t*)handle;
if (engine == NULL || opt == NULL) {
LOGE("engine handle(%p) or bot id(%p) is NULL", handle, opt);
return -1;
}
if (NULL == opt->bot_id || strlen(opt->bot_id) <= 0) {
LOGE("bot id is invalid");
return -1;
}
if (engine->status != VOLC_RT_STATE_CREATED && engine->status != VOLC_RT_STATE_STOPPED) {
LOGE("engine is not in CREATED state");
return -1;
}
engine->mode = opt->mode;
if (opt->mode == VOLC_MODE_WS) {
#if defined(ENABLE_WS_MODE)
ret = volc_ws_start(engine->ws, opt->bot_id, &engine->info, opt->params);
#else
LOGE("WS mode is not enabled");
ret = -1;
#endif
} else if (opt->mode == VOLC_MODE_RTC) {
#if defined(ENABLE_RTC_MODE)
ret = volc_rtc_start(engine->rtc, opt->bot_id, &engine->info, opt->params);
#else
LOGE("RTC mode is not enabled");
ret = -1;
#endif
}
if (ret != 0) {
engine->status = VOLC_RT_STATE_STOPPED;
LOGE("engine start failed: %d", ret);
return ret;
}
engine->status = VOLC_RT_STATE_STARTED;
LOGI("engine started successfully");
return ret;
}
int volc_stop(volc_engine_t handle) {
int ret = 0;
volc_engine_impl_t* engine = (volc_engine_impl_t*)handle;
if (engine == NULL) {
LOGE("engine handle is NULL");
return -1;
}
if (engine->status != VOLC_RT_STATE_STARTED) {
LOGW("engine is not in STARTED state");
return -1;
}
switch (engine->mode)
{
case VOLC_MODE_WS:
#if defined(ENABLE_WS_MODE)
ret = volc_ws_stop(engine->ws);
#else
LOGE("WS mode is not enabled");
ret = -1;
#endif
break;
case VOLC_MODE_RTC:
#if defined(ENABLE_RTC_MODE)
ret = volc_rtc_stop(engine->rtc);
#else
LOGE("RTC mode is not enabled");
ret = -1;
#endif
break;
default:
break;
}
engine->status = VOLC_RT_STATE_STOPPED;
LOGI("engine stopped successfully");
return ret;
}
int volc_update(volc_engine_t handle, const void* data_ptr, size_t data_len) {
int ret = 0;
volc_message_info_t info = { 0 };
volc_engine_impl_t* engine = (volc_engine_impl_t*)handle;
if (engine == NULL) {
LOGE("engine handle is NULL");
return -1;
}
if (engine->status != VOLC_RT_STATE_STARTED) {
LOGE("engine is not in STARTED state");
return -1;
}
info.is_binary = true;
return volc_send_message(handle, data_ptr, data_len, &info);
}
int volc_send_audio_data(volc_engine_t handle, const void* data_ptr, size_t data_len, volc_audio_frame_info_t* info_ptr) {
int ret = 0;
volc_data_info_t info = { 0 };
volc_engine_impl_t* engine = (volc_engine_impl_t*)handle;
if (engine == NULL || data_ptr == NULL || data_len == 0) {
LOGE("engine %p, data_ptr %p, or data_len %zu is NULL or zero", engine, data_ptr, data_len);
return -1;
}
if (engine->status != VOLC_RT_STATE_STARTED) {
LOGE("engine is not in STARTED state");
return -1;
}
info.type = VOLC_DATA_TYPE_AUDIO;
info.info.audio.data_type = info_ptr->data_type;
info.info.audio.commit = info_ptr->commit;
switch (engine->mode) {
case VOLC_MODE_RTC:
#if defined(ENABLE_RTC_MODE)
ret = volc_rtc_send(engine->rtc, data_ptr, data_len, &info);
#else
LOGE("RTC mode is not enabled");
ret = -1;
#endif
break;
case VOLC_MODE_WS:
#if defined(ENABLE_WS_MODE)
ret = volc_ws_send(engine->ws, data_ptr, data_len, &info);
#else
LOGE("WS mode is not enabled");
ret = -1;
#endif
break;
default:
break;
}
__record_media_bytes(&engine->audio_send_stats, data_len, "audio send");
return ret;
}
int volc_send_video_data(volc_engine_t handle, const void* data_ptr, size_t data_len, volc_video_frame_info_t* info_ptr) {
int ret = 0;
volc_data_info_t info = { 0 };
volc_engine_impl_t* engine = (volc_engine_impl_t*)handle;
if (engine == NULL || data_ptr == NULL || data_len == 0) {
LOGE("engine %p, data_ptr %p, or data_len %zu is NULL or zero", engine, data_ptr, data_len);
return -1;
}
if (engine->status != VOLC_RT_STATE_STARTED) {
LOGE("engine is not in STARTED state");
return -1;
}
info.type = VOLC_DATA_TYPE_VIDEO;
info.info.video.data_type = info_ptr->data_type;
switch (engine->mode) {
case VOLC_MODE_RTC:
#if defined(ENABLE_RTC_MODE)
ret = volc_rtc_send(engine->rtc, data_ptr, data_len, &info);
#else
LOGE("RTC mode is not enabled");
ret = -1;
#endif
break;
case VOLC_MODE_WS:
#if defined(ENABLE_WS_MODE)
ret = volc_ws_send(engine->ws, data_ptr, data_len, &info);
#else
LOGE("WS mode is not enabled");
ret = -1;
#endif
break;
default:
break;
}
return 0;
}
int volc_send_message(volc_engine_t handle, const void* data_ptr, size_t data_len, volc_message_info_t* info_ptr) {
int ret = 0;
volc_data_info_t info = { 0 };
if (handle == NULL || data_ptr == NULL || data_len == 0) {
LOGE("engine handle %p, data_ptr %p, or data_len %zu is NULL or zero", handle, data_ptr, data_len);
return -1;
}
volc_engine_impl_t* engine = (volc_engine_impl_t*)handle;
if (engine->status != VOLC_RT_STATE_STARTED) {
LOGE("engine is not in started state");
return -1;
}
info.type = VOLC_DATA_TYPE_MESSAGE;
info.info.message.is_binary = info_ptr ? info_ptr->is_binary : false;
switch (engine->mode) {
case VOLC_MODE_RTC:
#if defined(ENABLE_RTC_MODE)
ret = volc_rtc_send(engine->rtc, data_ptr, data_len, &info);
#else
LOGE("RTC mode is not enabled");
ret = -1;
#endif
break;
case VOLC_MODE_WS:
#if defined(ENABLE_WS_MODE)
ret = volc_ws_send(engine->ws, data_ptr, data_len, &info);
#else
LOGE("WS mode is not enabled");
ret = -1;
#endif
break;
default:
break;
}
return ret;
}
int volc_send_text_to_agent(volc_engine_t handle, const char* text, volc_agent_type_e type, int interrupt_mode) {
int ret = 0;
volc_engine_impl_t* engine = (volc_engine_impl_t*)handle;
if (handle == NULL) {
LOGE("engine handle is NULL");
return -1;
}
LOGD("interrupt: %d", engine->mode);
switch (engine->mode) {
case VOLC_MODE_RTC:
#if defined(ENABLE_RTC_MODE)
ret = volc_rtc_send_text_to_agent(engine->rtc,text,type,interrupt_mode);
#else
LOGE("RTC mode is not enabled");
ret = -1;
#endif
break;
case VOLC_MODE_WS:
#if defined(ENABLE_WS_MODE)
ret = -2;
LOGE("WS mode is not support send_text");
#else
LOGE("WS mode is not enabled");
ret = -1;
#endif
break;
default:
break;
}
return ret;
}
int volc_interrupt(volc_engine_t handle) {
int ret = 0;
volc_engine_impl_t* engine = (volc_engine_impl_t*)handle;
if (handle == NULL) {
LOGE("engine handle is NULL");
return -1;
}
LOGI("interrupt: %d, at: %" PRIu64"ms", engine->mode, volc_osal_get_time_ms());
switch (engine->mode) {
case VOLC_MODE_RTC:
#if defined(ENABLE_RTC_MODE)
ret = volc_rtc_interrupt(engine->rtc);
#else
LOGE("RTC mode is not enabled");
ret = -1;
#endif
break;
case VOLC_MODE_WS:
#if defined(ENABLE_WS_MODE)
ret = volc_ws_interrupt(engine->ws);
#else
LOGE("WS mode is not enabled");
ret = -1;
#endif
break;
default:
break;
}
return ret;
}