☰
ESP32-S3 呼微信接通后没声音之解决办法
2026/11/3 19:22:09 网站建设 项目流程

ESP32-S3 呼微信接通后没声音之解决办法

上一篇:移芯 EC718PM 上说话一卡一卡之解决办法

本文代码来自开源仓库github.com/tangeai/xiaotai-esp32(主工程lckfb-szpi-esp32s3-tirtc/)。代码有节选,常量和结构没改。

把微信语音通话接进开发板,最气人的一步:连得上,却没声音。信令显示一切正常,手机端来电能收到、能接听,WHIP 也报成功,结果两边都不出声,随后界面自动回到首页。

这个现象在starter_runtime.c里有明确的注释答案:

/* * WHIP 成功仅表示传输连接建立。微信 VoIP 与参考工程一致,必须 * 再等待服务端的 0x2000 / CALL_CONNECTED,收到后才允许启动 Opus * 媒体;过早发音频会被服务端关闭,表现为无声后自动回首页。 */

遇到这个现象,可以先查媒体是不是启动得太早——源码注释直接点名了这个原因。这篇把设备呼叫微信的信令流程走一遍,标出每一步会踩的坑。

一、先得让平台知道你会 VoIP:profile 上报

设备开机后,第一件相关的事是把 VoIP 能力上报给平台,拨号排在它后面。starter_runtime.c把设备信息拼成 JSON,发到/v1/device/profile,关键的一段是profiles里的voip:屏幕 320×240、单声道音频、上下行媒体类型:

