freeswitch筆記(3)-esl入門


題外話:昨天是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>
View Code

強烈建議,把第4行listen-ip改成0.0.0.0(或具體的本機ip地址),默認的::是ipv6格式,很多情況會導致esl client連接失敗,改成0.0.0.0相當於強制使用ipv4

b) 考慮到網絡可能發生抖動,在發送命令前,建議參考60行的做法,先判斷canSend()

c) 61行,client.sendAsyncApiCommand 這里以異步方式,發送了一個命令給fs(即:呼叫1000用戶,接通后再放段聲音)。異步方式下,命令是否發成功當時並不知道,但是這個方法會返回一個uuid的字符串,fs收到后,會在backgroundJobResultReceived回調中,把這個uuid再還回來,參見上面貼出的輸出結果。(基於這個機制,可以做些重試處理,比如:先把uuid存下來,如果約定的時間內,uuid異步回調還沒回來,可以視為發送失敗,再發一次)

 

重要提示:esl java client 0.9.2這個版本,inbound模式下,長時間使用有內存泄露問題,網上有很多這個介紹及修復辦法,建議生產環境使用前,先修改esl client的源碼。

 

三、outbound示例

3.1 修改dialplan配置

出於演示目的,這里修改/usr/local/freeswitch/conf/dialplan/default.xml,在文件開頭部分添加一段:

<extension name="socket_400_example">
      <condition field="destination_number" expression="^400\d+$">
            <action application="socket" data="localhost:8086 async full"/>
      </condition>
</extension>

即:當來電的被叫號碼為400開頭時,fs將利用socket,連接到localhost:8086

3.2 編寫業務邏輯

a) SampleOutboundHandler
package com.cnblogs.yjmyzz.freeswitch.esl.outbound;

import org.freeswitch.esl.client.outbound.AbstractOutboundClientHandler;
import org.freeswitch.esl.client.transport.SendMsg;
import org.freeswitch.esl.client.transport.event.EslEvent;
import org.freeswitch.esl.client.transport.message.EslHeaders;
import org.freeswitch.esl.client.transport.message.EslMessage;
import org.jboss.netty.channel.Channel;
import org.jboss.netty.channel.ChannelHandlerContext;

import java.util.ArrayList;
import java.util.List;

/**
 * @author 菩提樹下的楊過
 */
public class SampleOutboundHandler extends AbstractOutboundClientHandler {

    @Override
    protected void handleConnectResponse(ChannelHandlerContext ctx, EslEvent event) {
        System.out.println("Received connect response :" + event);
        if (event.getEventName().equalsIgnoreCase("CHANNEL_DATA")) {
            // this is the response to the initial connect
            System.out.println("=======================  incoming channel data  =============================");
            System.out.println("Event-Date-Local: " + event.getEventDateLocal());
            System.out.println("Unique-ID: " + event.getEventHeaders().get("Unique-ID"));
            System.out.println("Channel-ANI: " + event.getEventHeaders().get("Channel-ANI"));
            System.out.println("Answer-State: " + event.getEventHeaders().get("Answer-State"));
            System.out.println("Caller-Destination-Number: " + event.getEventHeaders().get("Caller-Destination-Number"));
            System.out.println("=======================  = = = = = = = = = = =  =============================");

            // now bridge the call
            bridgeCall(ctx.getChannel(), event);


        } else {
            throw new IllegalStateException("Unexpected event after connect: [" + event.getEventName() + ']');
        }
    }

    private void bridgeCall(Channel channel, EslEvent event) {
        List<String> extNums = new ArrayList<>(2);
        extNums.add("1000");
        extNums.add("1010");
        //隨機找1個目標(注:這里只是演示目的,真正分配時,應該考慮到客服的忙閑情況,通常應該分給最空閑的客服)
        String destNumber = extNums.get((int)Math.abs(System.currentTimeMillis() % 2));

        SendMsg bridgeMsg = new SendMsg();
        bridgeMsg.addCallCommand("execute");
        bridgeMsg.addExecuteAppName("bridge");
        bridgeMsg.addExecuteAppArg("user/" + destNumber);

        //同步發送bridge命令接通
        EslMessage response = sendSyncMultiLineCommand(channel, bridgeMsg.getMsgLines());
        if (response.getHeaderValue(EslHeaders.Name.REPLY_TEXT).startsWith("+OK")) {
            String originCall = event.getEventHeaders().get("Caller-Destination-Number");
            System.out.println(originCall + " bridge to " + destNumber + " successful");
        } else {
            System.out.println("Call bridge failed: " + response.getHeaderValue(EslHeaders.Name.REPLY_TEXT));
        }
    }

    @Override
    protected void handleEslEvent(ChannelHandlerContext ctx, EslEvent event) {
        System.out.println("received event:" + event);
    }

    @Override
    protected void handleDisconnectionNotice() {
        super.handleDisconnectionNotice();
        System.out.println("Received disconnection notice");
    }
}

重點看下bridgeCall這個方法,假設有2個客服號碼1000、1010可用,隨機挑1個,然后將來電接通到這個號碼。

b) AbstractOutboundPipelineFactory
package com.cnblogs.yjmyzz.freeswitch.esl.outbound;

import org.freeswitch.esl.client.outbound.AbstractOutboundClientHandler;
import org.freeswitch.esl.client.outbound.AbstractOutboundPipelineFactory;

/**
 * @author 菩提樹下的楊過
 */
public class SamplePipelineFactory extends AbstractOutboundPipelineFactory {

    @Override
    protected AbstractOutboundClientHandler makeHandler() {
        return new SampleOutboundHandler();
    }
}

還需要一個工廠類,包裝一下。

c)OutboundApp 程序入口
package com.cnblogs.yjmyzz.freeswitch.esl.outbound;

import org.freeswitch.esl.client.outbound.SocketClient;

/**
 * @author 菩提樹下的楊過
 */
public class OutboundApp {

    public static void main(String[] args) throws InterruptedException {

        new Thread(() -> {
            SocketClient socketClient = new SocketClient(8086, new SamplePipelineFactory());
            socketClient.start();
        }).start();

        while (true) {
            Thread.sleep(500);
        }
    }

}  

輸出結果:

Received connect response :EslEvent: name=[CHANNEL_DATA] headers=5, eventHeaders=169, eventBody=0 lines.
=======================  incoming channel data  =============================
Event-Date-Local: 2020-02-09 12:02:35
Unique-ID: bd659733-d460-4f0f-8c73-4cd4f1e39f68
Channel-ANI: 1002
Answer-State: ringing
Caller-Destination-Number: 4008123123
=======================  = = = = = = = = = = =  =============================
4008123123 bridge to 1010 successful
Received disconnection notice

 

文中示例代碼git地址: https://github.com/yjmyzz/freeswitch-esl-java-client-sample

  

參考文章:

https://freeswitch.org/confluence/display/FREESWITCH/mod_event_socket

https://freeswitch.org/confluence/display/FREESWITCH/Java+ESL+Client


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM