freeswitch 权威指南 --- 高级篇

本文围绕FreeSWITCH展开,介绍了基于它的四种开发方式,如使用嵌入式脚本、Event Socket等。详细阐述了Lua ELS开发,包括相关函数和示例。还说明了如何使用libfreeswitch库函数实现软交换机及音频数据处理,最后讲解了调试跟踪的方法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

官网文档:https://developer.signalwire.com/freeswitch/FreeSWITCH-Explained/
关于 freeswitch 的公开教程:https://zhuanlan.zhihu.com/p/451981734

内容来自 《FreeSWITCH 权威指南》:目录:https://juejin.cn/post/7020580794829635591
代码下载:https://book.dujinfang.com/download.html

1、客户端和开发接口

基于 FreeSWITCH 的开发一般有四种方式:后两种方法需要熟悉FreeSWITCH的源代码

  • 1. 使用嵌入式脚本在 FreeSWITCH 内部灵活地控制呼叫流程,以及通过共享数据库或简单API 等与现有业务系统集成;
  • 2. 使用 Event Socket 在外部程序中控制呼叫流程,控制更灵活,与其他系统更容易集成;
  • 3. 直接在FreeSWITCH中修改现有代码,或通过添加新模块以扩展FreeSWITCH的现有功能,并能结合前两种方式创建更强大的应用;
  • 4. 将FreeSWITCH的底层库 libfreeswitch 嵌入到其他系统中,这样被嵌入的用系统中就瞬间增加了 FreeSWITCH 的全部功能,这种方法用得比较少。

FreeSWITCH 默认使用 XML Dialplan 配置呼叫流程。XML 文件描述性很强,因而也可以描述比较复杂的呼叫流程。但在一些比较高级的 IVR 应用和呼叫交互流程中,仅靠简单的 XML 的配置很难满足要求。因而还需要更灵活、更强大的解决方案。除 XML Dialplan外,FreeSWITCH 支持使用嵌入式的脚本语言控制呼叫流程。不仅可以用它们写出灵活多样的 IVR,给用户带来更好的体验,

在内部,FreeSWITCH 通过使用 swig 工具来支持多种开发语言。简单来讲,swig 是一个包装工具(Wrapper),它可以将 FreeSWITCH 用C语言实现的一些功能包装成各种其他语言的接口、类或者方法,这样就可以在使用其他语言时以原生的方式调用。现在已知支持的语言有 C、Perl、PHP、Python、Ruby、Lua、Java、Tcl 以及由 Managed 支持的 .Net 平台语言,如 C#、VB.NET等。FreeSWITCH 源代码中的 swig 脚本和程序已被转换成各种语言的接口了,因而开发者不需要安装 swig 工具就可以使用。不过 JavaScript 语言比较特殊一些,对它的支持是基于Google 的V8库,在 mod_v8 模块中实现的。

理论上讲,可以使用任何语言,只要该语言支持 TCP Socket 就行。

SWIG 官网  :http://www.swig.org

SWIG 简介、安装、使用方法:https://blog.csdn.net/qq_41185868/article/details/103558686

SWIG:Python 调用 C++:https://zhuanlan.zhihu.com/p/462193340

XML Dialplan 已经体现了其非凡的配置能力,它配合 FreeSWITCH 提供的各种 App 使用时,也可以认为是一种脚本。当然,毕竟 XML 是一种描述语言,功能还比较有限,为了扩展其功能,FreeSWITCH 通过嵌入其他语言的解析器支持很多流行的编程语言。这些语言一般都能提供if...else 判断及等循环跳转控制等,因而控制呼叫流程更加灵活。

Lua ELS 开发

FreeSWITCH Lua脚本:https://www.cnblogs.com/garvenc/p/freeswitch_learning_lua.html

Lua 因其优雅的语法及小巧的身段受到很多开发者的青睐,尤其是游戏开发人员。FreeSWITCH 中 Lua 模块是默认加载的。在所有嵌入式脚本语言中,Lua 是最值得推荐的语言。首先它非常轻量级,mod_lua.so 经过减肥(Strip)后只有272KB;另外,它的语法相对的简单。有人做过对比,在嵌入式的脚本语言里,如果Python得2分,Perl得4分,JavaScript得5,则Lua语言可得10分。另外,Lua模块的文档也是最全的。

Lua 官网:https://www.lua.org/

Lua 下载:https://www.lua.org/download.html

Lua 教程:https://www.runoob.com/lua/lua-tutorial.html

Lua与JS(JavaScript的缩写,下同)有很多相似的地方,简述如下。

  • 变量无需声明。 Lua与JS都是“弱”类型的语言(不像C),不需要事先声明变量的类型。
  • 区分大小写。 Lua和JS都是区分大小写的。true和false分别代表布尔类型的真和假,
  • 函数可以接受个数不定的参数。 与JS类似,在Lua中,与已经声明的函数参数个数相比,实际传递的参数个数可多可少
  • 哈希可以用方括号或点方式引用。
  • 数字区别不大。 在JS和Lua中,整数和浮点数是没有区别是的。它们在内部都是以浮点数表示。在Lua中,所有的数字类型都是number类型。
  • 分号是可选的。
  • 默认全局变量。 在JS中,如果用var声明一个变量并赋值,则它是本地变量;如果不用var声明,默认就是全局的
  • 使用双引号和单引号表示字符串。
  • 函数是一等公民。 在JS和Lua中,函数是一等公民,这意味着,你可以将它赋值给一个变量,将它作为参数传递,或者直接加上括号进行调用。
  • 函数都是闭包。 在JS和Lua中,函数都是闭包。简单来说,这意味着函数可以随时访问该函数在定义时可以访问的本地变量,尽管在以后调用时这些本地变量逻辑上已经“失效”了。

将电话路由到Lua脚本:originate user/1000 &lua(test.lua)

lua是一个App,它的参数就是脚本的名字,脚本的默认路径在安装路径的scripts目录下,当然你也可以指定一个绝对路径,如/tmp/test.lua。在Dialplan XML中,使用下列配置便可将进入Dialplan的电话(Channel)交给Lua脚本接管。<action application="lua" data="test.lua"/>

除此之外,也可以直接使用uuid_transfer命令直接配合inline Dialplan将一个Channel路由到Lua脚本,如:uuid_transfer <uuid> lua:/tmp/test.lua inline

总之,这里的Lua是一个标准的App,在任何可以使用App的地方都可以使用它(如上面介绍的各种场景,以及后面要介绍的Event Socket等。甚至在Lua脚本中也可以再次使用lua App来调用下一个Lua脚本)。

Session 相关函数

在Lua环境中,FreeSWITCH会自动生成一个session对象(实际上是一个Table),因而可以在Lua脚本中使用Lua类似面向对象的语法特性编程,如以下脚本放播放欢迎声音

--
session:answer()
--
session:sleep(1000)
--
session:streamFile("/tmp/hello-lua.wav")
--
session:hangup()

大部分与Session有关的函数都是跟FreeSWITCH中的App是一一对应的,如上面的answer、hangup等。有一点要特别说明:streamFile对应playback这个App。如果在Lua中没有对应的函数,也可以通过session:execute()函数来执行相关的App,如session:execute("playback","/tmp/sound.wav") 与 session:streamFile("/tmp/sound.wav") 是等价的

需要注意,Lua脚本执行完毕后默认会挂断电话,所以上面的Hello Lua例子中不需要明确的session:hangup()。如果想在Lua脚本执行完毕后继续执行Dialplan中的后续流程,则需要在脚本开始处先设置不要自动挂机,语法如下:session:setAutoHangup(false)

例如下列场景,test.lua执行完毕后(假设没有session:hangup(),主叫也没有挂机),如果没有setAutoHangup(false),则后续的playback动作得不到执行。

<extension name="test-lua">
	<condition field="destination_number" expression="^1234$">
		<action application="answer"/>
		<action application="lua" data="test.lua"/>
		<action application="playback" data="lua-script-complete.wav"/>
	</condition>
</extension>

与 Ssession 相关的几个常用的函数:

  • getVariable。 取得变量的值
  • getUUID。 取得当前Session的UUID
    local uuid = session:get_uuid(]);  等价于 local uuid = session:getVariable("uuid")
  • setVariable。 设置通道变量,等价于Dialplan App里的set:session:setVariable("varname", "varval")
  • hangup。 挂断当前通话。session:hangup();  或者 session:hangup("USER_BUSY")
  • ready。 检查Session是否可正常使用,如果已经挂机就会返回false。在写脚本时,如果有循环,一定需要经常检测session:ready()是否为true,否则Session挂机后Lua脚本可能仍然在死循环地运行。
  • streamFile: 放音,相当于Dialplan App里的playback。session:streamFile("/tmp/test.wav")
  • recordFile: 录音,相当于Dialplan App里的record,参数是:file_name [,max_len_secs] [,silence_threshold] [,silence_secs]
        其中,各参数含义如下:
            file_name: 录音文件名。
            max_len_secs: 录音最长的秒数。
            silence_threashold: 一个声音阈值,如果声音小于该值,就认为是静音。
            silence_secs: 如果静音时长大于一定秒数,则停止录音。
        例如,以下函数将对当前的Channel录音,并存放到/tmp/test_record.wav中:
        session:recordFile("/tmp/test_record.wav")
  • read。 类似于Dialplan App中的read,用于播放一个声音并获取DTMF。它的5个参数与read含义相同:<min digits><max digits><file to play><inter-digit timeout><terminators>
    digits = session:read(15, 18, "/tmp/input-id-card.wav", "5000", "#");
    session:("log", "INFO ID Card Number: ".. digits .."\n");  可以发现Lua中的read比Dialplan App中的read少了一个参数。由于session:read()能返回值,因此那个参数就不需要了,实际收到的 DTMF 会返回到本例的 digits 变量中。
  • playAndGetDigits。 与Dialplan App中的play_and_get_digits类似,它的参数格式是:<min_digits>, <max_digits>, <max_attempts>, <timeout>, <terminators>,<prompt_audio_files>,<input_error_audio_files>,<digit_regex>, [variable_name], [digit_timeout], [transfer_on_failure])  其中,大部分参数都很直观,也跟play_and_get_digits中类似。其中timeout是收齐所有号的超时值,而digit_timeout是允许的两次按键之音的时间间隔最大值,最后transfer_on_failure指明如果失败后是否转到Dialplan中的一个Extension上去,它的格式应该是一个Dialplan三要素的格式串,如“failed XML dialplan”。
    重写如下:
    digits = session:playAndGetDigits(15, 18, 3, 10000, "#",
    "/tmp/input-id-card.wav", "/tmp/invalid_num.wav",
    "^\\d{15}|\\d{17}[0-9\\*]$")
    session:execute("log", "INFO ID Card Number: ".. digits .."\n");
  • setInputCallback。 在放音或录音时,用户按下的DTMF可以用于触发一些功能。所以在这些状态下,Lua支持如果收到DTMF等外部输入时,则调用相关的回调函数。setInputCallback的作用就是设置(安装)一个回调函数。

