官网文档: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、客户端和开发接口
- 命令行接口 (fs_cli)
- C# ESL
- 开发文档
- Faxlib documentation
- Golang ESL
- Ruby ESL
- Script Language Choice
- fs_logger.pl
- fs_rpt.pl
基于 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与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)
示例:
- Javascript Example - DISA (direct inward system access)
- JavaScript Example - Session in Hangup Hook
- JavaScript Example - Test Tones
- JavaScript Example - cidspoof
- JavaScript Example - cnam
- JavaScript Example - dbIVRmenu
- JavaScript example - XML
- Javascript Example - AfterHoursIVR
- Javascript Example - Answering Machine
- Javascript Example - Collect Account Number
- Javascript Example - DTMF Callback
- Javascript Example - FollowMe
- Javascript Example - HelloWorld
- Javascript Example - Intercom
- Javascript Example - Prompt For Digits
- Javascript Example - set hook
- Sched hangup javascript example
- Session getVariable
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 库
在 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)
示例:
- Lua: send SMS via Flowroute when voicemail is received
- Lua ASR TTS Directory example
- Lua DISA Example
- Lua Database agent login example
- Lua Directory example
- Lua Fakecall responder example
- Lua Group Pickup example
- Lua IVR Menu Example
- Lua Intercom example
- Lua Mail Call example
- Lua Mail on NoAnswer example
- Lua MythTV alert example
- Lua Numeric Paging Example
- Lua TeleCaptcha example
- Lua Welcome IVR example
- Lua arguments calling functions
- Lua example Bridging two calls with retry
- Lua example Send mail when no answer
交互 小游戏
装个软电话,拨“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