New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Whatsapp Voice Call #3258
Comments
Whatsapp VoiceCall客户端通过websocket连接到服务器,客户端发起语音通话请求,并且完成必要的协商之后,就可以直接将语音数据发送给服务器,服务器接收到对方的语音数据之后也会通过websocket将语音数据转发给客户端 获取协商秘钥XMPP 在发起语音通话请求的时候,需要带上一个秘钥,这个秘钥长32字节,通过特殊算法生成。这个算法需要三个参数:
发起XMPP 语音请求
<call to='接收方@s.whatsapp.net' id='随机生成32字节'>
<offer call-creator='发送方.0:0@s.whatsapp.net' call-id='随机生成32字节' device_class='2015'>
<privacy>联系人的token, 同步联系人的时候 privacy_token节点下 trusted_contact 数据 </privacy>
<audio rate='16000' enc='opus'/>
<net medium='3'/>
<capability ver='1'>AQT3CcT6</capability>
<enc v='2' type='msg'>从服务器获取的32字节秘钥序列化成pb之后加密</enc>
<encopt keygen='2'/>
</offer>
</call> //下面是消息pb 结构的一部分,需要将返回的32字节秘钥 设置到 Call->callKey 中,序列化之后加密
message Message {
optional string conversation = 1;
optional SenderKeyDistributionMessage senderKeyDistributionMessage = 2;
optional ImageMessage imageMessage = 3;
optional ContactMessage contactMessage = 4;
optional LocationMessage locationMessage = 5;
optional ExtendedTextMessage extendedTextMessage = 6;
optional DocumentMessage documentMessage = 7;
optional AudioMessage audioMessage = 8;
optional VideoMessage videoMessage = 9;
optional Call call = 10;
... ...
... ...
}
message Call {
optional bytes callKey = 1;
optional string conversionSource = 2;
optional bytes conversionData = 3;
optional uint32 conversionDelaySeconds = 4;
}
//xmpp 转xml 需要注意, 节点部分的值需要base64 之后再发过来
<ack from='对方@s.whatsapp.net' class='call' type='offer' id='xxxx'>
<relay attribute_padding='1' peer_pid='0' self_pid='1' uuid='xxx' call-creator='xxx@s.whatsapp.net' call-id='xxx' joinable='1'>
<participant pid='0' jid='xxx@s.whatsapp.net'/>
<token id='0'>base64的内容</token>
<token id='1'>xxx</token>
<token id='2'>xxx</token>
<token id='3'>xxx</token>
<token id='4'>xxxx</token>
<key>xxxx</key>
<te2 protocol='1' relay_id='0' token_id='0'>base64的内容</te2>
<te2 protocol='1' relay_id='0' token_id='0'>base64的内容</te2>
<te2 relay_id='0' token_id='0'>xxx</te2>
<te2 relay_id='0' token_id='0'>xxx</te2>
<te2 protocol='1' relay_id='1' token_id='1'>xxx</te2>
<te2 protocol='1' relay_id='1' token_id='1'>xx</te2>
<te2 relay_id='1' token_id='1'>xxx</te2>
<te2 relay_id='1' token_id='1'>xxx</te2>
<te2 protocol='1' relay_id='2' token_id='3'>xxx</te2>
<te2 protocol='1' relay_id='2' token_id='3'>xxx</te2>
<te2 relay_id='2' token_id='3'>xxx</te2>
<te2 relay_id='2' token_id='3'>xxx</te2>
<te2 protocol='1' relay_id='3' token_id='2'>xxx</te2>
<te2 protocol='1' relay_id='3' token_id='2'>xxx</te2>
<te2 relay_id='3' token_id='2'>xxx</te2>
<te2 relay_id='3' token_id='2'>xxx</te2>
<te2 protocol='1' relay_id='4' token_id='4'>xxx</te2>
<te2 protocol='1' relay_id='4' token_id='4'>xxx</te2>
<te2 relay_id='4' token_id='4'>xxx</te2>
<te2 relay_id='4' token_id='4'>xxx</te2>
<hbh_key>xxx</hbh_key>
</relay>
<user jid='xxx@s.whatsapp.net'>
<device jid='xxx@s.whatsapp.net'/>
</user>
<rte>xxx</rte>
<uploadfieldstat/>
<userrate/>
<voip_settings uncompressed='1'>xxxx</voip_settings>
</ack> //将服务器回的ack 包发给中转服务器
JSONObject result = new JSONObject();
result.put("command", "VoiceAck");
// 用于测试的音频文件ID,固定,正式部署的时候需要换成上传的文件
result.put("file_uuid", "aee4d52d-6ba7-4a65-80d4-b7341b1115f0");
result.put("ack", "服务器回的ack包打包成xml格式");
SendCommand(result);
//接收的包
<receipt from='xxx@s.whatsapp.net' id='xxx' t='xxx'>
<offer call-id='xxx' call-creator='xxx@s.whatsapp.net'/>
</receipt>
//需要回复ack
<ack id='xxx' to='xxx@s.whatsapp.net' class='receipt'/> //接收的包
<call from='xxx@s.whatsapp.net' id='xxx' t='xxx'><preaccept call-id='xxx' call-creator='xxx@s.whatsapp.net'><audio rate='16000' enc='opus'/><encopt keygen='2'/><capability ver='1'>xxx</capability></preaccept></call>
//需要回复ack
<ack id='xxx' to='xxx.0:0@s.whatsapp.net' class='call' type='preaccept'/> //接收的包
<call from='xxx@s.whatsapp.net' id='xxx' t='xxx'>
<relaylatency call-id='xxx' call-creator='xxx@s.whatsapp.net'>
<te latency='xxx'>xxx</te>
</relaylatency>
</call>
//需要回复ack
<ack id='xxx' to='xxx.0:0@s.whatsapp.net' class='call' type='relaylatency'/>
<call to="xxx@s.whatsapp.net" id="xxx">
<relaylatency call-creator="xxx.0:0@s.whatsapp.net" call-id="xxx">
<te latency="xxx">xxx</te>
</relaylatency>
</call> 总结一下步骤:1. 和中转服务器建立websocket 连接2. 从中转服务器获取 加密秘钥3. XMPP 发送call 请求,并且接收服务器返回的ack, 特别需要注意期间会收到很多包,都需要回ack,上面也列出了一些需要回ack的包4. 将WA 服务器的ack包转成xml 格式发给中转服务器, 特别需要注意xml格式节点值需要base64 编码5. 中转服务器会主动发送一些xml数据, 客户端需要将这些xml数据转成xmpp包发给服务器。 |
Whatsapp: +66627011785 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
XMPP 协商
发起或者接受语音通话第一步是发起XMPP 协商,这个协商过程非常重要。下面是协商一个包
Stun UDP 内网穿透
NAT为设备提供内网IP地址,以便在专用本地网络中使用,但是这个地址不能在外部使用。没有外部的ip地址, 双方就无法直接进行通信。为了解决这个问题,就需要Stun 技术,也就是传说中的UDP 打洞。
Turn 数据中转
UDP 打洞并不能一定成功,所以当点对点失败的时候,为了能正常进行语音通话,需要有一个保底策略,那就是在turn服务器中转数据流。
TURN服务器具有公共地址,因此即使端点位于防火墙或代理之后,也可以与其他端点进行通信。TURN服务器虽然只有这么一个简单的任务 —— 中继流, 但与STUN服务器不同,它们本身就消耗了大量带宽。换句话说,TURN服务器需要更强大。
RTP 数据包发送
依赖上面的STUN/TURN 服务器的协商结果, 如果STUN 成功,则直接将加密的RTP 数据发送给对方,否则将加密的RTP数据发给TURN 服务器。下面是RTP 数据格式
由于whatsapp的安全性,所有的RTP 数据都是被加密的, 加密使用的秘钥来源第一步XMPP 的秘钥协商。需要发送的音频数据需要使用 XMPP 协商的编码方式,否则可能不能播放。
The text was updated successfully, but these errors were encountered: