制作自己的xmpp/gtalk客户端

本文作者的这个研究过程跟我的好像,我也在xmpp和libjingle之间辗转研究,用了xmppframework,libjingle,gloox,其中libjingle我是以前都研究过,所有基于它的P2P进行数据传输这部分没有问题,也是会遇到用户系统的问题,我们需要使用自己的用户系统,又不想建立自己的STUN,而xmppframework这个又实现的不完整而且也没什么资料,所以最后又使用gloox来实现消息与数据的传输。确切的来讲使用了先后使用了libjingle和gloox来实现了两个不同的项目的不同需求。


[转贴]制作自己的xmpp/gtalk客户端

1.[转贴]制作自己的xmpp/gtalk客户端Copy to clipboard
Posted by: netboy
Posted on: 2010-04-26 10:07

这里有很多xmpp的开源库

http://xmpp.org/software/libraries.shtml

之前我研究了一下QXmpp,是基于qt的

然后又顺带跑去研究了一下qt

先去qt网站上把sdk下载并安装好,然后用qt creater打开.pro文件进行编译

就可以运行里面的example了,这里记住就是安装路径和源代码路径都不能有中文或者空格

这里能看出qt多么小家子气

当然了,之前接触qt是通过python的qt库,这回仔细看了一下qt的sdk

已经QXmpp对qt的调用,还是感叹一下qt确实是做的很不错的

很多东西都封装的很好,并且最大的优势是跨平台,源代码只需要到对应平台的sdk上编译一下就可以使用了,不需要任何修改

---------------------------------------------------------------------------------------

但我还是想用纯c++来做,毕竟在qt上写程序有了通用性缺也得碍手碍脚

所以又找到了gloox 0.9.9.7,这个是纯C++的,并且用VS2008编译很方便,不需要任何修改

建立一个静态库项目,把src里的文件拷贝并包含进项目,就可以编译了,记住不能用定义为UNICODE,否则会出错

因为gloox使用的是UTF-8编码,所以只需要将数据转换成UTF-8再传给gloox处理就可以了,不需要把整个库都改成UNICODE

不过现在正在研究怎么用gloox连接到gtalk服务器,看上去没有OXmpp方便

------------------------------------------------------------

吃完盒饭,回来继续研究看问题出在哪

研究了一下gloox的源码,发现有点不太好的地方,Client类会把domain和server搞混

gtalk的domain应该是gmail.com,而server应该是gtalk.google.com

而Client类都用的同一个,所以之前没法连接,初始化以后再手动改下server地址就可以了

把message_example那个例子里的

JID jid( "hurkhurk@example.net/gloox" );
j = new Client( jid, "hurkhurks" ); 

改成下面的代码就可以了登陆了

j = new Client("username","password","gmail.com","gtalk",5222);
j->setServer("talk.google.com");

然后用其他账号给这个账号发个消息,会收到消息回复

2.Re:[转贴]制作自己的xmpp/gtalk客户端 [Re: netboy]Copy to clipboard
Posted by: netboy
Posted on: 2010-04-26 10:10

做了一个MFC对话框程序把gloox程序包装了一下



填写server domain username password等信息就可以登陆到不同的服务器了

现在已经试验了gtalk和renren网的都没有问题

右侧为好友列表

下面是收到的消息列表 以及文本信息输入框



大概的思路是,点击登陆 把用户输入的数据获取过来然后开启一个线程去启动gloox的client服务

收到消息以后把MessageSession发送给主界面,当界面点击发送按钮的时候调用MessageSession::Send发送消息 

3.Re:[转贴]制作自己的xmpp/gtalk客户端 [Re: netboy]Copy to clipboard
Posted by: netboy
Posted on: 2010-04-26 10:10

遇到问题了

虽然基本的消息发送接收已经完成了,但是最关键的语音部分还是有问题

之前我看google的文档有点误解了google的意思,我以为libjingle是一个xmpp的语音插件

所以先去找了gloox库来实现xmpp相关的内容,等功能完善了再来看libjingle,没想到libjingle是整个xmpp解决方案

完全就是gtalk的底层

所以把libjingle找出来研究了一番,问题很严重,里面采用的语音GISP库是不开源的,需要另外下载

那就没法移植到手机上了,但还是抱着学习的态度历经千辛万苦到网上把那个库找过来了

编译libjingle成功,运行call example,一旦打入或者打出电话程序直接异常终止了

难道GISP已经过期了?

无奈继续到网上找资料

1、gloox已经准备在下个版本中集成jingle了,但今年10月份才发布了1.0版本,这要什么时候才能等到下个版本啊。。。

2、有个德国人用libjingle+speex语音库做了一个demo叫myjingle,可惜相关的资源下载地址全部失效了

http://www.bluehands.de/software/beat/myjingle/



实在没办法的话自己动手来实现libjingle+speex吧

4.Re:[转贴]制作自己的xmpp/gtalk客户端 [Re: netboy]Copy to clipboard
Posted by: netboy
Posted on: 2010-04-26 10:11

自己动手编译libjingle

编译的过程中有几点需要注意:

库有冲突,需要把项目属性->C/C++->代码生成->运行库 改成多线程调试(/MTd)

使用libjingle中example里的xmppthread类进行登录测试,不成功,直接报Logged out,截包发现除了查了下DNS其他压根什么都没法出去

使用pcp那个例子登录可以成功,里面没有采用xmppthread类,而是自己的main函数中写了个类似xmppthread过程

但会把主线程堵死,我想把这个过程放到另外一个线程中而不影响程序的主线程

另外开个线程执行pcp的main,成功

登录成功

5.Re:[转贴]制作自己的xmpp/gtalk客户端 [Re: netboy]Copy to clipboard
Posted by: netboy
Posted on: 2010-04-26 10:11

研究了两天的libjingle

了解了sigslot消息机制,智能指针,多看看这种高手的作品收获颇多啊

这里是google文档的一个粗译文 http://cyclone.blog.ubuntu.org.cn/2008/01/23/libjingle-create-a-program/

大概懂了libjingle的运行机制,自己重新用mfc+libjingle写客户端,已经可以监听好友的上线下线状态了

但不知道要怎么发消息,例子里面只提供了文件传输和语言,真想不通为了不提供一个最基本的文本消息收发的例子

然后找了一整个下午相关的资料,翻遍了libjingle的源码,也没找到任何发消息相关的类、方法

难道libjingle连最基本的消息收发都要自己写?

貌似是的,自己集成一个talk类开始动工吧

自己写了一个MessageTask类来截获消息

主要是重写XmppTask这两个函数就可以了

