在这一篇里记录下在Java sip softphone的基础上添加播放音乐文件的功能。前面介绍了几款sipphone,发现Java sip softphone这款开源软件功能简单易于修改,根据自己的需要选择是否保留其GUI,然后修改少部分代码即可实现在拨号后播放指定的音乐文件。但是仍然有几个问题有待以后解决:1是对整个源码的框架流程的分析,目前我也只是删除了其GUI部分,而底层的sip rtp传输没有涉及;2是我下载的版本在挂断、远端忙等情况时有问题;3是播放声音文件目前只是使用默认支持的wav格式,其它格式可以扩展,但是我实现的方式应该还是有问题,因为获取发送声音数据的间隔还没有确定,只是自己测试的20ms,后面会看到。
   要播放声音文件,首先得找到是从哪里获得数据的。在源码书中看到net.sourceforge.peers.media包,类Capture中看到buffer = soundManager.readData();,进而查看类SoundManager,可以发现:openAndStartLines()、 readData() 、writeData()等函数,正是我们需要的麦克风音频数据的捕获和写入扬声器的方法。查看readData()方法如下:

/**
     * audio read from microphone, read all available data
     * @return
     */
    public synchronized byte[] readData() {
        if (targetDataLine == null) {
            return null;
        }
        int ready = targetDataLine.available();
        while (ready == 0) {
            try {
                Thread.sleep(2);
                ready = targetDataLine.available();
            } catch (InterruptedException e) {
                return null;
            }
        }
        if (ready <= 0) {
            return null;
        }
        byte[] buffer = new byte[ready];
        targetDataLine.read(buffer, 0, buffer.length);
        if (mediaDebug) {
            try {
                microphoneOutput.write(buffer, 0, buffer.length);
            } catch (IOException e) {
                logger.error("cannot write to file", e);
                return null;
            }
        }
        return buffer;
    }


去掉调试信息,结合类Capture中的代码,发现这里仅是通过buffer将数据获取,然后通过PipedOutputStream类型的rawData将数据发送出去;因此只要在readData()中使buffer返回需要发送的声音文件数据即可。
   根据以上分析,剩下的就是读取声音文件;java默认支持AU、AIFF、WAVE、MIDI 四种声音格式,如果需要更多的格式,需要下载附加包(参考1),然后先从(参考2)常用的JavaSound类图如下:
基于JAVA的SIP代理PEERS的自动放音实现_JAVA SIP
   理解下从AudioSystem中获得系统的符合DataLine.info中指定的AudioFormat属性的混频器;从类SoundManager的构造函数和openAndStartLines()函数中看到如下代码:

// linear PCM 8kHz, 16 bits signed, mono-channel, little endian
        audioFormat = new AudioFormat(8000, 16, 1, true, false);
        targetInfo = new DataLine.Info(TargetDataLine.class, audioFormat);
        sourceInfo = new DataLine.Info(SourceDataLine.class, audioFormat);


public void openAndStartLines() {
        logger.debug("openAndStartLines");
        //我删除了调试部分代码
        try {
            targetDataLine = (TargetDataLine) AudioSystem.getLine(targetInfo);
                                   
                                             
            targetDataLine.open(audioFormat);
        } catch (LineUnavailableException e) {
            logger.error("target line unavailable", e);
            return;
        }
        targetDataLine.start();
        try {
            sourceDataLine = (SourceDataLine) AudioSystem.getLine(sourceInfo);
            sourceDataLine.open(audioFormat);
        } catch (LineUnavailableException e) {
            logger.error("source line unavailable", e);
            return;
        }
        sourceDataLine.start();
    }


虽然看了这么多,但是我们并不需要将数据写入扬声器,只是获得并返回,因此(参考1)我们从文件中获得输入流,代码如下:

