题外话:昨天是2020年元宵节,正值"新型肺炎"第二阶段防治关键时期,返沪后按规定自觉在家隔离14天,不出去给社会添乱,真心希望这次疫情快点过去。
废话不多说,继续学习,上篇借助工具大致体验了voip client的使用,这篇学习如何用代码来实现类似的功能。esl全称Event Socket Library, 通过它可以与freeswitch进行交互,esl client支持多种语言,本文将以esl java client为例,演示一些基本用法:
一、两种模式:inbound、outbound
freeswitch(以下简单fs)启动后,内置了一个tcp server,默认会监听8021端口,通过esl,java 应用可以监听该端口,获取fs的各种事件通知,这种模式称为inbound模式。
如上图,inbound模式下:java应用引用esl java client的jar包后(注:esl java client底层是依赖netty实现的),连接到fs(fs内置了mod_event_socket模块,会在本地默认监听2081端口),连接成功后,如果有来电,fs会触发各种事件,透过已经连上的通道,通知java应用,java应用可以针对特定事件做些处理(有必要的话,还可以发送命令给fs),当然连接成功后,java应用也可以直接向fs发送命令,比如对外呼叫某个号码。
如果反过来,java应用起1个端口,自己充当tcp server,fs连接java应用,就称为outbound模式,如下图:
java应用利用esl java client在本机监听某个端口,相当于启动了一个tcp server(底层仍然是基于nettty实现),当fs收到来电时,会连接java应用的tcp server(注:需要修改fs的配置,否则fs不知道tcp server的ip\port这些连接信息),然后java应用可以根据自身业务做些处理,发送命令给fs(比如:给客人放段音乐或转接到特定目标),通话结束后(比如:主叫方挂断,或被叫方拒接),fs会断开连接,直到下次再有来电。
tips:inbound/outbound 是站在fs的角度来看的,外部应用连进来,就是inbound;fs连出去,就是outbound。 二种模式基本上都可以完成大多数业务功能,如何选取看各自特点,比如:如果要监控所有来电情况或实现客人自助语音服务,inbound相对更方便(可以很轻松获取所有事件)。对于来电后的人工客服分配,outbound则更简单(比如:客人来电拨打某个对外暴露公用客服号码比如400电话时,fs把客人来电通过tcp connect最终给到java app,java应用按一定分配规则 ,比如哪个客服最空闲,把来电bridge到该客服分机即可)
二、inbound 代码示例
2.1 pom依赖
<dependency> <groupId>org.freeswitch.esl.client</groupId> <artifactId>org.freeswitch.esl.client</artifactId> <version>0.9.2</version> </dependency>
2.2 演示代码
下面的代码,演示了连接到fs后,利用client直接发起外呼。
package com.cnblogs.yjmyzz.freeswitch.esl;
import org.freeswitch.esl.client.IEslEventListener;
import org.freeswitch.esl.client.inbound.Client;
import org.freeswitch.esl.client.inbound.InboundConnectionFailure;
import org.freeswitch.esl.client.transport.event.EslEvent;
/**
* @author 菩提树下的杨过
*/
public class InboundApp {
public static void main(String[] args) throws InterruptedException {
Client client = new Client();
try {
//连接freeswitch
client.connect("localhost", 8021, "ClueCon", 10);
client.addEventListener(new IEslEventListener() {
@Override
public void eventReceived(EslEvent event) {
String eventName = event.getEventName();
//这里仅演示了CHANNEL_开头的几个常用事件
if (eventName.startsWith("CHANNEL_")) {
String calleeNumber = event.getEventHeaders().get("Caller-Callee-ID-Number");
String callerNumber = event.getEventHeaders().get("Caller-Caller-ID-Number");
switch (eventName) {
case "CHANNEL_CREATE":
System.out.println("发起呼叫, 主叫:" + callerNumber + " , 被叫:" + calleeNumber);
break;
case "CHANNEL_BRIDGE":
System.out.println("用户转接, 主叫:" + callerNumber + " , 被叫:" + calleeNumber);
break;
case "CHANNEL_ANSWER":
System.out.println("用户应答, 主叫:" + callerNumber + " , 被叫:" + calleeNumber);
break;
case "CHANNEL_HANGUP":
String response = event.getEventHeaders().get("variable_current_application_response");
String hangupCause = event.getEventHeaders().get("Hangup-Cause");
System.out.println("用户挂断, 主叫:" + callerNumber + " , 被叫:" + calleeNumber + " , response:" + response + " ,hangup cause:" + hangupCause);
break;
default:
break;
}
}
}
@Override
public void backgroundJobResultReceived(EslEvent event) {
String jobUuid = event.getEventHeaders().get("Job-UUID");
System.out.println("异步回调:" + jobUuid);
}
});
client.setEventSubscriptions("plain", "all");
//这里必须检查,防止网络抖动时,连接断开
if (client.canSend()) {
System.out.println("连接成功,准备发起呼叫...");
//(异步)向1000用户发起呼叫,用户接通后,播放音乐/tmp/demo1.wav
String callResult = client.sendAsyncApiCommand("originate", "user/1000 &playback(/tmp/demo.wav)");
System.out.println("api uuid:" + callResult);
}
} catch (InboundConnectionFailure inboundConnectionFailure) {
System.out.println("连接失败!");
inboundConnectionFailure.printStackTrace();
}
}
}
参考输出结果类似如下:
连接成功,准备发起呼叫... api uuid:54ae7272-62c1-4d1f-87a1-aab2080538dc 发起呼叫, 主叫:0000000000 , 被叫:1000 用户应答, 主叫:0000000000 , 被叫:1000 异步回调:54ae7272-62c1-4d1f-87a1-aab2080538dc 用户挂断, 主叫:1000 , 被叫:0000000000 , response:null ,hangup cause:NORMAL_CLEARING
代码稍微解释一下:
a) 18行,连接fs的用户名、密码、端口,可以在freeswitch安装目录下的conf/autoload_configs/event_socket.conf.xml 找到
1 <configuration name="event_socket.conf" description="Socket Client"> 2 <settings> 3 <param name="nat-map" value="false"/> 4 <param name="listen-ip" value="0.0.0.0"/> 5 <param name="listen-port" value="8021"/> 6 <param name="password" value="ClueCon"/> 7 <!--<param name="apply-inbound-acl" value="loopback.auto"/>--> 8 <!--<param name="stop-on-bind-error" value="true"/>--> 9 </settings> 10 </configuration>