语音推流(一) 适用有自己whatsapp 协议,可以使用协议登录账号
语音通话涉及到很多方面,打洞协商,编解码,加解密等等,所以很多协议并没有实现真正的语音通话,最多是发个xmpp协议,响铃,然后挂断。接下来如何接入语音通话服务。 语音数据的采集和播放
由于涉及到麦克风采集,扬声器播放,以及语音编解码,有一定的难度,所以高度封装了jni接口。使用起来极其简单。
//这个是封装的类
package org.voice;
import java.util.HashMap;
public class RecordAndPlayer {
static HashMap<Long, RecordAndPlayer> instanceMap = new HashMap<>();
public interface Delegate {
void OnRecord(byte[] data);
}
public RecordAndPlayer(Delegate delegate) {
this.delegate = delegate;
instance = CreatePlayer();
instanceMap.put(instance, this);
}
public void PutFrame(byte[] data, short serial, int timestamp) {
PutFrame(instance, data, serial, timestamp);
}
public void Stop() {
DestroyPlayer(instance);
//从表中移除
instanceMap.remove(instance);
}
private void OnRecord(byte[] data) {
delegate.OnRecord(data);
}
private long instance;
Delegate delegate;
public static native long CreatePlayer();
public static native void DestroyPlayer(long instance);
public static native void PutFrame(long instance, byte[] data, short serial, int timestamp);
public static void PullFrame(long instance, byte[] data) {
RecordAndPlayer player = instanceMap.get(instance);
if (player != null)
{
player.OnRecord(data);
}
}
}
//调用 封装接口采集和播放
recordAndPlayer = new RecordAndPlayer(new RecordAndPlayer.Delegate() {
@Override
public void OnRecord(byte[] data) {
//麦克风录制到声音回调
}
});
//当接收到解密之后的语音数据
recordAndPlayer.PutFrame(root.getBytes("data"), root.getShort("packetSerial"), root.getInteger("timeStamp"));
和服务器建议websocket 连接
websocket 服务器可以提供加解密 语音数据的功能,所以当麦克风回调数据的时候需要通过websocket 发给服务器,服务器会加密,然后再发回来,这样客户端就可以通过udp将 加密之后的数据发给whatsapp。 当udp接收到数据的时候同样需要 通过websocket 发给服务器解密, 解密之后又会发回来,然后就可以直接丢给扬声器播放。
主动打电话
客户端通过websocket连接到服务器,客户端发起语音通话请求,并且完成必要的协商之后,就可以直接将语音数据发送给服务器,服务器接收到对方的语音数据之后也会通过websocket将语音数据转发给客户端
1) 获取协商秘钥
XMPP 在发起语音通话请求的时候,需要带上一个秘钥,这个秘钥长32字节,通过特殊算法生成。这个算法需要三个参数:
1.自身jid
2.对方jid
3.时间戳(服务端自动获取,不需要生成)
//发送获取秘钥请求
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"
}
2) 发起XMPP 语音请求 1.发起语音请求。这个请求需要通过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; } 2.处理ack 回包。 发送完第一个包之后,服务器会返回一个ack包, 需要将这个ack包转成xml格式,然后通过websocket 发送给服务器 //xmpp 转xml 需要注意, 节点部分的值需要base64 之后再发过来, 如果是 16进制的字符串需要设置 result.put("format", 1) <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"); //如果节点内容是16进制字符串编码则需要 设置format //result.put("format", 1) // 用于测试的音频文件ID,固定,正式部署的时候需要换成上传的文件 result.put("file_uuid", "aee4d52d-6ba7-4a65-80d4-b7341b1115f0"); result.put("ack", "服务器回的ack包打包成xml格式"); SendCommand(result); 3.接收到的服务器的包必须回复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'/> 4.中转服务器会将一些需要发给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> 3) 发送/接收语音数据 @Override public void OnRecord(byte[] data) { //录音消息 //发送音频数据 JSONObject result = new JSONObject(); result.put("command", "SendAudio"); result.put("data", data); SendCommand(result); } private void HandleRecvAudioData(JSONObject root) { if (root.getBoolean("isRtp")) { if (recordAndPlayer != null) { recordAndPlayer.PutFrame(root.getBytes("data")); } } } 总结一下步骤: 1. 和中转服务器建立websocket 连接; 2. 从中转服务器获取加密秘钥; 3. XMPP 发送call 请求,并且接收服务器返回的ack, 特别需要注意期间会收到很多包,都需要回ack,上面也列出了一些需要回ack的包; 4. 将WA 服务器的ack包转成xml 格式发给中转服务器, 特别需要注意xml格式节点值需要base64 编码; 5. 中转服务器会主动发送一些xml数据, 客户端需要将这些xml数据转成xmpp包发给服务器。
|