更多与Session相关的函数可以参考相关的wiki文档:http://wiki.freeswitch.org/wiki/Mod_lua

非Session函数、独立的Lua脚本

Lua脚本中也可以使用跟Session不相关的函数,最典型的是freeswitch.consoleLog(),其用于输出日志,如:freeswitch.consoleLog("NOTICE", "Hello lua log!\n")

另外一个是freeswitch.API(),允许你在Lua中执行任意API,如:a.lua

api = freeswitch.API()
reply = api:execute("version", "")
freeswitch.consoleLog("INFO", "Got reply:\n\n" .. reply .. "\n")

上面 Lua 脚本可以直接在 FreeSWITCH 控制台上执行:freeswitch> lua /tmp/a.lua

除此之外,其他的非 Session 函数还有 freeswitch.bridge()、freeswitch.email() 等,

非Session函数一般运行在独立的Lua脚本中。独立的Lua脚本可以直接在控制台终端上执行(使用luarun),这种脚本大部分可用于执行一些非Session相关的功能(因为这里面没有Session)。读到这里读者已经了解到了,Lua是一个App,而luarun是一个API。

上面的 a.lua 就是一个典型的可独立运行的Lua脚本。独立运行的Lua脚本跟在Dialplan中用Lua App运行的不同,前者不会自动获得一个session对象(Table)。当然,独立运行的脚本也可以自行创建session对象。

Event 相关函数

FreeSWITCH使用事件机制进行异步通信。在Lua脚本中,可以“生产”事件,也可以“消费”事件,

FreeSWITCH的事件也跟一个SIP消息类似,它包含一些事件头(Header)和可选的事件正文(Body)。在FreeSWITCH内部使用C语言结构体表示,可以序列化成类似SIP消息的简单文本格式(Plain)、JSON或XML。

下面我们来看一下与事件相关的函数。

  • freeswitch.Event。 初始化一个事件,该事件类型需要在switch_event_types_t枚举类型中有定义,它是在switch_types.h中定义的。如果使用了未定义过的名字,则统一为MESSAGE。下面的例子初始化一个主事件:event = freeswitch.Event("MESSAGE_WAITING")  也可以初始化一个CUSTOM事件,其中第二个参数可以是任意字符串,它将作为事件中的EventSubclass,如:event = freeswitch.Event("CUSTOM", "freeswitch:book")
  • event:addHeader。 给事件增加一个事件头,如:

    event:addHeader("MWI-Messages-Waiting", "no")
    event:addHeader("MWI-Message-Account", "sip:1000@192.168.0.2")
    event:addHeader("Sofia-Profile", "internal")

  • event:fire。 产生(生产)事件,如:event:fire()

  • event:addBody。 给事件增加一个可选的正文,并使用Content-Type头标志正文的类型,如:
    event:addHeader("Content-Type", "text/plain")
    event:addBody("Hello FreeSWITCH")

  • event:delHeader。 从事件中删除一个头域,下面的例子可以替换from头:
    event:delHeader("from")
    event:addHeader("from", "1000@192.168.0.2")

  • event:getHeader。 在收到一个事件后,可以取得其头域的值,如:
    event:getHeader("from")
    event:getHeader("Caller-Caller-ID-Name")

  • event:getBody。 取得Body的值(如果有的话),如:event:getBody()

  • event:getType。 取得事件的类型(名字),以下两种方法是等价的:
    event:getType()
    event:getHeader("Event-Name")

  • event:serialize。 将事件序列化成可读的形式(字符串),支持plain text、JSON、XML三种类型,如:
    event:serialize()
    event:serialize("json")
    event:serialize("xml")

完整示例

function log(k, v)
    if not v then v = "[NIL]" end
    freeswitch.consoleLog("INFO", k .. ": " .. v .. "\n")
end
event = freeswitch.Event("CUSTOM", "freeswitch:book")
event:addHeader("Author", "Seven Du")
event:addHeader("Content-Type", "text/plain")
event:addBody("FreeSWITCH: The Definitive Guide")
type = event:getType()
author = event:getHeader("Author")
text=event:serialize()
json=event:serialize("json")
xml=event:serialize("XML")
log("type", type)
log("author", author)
log("text", text)
log("json", json)
log("xml", xml)
event:fire()
log("MSG", "Event Fired")

将上述内容保存到/tmp/event.lua中,执行结果如下

