Whatsapp VoiceCall
客户端通过websocket连接到服务器,客户端发起语音通话请求,并且完成必要的协商之后,就可以直接将语音数据发送给服务器,服务器接收到对方的语音数据之后也会通过websocket将语音数据转发给客户端
获取协商秘钥
XMPP 在发起语音通话请求的时候,需要带上一个秘钥,这个秘钥长32字节,通过特殊算法生成。这个算法需要三个参数:
- 自身jid
- 对方jid
- 时间戳(服务端自动获取,不需要生成)
//发送获取秘钥请求 JSONObject result = new JSONObject(); result.put("command", "GetSecret"); result.put("selfjid", "自己的@whatsapp.com"); result.put("otherjid", "对方@whatsapp.com"); SendCommand(result); //接收到服务器返回的消息, secret 字段是经过base64 编码,需要解码,解码之后是32字节 { "secret": "Xh+LtW/gRxC92B4UK/gLAzqERAqL9U2ArNetO3Zy0h0=", "command": "ResponseSecret" }
发起XMPP 语音请求
- 发起语音请求。这个请求需要通过xmpp 通道发送出去,发出去之后,WA服务器会回一个ack包,这个ack包需要通过websocket发给中转服务器
<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;
}
- 处理ack 回包。
发送完第一个包之后,服务器会返回一个ack包, 需要将这个ack包转成xml格式,然后通过websocket 发送给服务器
//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);
- 接收到的服务器的包必须回复ack,否则会被踢下线,下面几个常用的ack
//接收的包
<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'/>
- 中转服务器会将一些需要发给WA服务器的包发过来,这些包需要转成xmpp 格式的数据发给WA 服务器
<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>