intprofile_length=snprintf(profile,sizeof(profile),"{\"hardware\":{\"chip_model\":\"%s\",\"board_model\":\"%s\"},""\"firmware_version\":\"%s\",\"profiles\":{"..."\"voip\":{\"screen_width\":320,\"screen_height\":240,""\"audio_rate\":%u,\"audio_channels\":1,\"down_audio_mt\":\"%s\",""\"up_video_mt\":\"none\",\"down_video_mt\":\"none\","...

上报成功后有一行日志:

ESP_LOGI(TAG,"device profile accepted api=/v1/device/profile scenes=stream,call,voip; WeChat calling is ready");

没上报成功就拨号,呼叫会在本地直接被拒,连请求都发不出去:

if(contact->source==STARTER_CONTACT_WECHAT&&!s_voip_profile_ready){ESP_LOGW(TAG,"WeChat call blocked until VoIP profile is reported");returnfalse;}

MQTT 侧同样拦了一遍——收到微信来电但 profile 没就绪,忽略这条信令并重新请求 profile:

if(wechat&&strcmp(signal,"call_incoming")==0&&!s_voip_profile_ready){ESP_LOGW(TAG,"ignoring WeChat call before VoIP profile readiness");cJSON_Delete(root);request_voip_profile();return;}

所以排查"微信呼叫打不出去",第一件事是看串口里有没有WeChat calling is ready这行。重试间隔是 15 秒(VOIP_PROFILE_RETRY_MS 15000),网络慢时要等。

二、拨号:微信和设备互呼走的是两条路

点联系人呼叫后,begin_call_common()先结束当前会话并清理连接:结束 AI 会话、关掉"是否允许 H5 连接"的开关、断开旧连接,然后递增用来区分新旧通话的编号。之后按联系人类型分两条路:

if(s_call_wechat){(void)snprintf(body,sizeof(body),"{\"device_id\":\"%s\",\"wx_app_id\":\"%s\",""\"wx_user_openid\":\"%s\",\"wx_model_id\":\"%s\",""\"wx_room_type\":\"%s\",\"wx_version_type\":0}",s_device_id,s_call_wx_app_id,s_call_peer_id,s_call_wx_model_id,video?"video":"voice");service=PLATFORM_SERVICE_VOIP;path="/v1/voip/device/call";stage=CALL_HTTP_VOIP_DIAL;}else{esp_err_terr=submit_device_call();...}

微信走/v1/voip/device/call,设备互呼走/v1/call/request。注意一个容易想错的点:微信拨号的 HTTP 响应在代码里只读取了call_id,真正建连要用的 peer、token 要等 MQTT 的call_incoming推过来:

if(event->command==CALL_HTTP_VOIP_DIAL){(void)copy_json_string(data,"call_id",s_call_id,sizeof(s_call_id),false);cJSON_Delete(root);/* 真正 peer/token/room 来自匹配的 MQTT call_incoming。 */s_deadline_ms=now_ms()+CALL_CONNECT_TIMEOUT_MS;return;}

也就是说主叫流程是"HTTP 拨号 → 等 MQTT 回铃",回铃里才带建连要用的凭据。等不到回铃,HTTP 侧再成功也没用。

三、WHIP 建连在本工程放在独立任务里,注释点名了原因

MQTT 的call_incoming(微信侧字段是wx_room_id/peer_id/token)解析完,就该发起 WHIP 建连了。这里有个栈深度的坑,注释写得很直白:

/* * MQTT call_incoming 的解析帧里有多组 1 KiB peer/token 临时缓冲。不能在 * 该深栈函数中再进入 TiRtcWhipConnect,否则 SDK 的握手栈会压穿 * starter_session。下一轮事件在解析帧已退出后才提交 WHIP。 */

所以 WHIP 不在 MQTT 回调里调,而是 calloc 一个请求对象,丢给独立任务去执行。任务本体很短:调starter_tirtc_voip_connect(peer_id, token, generation),把连接结果写入EVENT_VOIP_CONNECT_RESULT事件再放入队列,然后删掉自己。

两个细节值得抄走。第一,任务栈从 PSRAM 分配(MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT),不动内部那块本来就紧张的 RAM——启动时仓库专门给 TiRTC 预留了一段连续的内部堆,留给这类握手。第二,这个任务用xTaskCreateWithCaps创建,结束时对应vTaskDeleteWithCaps,注释里提醒普通vTaskDelete会把静态注册的栈和 TCB 每次漏掉一份。

四、最大的坑:WHIP 成功只算一半

WHIP 建连成功,事件进来,状态还是"接通中":

if(event->mode==STARTER_TIRTC_VOIP){s_call_waiting_confirm=true;s_deadline_ms=now_ms()+VOIP_CONNECTED_WAIT_TIMEOUT_MS;ESP_LOGI(TAG,"VoIP WHIP connected; awaiting CALL_CONNECTED generation=%lu",(unsignedlong)s_connection_generation);return;}

注意s_call_waiting_confirm = true和那行日志:WHIP 成功只表示传输连接建立。真正的接通确认是服务端下发的0x2000(CALL_COMMAND_CONNECT),收到它才允许启动 Opus 媒体。命令字一共两个:0x2000接通、0x2001挂断,挂断那条同样从这条消息通道进来。

if(event->command!=CALL_COMMAND_CONNECT||state!=STARTER_RUNTIME_CALL_CONNECTING||!s_call_waiting_confirm){return;}...starter_tirtc_mode_tmode=s_call_wechat?STARTER_TIRTC_VOIP:STARTER_TIRTC_CALL;if(starter_media_start(mode,s_connection_generation)!=ESP_OK){finish_session(ESP_ERR_INVALID_STATE);return;}s_call_waiting_confirm=false;s_deadline_ms=0;publish_state(STARTER_RUNTIME_CALL_ACTIVE);

把这两个时序搞反,就是开头那个现象:媒体先起来,服务端不认,把会话关掉,手机端表现为无声后自动回首页。等0x2000的超时是 35 秒(VOIP_CONNECTED_WAIT_TIMEOUT_MS 35000)。

设备互呼(非微信)还要更严一层:主叫要等"对方已接听"加"P2P 已连上"两个条件都满足、并且拿到非零连接编号,才启动媒体。条件没齐就不开,等齐了才开:

if(s_call_wechat||!s_call_outgoing||state!=STARTER_RUNTIME_CALL_CONNECTING||!s_call_peer_answered||!s_call_p2p_connected||s_connection_generation==0U){return;}

五、被叫侧:回铃不能当成新来电

主叫微信时,MQTT 会推一条回铃。这条回铃的字段跟真实来电几乎一样,不处理就可能弹两次"来电"界面。代码用四个条件识别"这是我自己外呼的回铃":

/* 微信外呼的 MQTT 回铃是同一流程,不再弹一次"来电"。 */bool outgoing_wechat=wechat&&state==STARTER_RUNTIME_CALL_CONNECTING&&s_call_wechat&&s_call_outgoing&&(from_device[0]=='\0'||strcmp(from_device,s_device_id)==0)&&(s_call_id[0]=='\0'||call_id[0]=='\0'||strcmp(s_call_id,call_id)==0);

紧挨着还有一条防护:已经取消的外呼,迟到的回推不能再显示成来电。这条判断要求消息来自微信、from_device是本机,而且不符合前面的外呼回铃条件——四个条件都查了,只要有一条对不上就当垃圾消息丢掉。两种判定写在同一个函数里,相隔不到 10 行,处理的都是同一种消息:自己外呼产生的回铃。

另外两类来电会被直接拒掉:一是房间类型超出 voice/audio 的范围(S3 只做语音,注释原话是"避免出现’能接听却无视频’的假能力"),二是忙线。拒接时微信侧带原因码,本工程用到的取值:不支持的房间类型传 7,忙线传 8。

六、音频流号对不上,订阅就收不到

还有一处跟"有连接没声音"直接相关:TiRTC 收到音频订阅回调时,按当前模式和流号过滤。微信 VoIP 的音频流号是 10:

#defineAI_AUDIO_STREAM1U#defineCALL_AUDIO_STREAM10U
bool accepted=(mode==STARTER_TIRTC_H5&&stream_id==H5_AUDIO_STREAM)||((mode==STARTER_TIRTC_AI||mode==STARTER_TIRTC_ROOM)&&stream_id==AI_AUDIO_STREAM)||((mode==STARTER_TIRTC_VOIP||mode==STARTER_TIRTC_CALL)&&stream_id==CALL_AUDIO_STREAM);...ESP_LOGI(TAG,"audio subscribe mode=%d stream=%u accepted=%d",(int)mode,(unsigned)stream_id,accepted);

模式不对或流号不对,accepted=0,对面说什么你都收不到——这是另一类"单向无声"的来源:连接是通的,订阅没过。看这行日志能直接定位。

自己跑一遍

微信通话要在真实设备上验证:模拟器没有微信链路,绑定的微信账号授权也过不去,所以这篇的验证点全部依赖实机。

路径需要什么做什么
设备模拟器一台电脑,不用装 ESP-IDF拉tirtc-server-example仓库,thing-connect/device-sim/device-sim-py/下跑device_sim_main.py --mac AA:BB:CC:DD:EE:FF,验证的是通话流程框架,微信链路验不了
浏览器烧固件立创实战派 ESP32-S3 V1.0.1/N16R8 + USB 线,不用装 ESP-IDFRelease 页下载*-full-16MB.bin,浏览器打开 espressif.github.io/esptool-js,地址填0x0,Flash 三项都选keep;烧完连XiaoTai-XXXX热点,浏览器开 192.168.6.1 配网
改代码自己编ESP-IDF 环境在starter_runtime.c的信令路径里加日志重新编译,对着本文对照看

本文的验证点在串口日志,按信令顺序:

  1. profile 上报成功:device profile accepted api=/v1/device/profile scenes=stream,call,voip; WeChat calling is ready——没有这行,后面全免。
  2. 拨号提交:[DEBUG-call] dial submitted target=... type=wechat stage=...
  3. WHIP 完成:[DEBUG-voip] WHIP worker completed generation=... rc=0
  4. 等确认:VoIP WHIP connected; awaiting CALL_CONNECTED generation=...——这行出现后要等到第 5 步那行订阅日志,才算真接通。
  5. 订阅通过:audio subscribe mode=3 stream=10 accepted=1(mode 3 是 VOIP)。accepted=0表示当前模式和流号的组合没通过检查。

诚实限制:微信链路有两端,设备侧日志只能看到设备这一半;服务端怎么处理0x2000,仓库里看不到,只能从行为反推。另外信令的时延跟网络有关,本文不测时延。

代码仓库github.com/tangeai/xiaotai-esp32,本文代码都在lckfb-szpi-esp32s3-tirtc/components/starter_runtime/src/starter_runtime.c与components/starter_tirtc/src/starter_tirtc.c。国内访问慢可以用 Gitee 镜像gitee.com/tange-ai/xiaotai-esp32。
代码开源、可自由取用。手边没有板子也能先看效果。

把微信通话接进自己设备时,先查这几件事

  • 先确认 profile 上报成功,没有WeChat calling is ready就别往下查。
  • WHIP 成功不是接通。等0x2000再启动媒体,等确认的超时是 35 秒。
  • 主叫微信时,建连凭据在 MQTT 回铃里,拨号 HTTP 的响应在代码里只读了call_id。
  • 别在 MQTT 解析函数没返回时直接调 SDK 握手;本工程的做法是独立任务加 PSRAM 栈。
  • 收不到对方声音先看audio subscribe ... accepted=这行,检查当前模式和流号的组合。
  • 挂断要走统一的通话结束函数,媒体、连接、H5 连接开关、超时定时器一起复位,漏一样就是下次通话的隐患。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询