freeswitch> lua /tmp/event.lua
[INFO] switch_cpp.cpp:1288 type: CUSTOM
[INFO] switch_cpp.cpp:1288 author: Seven Du
[INFO] switch_cpp.cpp:1288 text: 'Event-Name: CUSTOM
...
Event-Subclass: freeswitch%3Abook
Author: Seven%20Du
Content-Type: text/plainContent-Length: 32
FreeSWITCH: The Definitive Guide'
[INFO] switch_cpp.cpp:1288 json: {
    "Event-Name": "CUSTOM",
    "Core-UUID": "bc647e68-47de-407f-b32a-d9bdf5c25786",
    "Event-Sequence": "5000",
    "Event-Subclass": "freeswitch:book",
    "Author": "Seven Du",
    "Content-Type": "text/plain",
    "Content-Length": "32",
    "_body": "FreeSWITCH: The Definitive Guide"
}
[INFO] switch_cpp.cpp:1288 xml: <event>
<headers>
<Event-Name>CUSTOM</Event-Name>
<Core-UUID>bc647e68-47de-407f-b32a-d9bdf5c25786</Core-UUID>
<Event-Sequence>5000</Event-Sequence>
<Event-Subclass>freeswitch%3Abook</Event-Subclass>
<Author>Seven%20Du</Author>
<Content-Type>text/plain</Content-Type>
</headers>
<Content-Length>32</Content-Length>
<body>FreeSWITCH: The Definitive Guide</body>
</event>
[INFO] switch_cpp.cpp:1288 MSG: Event Fired

Chat 相关函数

FreeSWITCH通过mod_sms支持文本消息。一个文本消息与一个Session类似,FreeSWITCH收到文本消息后将执行Chatplan,然后在Chatplan中可以执行Lua脚本。在Chatplan中的Lua脚本会自动获得一个message对象,该对象的内部表示跟event是一样的。因而与Event相关的函数,如addHeader、delHeader、addBody、serialize等,都是可以用的。除此之外,还有一个chat_execute函数,它可以执行mod_sms中支持的以下动作。

  • fire: 产生一个MESSAGE事件。
  • send: 发送消息。
  • reply: 回复消息。
  • set: 设置变量。
  • info: 显示信息。
  • stop: 停止消息路由。
  • system: 调用system函数执行系统调用。

下面的 Lua 脚本可以在 Chatplan 中执行,收到消息后先打印出来,然后修改目的号码和主机,并发送出去。

area_code = "010"
to_host = "192.168.0.2"
function log(k, v)
    if not v then v = "[NIL]" end
    freeswitch.consoleLog("INFO", k .. ": " .. v .. "\n")
end
log("Message", message:serialize())
to_user = message:getHeader("to")
message:delHeader("to")
message:addHeader("to", "internal/sip:" .. area_code .. to_user .. "@" .. to_host)
message:delHeader("to_host")
message:addHeader("to_host", to_host)
log("New Message", message:serialize())
message:chat_execute("send")

与在 Dialplan 中类似,在 Chatplan 中可以用以下方法调用 Lua 脚本,如:<action application="lua" data="test.lua"/>

LUA 拨号计划

拨号计划除XML外还有很多种,其中一种就是LUA拨号计划,即可以通过Lua脚本提供Lua风格的Dialplan。

下面的脚本入进路由阶段时将查询并生成一个Dialplan,FreeSWITCH接下来执行该Dialplan,打印一些Log,并执行answer和playback。

function log(k, v)
    if not v then v = "[NIL]" end
    freeswitch.consoleLog("INFO", k .. ": " .. v .. "\n")
end
cid = session:getVariable("caller_id_number")
dest = session:getVariable("destination_number")
log("From Lua DP: cid: ", cid)
log("From Lua DP: dest: ", dest)
-- Some Bussinuss logic here
ACTIONS = {
    {"log", "INFO I'm From Lua Dialplan"},
    {"log", "INFO Hello FreeSWITCH, Playing MOH ..."},
    "answer",
    {"playback", "local_stream://moh"}
}

首先,跟在Dialplan中执行Lua脚本类似,这里也有一个session对象,可以执行所有与Session相关的函数,如获取主被叫号码(cid、dest)等。获取到相关信息后可以通过Lua相关的函数,如判断日期时间、连接数据库检查主被叫号码合法性及黑白名单等(这里我们省略了跟逻辑相关的操作)。最后,生成一个Lua Table。该Table的名字必须是ACTIONS。其成员可以是一个字符串或一个子Table。FreeSWITCH在ROUTING阶段获得该Table后,便可以进入EXECUTING阶段执行ACTIONS中定义的一系列动作(Action)。

Dialplan 有三个要素:Extension、Context和Dialplan的名字,在Lua Dialplan中,Dialplan的名字当然是LUA,其 Context 就是 Lua 脚本的路径。把上面的脚本存为/tmp/db.lua,使用originate测试:freeswitch> originate user/1000 test LUA /tmp/dp.lua

originate命令首先呼叫user/1000,当它接听后,即转入LUA Dialplan中的/tmp/dp.lua这一Context进行路由,其对应的extension是test。其执行结果也很直观。用originate回呼是一种快速的测试方法,除此之外也可以尝试在 XML Dialplan 中转入 LUA Dialplan,如:

<action application="transfer" data="$1 LUA /tmp/dp.lua"/>

或直接修改 Profile,让电话在呼入时直接进入 LUA Dialplan。如将 internal.xml 中的:

<param name="dialplan" value="XML"/>
<param name="context" value="public"/>
修改为:
<param name="dialplan" value="LUA"/>
<param name="context" value="/tmp/dp.lua"/>

执行 sofia profile internal rescan 使之生效。如果是注册用户拨打的话,还需要修改User Directory中的user_context(因为它的优先级比Profile中的context要高),如在1000.xml中:

<variable name="user_context" value="/tmp/dp.lua"/>

连接数据库

JavaScript ELS 开发