if (testNum < 1) {
            File file = new File("test.wav");
            // AudioInputStream audioInputStream = null;// 文件流
            // AudioFormat audioFormat = null;// 文件格式
            // 取得文件输入流
            try {
                audioInputStream = AudioSystem.getAudioInputStream(file);
            } catch (UnsupportedAudioFileException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            } catch (IOException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
            audioFormat = audioInputStream.getFormat();
            // 这里可以加入转换文件编码的代码
            testNum++;
            System.out.println("testNum" + testNum);
        }
        testNum++;
        System.out.println("testNum" + testNum);
        int ready = 0;
        try {
            ready = audioInputStream.available();
            // while (ready == 0) {
            try {
                Thread.sleep(20);
                ready = audioInputStream.available();
            } catch (InterruptedException e) {
                return null;
            }
            // }
        } catch (IOException e1) {
            // TODO Auto-generated catch block
            e1.printStackTrace();
        }
        if (ready <= 0) {
            return null;
        }
        byte[] buffer = new byte[320];//readData是循环读取
        try {
            audioInputStream.read(buffer, 0, buffer.length);
        } catch (IOException e1) {
            // TODO Auto-generated catch block
            e1.printStackTrace();
        }
        System.out.println("ready:" + ready);
        return buffer;


然后,按照原定的采样频率、通道数等利用格式转换器将一个MP3或其它格式文件先转换成一个wav文件,就可以进行测试;特别是注意中间sleep的间隔,如果没有的话会导致数据无法及时处理而造成停顿,而且问题也在这里,这个间隔时间为多少合适,buffer读取的数据定多少呢?这个应该可以根据文件的属性来设置,但是应该有更好的方法吧?继续寻找。

=========================================================================================

从整理几款sip phone,到简单修改播放声音文件,发现少了最基本的环境搭建及源码的认识,现在重新记录下。
   1.sip简要介绍
   这里提及一些sip相关的必要知识,方便对该协议有个整体的了解,同时也可以为后续相关的应用开发提供准备。更详细的内容可以参考对应的RFC文档(RFC3261等等)。sip(Session Initiation Protocol)会话初始协议,简单来说就是负责多媒体通信(两个或者多个终端)会话的建立、修改和终止,而且是一个应用层控制协议。可以看到它是不足以支撑完整的多媒体会话的,但它可以方便的通过与SDP(会话描述协议,协商采用的协议)、RTP(实时传输协议)等结合提供完整多媒体通信功能。后面在介绍这款Java sip phone(peers)的源码框架时会看到。
  单就sip协议来说(RFC3261),sip协议的功能实现用户的注册,会话的邀请或者接受会话,会话参数服务的修改,会话的结束等(没有用官方的用户定位等功能描述,需要的可以自己查看),因此需要注册服务器、代理服务器实现用户注册,位置查找,进而实现会话邀请等sip消息(主要就是请求和响应两种)的转发,对应的客户端有UserAgent(包括UAC:UserAgent Client和UAS:UserAgent Server )来负责实现会话的邀请和响应等。
  sip协议栈如下图所示,但据说只是逻辑实体,真正的实现上可能会有所重合的

基于JAVA的SIP代理PEERS的自动放音实现_JAVA SIP_02
  SIP的最底层是语法和编码层(对应上图协议栈),其上是传输层(记住sip协议是应用层协议),传输层之上是事务层,再上是事务用户层;其中一个事务定义为UAC发送的一个请求报文和由这个请求报文所引起的所有由UAS发送的响应报文。事务层负责处理应用层的报文超时重传、定时器设置、报文排序、重复报文处理和响应报文与请求报文的匹配等等;事务用户层它描述了不同SIP实体在事务层之上对SIP报文的处理,例如当要发送一个请求时,事务用户会创建一个客户机事务的实例并将请求报文和目标必要信息(包括目标IP地址、端口号、传输层协议等)传递给该事务进行处理。
   2.环境搭建及使用
   在了解上述概念后,我们来看环境搭建,本人是在Windows XP下配合使用虚拟机完成。sip phone一般的使用方式还是C/S模式的,因此,需要一个服务器来实现注册、代理转发、重定向等功能,我这里使用的是sipfoundry,客户端使用Peers,配合X-Lite,Jitsi(功能强大的软电话,前者是个商用产品,后者也是个开源软件,前文里有说。)sipfoundry可以直接下载光盘镜像,我的是和CentOS系统一起的Iso文件,从虚拟机中安装即可,官方有详细的安装文档,当系统安装完成后,启动有如下界面:
基于JAVA的SIP代理PEERS的自动放音实现_JAVA SIP_03

   然后可以通过上述的IP从浏览器中直接访问,当然那个IP是需要在安装时配置的。进入管理界面后的添加用户之类的操作之类不再细说。
   X-Lite和Jitsi下载后直接可以使用,Peers下载后点击bat文件直接运行,当下载的为源码时,可以通过Eclipse导入工程,然后运行即可。注意Jitsi和Peers拨号时的名称完整为sip:user@server.com,在Jitsi中省略了sip:。可以来体验下,现在服务器中添加200 和201用户,假设服务器地址为192.168.0.50,那么启动X-Lite,Accounts setting中配置用户名和地址(200),启动Peers设置用户名201或者不设也行,在Peers中输入200@192.168.0.50,点击call即可拨打,后面都简单了。
  3.peers源码框架
  peers的文档也非常详细,可以好好阅读下。Peers的源码包目录如下:

基于JAVA的SIP代理PEERS的自动放音实现_JAVA SIP_04
  其中conf中包括peers的配置文件,有用户名、密码等等;logs里是日志文件,media中在调试模式下时,会产生媒体调试输出信息,一般导出的可执行Jar包包括这三个目录即可。

  net.sourceforge.peers目录中为Log,config等信息代码,其它的可以从下图中看到其作用,因此如果没有特别要求,标准的JDK已经足够,不需要下载额外的库。

基于JAVA的SIP代理PEERS的自动放音实现_JAVA SIP_05

  在源码目录中的core.useragent也正是上面阐述的,包括UAC和UAS,而我们接触最多的也正是这两个。在peers运行中,gui目录中维持了会话状态的状态机,而其中实现各种事件响应的主要是CallFrame和Eventmanager,其中CallFrame主要负责界面上的按键响应,Eventmanager则一方面实现CallFrameListener的事件响应,另一方面建立了UserAgent实例,并实现了SipListener的事件响应。UserAgent则是sip phone中最重要的部分了,里面包括了UAC UAS等等一系列实现多媒体通信的实例对象,因此,集成自己的应用可以再gui目录中提取出自己想要的部分,主要是EventManager类。

  最后,提醒注意几个问题,一是如果本机有虚拟机,会造成Peers接收挂断等消息错误,主要是因为其绑定的IP可能是虚拟机IP,因此需要正确设置IP;另一方面是如果想建立多个sip客户端时,即使在gui等方面取消了限制,注意端口绑定的问题。

=========================================================================================

基于Peers与miniSipServer的VoIP测试环境的搭建

摘要:本文介绍了一个简单的VoIP测试环境的搭建,SIP客户端和服务器分别采用开源的SIP客户端Peers和轻量级的服务器miniSipServer。环境搭建成功后,可以实现简单的VoIP语音通信。本文的侧重点在于调试Peers源代码,miniSipServer只是作为支持环境。


硬件清单:

  1. 一台4口或以上的集线器/交换机/路由器;

  2. 两台PC,服务器可部署在其中一台上;

  3. 两根网线(如果用无线路由器可省略网线)。


第一篇:基于Peers源代码的SIP客户端构建

第一步:下载Peers源代码,下载地址ttps://peers.svn.sourceforge.net/svnroot/peers/trunk

可以在本地安装一个TortoiseSVN客户端,然后直接下载。

第二步:将Peers源代码导入Eclipse并编译成功。

1.      打开Eclipse,新建一个Java Project。例如名为DPeers,然后直接点击完成。

2.      在Eclipse中左侧的Package Explorer中右击该工程下的src,选择Import…菜单。然后在弹出的Import窗口中选择”General”à” FileSystem”,点击下一步。

3.      将peers-lib的源代码路径D:\ProgramFiles\eclipse\workspace\trunk\peers-lib\src\main\java拷贝并粘贴在”From directory”编辑框中,然后点击左下方的浏览窗口,选中java的下一级目录net。最后点击完成,lib部分的源代码将被导入。

4.      将peers-gui的源代码路径D:\ProgramFiles\eclipse\workspace\trunk\peers-gui\src\main\java拷贝并粘贴在”From directory”编辑框中,然后点击左下方的浏览窗口,选中java的下一级目录net。最后点击完成,gui部分的源代码将被导入。如果提示/src/net/.svn下的文件将被覆盖,选择”Yes to All”,因为这个是svn相关的,这里可以忽略。

5.      将peers-gui的资源路径D:\ProgramFiles\eclipse\workspace\trunk\peers-gui\src\main\resources拷贝并粘贴在”From directory”编辑框中,然后点击左下方的浏览窗口,选中resources的下一级目录net。最后点击完成,gui的资源部分将被导入。

6.      peers-jws和peers-doc部分的源代码这里用不上,因此不导入。

7.      导入配置文件:在Eclipse中左侧的Package Explorer中右击该工程名DPeers,选择Import…菜单。然后在弹出的Import窗口中选择”General”à” FileSystem”,点击下一步。将D:\ProgramFiles\eclipse\workspace\trunk拷贝并粘贴在”From directory”编辑框中,然后点击左下方的浏览窗口,选中trunk的下一级目录conf。最后点击完成,配置文件部分将被导入。

注意:这里必须右击DPeers,否则可能导致路径不对。

第三步:运行基于Peers的SIP客户端。

          右击net.sourceforge.peers.gui.MainFrame,选择”RunAs”à”JavaApplication”,一个GUI界面窗体将被弹出。通过菜单可以配置账号。

               注意:如果本机没有网络连接,运行将失败。如果有多个IP地址,可以在/conf/peers.xml中进行配置。


第二篇:基于miniSipServer的SIP服务器的搭建

第一步:下载miniSipServer。http://www.myvoipapp.com/不多讲。

第二步:安装miniSipServer。安装过程无需任何配置,不多讲。

第三步:配置miniSipServer。

运行后,系统会默认选择一个地址作为SIP服务器的地址。启动后,通过系统配置菜单更改成自己所需的。本例在一个局域网中完成,将其设置为192.168.1.100.

        更改地址后需重启生效。系统默认已经配置了三个分机:100,101和102.这里直接使用,当然也可以自己增加。


第三篇:客户端和服务器联调

  1. 配置Peers客户端1,假定该用户是分机100.点击Peers的EditàAccount,在弹出的对话框中依次填写:

User: 100

Domain: 192.168.1.100

Password: 100

Outbound Proxy不用填写。

  1. 配置Peers客户端2,假定该用户是分机101.点击Peers的EditàAccount,在弹出的对话框中依次填写:

User: 101

Domain: 192.168.1.100

Password: 101

Outbound Proxy不用填写。

  1. 用100分机呼叫101。

在100的Peers界面的Call前面的编辑框中填入sip:101@192.168.1.101,然后点击Call,此时本地将弹出带有拨号盘的呼叫窗体,显示有Calling,带有”Hangup”挂断按钮。对端101收到请求后,将弹出一个类似窗口,显示有Incoming call,带有”Busy here”拒绝和”Pickup”接收两个按钮。

  1. 101端点击”Pickup”即可接通电话。

通过测试发现,通话的语音质量不是很好,感觉有些粗糙。下一步就是通过调试来提高语音质量。