bool MessageTask::HandleStanza(const XmlElement * stanza) {
if (stanza->Name() != QN_MESSAGE)
return false;
QueueStanza(stanza);
return true;


int MessageTask::ProcessStart() {
const XmlElement * stanza = NextStanza();
if (stanza == NULL)
return STATE_BLOCKED;
TextMessage msg; 
msg.from_jid = Jid(stanza->Attr(QN_FROM));
const XmlElement * body_element = stanza->FirstNamed(QN_BODY);
if( body_element == NULL)
return STATE_BLOCKED;
msg.body = body_element->BodyText();
SignalRecieveMessage(msg);
return STATE_START;
}

其中TextMessage也是自己写的一个容器类

发消息的话调用下面就可以了

buzz::XmlElement *message = new buzz::XmlElement(buzz::QN_MESSAGE);
message->AddAttr(buzz::QN_FROM,pump.client()->jid().Str());
message->AddAttr(buzz::QN_TO,status.jid().BareJid().Str());
message->AddAttr(buzz::QN_TYPE,buzz::STR_CHAT);
buzz::XmlElement* bodymsg = new buzz::XmlElement(buzz::QN_BODY);
bodymsg->AddText("I'm here");
message->AddElement(bodymsg);
pump.client()->SendStanza(message); 

真想不通,难道google为了保证文本消息这块的超强可定制性干脆就不写了留给别人去写? 

这可是XMPP最基本的功能

6.Re:[转贴]制作自己的xmpp/gtalk客户端 [Re: netboy]Copy to clipboard
Posted by: netboy
Posted on: 2010-04-26 10:12

libjingle基本消息发送功能已经完成了,正准备开始往手机上移至这部分功能了

结果。。。

我的目的就是想做一个通用的xmpp协议客户端,不只是gtalk,还有校内等等所有的xmpp协议服务器都能登录

我测试了一下,发现libjingle除了gtalk服务器,其他一概无法登录

网上也有说改saslplainmechanism.h文件的方法来支持其他服务器

我试了一下没用,根本就没调用到里面的功能就已经中断了

先是talk.renren.com来回一个stream:stream头就没下文了

然后是jabber.org,进行PLAIN验证的时候出错了,错误内容:SEC_I_INCOMPLETE_CREDENTIALS

发现libjingle里面写着这么一句// We don't support client authentication in schannel.



昨天和gloox那边通信过了,他们下个版本中准备支持jingle,但是没准备支持语音

语音的话还是得自己写,为了保证客户端的通用性我只能又回头用gloox了

绕了一个大圈,回到了原点

7.Re:[转贴]制作自己的xmpp/gtalk客户端 [Re: netboy]Copy to clipboard
Posted by: netboy
Posted on: 2010-04-26 10:12

重新到网上把最新的gloox1.0下下来

仔细的测试了一下,功能确实很强大,很方便,很易用

我的做法是开个子线程来运行gloox的消息循环,在handle函数中调用sendmessage把内容发给主线程

这里采用sendmessage的原因就是保证线程安全

虽然效率是没有postmessage高,但至少不会出问题

pc上的测试完善以后就开始动手去移植到wince上了

莫名其妙M8 SDK的time.h里面居然没有time这个函数

只能自己写了个mytime.h和mytime.cpp手动写了time函数加到项目中

接着全部编译错误清了以后来了链接错误

error LNK2019: 无法解析的外部符号 DnsRecordListFree

等等几个dns相关的函数全都出问题了

奇怪,到wince6和wince5的sdk目录下都找了一下,都有windns.h这个文件,但没有dnsapi.lib这个库

MSDN上是说在dnsapi.lib里的,难道是wince上收缩到其他库里面了?几乎把其他所有的库都加进去了还是一样报错

奇怪了,难道wince只提供了dns相关函数的定义头文件,没有lib的实现?



找到原因了,根据readme.wince的说明把config.win里面全部的设置项全部去掉了,其中就有一项HAVE_WINDNS_H是关键

去掉以后就编译成功了,但编译出来的只有dll没有lib

对比我新建的动态库项目和他自带的库项目发现时少了DLL_EXPORT这个预处理宏

加上以后再等个几分钟编译,终于成功了

8.Re:[转贴]制作自己的xmpp/gtalk客户端 [Re: netboy]Copy to clipboard
Posted by: netboy
Posted on: 2010-04-26 10:13

成功把gloox1.0在wince平台下编译,这里注意一点gloox动态链接库编译方式的时候有个bug,会提示gloox.h里面的那些常量 无法解析的外部符号 错误

没找到原因,简单的做法是在自己的项目里添加gloox.cpp

万事ok,写了个简单的窗口开始进行wince上的登录测试

问题一大堆

首先,因为wince上编译的时候把TLS支持给去掉了,不去掉会出错,而去掉了又没法登录gtalk服务器。。。

然后登录renren的时候,因为后缀不是服务器地址,所以要这样写

JID login_jid;
login_jid.setUsername(这里写校内全名账号);
login_jid.setServer("talk.renren.com");
login_jid.setResource("xmppforwince");
j = new Client(login_jid,m_sPassWord,m_iPort);
j->setServer("talk.renren.com"); 

// 

不支持TLS的问题,是因为wince6不支持两个函数 

InitializeSecurityContextA 

CertVerifyCertificateChainPolicy 

尝试了一下用第三方库去代理系统本身的TLS功能,GUNTLS库 

但对windows开放支持不是太好,主要是linux平台的东西,要研究又得浪费很多时间 

所有又把眼光放回着两个函数身上,因为比较奇怪的是wince6并不是完全没有这两个函数 

在头文件里面都写着的,但奇怪的是库文件里面没有,所以是导致的链接错误 

我想会不会可能系统本身的dll里已经带了呢 

于是把wince6内核解包,然后用010Editor工具搜 

果然找到了InitializeSecurityContextA函数在schannel.dll里面 

可是任然没有CertVerifyCertificateChainPolicy这个函数 

我看了下CertVerifyCertificateChainPolicy这个函数主要是进行证书认证返回一个bool 

就干脆把它注释掉了,应该影响不会太大 

然后用动态载入dll的方法去使用InitializeSecurityContextA函数 

HINSTANCE hInst;
hInst = LoadLibrary(L"schannel.dll");
typedef SECURITY_STATUS (_stdcall *INITPROC)(PCredHandle phCredential,
PCtxtHandle phContext, 
SEC_CHAR SEC_FAR* pszTargetName, 
ULONG fContextReq,
ULONG Reserved1, 
ULONG TargetDataRep, 
PSecBufferDesc pInput, 
ULONG Reserved2, 
PCtxtHandle phNewContext, 
PSecBufferDesc pOutput,
ULONG SEC_FAR* pfContextAttr, 
PTimeStamp ptsExpiry 
);
INITPROC Initlsc = (INITPROC)GetProcAddress(hInst,L"InitializeSecurityContextA");
ASSERT(Initlsc);
error = Initlsc( (PCtxtHandle)&m_credHandle,
(PCtxtHandle)0,
hname,
request,
0,
SECURITY_NETWORK_DREP,
0,
0,
&m_context,
&obufs,
&return_flags,
NULL );
FreeLibrary(hInst); 

好啦 这下gloox库终于能在wince6上支持TLS了

9.Re:[转贴]制作自己的xmpp/gtalk客户端 [Re: netboy]Copy to clipboard
Posted by: netboy
Posted on: 2010-04-26 10:13

由于gloox本身对于服务器地址和域名不同的情况支持不好

比如gtalk就是,服务器地址是talk.google.com而域是gmail.com

所以我准备自己来对gloox进行改造一下

把JID这个类彻底改了一下,就只需要username这个属性存储全部用户名,而废掉domain属性

因为单纯的修改几个提交domain的地方会导致身份验证加密那边会运算出错

等等等等改了好多地方,终于完美的支持了gtalk和人人网的登录了

然后整体程序的框架也出来了

现在主要是3个窗口:登录、主窗口、聊天

因为gloox的handle机制,就是首先对你关心的事件进行注册

然后再实现相关的借口,一些handle函数

当发生事件时,比如好友发来消息的时候,gloox就会自动调用messagehandle事件

所以只需要把自己的代码写在handle函数里让gloox线程调用就可以了

我之前的想法是另外写个类专门做handle的容器,然后再在handle事件发生时用消息的形式通知前台窗口

后来觉得这样太麻烦了,所以直接让窗口类继承了那些handle接口,直接处理

首先登录窗口就不需要了,登录窗口只是收集用户输入的登录数据然后告诉主窗口

主窗口去启动一个后台线程运行gloox

本来是准备把消息的处理的messagehandle直接注册到聊天窗口上去

但后来发现这样做会导致永远手不到别人发的第一条消息

原因是注册太慢,新建聊天窗口的过程太慢,导致消息已经过了,还没有注册handle函数

所以改成了全部由主窗口去接受所有人发的消息,用一个容量类装着,这个容器类里面主要是个map<string,list<msg>>

可以通过用户的JID去找到所有的聊天记录

新建聊天窗口的时候直接从map容器里面读取之前的消息,新收到的消息会有主窗口来处理,如果是当前聊天窗口的,就发送过来,不是就先储存起来

10.Re:[转贴]制作自己的xmpp/gtalk客户端 [Re: netboy]Copy to clipboard
Posted by: netboy
Posted on: 2010-04-26 10:13

基于gloox的xmpp for m8初步已经完工了

现在已经可以很方便的添加好友,聊天

下一步是做文件传输,但一开始就碰钉子了

gloox1.0里面提供的文件传输功能好像是xmpp协议本身的扩展

而gtalk支持的文件传输是jingle,虽然jingle也被xmpp作为扩展被接受,但是这是两个不同的东西

所以貌似gloox现在还无法给gtalk发文件

用psi试验了一下,确实psi无法和gtalk 人人桌面之前相互发送文件

但是psi自身之前是可以的,这说明不需要服务器的特殊支持,而只是客户端都同时支持就能够发现了

所以就算不能让xmpp for m8和gtalk 人人桌面之前文件传输的互通

但至少可以让都用xmpp for m8登陆的人之前传输文件,当然啦,还以让登陆psi或者其他第三方xmpp客户端之间发送文件

但国内基本上没什么人用,所以可以忽略



下面是文件传输试验过程中遇到的一些问题字节处理方便的问题和解决方案

最开始我是使用ofstream进行文件读写的,收到数据以后然后用ofstream<<data.c_str()的形式把字节流写入文件

但问题是传输文本文件没有问题,但传输图片的时候,本来13K的图片就剩下了4K

后来想明白了,是因为<<这种流写入的形式碰到0就会停止,而实际上还没写完data

所以改成了ofstream.write(data.c_str(),data.length())的形式写入

文件大小没有问题了,传过来以后也是13K,但图片确依然打不开

我用ultraedit仔细对比了源文件和接受后的文件,发现里面的10前面全被加了一个13

看样子是ofstream进行了自动的回车符替换了

然后知道是因为需要使用二进制文件读取的方式才能防止回车符自动被替换打开文件的时候加上这个属性fstream::binary

但结果依然不行,文本文件传输没有问题,但图片文件写入的时候还是会导致编码莫名其妙被篡改了

难道是因为utf-8和ANSI的问题?但因为我的程序中编码转换的模块没法转换中间含有0的字符串

所以只能放弃用ofstream改用

FILE *mStream

fwrite(data.c_str(), data.length(),1, mStream);

fclose(mStream);

这种形式,终于完美的传输了文件

-----------------------------------------------------------

上面是接受,下面是发送了

例子里面看上去也挺复杂的

gloox首先得绑定一下自己的IP,然后再gloox的主消息循环里面也写了一堆代码

但运行的时候有好几点要注意的

首先是gtalk的特殊性,会在绑定资源以后改写你的resouce,比如你要绑定的是"myresource"

而绑定成功后会改写成myresource0F91240D的形式

这个在gloox中有处理,会自动改写成服务器返回的resouce

但在例子中就有问题了,这句

f->addStreamHost( j->jid(), "211.103.98.203", 6666 );

在运行的时候jid的resouce还没有被改写,这时候就会造成503错误

所以我把这句放在了onConnect里面

然后是发送给对方的jid需要是全名的,需要包括资源名在内的,如果只写了bare,那么会报501错误

解决了上面的问题就可以成功发送文件了

11.Re:[转贴]制作自己的xmpp/gtalk客户端 [Re: netboy]Copy to clipboard
Posted by: netboy
Posted on: 2010-04-26 10:14

之前一篇文章写完后,发现只能两个客户端在同一网段内才能收发文件

原来bytestream这个协议没有穿透能力

如果需要跨网跨防火墙进行传输文件就必须要文件中转站(这不是废话嘛,不然不都用飞鸽去了)

socket5 bytestream 大概的工作原理是这样

首先进行一些前戏,发送文件方和接受方沟通沟通,协商了好了以后,发送方就会发起一个iq,内容如下 

<iq to='lomiou@jabber.org/proxy.netlab.cz' from=zhuliye@gmail.com/gtalk583984' id='uid:4b32d84e:000072ae' type='set' xmlns='jabber:client'>
<query xmlns='http://jabber.org/protocol/bytestreams' sid='uid:4b32d84e:0000701f' mode='tcp'>
<streamhost jid=zhuliye@gmail.com/gtalk583984' host='10.10.10.113' port='7777'/>
<streamhost jid='proxy.eu.jabber.org' host='91.121.109.155' port='7777'/>
</query></iq>

其中可以有多个streamhost节点,就是告诉接收方有这么些个文件中转服务器可以使用

首先别忘了把自己的jid 和ip地址写上去,因为你们可能处于同一网段,就没必要使用中转服务器了

然后另外再找一些可用的中转服务器

这里需要利用server discovery来寻找,具体请参考http://xmpp.org/extensions/xep-0065.html

找到以后把相关信息填上作为一个streamhost节点,多几个比较好,一个出问题其他的还能顶上



然后接收方收到这个xml流就会对streamhost一一尝试连接,直到找到一个可以连得通的

然后就会回复下面的内容给发送方
<iq from='bottlerun@jabber.org/proxy.netlab.cz' to='bottlerun@gmail.com/xmppform850286462' xml:lang='en' type='result' id='uid:4b32d84e:000072ae'>
<query xmlns='http://jabber.org/protocol/bytestreams'>
<streamhost-used jid='proxy.eu.jabber.org'/></query></iq>

这里就一个streamhost,就是告诉发送方自己的选择,然后发送方和接收方就会同时和这个streamhost建立连接,一边发一边收了



但使用SOCKS5Bytestream有个细节要注意,不使用中转直接发的话很正常,但如果用了中转的话就会出现一个问题

发送方是先把数据发送给中转服务器,而中转服务器再发送给接收方的,这里就会形成一个异步

发送方把数据发送完毕以后,按例子中就直接关闭连接了,而这时候接收方确还没有接受完

但又接受到发送方的断开连接的通知,因此就会导致传输出错

所以我改成了:发送方发送完毕后不接受,接收方直到接受

而这样还是会有问题,发送方如果不在发送完毕后断开连接,就不知道该在什么时候断开了

因为接收方接受完毕以后,即时关闭连接,发送方也不会受到任何通知(这里我也很奇怪,为什么发送方断开连接的时候,接收方也会断开,但接收方断开的时候发送方没任何通知)

就会导致发送方一直处于发送中状态

最后的做法是,在发送方使用send发送数据以后再调用一次recv(1),这样就可以检测到接收方是否已经关闭了

12.制作自己的xmpp/gtalk客户端12之In-Band和Out-of-Band [Re: netboy]Copy to clipboard
Posted by: netboy
Posted on: 2010-04-26 10:15

今天一直在看xmpp协议相关的扩展协议文件 http://xmpp.org/extensions

想找更多的关于jingle的资料,搞明白了一些事情

之前有点奇怪,jingle可以用来传输数据,包括多媒体,文件共享等等

但socket5 bytestream也是实现这个功能,那xmpp协议同时要这两个扩展干嘛?只是提供更丰富的连接方式吗?

后来搞明白,socket5 bytestream和jingle是两个不同的概念

jingle主要的作用是利用ICE和STUN等技术实现点对点的连接的一种解决方案

而连接方式和传输内容是分离的,socket5 bytestream就是其中一种tcp的连接方式,另外还有UDP、in-band等方式

而内容也可以是各种数据,文件 多媒体数据等等

理论上不需要任何服务器的额外支持,也不需要像单纯的使用socket5 bytestream那样需要中转服务器

只需要两个客户端都支持jingle,就可以实现点对点传输

如果连不上的话也可以利用第三方的stun服务器进行连接



这里又多出一个概念In-Band,可以翻译成“带内”,和它对应的是 Out-Of-Band“带外”

http://xmpp.org/extensions/xep-0047.html

所谓In-Band就是把需要传输的数据转换成字节码的形式用Xmpp协议的形式比如 iq 或者 message节点发出去

就是用的XMPP协议走的通道传输附加数据,所以称为带内

而“带外”就是socket5 bytestream这种形式,先利用xmpp进行协商,然后另外去建立一个socket连接

进行数据传输,这种一般是利用第三方的proxy中转服务器,好处是不占用xmpp服务器的资源

因为大家都用In-Band方式来收发文件的话,会把XMPP服务器拖垮的

所以文档中写到

Generally, in-band bytestreams SHOULD be used only as a last resort. SOCKS5 Bytestreams will almost always be preferable. 
A server MAY rate limit a connection, depending on the size and frequency of data packets. 
A server MAY disconnect a connection that sends overly large packets as defined by server policy. 
It is RECOMMENDED to use a 'block-size' of 4096. 


看了一下gloox目录下有InBandBytestream相关的类,以及一个ibb_excample,一会试试看怎么样



现在我的程序文件传输基本上用socket5 bytestream解决了

接下来是语言了,刚刚和gloox协议库的开发者交流了一下,下一个版本正在开发中并将支持jingle

但是需要等到明年2月到5月才能发布

看了一下jingle那么多复杂的技术ICE STUN等等

要自己来实现有点不太可能了,而现在正在开发的gloox源码中虽然已经有了不少jingle相关的类

但听gloox开发者说目前还没法用

所以我想先试试看In-Band方式来实现试试传输语言数据吧

这样首先就放弃了和其他客户端的通用性,当然目前好像也只有gtalk支持jingle吧,

其他的客户端我看连文件传输都没怎么支持好

比如psi的文件发送居然不支持中转服务器,同样的也不支持jingle,照样发布的好好的

上次看到一篇文章说语言每秒钟能压缩到5K就够了

如果In-Band支持不了那么大流量的话也可以进行一些功能上的缩减

比如把实时的语音换成对讲机的形式,按住键说一句话,对方等传完的听完了再按住键回复一句话这样子

当然啦,如果In-Band实在没法支持也可以利用传输文件用的那些个服务器来进行数据中转

根据今天和别人手机对发消息的经验来看,速度还是不错的,GPRS本身速度也是个限制

但问题是延迟比较大,所以还是实时性会比较差



如果网络方面的问题都解决了,接下来就是要去解决语言的采集、压缩和播放了

13.NAT穿越原理——STUN [Re: netboy]Copy to clipboard
Posted by: netboy
Posted on: 2010-04-26 10:16

NAT穿越原理——STUN
知道了jingle是采用STUN技术来实现点对点的连接,因此到网上把STUN相关的资料找了过来 

转自:http://www.yuanma.org/data/2007/0323/article_2446.htm 

STUN是RFC3489规定的一种NAT穿透方式,它采用辅助的方法探测NAT的IP和端口。毫无疑问的,它对穿越早期的NAT起了巨大的作用,并且还将继续在ANT穿透中占有一席之地。 

STUN的探测过程需要有一个公网IP的STUN server,在NAT后面的UAC必须和此server配合,互相之间发送若干个UDP数据包。UDP包中包含有UAC需要了解的信息,比如NAT外网 IP,PORT等等。UAC通过是否得到这个UDP包和包中的数据判断自己的NAT类型。 

假设有如下UAC(B),NAT(A),SERVER(C),UAC的IP为IPB,NAT的IP为 IPA ,SERVER的 IP为IPC1 、IPC2。请注意,服务器C有两个IP,后面你会理解为什么需要两个IP。 

(1)NAT的探测过程:(吃个芒果先,呵呵,老妈给的) 

STEP1:B向C的IP1的pot1端口发送一个UDP 包。C收到这个包后,会把它收到包的源IP和port写到UDP包中,然后把此包通过IP1和port1发还给B。这个IP和port也就是NAT的外网 IP和port(如果你不理解,那么请你去看我的BLOG里面的NAT的原理和分类),也就是说你在STEP1中就得到了NAT的外网IP。 

熟悉NAT工作原理的朋友可以知道,C返回给B的这个UDP包B一定收到(如果你不知道,去读下我的其它文章)。如果在你的应用中,向一个STUN服务器发送数据包后,你没有收到STUN的任何回应包,那只有两种可能:1、STUN服务器不存在,或者你弄错了port。2、你的NAT拒绝一切UDP包从外部向内部通过(我们公司的NAT就是)。 

当B收到此UDP后,把此UDP中的IP和自己的IP做比较,如果是一样的,就说明自己是在公网,下步NAT将去探测防火墙类型,我不想多说。如果不一样,说明有NAT的存在,系统进行STEP2的操作。 

STEP2:B向C的IP1发送一个UDP包,请求C通过另外一个IP2和PORT(不同与SETP1的IP1)向B返回一个UDP数据包(现在知道为什么C要有两个IP了吧,虽然还不理解为什么,呵呵)。 

我们来分析一下,如果B收到了这个数据包,那说明什么?说明NAT来着不拒,不对数据包进行任何过滤,这也就是STUN标准中的full cone NAT。遗憾的是,full cone nat太少了,这也意味着你能收到这个数据包的可能性不大。如果没收到,那么系统进行STEP3的操作。 

STEP3:B向C的IP2的port2发送一个数据包,C收到数据包后,把它收到包的源IP和port写到UDP包中,然后通过自己的IP2和port2把此包发还给B。 

和step1一样,B肯定能收到这个回应UDP包。此包中的port是我们最关心的数据,下面我们来分析: 

如果这个port和step1中的port一样,那么可以肯定这个NAT是个CONE NAT,否则是对称NAT。道理很简单:根据对称NAT的规则,当目的地址的IP和port有任何一个改变,那么NAT都会重新分配一个port使用,而在step3中,和step1对应,我们改变了IP和port。因此,如果是对称NAT,那这两个port肯定是不同的。 

如果在你的应用中,到此步的时候PORT是不同的,恭喜你,你的STUN已经死了。如果不同,那么只剩下了restrict cone 和port restrict cone。系统用step4探测是是那一种。 

STEP4:B向C的IP2的一个端口PD发送一个数据请求包,要求C用IP2和不同于PD的port返回一个数据包给B。 

我们来分析结果:如果B收到了,那也就意味着只要IP相同,即使port不同,NAT也允许UDP包通过。显然这是restrict cone NAT。如果没收到,没别的好说,port restrict NAT. 

(2)SIP怎么使用STUN 

个人认为这是个很不值得提的问题,不过有许多人问我,还是简要提一下。其实这是个很简单的问题,SIP通过STUN得到NAT的外网IP和SIP的信令监听端口的外网port,替换SIP注册包中的contact头中的IP和port,然后注册。这样就可以确保当有人呼叫你的的时候注册服务器能找到你。需要提醒你的是,NAT发现一个连接超过一段时间后没有活动,它就会关闭这个影射,因此你必须间隔一端时间发送一个数据包出去以keep alive。 

另外,当你要和别人建立RTP通讯的时候,不要忘记把你的SDP中的IP和PORT改成公网IP和PORT。 

也许文章到这里就可以完了,不过很遗憾,显然我们还有工作没有完成,那就是对称NAT。在我后面的文章中,我会对对称NAT对一些探讨,希望你关注我的BLOG,谢谢。

14.制作自己的xmpp/gtalk客户端13之语言压缩——牛X的speex [Re: netboy]Copy to clipboard
Posted by: netboy
Posted on: 2010-04-26 10:16

要实现voip需要解决的几个问题

语言的采集、压缩编码、传输、解码、播放

前面已经讨论过了传输可以先尝试用In-Band或者类似文件中转的方式试试,暂时不考虑比较难实现的jingle

昨天也运行了一下In-Band的例子,用gtalk的服务器也能传输数据,每个包大概限制在4K的样子

而奇怪的psi不支持这种传输方式,自己用两个客服端互相发没问题

暂时先用In-Band作为语言传输方式试试吧,因为简单,不需要第三方支持,也不需要穿透NAT之类的

但大规模应用的肯定要把google的服务器给拖垮了吧

所以只是作为一个目前试验阶段的载体



之前在研究libjingle的时候层找到了Myjingle的源码,是德国人写的一个MFC对jingle的简单封装

说是简单。。。其实很复杂,我看了老半天也没怎么看懂,语音那块太复杂了

但里面有一点非常不错,就是替换掉了libjingle本身用的GISP收费语音库,而是用了免费的speex库

今天又把speex库拿出来研究了一番

充分体会到了什么叫神奇啊

我先用windows自带的录音机分别录了一段说话和一段唱歌,两个文件都是10秒钟,大小都是450多K左右,325kpbs

然后用speex尝试不同的压缩率

采用默认的压缩编码方式,8kHz 质量 8 ,压缩完以后是50K左右,然后解码后播放

不管是说话还是唱歌,都非常清晰,完全听不出有任何任何压缩的痕迹

然后换成16kHz 质量8,压缩完以后是27K的样子(不要问我16kHz为什么压缩后比8kHz要小,我也没明白),解码后播放

依然都很清晰

再换成32kHz 质量2,压缩后只有8K多(天哪!真小),解码后播放

会有杂音,但是声音本身基本没什么很大的变化,在每个字收尾的时候特别是唱歌时候拖同一个音比较长的时候会有点噪音(怪不得说speex不适合压缩歌曲,而只适合压缩说话内容),但依然还是算很清晰的,完全是可以接受的范围

至少比手机打电话时候的音质要好

最后换成32kHz 质量0,压缩后只有5K,解码后播放

这里就很明显的听得出来压缩的痕迹了,杂音很重,声音也很多走样了,虽然还是能明白说话的内容,但体验太差了,放弃



而另外有个属性bitrate没太理解,感觉调了对大小和音质都影响不大

根据上面的结果,采用32kHz 质量2来压缩,完全够voip使用了

这里来计算一下,10秒的声音数据,被压缩到8K,也就是说每秒只有1K不到

之前有听说比较成熟的GSM,也就是现在我们的手机传输量就是1K/S左右

语言数据数据是1K,再加上一些传输协议的消耗,怎么也就是1K多点

现在手机的GPRS速度,几K还是很轻松能达到的,我用的E网,更是经常能达到20K速度,最快的时候甚至能突破30

所以基于2G手机的GPRS来实现VOIP,传输速度绝对不会算是个瓶颈

主要还是高延迟,体验方面可能会比较差



下面要做的是自己写程序来实现编码,之前测试是用speex提供的程序

然后把speex移植到wince上,好像speex本身就是跨平台产品

(本来就是把一堆0101运算成另一堆0101的程序,没涉及到任何和系统有关的东西,当然很容易跨平台啦)

还有就是语言采集和播放,到网上找了一个源码利用waveInAddBuffer相关的函数,还挺好用的

而且在手机上也运行成功了

那么多人骂微软,却都看不到微软的优点啊,pc上的程序,几乎只要进行很少的修改,甚至不用修改,就可以直接用wince的sdk编译通过

这才是大公司大手笔啊,通用性这么高,大大减少了开发者的时间,而且文档,函数都是通用的,学习也很方便

15.基于XMPP协议的网络游戏开发 [Re: netboy]Copy to clipboard
Posted by: netboy
Posted on: 2010-04-26 10:17

今天突然就想到的点子

之前看到google app engine已经提供xmpp协议的支持的了,我想了好久也没想到可以用这个干吗

服务器上随时开着一个gtalk账号,客户端发送消息过去,服务器进行一些处理然后再可以回复一些消息

就这样可以干嘛呢,一些服务提供?输出一个列表,告诉客户端输入1可以看什么,输入2可以干嘛,输入m返回主界面?

客户端通过向某个服务器上开着的好友发送消息来更新自己的微博?

总举得这些个功能有点鸡肋

但前端时间看到说google wave也是基于xmpp协议的,只不过不像gtalk,xmpp协议只是作为底层,用于当做消息的载体

而更高一层另外还有协议

所以我突然想到能不能我也来把xmpp协议作为一个载体,另外去开发一层更高层的协议实现游戏通信呢

大概原理是这样

用户采用gtalk账号登陆游戏客户端(因为现在gae只支持gtalk账号)

登陆成功后列出房间列表,用户选择房间1,则自动加“room1@gmail.com”为好友

room1@gmail.com收到用户添加好友的消息,就知道有用户进房间了

room1@gmail.com就把当前房间所有的游戏桌信息通过xmpp消息的形式发送给用户(用户是几个几个在一个桌子上玩的)

并且还可以发送一些其他消息,比如所有在线的好友列表(既当前房间的在线玩家),发送给该用户

客户端把收到的该房间所有的游戏桌状态列表给用户看,用户选择一个游戏桌选择进入,客户端发送一条消息告诉room1@gmail.com用户选择的房间

room1@gmail.com就把用户选择的游戏桌上所有的玩家列表发送给用户

当所有的玩家都把准备完毕的消息告诉room1@gmail.com以后,room1@gmail.com就会给每个人发送一条消息告诉大家游戏开始

到这里为止,服务器的任务就OK了,一直到游戏结束前,服务器都不需要再和这个游戏桌上的用户交互

玩家之间自己相互发送消息就可以了

比如飞行棋,玩家1掷骰子点数是3,然后就分别发3条消息给3个对手,告诉对方自己的点数

并且把选择走的棋子发消息告诉3个对手,只需要这6条消息,就完成了一个玩家的操作过程

然后其他几个玩家的客户端根据收到的消息来绘制界面就行了

游戏结束后,玩家发一条消息告诉room1@gmail.com已经结束游戏,room1@gmail.com再把最新的房间内游戏桌信息发送给客户就行了



这里的优点很明显:

用户和服务器端的交互很少,服务器只负责帮用户找到玩家,接下来只需要玩家之间相互通信就可以了

大大减少了服务器的负担(当然没有减轻gtalk服务器的负担,不过gtalk应该还是能抗住的)

成本很简单,初期运营的时候google app engine免费的服务器会非常方便,当然gtalk也是免费的



缺点也很明显:

对游戏有要求,只能是那种对实时性要求不高的游戏,比如飞行棋,扑克牌,象棋,五子棋等等

因为xmpp传输需要消耗的时间还是挺长的



还有个问题就是room1@gmail.com的好友是有上限的,所以我想到的做法是用户加入房间既加为好友,用户退出房间既删除好友

这样保持所有好友都是在线的,好友上限正好是房间上限,一个房间几百个人还是够了的

但不知道这么频繁的增删好友会不会被gtalk给盯上啊

16.制作自己的xmpp/gtalk客户端14之语言的采集和编码 [Re: netboy]Copy to clipboard
Posted by: netboy
Posted on: 2010-04-26 10:17

一直尝试着自己去手动写speex的编码和解码

因为我觉得没有必要搞跟speex自带的编码解码程序那么复杂

这里我使用的是speex1.0.5的源码版本

首先把libspeex项目用vs2008直接打开编译一下就可以了,如果要编译那两个编码解码的项目需要下载ogg库,是一种文件格式库

拿到所有的头文件和lib文件拷贝到程序目录下面

用windows录音机录一个几秒钟的语言文件

然后编写语言编码代码如下

FILE *fin,*fout;
short in[MAX_FRAME_SIZE];
float input[MAX_FRAME_SIZE];
char cbits[MAX_FRAME_SIZE];
int nbBytes;
void *state;
SpeexBits bits;
state = speex_encoder_init(&speex_nb_mode);//设置压缩模式
int quality=8;
int frame_size;
speex_encoder_ctl(state, SPEEX_GET_FRAME_SIZE,&frame_size );//这个值获取到一般都是160
speex_encoder_ctl(state, SPEEX_SET_QUALITY, &quality);//设置压缩质量
fin = fopen("hello.wav", "rb");//打开语言文件
fout = fopen("encode.speex","wb");//创建要输出的编码文件
int format,rate,channels,size;
char file_type[12];
fread(file_type, 1, 12, fin);//把wav文件头12个字节读出来
read_wav_header(fin,&rate,&channels,&format,&size);//分析wav头文件,获取相关的信息,默认rate是22050,channels是1,format是16
speex_bits_init(&bits);
while (1)
{
fread(in, sizeof(short), frame_size, fin);//读取一段出来
if (feof(fin))
break;
speex_bits_reset(&bits);//初始化SpeexBits 
speex_encode_int(state, in, &bits);//最关键的一句,进行编码,输出到bits
nbBytes = speex_bits_write(&bits, cbits, MAX_FRAME_SIZE);//把bits拷贝到cbits数组
fwrite(&nbBytes, sizeof(int), 1, fout);//将把该段编码后的大小写入文件,按我这种配置编码后是38
fwrite(cbits, 1, nbBytes, fout);//将编码后的该段内容写入文件
}
speex_encoder_destroy(state);//清理
speex_bits_destroy(&bits);
fclose(fin);
fclose(fout); 

下面是解码代码,跟上面一样的就不多说了

FILE *fin,*fout;
float output_frame[MAX_FRAME_SIZE];
char cbits[MAX_FRAME_SIZE];
short char_out[MAX_FRAME_SIZE];
void *dec_state;
SpeexBits bits;
speex_bits_init(&bits);
dec_state = speex_decoder_init(&speex_nb_mode);
int quality=8;
int frame_size;
speex_decoder_ctl(dec_state, SPEEX_GET_FRAME_SIZE,&frame_size );
speex_decoder_ctl(dec_state, SPEEX_SET_QUALITY, &quality);
fin = fopen("encode.speex", "rb");
fout = fopen("decode.wav","wb");
int format,rate,channels,size;
format = 16;
channels = 1;
rate = 22050;
size = 0;
write_wav_header(fout, rate, channels, format, size);//写入文件头
speex_bits_init(&bits);
while (1)
{
int this_frame_size;
fread(&this_frame_size, sizeof(int), 1, fin);//读取该段大小
fread(cbits, 1, this_frame_size, fin);//读取编码
if (feof(fin))
break;
speex_bits_read_from(&bits, cbits, this_frame_size);//将编码内容写入bits
speex_decode_int(dec_state, &bits, char_out);//解码
fwrite(char_out, sizeof(short), frame_size, fout);
}
speex_bits_destroy(&bits);
speex_decoder_destroy(dec_state);
fclose(fin);
fclose(fout);

这里是把事先录好的声音文件转码,然后再把转码文件解码

接下来要做的就是做成实时录音转码,然后再试试解码并播放了

录音要用到waveInAddBuffer和相关的函数,功能是把音频数据输出到指定内存块

而播放要用到waveOutWrite相关的函数,功能是把内存中的音频数据发送给声卡进行播放

用这两个函数的时候一定要用缓存技术,我的做法是建立10个WAVEHDR结构

初始化的时候先把这10个结构都填上数据,然后再不断的循环去读取音频数据

for(;;){
while (pWaveHdr[nhdr]->dwFlags & WHDR_DONE){
printf("do some output: %d!\n", nhdr);
int offset, writed=0;
int length = pWaveHdr[nhdr]->dwBytesRecorded;
for (offset = 0; writed >= 0 && offset < length; offset += writed){
writed = fwrite(pWaveHdr[nhdr]->lpData+offset, 1, length-offset, fp);
printf("write: %d\n", writed);
}
error = waveInAddBuffer(hWaveIn, pWaveHdr[nhdr], sizeof(WAVEHDR)); 
if (error != MMSYSERR_NOERROR){
printf("waveInAddBuffer error\n");
return -1;
}
nhdr = (nhdr+1)%HDRCOUNT;
}
}

这里就利用pWaveHdr[nhdr]->dwFlags这个标志,因为waveInAddBuffer和waveOutWrite都是异步的

就靠判断pWaveHdr[nhdr]->dwFlags是不是等于WHDR_DONE来得知是否执行完毕

播放声音也是一样,先在初始化的时候先调用waveOutPrepareHeader再把dwFlags 标志全置为WHDR_DONE,

for(;;){
while (pWaveHdr[nhdr]->dwFlags & WHDR_DONE){
printf("do some output: %d!\n", nhdr); 
if(feof(fp))
break;
error = waveOutWrite(waveout, pWaveHdr[nhdr], sizeof(WAVEHDR));
if (error != MMSYSERR_NOERROR){
printf("waveOutWrite \n");
return -1;
}
int size = fread(pWaveHdr[nhdr]->lpData,1,frame_size,fp);
pWaveHdr[nhdr]->dwBufferLength =size;
nhdr = (nhdr+1)%HDRCOUNT;
}
}

17.制作自己的xmpp/gtalk客户端15之语音功能移植到wince上 [Re: netboy]Copy to clipboard
Posted by: netboy
Posted on: 2010-04-26 10:18

PC上将语音采集、编码、解码、播放功能全做好以后开始往wince平台移植了

移植的过程困难重重啊

首先就是采集,我之前有一篇文章还夸微软大公司,产品通用性好呢

结果奇怪的是,我前一天把语音采集用到的相关wavein的那段代码在wince上运行还好好的,录音完美

但第二天怎么试都不行了,只录下来了雪花声,根本没录下来

试了很多方法,网上也找了很多源码,但就是怎么都不行,尝试了各种方法整整一天

最后没办法了想用directsound试试,不过后来又想起来M8的SDK里面有提供录音机相关的COM接口IRecord

可以录音,但可定制性太差了,那个接口就是会自动把录音结果保存到一个固定目录下生成一个文件

不过还好中途读取的声音会以消息的形式发送给windows,我拿到消息进行编码就可以了

而他自动生成的录音文件我只能在每次聊完以后删掉了



采集终于算是解决了,然后编码的时候碰到一个另一个大麻烦

手机的CPU浮点运算能力太差了,在PC上,对400多K 10秒钟的一个WAV文件用8的质量进行编码大概是700毫秒,0质量是180毫秒

而直接移植到手机上同样的文件进行编码,8质量需要130秒(2分钟多),0质量需要40多秒

天哪!10秒钟的语音数据需要130秒来编码,这还得了,离实际需求差远了

说明speex里面基本上都是浮点运算

无奈只能到网上找资料,看有没有办法能提速,而且不只是提一点点速度,也看到有不少人说到这个问题

最后在speex官方网站上终于找到了解决方法,我之前用的是1.0.5版的speex

而1.2beta3版中加入了禁用浮点相关API的设置,我真庆幸我是在不是在1.0.5版发布之后1.2版发布之前来开发这个软件的

(有这个感叹是因为gloox,我现在是在1.0版之后和1.1版之前发布这个软件的,而gloox要1.1版才能支持jingle,所以在那之前我只能自己想把法解决语言传输问题)

把最新的1.2rc1版源码下载下来,在宏定义里面加上DISABLE_FLOAT_API和DISABLE_VBR禁用掉浮点运算

用WM5的SDK编译以后再重新编译了一下测试用的程序

速度果然是突飞猛进啊,采用speex_uwb_mode质量8编码同一个文件是2秒,0质量只需要900毫秒

基本上能满足要求了



接下来编写了一大堆代码,终于可以实现电脑上说话手机上接受了,并且GPRS也没问题,说明带宽完全够了

但现在因为使用的国外的中转服务器进行收发,速度慢到无法想象。。。

测试了一下,大概延时在20S都30S之间,也就是说你说句话,对方20秒后能接受

然后又试了试In-Band方式,延时大概10S左右

但至少有点GPRS和普通网络的延时几乎一样,说明带宽已经完全不算个问题了

但由于禁用掉了浮点运算,音质要比PC上压缩的差,杂音比较多

但能听清楚对方说什么,可能还要试试看speex的杂音消除功能

18.制作自己的xmpp/gtalk客户端16之gloox双向socket5proxy数据传输 [Re: netboy]Copy to clipboard
Posted by: netboy
Posted on: 2010-04-26 10:18

本来是打算另外建立一个窗口,建立一个新的SIProfileFT单独对语言的数据进行管理

结果发现gloox对多个SIProfileFT支持不太好,因此暂时还是把语言传输的功能结合到文件传输当中

通过文件类型这一个属性来区分是语音还是文件,自己瞎编了一个类型叫binary/speex-voice-stream

另外解码和编码还是非常耗时的,为了不对主线程影响,单独建立了一个线程用来解码编码和播放



因为采用类似文件传输的功能来发送语言,因此我最开始的想法是建立两个连接一个用来收,一个用来发

后来发现自己很傻,这不是socket连接嘛,本来就支持双向,一个连接就够了

然后昨天pc上写了个收发测试程序,果然成功了,双方既能发又能收

结果今天把wince上接受语言的功能做的差不多,准备做发送的时候,不管怎么发数据,另一方就是收不到

用单步跟踪进去,发现对方其实是收到了数据的,只不过因为对方是连接发起者,所以ConnectionSOCKS5Proxy的状态m_s5state一直处于S5StateDisconnected

然后就把收到的数据给丢弃了,所以就导致了发起方只能发送数据,而不能接收数据

可以理解,因为这个类本来就是给文件传输做的,而文件传输的发起方是不需要接受数据的

但奇怪的是,昨天我是怎么试验成功了,我把源码尽量改回昨天的样子,但还是不行,真见鬼了

看样子以后还是多做手记,把当时详细的过程写清楚

不然老碰到这样的问题,上次在wince上录音就是,第一次录音成功后以后再也没成功过

没办法只能自己手动试试看来改改gloox的源码

找到connectionsocks5proxy.cpp里面的ConnectionSOCKS5Proxy::handleReceivedData方法

这里有个switch判断状态,并且没有对S5StateDisconnected的处理

所以自己手动在switch的最后面加上

case S5StateDisconnected:
m_handler->handleReceivedData( this, data ); 

应该没什么影响



接下来再解决了一些语言编码方面的问题后,基本的语音通话功能就算完成了

把软件发给群上面一些人帮忙测试了,完全没有问题

GPRS也很流畅,除了延迟会比较夸张以外

19.制作自己的xmpp/gtalk客户端17之语音延迟问题 [Re: netboy]Copy to clipboard
Posted by: netboy
Posted on: 2010-04-26 10:19

这里说的语音延迟问题不是网络延迟,那个取决于网络状况,基本上是固定的,除非换个传输方法

这里说的语音延迟问题造成的原因是这样:

A发送说了十秒钟的话,网络延迟是3秒

那么正常情况B会在3秒后开始听到这句话,并在13秒的时候听完

但如果这时候在第8秒的时候,B的网络卡了1秒(这种情况出现很正常)

那么A说的后面5秒的内容,B会在9~14秒听到

那么这里问题就出来了,如果多卡几次,B听到的内容延迟就会越来越大,缓冲区里面的数据也会越来越多

但是后面收到的数据又必须等到之前收到的数据被播放完以后再播放

所以结果就是延迟会越来越长

那么解决这个问题有下面这些办法

1、因为我现在的这款软件本来就是采用中转式的传输,本来就延迟很慢,很难满足正常通话要求,干脆换成对讲机的形式,就不会有这种情况出现了,按住一个键说话,松开话就被发送出去了,这样本来就是异步的

JS:我还是觉得对讲机不太友好,争取努力解决延迟问题,实在不行的话作为最后的选择吧

2、丢弃掉那些延迟的包,就比如说刚才的问题,B在9秒同时收到了A在5秒和6秒说的内容,这时候直接把5秒的包丢了,播放6秒的内容,用这种方法来赶上对方的说话速度

JS:这种方法固然能解决延迟越来越长的问题,但问题是某些内容被丢弃了,用户体验会很差,老是莫名其妙少了一句话,会让人抓狂的

3、如果积累了很多过多的包,则不播放那些没声音的包。这个方法就是利用人说话的空隙时间,接收方收到了过多的包,则说明出现了网络延迟的问题,这时候去分析包,如果没有声音,就干脆不播放直接丢弃,去播放后面的包,以此来赶上说话放的速度。

上面的方法中我最终选择了第三种,因为首先不会影响用户体验,只丢弃那些没声音的包来空出时间,利用对方不说话那段时间把速度赶上来

谁会无止境的说上几个小时呢是吧

但也有弊端,就是背景太过嘈杂的话,就不好分辨了,无法得知对方对方是不是在说话,但这个问题暂时不考虑吧

最开始我是想去把包解码然后分析wave数据,求这个包的平均值,如果平均值低于某个零界则认为是无声包,丢弃

要做到这个功能其实还挺简单的,因为wave数据还是很好看懂的

不过后来找到了更好的办法,那就是speex本身提供的 静音检测VAD 这个选项来做

静音检测(VAD)将检测被编码的音频数据是语音还是静音或背景噪声。这个特性在用变比特率(VBR)进行编码是总是开启的,所以选项设置只对非变比特率(VBR)起作用。在这种情况下,Speex检测非语音周期并对用足够的比特数重新生成的背景噪声进行编码。这个叫“舒适噪声生成(CNG)”。

int dtx = 1;
speex_encoder_ctl(state, SPEEX_SET_VAD, &dtx);

我跟踪看了一下,不开启这个选项的时候,每个包都是固定大小,如果开启的话,有的包会是15字节,有的则只有2字节

所以我想当积累的过多的包时,直接丢弃掉只有2字节的包,当然现在还是在理论阶段,能不能成功还得试验

另外还有两个与此相关的功能 变比特率(VBR)和 非连续传输(DTX)

变比特率是比较重要的功能,默认情况下speex压缩后每个包大小都是固定的,如果采用了变比特率那么会根据每个段内实际的语音内容而压缩出不同长度的内容

非连续性传输(DTX)是静音检测(VAD)/变比特率(VBR)操作的额外选项,它能够在背景噪声固定时,完全的停止传输。如果是基于文件的操作,由于我们不能停止对文件的写入,会有5个比特被用到这种帧内(相对于250bps)。

如果这三个选项开启,能够极大的减少编码后的数据长度,我测试了一下,大概减少了一倍左右

不过可惜,因为wince上因为性能原因我把浮点运算禁用掉了,而变比特率完全是基于浮点运算的,因此也得禁用掉

不过只开启静音检测和非连续性传输的话也能一定量的减少传输量



pc上测试没有问题后我就去看wince平台上表现怎么样了

结果发现根本就完全没反应,加了VAD和DTX特性后和没加效果一样

后来想起来因为M8上的浮点运算能力有限,所以禁用掉了浮点运算,而VBR是基于浮点运算的,因此得一起禁用

而在到网上找了下资料发现VAD和DTX都是基于VBR的

这下难道又进死胡同啦,难道wince平台上就没法使用VAD特性?

我重新把speex说明书上面关于CPU性能优化那段拿了出来好好看了一下

The single that will affect the CPU usage of Speex the most is whether it is compiled for floating point or fixed-point. If your
CPU/DSP does not have a floating-point unit FPU, then compiling as fixed-point will be orders of magnitudes faster. If there
is an FPU present, then it is important to test which version is faster. On the x86 architecture, floating-point is generally
faster, but not always. To compile Speex as fixed-point, you need to pass –fixed-point to the configure script or define the
FIXED_POINT macro for the compiler. As of 1.2beta3, it is now possible to disable the floating-point compatibility API,
which means that your code can link without a float emulation library. To do that configure with –disable-float-api or define
the DISABLE_FLOAT_API macro. Until the VBR feature is ported to fixed-point, you will also need to configure with
–disable-vbr or define DISABLE_VBR. 

这才想起来好像当时在编译speex1.2rc1版时确实看到一个宏定义叫fixed-point,当时也没在意

说不定以M8上这么强大的CPU运行定点数也完全够呢

于是把DISABLE_FLOAT_API和DISABLE_VBR特性删掉以后再次编译了一遍libspeex

然后再次运行wince上的程序,发现速度很快,几乎和禁用掉浮点运算速度差不多

而且因为有了VBR的特性,VAD和DTX都运行得很好

我拿了一个826k 19秒的一个wav文件做测试,因为为了测试静音检测功能,所以19秒中只稍微讲了几句话,其他都是没有声音

统一都采用了speex_uwb_mode的压缩方式

平台 speex版本 不开启任何特性 不开启任何特性 开启VAD和DTX 开启VAD和DTX 开启VAD和DTX和VBR 开启VAD和DTX和VBR 
质量8 质量2 质量8 质量2 质量8 质量2 
PC 1.0.5 50K 17K 32K 12K 24K 24K 
WINCE 1.2rc1 40K 16K 19K 8K 16K 16k 



这里测试结果可以看出,不管是高质量还是低质量,VAD和DTX这两个属性都可以减少压缩后的数据量

而VBR这个属性在高品种的时候可以减少数据量,但低品质的时候反而增加了数据量

而且在wince上加入了VBR以后运行速度明显要慢一两秒

最后再汗一个800K压缩到8K,压缩了100倍,而解压后仍然很清晰,技术的力量真强大啊
参与评论 您还未登录,请先 登录 后发表或查看评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
©️2022 CSDN 皮肤主题:大白 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值