JavaScript 是 Web 浏览器上最主流的编程语言,它最早用于配合HTML渲染页面,由于node.js 的发展使它在服务器端的应用也发扬光大。它遵循EMCAScript标准。FreeSWITCH  通过加载 mod_v8 模块可以使用 JavaScript 解析器,该模块基于Google的V8 JavaScript库。

在FreeSWITCH中,二者除了语法不同外,其用法类似。如使用JavaScript(它是一个App)执行一个Session相关的脚本,或jsrun(它是一个API)执行一个非Session相关的脚本。

上面的 Lua 脚本可以用 JavaScript 重写如下:

session.answer();
session.sleep(1000);
session.streamFile("/tmp/hello-js.wav");
session.hangup();

在XML Dialplan中使用如下配置将来话交给上述脚本处理(假设文件名为test.js):

<action application="javascript" data="/tmp/test.js"/>

在源代码目录中的 scripts/javascript目录下有几个JavaScript应用的例子,可自行研究。

官网示例:

Javascript Examples | FreeSWITCH Documentation (signalwire.com)

示例:

Python ELS 开发

freeswitch 在使用 python 做业务开发时,有2种接入方式,

  • 一种是 mod_python 模块。freeswitch 源码安装时,默认不安装 mod_python 模块,需要进入源代码目录中安装 Python 模块。freeswitch python模块:https://zhuanlan.zhihu.com/p/410634433
  • 一种是 ESL 接口。通过 socket 套接字与 freeswitch 进行命令交互,包括发送命令、命令响应和事件回调等,类似于在外部增加一个第三方模块控制 fs 行为。pip install python-ESL

python-ESL 库

官网文档:https://developer.signalwire.com/freeswitch/FreeSWITCH-Explained/Client-and-Developer-Interfaces/Python-ESL/

在 FreeSWITCH 源目录中,更改为 libs/esl 并运行:

make pymod
make pymod-install

这会将 ESL 模块安装到 python site-packages 文件夹中。如果想手动安装它或将其保留在本地,你仍然必须运行 make pymod 命令来编译它,随后可以将 libs/esl/_ESL.so 和 libs/esl/ESL.py 复制到你选择的文件夹中。

示例 1:

#!/usr/bin/env python

'''
events.py - subscribe to all events and print them to stdout
'''
import ESL

con = ESL.ESLconnection('localhost', '8021', 'ClueCon')

if con.connected():
    con.events('plain', 'all')
    while 1:
        e = con.recvEvent()
        if e:
            print e.serialize()

示例 2:

#!/usr/bin/env python

'''
server.py
'''
import SocketServer
import ESL

class ESLRequestHandler(SocketServer.BaseRequestHandler):
    def setup(self):
        print self.client_address, 'connected!'

        fd = self.request.fileno()
        print fd

        con = ESL.ESLconnection(fd)
        print 'Connected: ', con.connected()
        if con.connected():

            info = con.getInfo()

            uuid = info.getHeader("unique-id")
            print uuid
            con.execute("answer", "", uuid)
            con.execute("playback", "/ram/swimp.raw", uuid);
        
# server host is a tuple ('host', port)
server = SocketServer.ThreadingTCPServer(('', 8040), ESLRequestHandler)
server.serve_forever()

示例 3:

#!/usr/bin/env python

'''
single_command.py - execute a single command over ESL
'''
from optparse import OptionParser
import sys

import ESL


def main(argv):
    parser = OptionParser()
    parser.add_option('-a', '--auth', dest='auth', default='ClueCon',
                      help='ESL password')
    parser.add_option('-s', '--server', dest='server', default='127.0.0.1',
                      help='FreeSWITCH server IP address')
    parser.add_option('-p', '--port', dest='port', default='8021',
                      help='FreeSWITCH server event socket port')
    parser.add_option('-c', '--command', dest='command', default='status',
                      help='command to run, surround multi-word commands in ""s')

    (options, args) = parser.parse_args()

    con = ESL.ESLconnection(options.server, options.port, options.auth)

    if not con.connected():
        print 'Not Connected'
        sys.exit(2)

    # Run command
    e = con.api(options.command)
    if e:
        print e.getBody()

if __name__ == '__main__':
    main(sys.argv[1:])

greenswitch 库

  • scripts/python/freepy Twisted 实现的客户端示例
  • eventsocket:备用的 twisted protocol,支持入站和出站方法+示例
  • PySWITCH:为Python和Twisted程序员提供了又一个库。它广泛支持 FreeSWITCH API, bgapi 和 Dialplan 工具。
  • greenswitch:使用 gevent greenlet 的事件套接字协议的实现。它已经投入生产,每天处理数百个电话。
  • switchio:asyncio 支持的集群控制,它利用了现代 Python 新的本地协程。带有一个完全成熟的自动拨号器,最初是为压力测试而构建的。

python-ESL 好久没更新,可以使用 greenswitch:https://github.com/EvoluxBR/greenswitch

greenswitch 是基于 Gevent 开发,并且是经过实战验证的 FreeSWITCH Event Socket Protocol 客户端。 完全支持 Python3!

安装:pip install greenswitch

入站套接字模式

import greenswitch
fs = greenswitch.InboundESL(host='127.0.0.1', port=8021, password='ClueCon')
fs.connect()
r = fs.send('api list_users')
print(r.data)

示例:

import gevent
import greenswitch
from loguru import logger

fs = greenswitch.InboundESL(host='127.0.0.1', port=8021, password='ClueCon')
fs.connect()


def func_reg_event():
    # 自定义事件处理函数
    def my_event_handle(my_event: greenswitch.esl.ESLEvent = None):
        logger.info(f"event_header ---> {my_event.headers}")
        if my_event.headers.get('Event-Name') == 'CHANNEL_ANSWER':
            logger.info("CHANNEL_ANSWER ---> 开始设置录音")
            # 当呼叫被接听,启动录音
            unique_id = my_event.headers.get("Unique-ID")
            record_path = f'd:\\{unique_id}.wav'
            # uuid_record <uuid> [start|stop|mask|unmask] <path> [<limit>] [<recording_vars>]
            fs.send(f'bgapi uuid_record {unique_id} start {record_path}')
            logger.info("CHANNEL_ANSWER ---> 设置录音成功")
        pass

    # 查看所有事件: switch_event.c 中数组 EVENT_NAMES 定义了所有事件
    # ###################### channel 相关事件 ##################
    # "CHANNEL_CREATE",
    # "CHANNEL_DESTROY",
    # "CHANNEL_STATE",
    # "CHANNEL_CALLSTATE",
    # "CHANNEL_ANSWER",  应答
    # "CHANNEL_HANGUP",  挂断
    # "CHANNEL_HANGUP_COMPLETE",
    # "CHANNEL_EXECUTE",
    # "CHANNEL_EXECUTE_COMPLETE",
    # "CHANNEL_HOLD",
    # "CHANNEL_UNHOLD",
    # "CHANNEL_BRIDGE",
    # "CHANNEL_UNBRIDGE",
    # "CHANNEL_PROGRESS",
    # "CHANNEL_PROGRESS_MEDIA",
    # "CHANNEL_OUTGOING",
    # "CHANNEL_PARK",
    # "CHANNEL_UNPARK",
    # "CHANNEL_APPLICATION",
    # "CHANNEL_ORIGINATE",
    # "CHANNEL_UUID"
    # * 代表所有事件
    fs.register_handle('*', my_event_handle)
    print("注册事件成功")
    r = fs.send("event plain ALL")
    print(f"订阅事件成功 ---> {r.data}")


def func_dial():
    # send 返回的是 'greenswitch.esl.ESLEvent' 类型
    # 事件有两部分: header; data
    resp_event = fs.send('api originate user/1010 &echo')
    print(f"headers ---> {resp_event.headers}")
    print(f"data ---> {resp_event.data}")


def func_show():
    # show api 查看所有命令
    resp_event = fs.send('api show api')
    print(f"headers ---> {resp_event.headers}")
    print(f"data ---> {resp_event.data}")


def main():
    func_reg_event()
    func_dial()
    while True:
        try:
            gevent.sleep(1)
        except KeyboardInterrupt:
            fs.stop()
            break
    print('ESL Disconnected.')


if __name__ == '__main__':
    func_show()
    main()
    pass

详细使用示例:https://github.com/EvoluxBR/greenswitch/blob/master/tests/test_lib_esl.py

出站套接字模式

出站是通过同步和异步支持实现的。主要思想是创建一个应用程序,该应用程序将被调用,将 OutboundSession 作为参数传递。此 OutboundSession 表示由 ESL 连接处理的调用。基本功能已经实现:

  • playback 回放
  • play_and_get_digits
  • hangup 挂断
  • park 公园
  • uuid_kill
  • answer 答
  • sleep 睡

使用当前的 api,很容易混合同步和异步操作,例如: play_and_get_digits方法将在块模式下返回按下的 DTMF 数字,这意味着只要您在 Python 代码中调用该方法,执行流就会阻塞并等待应用程序结束,只有在结束应用程序后才能返回下一行。但是在获取数字后,如果您需要使用外部系统,例如将其发布到外部 API,您可以在 API 调用完成时让调用者听到 MOH,您可以使用 block=False、playback('my_moh.wav', block=False) 调用 playback 方法,在您的 API 结束后,我们需要告诉 FreeSWITCH 停止播放文件并返回调用控制权, 为此,我们可以使用uuid_kill方法。

'''
Add a extension on your dialplan to bound the outbound socket on FS channel
as example below

<extension name="out socket">
    <condition>
        <action application="socket" data="<outbound socket server host>:<outbound socket server port> async full"/>
    </condition>
</extension>

Or see the complete doc on https://freeswitch.org/confluence/display/FREESWITCH/mod_event_socket
'''
import gevent
import greenswitch

import logging
logging.basicConfig(level=logging.DEBUG)


class MyApplication(object):
    def __init__(self, session):
        self.session = session

    def run(self):
        """
        Main function that is called when a call comes in.
        """
        try:
            self.handle_call()
        except:
            logging.exception('Exception raised when handling call')
            self.session.stop()

    def handle_call(self):
        # We want to receive events related to this call
        # They are also needed to know when an application is done running
        # for example playback
        self.session.myevents()
        print("myevents")
        # Send me all the events related to this call even if the call is already
        # hangup
        self.session.linger()
        print("linger")
        self.session.answer()
        print("answer")
        gevent.sleep(1)
        print("sleep")
        # Now block until the end of the file. pass block=False to
        # return immediately.
        self.session.playback('ivr/ivr-welcome')
        print("welcome")
        # blocks until the caller presses a digit, see response_timeout and take
        # the audio length in consideration when choosing this number
        digit = self.session.play_and_get_digits('1', '1', '3', '5000', '#',
                                                 'conference/conf-pin.wav',
                                                 'invalid.wav',
                                                 'test', '\d', '1000', "''",
                                                 block=True, response_timeout=5)
        print("User typed: %s" % digit)
        # Start music on hold in background without blocking code execution
        # block=False makes the playback function return immediately.
        self.session.playback('local_stream://default', block=False)
        print("moh")
        # Now we can do a long task, for example, processing a payment,
        # consuming an APIs or even some database query to find our customer :)
        gevent.sleep(5)
        print("sleep 5")
        # We finished processing, stop the music on hold and do whatever you want
        # Note uuid_break is a general API and requires full permission
        self.session.uuid_break()
        print("break")
        # Bye caller
        self.session.hangup()
        print("hangup")
        # Close the socket so freeswitch can leave us alone
        self.session.stop()

server = greenswitch.OutboundESLServer(
    bind_address='0.0.0.0',
    bind_port=5000,
    application=MyApplication,
    max_connections=5
)
server.listen()

2、嵌入式(Lua) 及 HTTP开发

官网 lua 示例

Lua examples | FreeSWITCH Documentation (signalwire.com)

示例:

交互 小游戏

装个软电话,拨“1”就会进入FreeSWITCH上的一个Lua程序,该程序会提示输入一个数字,并使用TTS读出这个数字。如果输入的是“*”,就将数字减 1,如果按的是“#”,就将数字加 1。

local x = 1
function onInput(s, type, obj, arg)
	if (type == "dtmf") then
		freeswitch.consoleLog("INFO","DTMF: " .. obj.digit .. " Duration: " .. obj.duration .. "\n")
		if (obj.digit == "*") then
			x = x - 1
			if (x < 0) then x = 0 end
			n = x
		elseif (obj.digit == "#") then
			x = x + 1
			n = x
		else
			n = obj.digit
		end
		s:execute("system", "banner -w 40 " .. n)
		s:speak(n)
	end
	return ''
end
session:set_tts_params("tts_commandline", "Ting-Ting")
session:answer()
session:speak("请按一个数字")
session:setInputCallback('onInput', '')
session:streamFile("local_stream://moh")

程序的代码很简单。在第2行定义了一个onInput函数,当有按键输入时,系统会调用该回调函数,它用一个简单的算法计算一个变量值n,然后在第 15 行调用banner在控制台上输出n(其中的s变量就是传入的当前的“session”),并在第16行使用TTS技术“说”出n的值。

真正脚本的执行是从第20行开始的。该脚本在执行时会自动获得一个session变量,它唯一标志了当前的通话。在第20行,首先设置了将要使用的TTS的参数;然后在第21行进行应答;第22行播放一个提示音;第23行安装一个回调函数,当该session上有输入时,它将回调该函数;第24行播放保持音乐。

这里只是简单地按“1”就呼叫到该脚本,Dialplan 如下:

<extension name="Number Game">
	<condition field="destination_number" expression="^1$">
		<action application="lua" data="numbers_game.lua"/>
	</condition>
</extension>

按“2”时来一阵视频通话:

<extension name="Video Me">
	<condition field="destination_number" expression="^2$">
		<action application="bridge" data="user/1007"/>
	</condition>
</extension>

用 Lua 实现 IVR

Lua Welcome IVR example | FreeSWITCH Documentation (signalwire.com)

IVR (Interactive Voice Response)交互式语言应答,是呼叫中心的1个经典应用场景,FreeSwitch官方有一个利用 lua 实现的简单示例,大致原理是利用 lua 脚本 + TTS实现

步骤1:安装TTS

FreeSwitch自带了1个TTS引擎(发音效果比较生硬,仅支持英文,不过用来学习足够了),找到安装目录下的 freeswitch/conf/modules.conf.xml

    <!-- ASR /TTS -->
    <load module="mod_flite"/>
    <!-- <load module="mod_pocketsphinx"/> -->
    <!-- <load module="mod_cepstral"/> -->
    <!-- <load module="mod_tts_commandline"/> -->
    <!-- <load module="mod_rss"/> -->

找到ASR /TTS这一节,把mode_flite注释去掉,然后重启FreeSwitch 生效(如果没生效,检查是否有mod_flite.dll这个文件)

步骤2:配置路由

\FreeSWITCH\conf\dialplan\default\welcome.xml,在default目录 下,创建welcome.xml文件,内容如下:

<include>
  <extension name="welcome_ivr">
    <condition field="destination_number" expression="^2910$">
      <action application="lua" data="welcome.lua"/> 
    </condition>
  </extension>
</include>

这段的意思是 如果被叫号码是2910,将由welcome.lua脚本来执行后续逻辑。

步骤3:编写交互逻辑lua脚本

\FreeSWITCH\scripts\welcome.lua (创建该文件),内容如下:

-- 先应答,防止电话断掉
session:answer();
while (session:ready() == true) do
    -- 防止自动挂断
    session:setAutoHangup(false);
    -- 设置TTS引擎参数
    session:set_tts_params("flite", "kal");
    -- 播放欢迎语音
    session:speak("Hello. Welcome to the VoIp World!");
    -- 睡100ms
    session:sleep(100);
    -- 播放提示语音
    session:speak("please select an Action.");
    session:sleep(100);
    -- 按1转到1001分机
    session:speak("to call 1001, press 1");
    session:sleep(100);
    -- 按2挂断
    session:speak("to hangup , press 2");
    session:sleep(2000);
    -- 等待按键(5秒超时)
    digits = session:getDigits(1, "", 5000);
    if (digits == "1")  then
        -- 按1,转到1001分机
        session:execute("bridge","user/1001");
    end
    if (digits == "2")  then
        -- 按2,播放bye,bye语音,然后挂断
        session:speak("bye bye");
        session:hangup();
    end
end

在会议中呼出

在会议中通过DTMF去呼叫其他人加入会议?实现的方法有很多种,这里来看一下如何通过Lua脚本来实现。

prompt="tone_stream://%(10000,0,350,440)"
error="error.wav"
result = ""
extn = session:playAndGetDigits(1, 4, 3, 5000, '#', prompt, error, "\\d+")
arg = "3000 dial user/" .. extn
session:execute("log", "INFO extn=" .. extn)
session:execute("log", "INFO arg=" .. arg)
if not (extn == "") then
	api = freeswitch.API()
	result = api:execute("conference", arg)
	session:execute("log", "INFO result=" .. result)
else
	session:execute("log", "ERR Cannot result=" .. result)
end

在会议中,可以使用DTMF进行控制。我们先把会议控制中的call-controller部分的*和#号键对应的功能修改一下,让*号键对应执行我们刚写的Lua脚本(conference_dial.lua),并把#号键对应的功能注释掉,以防止产生冲突。autolocal_configs/conference.conf.xml中对应的配置如下:

<caller-controls>
	<group name="default">
		<control action="execute_application" digits="*" data="lua conference_dial.lua"/>
		<!-- <control action="hangup" digits="#"/> -->
	</group>
</caller-controls>

然后,打个电话呼入名称为3000的会议,在会议中就可以通过按*号键在听到拨号音后输入一个号码进行外呼了。如果只想会议管理员才能使用上述功能,也可以将上述功能键的映射关系放到单独的group中(如group name="modrator")并通过会议Profile中的moderator-controls指定该组以确保只有管理员才可以使用这些按键来进行控制。

在FreeSWITCH中外呼的脚本

能否实现在FreeSWITCH中外呼,然后放一段录音?当然能!写个简单的脚本就行。实现思路是:将待呼号码放到一个文件中,每个号码一行,然后用Lua依次读取每一行,并进行呼叫。呼通后播放一个声音文件,并将呼叫(通话)结果写到一个日志文件中。但如果要求还要知道呼叫是否成功,那实现就要复杂一点了。

prefix = "{ignore_early_media=true}sofia/gateway/gw1/"
number_file_name = "/usr/local/freeswitch/scripts/number.txt"
file_to_play = "/usr/local/freeswitch/sounds/ad.wav"
log_file_name = "/usr/local/freeswitch/log/dialer_log.txt"

function debug(s)
	freeswitch.consoleLog("notice", s .. "\n")
end

function call_number(number)
	dial_string = prefix .. tostring(number);
	debug("calling " .. dial_string);
	session = freeswitch.Session(dial_string);
	if session:ready() then
		session:sleep(1000)
		session:streamFile(file_to_play)
		session:hangup()
	end
	-- waiting for hangup
	while session:ready() do
		debug("waiting for hangup " .. number)
		session:sleep(1000)
	end
	return session:hangupCause()
end

number_file = io.open(number_file_name, "r")
log_file = io.open(log_file_name, "a+")
while true do
	line = number_file:read("*line")
	if line == "" or line == nil then break end
	hangup_cause = call_number(line)
	log_file:write(os.date("%H:%M:%S ") .. line .. " " .. hangup_cause .. "\n")
end

将上述脚本保存到FreeSWITCH的scripts目录中(通常是/usr/local/freeswitch/scripts/),命名为dialer.lua,然后在FreeSWITCH控制台上执行如下命令便可以开始呼叫了:

freeswitch> luarun dialer.lua

除此之外,还有一个batch_dialer,用于批量外呼,感兴趣的可以研究下:http://fisheye.freeswitch.org/browse/freeswitch-contrib.git/seven/lua/batch_dialer.lua?hb=true

使用 Lua 通过多个网关循环外呼

有时候,外呼需要通过多个网关。除了可以使用 mod_distributor 将呼叫分配到多个网关。也可以使用 Lua 脚本来实现功能。

关键位置

retries = 0
bridge_hangup_cause = ""
gateways = {"gw1", "gw2", "gw3", "gw4"}
dest = argv[1]

function call_retry()
	freeswitch.consoleLog("notice", "Calling [" .. dest .. "] From Lua\n");
	retries = retries + 1
	if not session.ready() then
		return;
	end
	dial_string = "sofia/gateway/" .. gateways[retries] .. "/" .. dest;
	freeswitch.consoleLog("notice", "Dialing [" .. dial_string .. "]\n");

	session:execute("bridge", dial_string);
	bridge_hangup_cause = session:getVariable("bridge_hangup_cause") or session:getVariable("originate_disposition");
	if (retries < 4 and
		(bridge_hangup_cause == "NORMAL_TEMPORARY_FAILURE" or
		bridge_hangup_cause == "NO_ROUTE_DESTINATION" or
		bridge_hangup_cause == "CALL_REJECTED" or
	
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值