官网文档: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
bridge_hangup_cause == "INVALID_GATEWAY") ) then
freeswitch.consoleLog("notice",
"On calling [" .. dest .. "] hangup. Cause: [" ..
bridge_hangup_cause .. "]. Retry: " .. retries .. " \n");
session:sleep(1000);
call_retry();
else
freeswitch.consoleLog("notice", "Retry Exceed, Now hangup!\n");
end
end
session:preAnswer();
session:setVariable("hangup_after_bridge", "true");
session:setVariable("continue_on_fail", "true");
call_retry();
Dialplan调用该脚本:
<extension name="Lua Multi-GW Example">
<condition field="destination_number" expression="^0(.*)$">
<action application="lua" data="call_gw.lua $1"/>
</condition>
</extension>
其中,如果匹配到以0开头的被叫号码,我们“吃掉”0,把剩余的部分作为参数传给Lua脚本,然后在Lua脚本中就可以从argv[1]中获得这些被叫号码的值了
在FreeSWITCH中执行长期运行的嵌入式脚本
上面的Lua脚本都是“短暂”运行的——它们或者存在于一路通话的会话期内,或者是在命令行上执行一个短暂的命令。但在有些情况下,我们可能希望脚本能永远不停地运行,下面来看一个例子。
写一个Lua脚本,用于监控网关的状态。实现的思路是:如果接收到挂机事件,就判断该通话是否是经过一个网关出去的;如果是,就判断通话是否成功;然后记录统计结果,并将统计结果以几种方式呈现:
- 在FreeSWITCH中触发一个事件,由其他程序进行处理;
- 发送到一个远端的HTTP服务器上;
- 直接写入数据库;
- 其他方式,如写入一个文件等。
其他的程序在收到这些统计结果后再使用Web方式呈现,进而我们可以知道哪些网关(运营商提供的SIP中继)比较好,哪些网关总是出问题。
既然是长期运行的脚本,那为什么要停止呢?大部分时间是不需要停止的,但是都是开发人员,如果在开发过程中你需要调试和修改脚本,总不能每次都重启FreeSWITCH吧。
- 通过使用事件机制构造另一个循环,然后就可以在检测到一个特殊事件后停止该循环。
- 在循环体内通过检测一个FreeSWITCH全局变量的值来终止循环
使用 Lua 提供 XML Binding
上面用 Lua 实现了动态拨号计划,下面再看下 Lua 能提供的另外一个功能:XML绑定(Binding)
前面学习过的 XML 配置文件都是静态的,在很多时候编辑静态的XML很不方便。FreeSWITCH 提供了一种机制可以在 XML 配置节点上绑定一些回调(函数或方法),然后当FreeSWITCH用到一段XML时,就可以调用该回调功能来取得XML。我们可以使用Lua绑定一个回调,并通过Lua脚本来产动态的XML内容。
语音识别
语音识别与TTS技术可以说是一对孪生兄弟,但“长相”却迥然不同。
TTS是把文字转成语音,而语音识别则是把声音转换成文字 。
- TTS技术是比较容易实现的,最简单的实现仅需要用查表法将与文字对应的录音逐个查找到并读出,高级一些的借助一些语法和词法分析并借助语音合成技术能读出抑扬顿挫的声调;
- 语音识别就不同了,它不仅需要语法和词法分析,还需要“理解”声音的内容,以转换成合适的文字。语音识别分为基于关键词的识别和自然语音识别。基于关键词的识别比较成熟,因为词汇数量有限,比较容易做到精确。这类应用一般用于声控场合,如发出打开、关闭(设备或程序)、呼叫(某人)等命令。基于自然语言的识别则比较难
通过商业语音识别软件进行识别
使用 mod_xml_curl 提供动态用户管理
可以使用 Lua 来绑定一个回调为FreeSWITCH提供XML Dialplan,但Lua脚本的灵活性还是稍微差一点。因此,这里再来看一个用外部的脚本来提供XML用户目录的例子。
FreeSWITCH默认使用静态的XML文件配置用户,但如果要动态认证,就需要跟数据库相关联。FreeSWITCH通过使用mod_xml_curl模块完美解决了这个问题。它的实现思路是你自己提供一个HTTP服务器,当有用户有注册请求时(或INVITE或其他,总之需要XML的请求时),FreeSWITCH向你的HTTP服务器发送请求,你查询数据库生成一个标准的XML文件,FreeSWITCH进而通过这一文件所描述的用户信息对用户进行认证。
FreeSWITCH会将每次请求得到的XML文件存放到文件名类似/tmp/xxx.xml的文件中
使用 mod_xml_cdr 模块处理话单
与mod_xml_curl模块类似,mod_xml_cdr会在每次生成话单后请求一个HTTP服务器,然后HTTP服务器就可以进行一些逻辑处理和写入数据库等操作。在HTTP服务器端,用户可以使用任何熟悉的语言(如Java、PHP、Ruby、Python、C#等)来开发。
3、Event Socket
参见:http://wiki.freeswitch.org/wiki/Event_Socket
与Lua之类的嵌入式语言不同,通过 Event Socket 方式,可以使用运行在FreeSWITCH外部的程序控制FreeSWITCH。Event Socket是操控FreeSWITCH的“瑞士军刀”。它可以通过Socket方式使用FreeSWITCH提供的所有的App程序和API命令。由于绝大多数程序语言都支持Socket,因而它几乎可以跟任何语言开发的程序通信,也就是说,它几乎可以跟任何系统进行集成。
FreeSWITCH 使用 SWIG 来支持多语言。简单来讲,FreeSWITCH 用C语言写了一些库函数,通过 SWIG 包封装成其他语言接口。现在已知 FreeSWITCH 支持的语言有 C、Perl、PHP、Python、Ruby、Lua、Java、Tcl, 以及由 Managed 支持的 .Net平台语言如 C#、VB.NET 等。
Simplified Wrapper and Interface Generator,即简单包装及接口生成器,用于帮助使用C或C++语言写的程序生成其他高级语言的接口。参见:http://www.swig.org/
Event Socket 其实并没有提供什么新的功能,只是提供了一个开发接口,所有的通道处理及媒体控制都是由FreeSWITCH内部提供的App和API来完成的。
Event Socket 架构
Event Socket 有两种模式 ( 内和外都是针对FreeSWITCH而言 ):
- 内连( Inbound )模式
- 外连( Outbound )模式
初学者往往比较容易理解 Event Socket 的 Inbound模式 和 Outbound模式,但对于何时该使用哪种模式不是很清楚。一般来说,
- Outbound 比较适合控制单腿的呼叫,实现复杂的 IVR 应用;Outbound 模式的 Socket 是由 FreeSWITCH 建立的,它是建立在 Channel 的基础上的,每一个Channel 均会为外部的 TCP Server 建立一个连接,在 Channel 挂机时释放。因此,Outbound 的连接要考虑 Channel 的生命周期(即 Socket 的生命周期)。在Outbound 模式中,又分为同步模式和异步模式,同步模式控制比较简单,但自由度较小;异步模式需要更多的编程技巧,但会更强大。
- Inbound 更适合接收所有的事件,与多条腿进行交互,进行更复杂的呼叫控制。Inbound 的连接由客户端主动向FreeSWITCH 发起连接,只需要考虑断线重连等问题。
当然,上述说法不是绝对的,Inbound 和 Outbound两种模式都能完成所有的控制功能。在实际开发应用中,具体使用哪种模式需要具体问题具体分析,解决问题需要自己动手进行实验,而不能盲目迷信别人说的。如果你使用C语言进行ESL开发,可以参考一下源代码目录中的 fs_cli.c(在libs/esl目录中),里面有各种函数的真实使用方法。
内连 ( Inbound ) 模式
在内连模式下,FreeSWITCH 作为一个服务器,而用户的程序可以作为一个TCP Client主动连接到FreeSWITCH上。
FreeSWITCH 允许多个客户端连接。每个客户端连接上来以后,可以订阅 FreeSWITCH 的一些内部事件。FreeSWITCH 通过 EventSocket 向外部发送信息,这些信息就是以事件的形式体现的。同样,在内部好多功能也是事件驱动的。用户的 TCP Client 收到这些事件后,可以通过执行 App和 API 来控制FreeSWITCH的行为。
- 对于内连模式,可以把 TCP Client 看作是一个第三者,它通常不是 Channel 的一部分,而是监听到一个感兴趣的事件以后,通过 API( uuid_一族的API )来对 Channel 进行操作。
示例:FreeSWITCH 启动后,会启动一个 Event Socket TCP Server,IP、端口号和密码均可以在conf/autoload_configs/event_socket.conf.xml文件里配置。
这里使用 nc 作为客户端,使用以下命令连接 FreeSWITCH:nc localhost 8021
连接上以后,会看到如下消息:Content-Type: auth/request
这表示已经连接上FreeSWITCH的Socket了,并且它告诉你,应该输入密码进行验证。这时我们输入“auth ClueCon”,记得按两下回车。FreeSWITCH默认监听在8021端口上,默认的密码是ClueCon,因此我们在上面使用了这些默认值,当然需要的话也可以根据情况在conf/autoload_configs/event_socket.conf.xml 中修改。
如果一切顺利的话,我们就已经作为一个客户端连接到FreeSWITCH上了。可以输入下列命令试一试(记得每个命令后面都按两下回车):
api version
api status
api sofia status
api uptime
肯定经常在fs_cli中使用这些命令,只不过在此我们在每个命令前面多加了个“api”。其实fs_cli作为一个客户端也是使用ESL与FreeSWITCH通信的,只是它帮你做了好多事,你不用手工敲一些协议细节了。但在这里,我们手工输入各种命令,更有助于理解这些细节,例如:
api status
Content-Type: api/response
Content-Length: 327
UP 0 years, 0 days, 17 hours, 13 minutes, 19 seconds, 959 milliseconds, 304 microseconds
FreeSWITCH (Version 1.2.11 git b9c9d34 2013-07-20 19:06:40Z) is ready
27 session(s) since startup
0 session(s) - 0 out of max 30 per sec peak 2 (0 last 5min)
1000 session(s) max
min idle cpu 0.00/100.00
Current Stack Size/Max 240K/8192K
键入如下命令接收事件:event plain ALL 可以订阅所有的事件,当然如果你看不过来可以少订一些。比如命令仅订阅CHANNEL_CREATE事件:even plain CHANNEL_CREATE
它等效于在fs_cli中输入以下命令:/event plain CHANNEL_CREATE
1. 使用nc localhost 8021链接freeswitch,会出现
Content-Type: auth/request
2. 输入 “auth ClueCon”按两下回车,显示以下表示成功
Content-Type: command/reply
Reply-Text: +OK accepted
3. 可以使用命令控制freeswitch
1. api version 查看freeswitch版本
2. api status 查看状态
3. api sofia status 查看sofia状态
4. event plain ALL订阅所有事件
5. 订阅某一个事件
event plain <事件名称>
event plain CHANNEL_CREATE
外连 ( Outbound ) 模式
如图所示,FreeSWITCH 作为一个TCP客户 端连接到一个 TCP Server上。
TCP Server 就是用户自己需要开发的部分,用户可以实现自己的业务逻辑,以及连接数据库获取数据帮助决策等。
怎么让 FreeSWITCH 去连接 用户的TCP Server呢?FreeSWITCH是一个B2BUA,当Bob呼叫Alice时,首先电话会到达FreeSWITCH(通过SIP),建立一个单腿的Channel(a-leg),然后电话进入路由状态,FreeSWITCH查找Dialplan,然后可以通过以下动作建立一个到TCP Server的连接:<action application="socket" data="127.0.0.1:8040"/> 到此为止,还是只有一个Channel。其中socket是一个App,它会先把这个Channel置为Park状态,然后FreeSWITCH作为一个TCP客户端连接到TCP Server上,把当前呼叫的相关信息告诉它,并询问下一步该怎么做。当然,这里FreeSWITCH跟TCP Server说的语言称为ESL,该语言只有它们两个人懂,与SIP及RTP没有任何关系。也就是说,TCP Server 只是发布控制指令,并不实际处理语音数据。
接下来,TCP Server 收到 FreeSWITCH 的连接请求后,进行决策,如果它认为Bob想要呼叫Alice(根据来话信息和主被叫号码判断),它就给FreeSWITCH发一个执行bridge App的消息,告诉它应该继续呼叫Alice(给Alice发SIP INVITE消息)。
在Bob挂机之前,FreeSWITCH会一直向TCP Server汇报Channel的相关信息,所以这个TCP Server就可掌握这路电话所有的详细信息,也可以在任何时间对它们发号施令。
Bob挂机后,与TCP Server的连接也会断开,并释放资源。读到这里,读者应该明白了。之所以叫做TCP Server,是因为它应该是一个服务器应用,永远在监听一个端口(在上面的例子中是8040)等待有人连接。如果需要支持多个连接,这个服务器就应该使用Socket的Select机制或做成多线程(多进程)的。
所谓 Inbound 和 Outbound 都是针对 FreeSWITCH 而言的。由于在这种模式下,FreeSWITCH 要向外连接到 TCP Server,因此称为 外连模式。
示例:使用如下 Dialplan 进行测试
<extension name="socket">
<condition field="destination_number" expression="^1234">
<action application="socket" data="localhost:8040 async full"/>
</condition>
</extension>
当电话呼叫1234时,FreeSWITCH便会使用Outbound模式,使用socket App启动Socket连接。注意这里的两个参数 async 和 full。
- async 表示异步执行。默认是同步的,比如在同步状态下,如果FreeSWITCH正在执行playback操作,而playback是阻塞的,因而在playback完成之前向它发送任何消息都是不起作用的,但异步状态可以进行更灵活的控制。当然,异步状态增加了灵活性的同时也增加了开发的复杂度,在实际的开发过程中可以对比一下它们的异同。
- full 指明可以在外部程序中使用全部的API,默认只有少量的API是有效的。至于哪些API跟这个参数相关,可以自行练习。总之一句话,如果你不确定,那么加上full是没有错的。
好了,电话来了,由于还没有准备好TCP Server,因此连接会失败,电话就断掉了。下面我们需要实现一个TCP Server,在这里使用 netcat 这个工具来讲解。
netcat 是一般 Linux 系统自带的一个工具,它可以启动一个 Socket,做服务器或客户端。如果作为客户端,你可以认为它类似于你更熟悉的 telnet 命令。虽然它叫 netcat,但程序的名字是 nc。
打开一个终端A,启动一个ServerA,监听8040端口(其中-l表示监听,即listen;-k表示客户端断开后继续保持监听。注意,有些版本的netcat参数稍有不同,使用时请查看相关的man文档)。
nc -l -k localhost 8040
打开另一个终端(Terminal)B,启动一个Client B连上它:
nc localhost 8040
然后在这个客户端中打些字,回车,在终端A中就应该能看到你输入的文字了。
好了,接下来按Ctrl+C退出终端B,到这里就该让FreeSWITCH上场了,即我们用FreeSWITCH来替换终端B。
拿起电话拨打1234,电话路由到Socket,FreeSWITCH就会连到ServerA上。这时候你听不到任何声音,因为Channel处于Park状态。但是你在ServerA里也看不到任何连接的迹象。不过,如果你打开另一个终端,使用如下命令可以显示8040端口已处于连接(ESTABLISHED)状态。
$ netstat -an|grep 8040
tcp4 0 0 127.0.0.1.8040 127.0.0.1.60588 ESTABLISHED
tcp4 0 0 127.0.0.1.60588 127.0.0.1.8040 ESTABLISHED
tcp4 0 0 127.0.0.1.8040 *.* LISTEN
现在回到终端A,输入 connect 然后按两下回车,奇迹出现了吧?会看到类似下面的输出:
Event-Name: CHANNEL_DATA
Core-UUID: 4bfcc9bd-6844-4b45-96a7-4feb2a4f9795
...
这便是 FreeSWITCH 发给 ServerA 的第一个事件消息,里面包含了该Channel所有的信息。下面该Channel何去何从就完全看你的了。比如,发送如下消息给它放段音乐(local_stream://moh):
sendmsg
call-command: execute
execute-app-name: playback
execute-app-arg: local_stream://moh
建议你直接粘贴上面的命令到ServerA的窗口里,记得完成后按两下回车。sendmsg的作用就是发送一个App命令(这里是playback)给FreeSWITCH,然后FreeSWITCH就乖乖地照着做(执行该App)了。如果你玩够了,就把上面的playback换成hangup再发一次,电话就挂断了。这些命令就跟直接写到Dialplan里一样,不同的是现在是由你来控制,以后你可以用自己的程序控制什么时候该发什么命令。
1. nc -l -k localhost 8040 启动监听
2. nc localhost 8040 开启终端监听
3. ctrl+c退出B终端,电话拨打1234,链接到8040,
4. 回到A终端输入connect然后打两下回车,会出现
Event-Name: CHANNEL_DATA
Core-UUID: 5ed01200-5c09-11e9-8ae3-6733192b29d4
...
5. 测试播放一段音乐
sendmsg
call-command:execute
execute-app-name:playback
execute-app-arg:local_stream://moh
6. 结束,把上面得playback改为hangup再发一遍,电话就挂断了
Event Socket 命令
ESL 还提供了更多的命令,用于各种控制。下面是部分在 Event Socket 中可以使用的命令。
- auth:对于 Inbound 连接来说,auth 是第一个需要发送的命令,用于向FreeSWITCH认证,格式如下:auth <password> 例如:auth ClueCon
实际的密码(这里是Cluecon)是在conf/autoload_configs/event_socket.conf.xml中定义的。 - api:用于执行FreeSWITCH的API,语法如下:api <command> <args>
其中,command和args分别是FreeSWITCH实际的命令和参数 - bgapi:命令是阻塞执行的,因此,对于执行时间比较长的API命令(如originate),会有一段时间得不到响应结果。因此,可以使用bgapi将这些命令放到后面执行,语法是:bgapi <command> <args> 命令会立即执行,在后台建立一个任务(Job)并返回一个Job-UUID,当真正需要执行的API命令返回后,FreeSWITCH会产生一个BACKGROUND_JOB事件,该事件带了原先的Job-UUID以及命令执行的结果。因而客户端可以通过匹配Job-UUID知道上一次命令的执行结果。FreeSWITCH也允许客户端提供自己的Job-UUID,这样匹配起来就更容易一些(但客户端需要保证产生的Job-UUID要全局唯一),命令格式是:
bgapi <command> <args>
Job-UUID: <uuid>
linger 和 nolinger
在外连模式下,当一个Channel挂断时,FreeSWITCH会断开与 TCP Server 的 Socket 的连接。这时,可能还有与该Channel相关的事件没有来得及发送到TCP Server上,因而会“丢失”事件。为避免这种情况发生,TCP Server可以明确告诉FreeSWITCH希望它能在断开之前 "逗留、徘徊"(linger)一段时间,以等待把所有事件都发完。格式如下:linger <seconds>
例如:linger 10
如果开启 linger 又后悔了,可以再用 nolinger 命令撤销,该命令没有参数。
event (事件)
执行 fs_cli 连接到 freeswitch 并进入控制台。
event 用于订阅事件。让 FreeSWITCH 把相关的事件发送过来。格式是:event [type] <events>
其中,type(即事件类型)有 plain、json 和 xml 三种,默认为 plain,即纯文本;events 参数可以指定事件的名字,或者使用 ALL 表示订阅全部事件。
订阅全部事件的命令如下:event plain ALL
仅订阅部分事件,事件名字之间以空格隔开:event plain CHANNEL_CREATE CHANNEL_ANSWER CHANNEL_HANGUP_COMPLETE
要想订阅 CUSTOM 事件该怎么做呢?CUSTOM 事件即自定义事件,它是一类特殊的事件,它主要是为了扩充 FreeSWITCH 的事件类型,它具体的类型是在 Subclass 中指定。
指定订阅 Subclass 为 "sofia::register" 的事件:event plain CUSTOM sofia::register
可以一次订阅多个CUSTOM事件:event plain CUSTOM sofia::register sofia::unregister sofia::expire
也可以使用多个event命令混合订阅:
event plain CHANNEL_ANSWER CHANNEL_HANGUP
event plain CHANNEL_BRIDGE CUSTOM sofia::register sofia::unregister
但参数中一旦出现了 CUSTOM,后面就不能有普通的事件类型了。
如下面的订阅方法是达不到预期的效果的(FreeSWITCH会把 CHANNEL_ANSWER 当成 CUSTOM 事件的 Subclass 对待,因而不是你想要的):event plain CHANNEL_CREATE CUSTOM sofia::register CHANNEL_ANSER
另外CUSTOM事件必须逐一明确订阅,这种订阅是不对的:event plain CUSTOM ALL
其他的例子还有:
event json CHANNEL_CREATE
event xml CHANNEL_CREATE
event plain DTMF
event plain all 订阅所有事件
event plain CHANNEL_ANSWER CHANNEL_HANGUP 仅订阅电话应答和挂断事件。
noevents 取消 订阅的所有事件
oevent CHANNEL_ANSWER CHANNEL_HANGUP 仅取消订阅的特定事件
最后,值得一提的是,HEARTBEAT 是一个特殊的事件,它每20秒就产生一次,用于汇报FreeSWITCH 的当前状态。有时候可以用它做心跳,如果超过20秒没收到事件,就可以认为网络或FreeSWITCH异常。下面是HEARTBEAT事件的三种不同输出格式:
json格式:
Content-Length: 939
Content-Type: text/event-json
{
"Event-Name": "HEARTBEAT",
...
"Event-Info": "System Ready",
"Up-Time": "0 years, 0 days, 22 hours, 19 minutes, 14 seconds, ...
...
}
XML格式:
Content-Length: 1432
Content-Type: text/event-xml
<event><headers>
<Event-Name>HEARTBEAT</Event-Name>
...
<Event-Info>System%20Ready</Event-Info>
<Up-Time>0%20years,%200%20days,%2022%20hours,%2019%20minutes,%2034%20seconds,...</Up-Time>
...
</headers>
</event>
PLAIN(纯文本)格式:
Content-Length: 848
Content-Type: text/event-plain
Event-Name: HEARTBEAT
...
Event-Info: System%20Ready
Up-Time: 0%20years,%200%20days,%2022%20hours,%2019%20minutes,%2054%20seconds,...
...
查看 所有 事件
所有的 event 都能被 FreeSWITCH 的 event system 处理
最新的所有事件 event 是在 src/switch_event.c 的 EVENT_NAMES数组 中定义
static char *EVENT_NAMES[] = {
"CUSTOM",
"CLONE",
"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",
"API",
"LOG",
"INBOUND_CHAN",
"OUTBOUND_CHAN",
"STARTUP",
"SHUTDOWN",
"PUBLISH",
"UNPUBLISH",
"TALK",
"NOTALK",
"SESSION_CRASH",
"MODULE_LOAD",
"MODULE_UNLOAD",
"DTMF",
"MESSAGE",
"PRESENCE_IN",
"NOTIFY_IN",
"PRESENCE_OUT",
"PRESENCE_PROBE",
"MESSAGE_WAITING",
"MESSAGE_QUERY",
"ROSTER",
"CODEC",
"BACKGROUND_JOB",
"DETECTED_SPEECH",
"DETECTED_TONE",
"PRIVATE_COMMAND",
"HEARTBEAT",
"TRAP",
"ADD_SCHEDULE",
"DEL_SCHEDULE",
"EXE_SCHEDULE",
"RE_SCHEDULE",
"RELOADXML",
"NOTIFY",
"PHONE_FEATURE",
"PHONE_FEATURE_SUBSCRIBE",
"SEND_MESSAGE",
"RECV_MESSAGE",
"REQUEST_PARAMS",
"CHANNEL_DATA",
"GENERAL",
"COMMAND",
"SESSION_HEARTBEAT",
"CLIENT_DISCONNECTED",
"SERVER_DISCONNECTED",
"SEND_INFO",
"RECV_INFO",
"RECV_RTCP_MESSAGE",
"SEND_RTCP_MESSAGE",
"CALL_SECURE",
"NAT",
"RECORD_START",
"RECORD_STOP",
"PLAYBACK_START",
"PLAYBACK_STOP",
"CALL_UPDATE",
"FAILURE",
"SOCKET_DATA",
"MEDIA_BUG_START",
"MEDIA_BUG_STOP",
"CONFERENCE_DATA_QUERY",
"CONFERENCE_DATA",
"CALL_SETUP_REQ",
"CALL_SETUP_RESULT",
"CALL_DETAIL",
"DEVICE_STATE",
"TEXT",
"SHUTDOWN_REQUESTED",
"ALL"
};
sofia 相关 事件
查看:mod_sofia.h
myevents
myevents是event的一种特殊情况,它主要用于Outbound模式中。在Outbound模式中,对于每一个呼叫(对应一个Channel),FreeSWITCH都会向外部的TCP Server请求以建立一个新的连接。外部的TCP Server就可以通过myevents订阅与该Channel(UUID)相关的所有事件。使用的格式为:myevents <type><UUID> 。当然,myevents也支持json及XML形式,如:
myevents
myevents json
myevents xml
当然,在 Inbound 模式中也可以调用 myevents,这样它看起来类似于一个Outbound模式的连接,但它要指定 UUID,原因是显而易见的,如:myevents 289fe829-af62-47be-9a59-7519a77d0d40
divert_events
还有一类特殊的事件,它们是作为InputCallback产生的。那么什么时候会产生Input-Callback呢?当Channel上通过setInputCallback()函数安装了相关的回调函数并遇到某些事件,如收到用户按键(DTMF)或收到语音识别的结果(DETECTED_SPEECH),就会产生InputCallback这样的事件,并回调指定的回调函数,但这些InputCallback默认只能在嵌入式脚本的回调函数中捕获。通过使用divert_events,就能将这些事件转发到Event Socket连接上来,进而在通过Event Socket连接的外部程序中也能收到相关事件。使用格式是:
divert_events on #开启
divert_events off #关闭
filter 过滤器
filter 用于安装一个过滤器。这里的过滤不是 “滤出”,而是“滤入”,即把符合过滤条件的过滤进来,也就是要收到它们。可以同时使用多个过滤器。
使用格式是:filter <EventHeader> <ValueToFilter>
例如,下面的例子与 myevent<uuid>的作用是相同的:
event plain all
filter Unique-ID <uuid>
又如,下面的例子会订阅所有事件,但只接收匹配主叫号码是1001的事件:
event plain all
event filter Caller-Caller-ID-Name 1001
为了理解滤入的概念,可以看下面的例子以加深印象,它可以接收3个 Channel 事件:
event plain all
filter Unique-id uuid1
filter Unique-ID uuid2
filter Unique-ID uuid3
如果过滤器写错了,或不想使用某些过滤器了,则可以将其取消掉,如:
filter delete # 取消所有过滤器
filter delete Unique-ID uuid2 # 只取消 uuid2 相关的过滤器
nixevent 与 noevent
nixevent是event的反义词,与event的语法一样,只是取消某些已订阅的事件。如:
nixevent CHANNEL_CREATE
nixevent all
另外,还有一个 noevent 命令用于简单取消使用event订阅的所有事件,相当于“nixevent all”。
log、nolog
跟使用event订阅事件类似,log用来订阅日志,它的使用格式是:log <level>
log info
log 6
一个完整的例子。这个例是在开启了info级别的日志以后打了一个电话,此时会收到很多日志信息,下面是两条信息:$ nc 127.0.0.1 8022
nolog 关闭使用 log 命令订阅的日志。
这里的level是整数值,对应关系如下:
- 0 为 EMERG
- 1 为 ALERT
- 2 为 CRIT
- 3 为 ERROR
- 4 为 WARNING
- 5 为 NOTICE
- 6 为 INFO
- 7 为 DEBUG
exit
告诉 FreeSWITCH 关闭 Socket 连接。FreeSWITCH 收到该命令后会主动关闭Socket连接。
sendevent
向FreeSWITCH的事件系统发送事件。使用格式是:sendevent <event-name>
比如,你可以发送一个NOTIFY消息:
sendevent NOTIFY
profile: internal
event-string: check-sync
user: 1002
host: 192.168.7.5
content-type: application/xml
content-length: 29
<xml>FreeSWITCH IS COOL</xml>
FreeSWITCH收到NOTIFY消息后,将启用内部处理机制,最后它可能会生成一个SIP NOTIFY消息,如:
NOTIFY sip:1002@192.168.7.5:32278;rinstance=3db08ce44e5166a4 SIP/2.0
Via: SIP/2.0/UDP 192.168.7.5;rport;branch=z9hG4bKXDtD32g0N9atg
Max-Forwards: 70
From: <sip:1002@192.168.7.5>;tag=ZF9SFyaUHeZ4p
To: <sip:1002@192.168.7.5>
...
Event: check-sync
Subscription-State: terminated;reason=noresource
Content-Type: application/xml
Content-Length: 29
<xml>FreeSWITCH IS COOL</xml>
当然,也可以使用它发送MESSAGE消息,如:
sendevent SEND_MESSAGE
profile: internal
user: 1002
host: 192.168.7.5
content-type: text/plain
content-length: 10
Hello 1002
上述命令将会产生如下的SIP消息:
send 623 bytes to udp/[192.168.7.5]:32278 at 16:01:23.686473:
------------------------------------------------------------------------
MESSAGE sip:1002@192.168.7.5:32278;rinstance=3db08ce44e5166a4 SIP/2.0
Via: SIP/2.0/UDP 192.168.7.5;rport;branch=z9hG4bK085Q8K3aD4DjK
Max-Forwards: 70
From: <sip:1002@192.168.7.5>;tag=11UBKmc2B0Bae
To: <sip:1002@192.168.7.5>
...
Content-Type: text/plain
Content-Length: 10
Hello 1002
当然,在实际使用中更多的是产生CUSTOM的事件,可以自定义一些事件类型。使用这种方式甚至可以把FreeSWITCH当成一个消息队列(Message Queue)来用,如你可以启动一个客户端订阅以下消息:event plain CUSTOM freeswitch:book
我们可以在另外的客户端上发送一条消息,如:
sendevent CUSTOM
Event-Subclass: freeswitch::book
content-type: text/plain
content-length: 44
This Message comes from the FreeSWITCH Book!
我们可以在订阅该消息的客户端上收到如下信息:
Content-Length: 688
Content-Type: text/event-plain
Event-Subclass: freeswitch%3A%3Abook
Command: sendevent%20CUSTOM
...
content-type: text/plain
Content-Length: 44
This Message comes from the FreeSWITCH Book!
总 结
1. auth <密码> 第一个需要发送得命令,用于向freeswitch认证,例: auth ClueCon
2. api <command> <args> 其中command和args分别是freeswitch实际得命令和参数
3. bgapi <command> <args> api执行时间比较长,有一段时间会得不到响应,可以使用bgapi将命令放到后面执行;
1. 会建立一个任务(job),并返回job-UUID;
2. 执行完成后fs会产生一个BACKGROUND_JOB事件,事件中带了job-UUID和命令执行结果
3. fs允许自己提供job-UUID(要保证全局唯一)
4. linger和nolinger
1. 当channel挂断时,fs会断开与socket得连接,可能有一些channel相关得事件还没有发送过去避免这种“丢失”事件,tcp server告诉fs在断开之后逗留(“linger”)一段时间,等待把所有事件发完
linger <seconds> 延时10秒 linger 10
2. linger开启后悔了,使用nolinger命令撤销,没有参数
5. event [type] <events>
1. event用于订阅事件,type(时间类型)有plain、json、xml三种,默认plain(纯文本)
2. events可以指定事件得名字,ALL表示订阅全部事件,事件之间用空格隔开
3. 订阅CUSTOM事件(自定义事件),它具体的类型是在Subclass中指定得,如指定订阅Subclass为"sofia::register"事件:
1. event plain CUSTOM sofia::register
2. 可以一次订阅多个
event plain CUSTOM sofia::register sofia::unregister
3. 使用多个event命令混合订阅
event plain CHANNEL_ANSWER CUSTOM sofia::register sofia::unregister
4. 参数中一旦出现CUSTOM后面就不能跟普通得事件类型了
5. CUSTOM事件只能逐一订阅,不能使用ALL
6. HEARTBEAT是一个特殊事件,每20秒产生一次,用于回报fs得当前状态,当20秒没有收到fs事件,可以认为网络或fs异常
6. myevents
1. 主要用于outbound模式,在outbound模式中,外部得TCP server可以通过myevents订阅与该channel相关得所有事件
2. 使用格式myevents <type><UUID>
myevents
myevents json
myevents xml
myevents 289dwd-af35-47de-9a58-754191454d0d2
7. divert_events
1. 作为InputCallback产生的,当channel通过setInputCallback()函数安装了相关的回调函数并遇到某些事件,如收到用户按键(DTMF)或语音识别的结果(DETECTED_SPEECH),产生InputCallback时间,默认是在嵌入式脚本的回调函数捕捉,通过使用diver_events,可以将这些时间转发到Event Socket,外部程序中也能收到相关事件
8. filter
1. filter用于安装一个过滤器,只做“虑入”,把符合条件的过滤进来,可以同时使用多个过滤器
2. 格式:filter <EventHeader> <ValueToFilter>
3. 例如:订阅所有事件,只接受匹配主叫号码1001事件
event plain all
event filter Caller-Caller-ID-Name 1001
4. 取消过滤器
先接受三个过滤器
event plain all
filter Unique-id uuid1
filter Unique-id uuid2
filter Unique-id uuid3
filter delete 取消所有的过滤器
filter delete Unique-ID uuid2 取消与uuid2相关的过滤器liru
9. nixevent与noevent
1. nixevent与event相反,是取消订阅事件
nixevent CHANNEL_CREATE
nixevent all
2. noevent相当于“nixevent all”取消所有event订阅
10. log订阅日志
1. 格式:log <level>
1. level包含(数目越高越详细):
0-CONSOLE
1-ALERT
2-CRIT
3-ERR
4-WARNING
5-NOTICE
6-INFO
7-DEBUG
2. log info 或 log 6
11. nolog log的反义词,关闭使用log命令订阅的日志
12. exit 告诉fs关闭socket连接
13. sendevent
1. 通过sendevent可以向fs的事件系统发送事件
2. 格式:sendevent <event-name>
例如发送MESSAGE消息
sendevent SEND_MESSAGE
profile: internal
user: 1002
host: 192.168.0.126
content-type: text/plain
content-length: 10
hello 1002
Event Socket 库
官网文档:FreeSWITCH 文档 --- Event Socket Library
通过 ESL( Event Socket Library,即 Event Socket库的缩写)可以让 FreeSWITCH 跟外部的程序 "交流"。ESL协议是纯文本的协议。它的设计思想来自于大家熟悉的 HTTP 协议及 SIP 协议
ESL提供了一些库函数,通过这些库函数可以很方便地使用ESL协议与FreeSWITCH交互,进而控制FreeSWITCH的各种功能。
- 0. About
- 1. Prerequisites
- Installation
- Reference
- Quoting and Escaping
- ESL Object
- eslSetLogLevel
- ESLevent Object
- ESLconnection Object
- Examples
- Getting a uuid
- Simple Perl Example
- Ruby Example
- Java Example
- C Example
- See Also
Event Socket 示例 (Ruby)
Ruby客户端:下面是一个使用Ruby语言通过ESL控制FreeSWITCH的例子。脚本内容如下:
require 'ESL'
con = ESL::ESLconnection.new('127.0.0.1', '8021', 'ClueCon')
esl = con.sendRecv('api sofia status')
puts esl.getBody
上述脚本只有短短的4行代码,首先它加载了ESL库,然后连接到FreeSWITCH,接着执行sofia status命令,最后将结果输出到控制台。
Event Socket 示例 (C)
在源代码目录 lib/esl 中有 入站(testclient.c) 和 出站(testserver.c) 的C示例。
入站 (testclient.c):https://github.com/signalwire/freeswitch/blob/master/libs/esl/testclient.c
#include <stdio.h>
#include <stdlib.h>
#include <esl.h>
int main(void)
{
// 初始化一个handle,用于标志到FreeSwitch的Socket连接
esl_handle_t handle = {{0}};
// 连接服务器。如果成功,handle 就代表连接成功
esl_connect(&handle, "localhost", 8021, NULL, "ClueCon");
// 发送一个命令,并接收返回值
esl_send_recv(&handle, "api status\n\n");
// last_sr_event 应该是 last server response event,即针对上面命令的响应
if (handle.last_sr_event && handle.last_sr_event->body) {
// 打印返回结果
printf("%s\n", handle.last_sr_event->body);
} else {
// 这在API或bgapi(上面硬编码的)中不太可能发生,但对于其他命令可能会执行到这里
printf("%s\n", handle.last_sr_reply);
}
// 断开连接
esl_disconnect(&handle);
return 0;
}
可以看出,该程序很简单,它运行之后向FreeSWITCH建立一个连接,运行一个API命令,然后输出命令的执行结果。
出站 (testserver.c):https://github.com/signalwire/freeswitch/blob/master/libs/esl/testserver.c
#include <stdio.h>
#include <stdlib.h>
#include <esl.h>
static void mycallback(esl_socket_t server_sock, esl_socket_t client_sock, struct sockaddr_in *addr, void *user_data)
{
esl_handle_t handle = {{0}};
int done = 0;
esl_status_t status;
time_t exp = 0;
// 将 handle 与 socket 绑定
esl_attach_handle(&handle, client_sock, addr);
// 打印一条日志
esl_log(ESL_LOG_INFO, "Connected! %d\n", handle.sock);
// 添加一个过滤器(filter),只收取与本次连接的Channel相关的事件(与本次连接的Channel UUID相同的事件)
esl_filter(&handle, "unique-id", esl_event_get_header(handle.info_event, "caller-unique-id"));
// 订阅各种类型的事件
esl_events(&handle, ESL_EVENT_TYPE_PLAIN, "SESSION_HEARTBEAT CHANNEL_ANSWER CHANNEL_ORIGINATE CHANNEL_PROGRESS CHANNEL_HANGUP "
"CHANNEL_BRIDGE CHANNEL_UNBRIDGE CHANNEL_OUTGOING CHANNEL_EXECUTE CHANNEL_EXECUTE_COMPLETE DTMF CUSTOM conference::maintenance");
// 发送一个linger命令开启逗留模式。逗留模式的目的就是告诉FreeSWITCH晚一些断开这个Socket。
//如果不开启该模式,则主叫挂机后,FreeSWITCH会立即断开它主动建立的Socket,就会导致一些后续的事件收不到
esl_send_recv(&handle, "linger");
// 执行answer App对来话进行应答(跟在Dialplan中类似)
esl_execute(&handle, "answer", NULL, NULL);
// 将来话送入一个会议
esl_execute(&handle, "conference", "3000@default", NULL);
// 无限循环,用于不断地接收事件
// 接收事件函数 esl_recv_timed 是非阻塞的,如果在1秒内没有收到任何事件,它就会返回,然后程序进入循环体。
while((status = esl_recv_timed(&handle, 1000)) != ESL_FAIL) {
// 检测done变量,它是个结束标志,用于结束循环
if (done) {
if (time(NULL) >= exp) {
break;
}
} else if (status == ESL_SUCCESS) {
const char *type = esl_event_get_header(handle.last_event, "content-type");
if (type && !strcasecmp(type, "text/disconnect-notice")) {
const char *dispo = esl_event_get_header(handle.last_event, "content-disposition");
esl_log(ESL_LOG_INFO, "Got a disconnection notice dispostion: [%s]\n", dispo ? dispo : "");
// 由于使用了逗留模式,因此FreeSWITCH返回的消息中将包含linger字符(可以从打印出的日志中看到)
if (dispo && !strcmp(dispo, "linger")) {
done = 1;
esl_log(ESL_LOG_INFO, "Waiting 5 seconds for any remaining events.\n");
exp = time(NULL) + 5;
}
}
}
}
esl_log(ESL_LOG_INFO, "Disconnected! %d\n", handle.sock);
esl_disconnect(&handle);
}
int main(void)
{
esl_global_set_default_logger(7);
esl_listen_threaded("localhost", 8040, mycallback, NULL, 100000);
return 0;
}
该程序也很简单,其运行于 Outbound 模式,是多线程的。它启动后监听一个端口,每次有电话进来时,通过 Dialplan 路由到 socket App。该 App 便会从 FreeSWITCH 中向testserver 发起一个TCP连接。大致意思就是:当FreeSWITCH中有来话路由到它时便启动一个新线程为新的Channel进行服务(具体的服务就是应答),并将来话送入一个会议。由于它是多线程的,因而可以同时为很多来话服务。
main 函数
首先它调用 esl_global_set_default_logger 函数设置日志级别(代表DEBUG级别,即最大级别,这样能看到详细的日志,包括所有协议的细节)。这里的 level 是整数值,对应关系如下:
- 0 为 EMERG
- 1 为 ALERT
- 2 为 CRIT
- 3 为 ERROR
- 4 为 WARNING
- 5 为 NOTICE
- 6 为 INFO
- 7 为 DEBUG
然后它通过 esl_listen_threaded 启动一个 Socket 监听本地回环地址(localhost)的8040端口。如果有连接(从FreeSWITCH)到来,它便回调 mycallback 函数为该连接服务。其中,NULL是一个空指针,该参数的位置是一个无类型(void*)的指针,即允许你传入任何类型的指针,该指针将作为 mycallback 函数的参数(user_data)在回调中携带
回调函数,它有4个参数,
- 服务器端的Socket标志、
- 客户端的Socket标志、
- 连接地址
- 用户私有数据
使用 ESL 发送 SIP MESSAGE 消息
执行下面程序后,在1000这个SIP电话上将会收到一个Hello消息(如果该用户正常注册的话)。
#include <stdio.h>
#include <stdlib.h>
#include <esl.h>
int main(void)
{
esl_handle_t handle = {{ 0 }};
struct esl_event *event;
struct esl_event_header header;
esl_event_create_subclass(&event, ESL_EVENT_CUSTOM, "SMS::SEND_MESSAGE");
esl_event_add_header_string(event, ESL_STACK_BOTTOM, "to", "1000@192.168.0.7");
esl_event_add_header_string(event, ESL_STACK_BOTTOM, "from", "seven@192.168.0.7");
esl_event_add_header_string(event, ESL_STACK_BOTTOM, "sip_profile", "internal");
esl_event_add_header_string(event, ESL_STACK_BOTTOM, "dest_proto", "sip");
esl_event_add_header_string(event, ESL_STACK_BOTTOM, "type", "text/plain");
esl_event_add_body(event, "Hello");
esl_connect(&handle, "localhost", 8021, NULL, "ClueCon");
esl_send_recv(&handle, "api version\n\n");
if (handle.last_sr_event&& handle.last_sr_event->body) {
printf("%s\n", handle.last_sr_event->body);
printf("sending event....\n");
esl_sendevent(&handle, event);
esl_event_destroy(&event);
} else {
printf("%s\n", handle.last_sr_reply);
}
esl_disconnect(&handle);
return 0;
}
ESL :ESLevent 对象
FreeSWITCH 通过ESL库包装了大量的易于使用的函数。ESL本身与FreeSWITCH没有任何依赖关系,可以单独编译和使用。它在底层是使用C语言实现的,并通过swig包装成了其他程序语言惯用的格式。
使用 C语言的 ESL连接 FreeSwitch:https://blog.csdn.net/xxm524/article/details/125840597
- ELS github 源码 ( C语言 ):https://github.com/signalwire/freeswitch/tree/master/libs/esl/src
- ESL Object (官网文档是 Perl 语言形式的接口,可以对照 github 上 C语言接口一块查看):Event Socket Library | esl-object
ESL Event 是一个事件对象,在C语言中的定义如下(在esl_event.h中定义)
当从FreeSWITCH中收到一个事件后,你就得到一个事件对象。ESL定义了一些函数用于从该对象中获取信息或构造新的对象。
- new($event_type [, $event_subclass]) 实例化一个新的 event对象,方法的新事件对象。
- serialize([$format]) 可以将事件序列化成可读的形式。$format 可以是:"xml"、"json"、"plain" (default)
- setPriority([$number]) 设置事件的级别。
- getHeader($header_name) 从事件中获取头域的值。
- getBody() 从事件中获取正文
- getType() 获取事件的类型。
- addBody($value) 向事件中增加正文,可以调用多次。
- setBody($value) 设置事件的正文,可以多次调用,但后者将覆盖前者。
- addHeader($header_name, $value) 向事件中增加一个头域。
- delHeader($header_name) 从Event中删除头域。
- firstHeader() 将指针指向Event的第一个头域,并返回它的Key值。C语言中没有明确的定义,靠使用ESL Event的headers结构体成员实现。
- nextHeader() 移动指针指向下一个header,在调用该函数前必须先调用firstHeader(),同样在C语言中没有明确定义,靠访问esl_event_header结构体的next成员实现。
ESL :ESLconnection 对象
ESLConnection对象维护与FreeSWITCH之间的连接,以发送命令并进行事件处理。在C语言中使用如下定义(在esl.h中)
相关函数
- new($host, $port, $password) 该函数初始化一个新的连接,仅用于inbound模式。
- new($fd) 根据已存在的Socket句柄建立一个ESLconnection对象。仅用于outbound模式。
- socketDescriptor() 该函数返回连接的UNIX文件句柄。
- connected() 判断是否已连接,连接返回1,否则返回0。
- getInfo() 当FreeSWITCH使用outbound模式连接时,它将首先发出一个CHANNEL_DATA事件,getInfo会返回该事件。在inbound模式中它返回NULL。
- send($command) 向FreeSWITCH发送一个ESL命令,它不会等待接收事件,而需要明确地使用recvEvent或recvEventTimed以接收返回的事件。返回事件的类型为api/response或command/reply。使用sendRecv()可以自动获取返回结果。
- sendRecv($command) 在ESL内部,sendRecv(command)首先调用send(command),然后调用recvEvent()并最终返回一个ESLevent对象。recvEvent()会在一个循环中调用并一直阻塞直到收到头域为api/response或command/reply的事件为止。在此期间所有收到的其他的将保存到一个内部队列中,这些队列中的事件会能在后续的recvEvent()中取到。
- api($command[, $arguments]) 向FreeSWITCH发API命令,它是阻塞执行的。它与sendRecv("api$command$args")是等价的。
- bgapi($command[, $arguments][,$custom_job_uuid]) 后台执行API,要执行的API将在新的线程中执行,因而不会阻塞。该函数与sendRecv("bgapi$command$args")也是完全等价的。它执行后也返回一个Job-UUID,与我们上面讨论的bgapi使用场景类似。
- sendEvent($send_me) 向FreeSWITCH发送一个事件。
- recvEvent() 从FreeSWITCH中接收事件,如果此时没有事件,它将一直阻塞直到有新事件到来。如果在调用它之前曾经调用了sendRecv(),并且sendRecv()曾经将收到的事件放到队列中,则该函数会返回队列中的第一个事件,否则,它会一直等待。
- recvEventTimed($milliseconds) 该函数与recvEvent类似,不同的是它不会永远阻塞,而是将在参数指定的毫秒数会返回。recvEventTimed(0)可以立即返回,可以用于事件轮循。
- filter($header, $value) 类似上面提到的filter命令,用于过滤事件。
- events($event_type,$value) 订阅事件,类似上面的event命令。
- execute($app[, $arg][, $uuid]) 执行DialplanApp,并阻塞等待返回。它将最终返回一个ESLevent对象,通过getHeader("Reply-Text")方法可以获取返回值,一般来说“+OK[成功的信息]”表示执行成功,“-ERR[错误信息]”表示执行失败。
- executeAsync($app[, $arg][, $uuid]) 与execute()相同,但非阻塞。两者实际上都调用了上面的execute命令,只是executeAsync带了“async:true”头。
- setAsyncExecute($value) 强制将Socket连接设为异步模式。value为1为异步,0为同步。调用该函数后,所有使用execute()执行的App都将带有“async:true”头域,因而执行是异步的。除此之外本函数对其他函数没有影响。在C语言中可以通过设置esl_handle结构体成员async_execute=1来实现。
- setEventLock($value) 设置所有后续的execute()调用都将带有“event-lock:true”头域。
- disconnect() 主动中断与FreeSWITCH的Socket连接。
FreeSWITCH 的 event 系统
FreeSWITCH 的 事件系统:Event System
- Debugging Event Socket Message
- ESL Example Clients
- Event Handlers
- Event List
- Event headers
- Events
- List of CUSTOM Events
- Making Event Socket behave like the console
FreeSWITCH都会产生哪些事件啊?这些事件都是在什么情况下产生的?FreeSWITCH的事件那么“长”,里面的都是什么意思?首先,FreeSWITCH 事件的机制就是在特定的情况下产生特定的事件。这些事件都是在源代码的switch_types.h 文件中定义的,从定义一般能很直观地看到事件的含义。只要了解了这些事件的含义,那么可能在什么情况下产生什么样的事件就能大体猜出,剩下的只需要去实践中验证。比如,跟踪所有的事件,然后打个电话看一下产生的事件是否跟你想的一样。
FreeSWITCH中的事件分为 主事件 和 CUSTOM事件,但其实FreeSWITCH中也没有严格的分类方法。下面就以CUSTOM事件、CHANNEL事件、CHANNEL相关的事件、系统事件和其他事件这种分类来进行说明。
CUSTOM 事件
- CUSTOM 事件:主要是用于跟系统内部核心逻辑关系不大的一些事件,一般用于模块内部,且便于扩展。它的 Event-Name 永远是 CUSTOM,不同事件的类型使用 Event-Subclass 区分。Event-Subclass 可以是任意的字符串,可以任意定义,但 FreeSWITCH 中也有约定俗成的命名空间,如 mod_sofia 模块中常用的 sofia::register、sofia::unregister 等,都使用以“::”隔开、以 sofia 开头的命名空间。类似的还有,会议模块(mod_conference)中的conference::maintenaince、FIFO 模块(mod_fifo)中的 fifo:info 等。
Channel 事件
Channel 事件是以Channel开头的,它主要跟Channel的状态有关,可用于监视 calls 进入 extension ,以及当前 calls 处于什么状态。
以 Channel 的生命周期大体说一下 Channel 事件:
- 首先,系统中有来话或去话时,将生成一个新的 Channel,并产生一个 CHANNEL_CREATE事件。该事件中会包含该Channel的UUID(其对应的字段名字是Unique-ID)、主叫号码、被叫号码、Channel的创建时间等。
- 接下来,如果 Channel 正常继续进行,则会产生 CHANNEL_PROGRESS 事件(如在SIP中收到对方的100或180消息)。
- 如果在应答之前能收到 Early Media(如在SIP中收到对方的)183消息,则会产生 CHANNEL_PROGRESS_MEDIA 事件。
- 如果一个 Channel 被应答,就会产生 Channel Answer 事件。
- 如果一个 Channel 与另外一个 Channel bridge 成功,则会产生CHANNEL_BRIGE 事件。注意,bridge是由两个Channel参与的,其中两个Channel分别称为 a-leg 和 b-leg。在SIP中,如果使用 bridge 这个App 等待 bridge 的 Channel 收到对端 Channel 发来的 Early Media 消息(如SIP中的183消息)即 bridge 成功,可以在 Channel 创建时使用 ignore_early_media 通道变量延迟 bridge 的返回(直到应答,如收到SIP中的200消息。但结果是a-leg,则听不到b-leg发来的Early Media(回铃音)。另外,CHANNEL_BRIDGE 事件只在一个Channel上发生,即只发生在主动的那个 leg 上。例如,终端A通过FreeSWITCH呼叫B,A端的呼叫到达FreeSWITCH 后产生一个新的Channel,FreeSWITCH使用bridge App去呼叫B,又产生一个新的Channel。如果B应答,则 b-leg 上会产生一个 CHANNEL_ANSWER 消息,同时,该应答信号通过 bridge App 传递到 A 上,a-leg上也会产生一个 CHANNEL_ANSWER 消息。bridge 成功后(假设没有Early Media参与,因此会直到应答bridge才完成),a-leg 上会产生CHANNEL_BRIGE事件,而 b-leg 上不会。CHANNEL_BRIGE 事件与上面单腿的事件不同的地方在于它里面包含了b-leg的信息,如 Other-Leg-Unique-ID 是 b-leg 的 Channel UUID,Other-Channel-Caller-ID-Number 是 b-leg 上的主叫号码等。
- 挂机后将产生 CHANNEL_HANGUP事件 和 CHANNEL_HANGUP_COMPLETE事件,其中,后者比前者内容丰富一些,比如后者带有variable_duration(通话时长,从Channel创建开始计时)及variable_billsec(计费时长,从应答后开始计时)。因此一般使用该事件取计费信息。
- 最后还将产生 CHANNEL_DESTROY 事件,表示该 Channel 已经完全释放(销毁)。
- 在 Channel 生存期间,在执行 App 的时候,将产生 CHANNEL_EXECUTE 事件,表示一个App已开始执行。有的App执行非常快,如 set;有的则可能比较慢,如 playback(要等声音文件播放完毕)。在 App 执行完毕后,会产生 CHANNEL_EXECUTE_COMPLETE 事件。在使用异步方式进行 ESL 编程时可以在收到该事件后执行下一个 App。其中, 这两个事件都会包含一个 Application字段,标志当前正在执行或已完成的App的名字。对于大部分的 Channel 事件,与Channel相关的Channel Variable都会附加在Channel相关的事件上。与系统内部的字段名字不同(大写字母开头加中横线方式命名,如Unique-ID),这些 Channel Variable 都是以“variable_”开头的,如 variable_effective_caller_id_name。某些 Channel 事件默认没有 variable_ 开头的字段,如 CHANNEL_CALLSTATE。这主要是为了减少消息量。如果希望这些消息中也包含所有的variable,则可以在Dialplan中使用verbose_events App来打开,如:<action application="verbose_events" data="true"/>
Channel states(状态) 。channel 处于的状态是由 Channel-State 和 Channel-State-Number 指定。它们与所有CHANNEL事件一起发送。
Channel 相关事件
有一部分事件虽然与Channel相关的,但是它们的名字不是以CHANNEL_开头的,例如PLAYBACK_START(放音开始)、PLAYBACK_STOP(放音结束)、RECORD_START(录音开始)、RECORD_STOP(录音结束)、DTMF(双音多频按键信息)等。
这类事件也有 Channel UUID(Unique-ID),但它们一般与 Channel 的状态无关。
系统 事件
系统事件包含 STARTUP(系统启动)、SHUTDOWN(系统关闭)、MODULE_LOAD(模块加载)、MODULE_UNLOAD(模块卸载)等。另外,系统每隔20秒会产生一个HEARTBEAT(心跳)事件,可以用于检测FreeSWITCH是否正常运行。
其他还有许多事件,如API(执行API时产生)、BACKGROUND_JOB(使用 bgapi 后台执行API时产生)等。
- SHUTDOWN
- MODULE_LOAD
- MODULE_UNLOAD
- RELOADXML
- NOTIFY
- SEND_MESSAGE
- RECV_MESSAGE
- REQUEST_PARAMS
- CHANNEL_DATA
- GENERAL
- COMMAND
- SESSION_HEARTBEAT
- CLIENT_DISCONNECTED
- SERVER_DISCONNECTED
- SEND_INFO
- RECV_INFO
- CALL_SECURE
- NAT
- RECORD_START
- RECORD_STOP
- PLAYBACK_START
- PLAYBACK_STOP
- CALL_UPDATE
其他事件
4、使用 ESL 开发 案例
ESL是一个客户端库,它主要用于对 FreeSWITCH 进行逻辑控制,因此,很多实际的功能还得靠freeswitch 的 App 和 API 来完成。
创建独立的 ESL 应用
:https://blog.csdn.net/MMsmileNN/article/details/118147532
创建目录和分离 esl 源文件
1. 创建一个 myesl 目录,然后创建一个 myesl.c 的文件。
2. 直接把fs源码目录下的testclient.c中的内容原样复制过来。
3. 将/usr/local/freeswitch/lib 下的 libesl.a 拷贝到 /media/sf_share/freeswitch/libs/esl 目录下
4. 创建一个MakeFile文件,保存在myesl.c相同的目录,内容如下
ESLPATH = /media/sf_share/freeswitch-1.6.20/libs/esl
CFLAGS = -I$(ESLPATH)/src/include
LIBESL = $(ESLPATH)/.libs/libesl.a
#LIBESL = $(ESLPATH)/libesl.a
all: myesl charge acd
myesl: myesl.c
gcc $(CFLAGS) -o myesl myesl.c $(LIBESL) -ldl -lm -lpthread
5. 然后 make 编译生成 myesl 运行文件,然后执行
1. make myesl.c 编译
2. ./myesl 执行
myesl.c:https://github.com/seven1240/myesl/blob/master/myesl.c
#include <stdio.h>
#include <stdlib.h>
#include <esl.h>
int main(void)
{
esl_handle_t handle = {{0}};
esl_connect(&handle, "127.0.0.1", 8022, NULL, "ClueCon");
esl_send_recv(&handle, "api status\n\n");
if (handle.last_sr_event && handle.last_sr_event->body) {
printf("%s\n", handle.last_sr_event->body);
} else {
// this is unlikely to happen with api or bgapi (which is hardcoded above) but prefix but may be true for other commands
printf("%s\n", handle.last_sr_reply);
}
esl_disconnect(&handle);
return 0;
}
用 ESL 重写空中充值服务
用 ESL 实现 呼叫中心
inbuound 模式实现 IVR
使用 Erlang 控制呼叫流程
在 https://github.com/seven1240/myesl 中有 ESL 书写的案例
- ESL空中充值服务 charge.c
- acd 呼叫中心 acd.c
- inbuound 模式实现 IVR icharge.c
- 使用 Erlang 控制呼叫流程 echarge.erl
步骤:
1. 编辑 charge.c 等案例文件,在main函数中设置连接的地址为:127.0.0.1,端口号为8021。
2. 修改 Makefeile 如下
ESLPATH = /media/sf_share/freeswitch-1.6.20/libs/esl
CFLAGS = -I$(ESLPATH)/src/include
LIBESL = $(ESLPATH)/.libs/libesl.a
all: myesl charge acd
myesl: myesl.c
gcc $(CFLAGS) -o myesl myesl.c $(LIBESL) -ldl -lm -lpthread
charge: charge.c
gcc $(CFLAGS) -o charge charge.c $(LIBESL) -ldl -lm -lpthread
acd: acd.c
gcc $(CFLAGS) -o acd acd.c $(LIBESL) -ldl -lm -lpthread
3. 在myesl目录下make编译,会执行all后面的所有的.c文件,生成对应的可执行文件
4. 配置freeswitch设置连接
1. vi ../autoload_configs/event_socket.conf.xml
2. 默认的监听地址配置
<param name="listen-ip" value="192.168.0.126"/>
3. 去掉下面的注释
<!-- <param name="apply-inbound-acl" value="lan"/> -->
4. freeswitch中重启mod_event_socket模块
5. diaplan中添加socket
<!--eventsockt外连配置-->
<extension name="socket">
<condition field="destination_number" expression="^12345$">
<action application="socket" data="127.0.0.1:8040 async full"/>
</condition>
</extension>
5. 执行可执行文件,设置监听
6. 手机拨打12345建立连接,执行 esl 内部操作
java esl_client 的使用配置
下载地址:https://github.com/esl-client/esl-client
1. 下载项目到本地
2. 复制项目的src文件到IntelliJ Idea的gradle项目目录下
3. 修改idea的sources->language level选择8
4. IDEA Error:java: Compilation failed: internal java compiler error错误解决办法:
File-->Setting...-->Build,Execution,Deployment-->Compiler-->Java Compiler 设置相应Module的target bytecode version的合适版本(跟你jkd版本一致)
5. 运行文件main函数,注意连接地址为freeswitch地址(192.168.0.1)
定时 呼叫
定时呼叫类似于现有PSTN网络中的叫醒服务,但在FreeSWITCH中肯定能玩出更多的花样。
一个最简单的例子。在UNIX类的系统中,使用crontab作定时服务,可以用它定时执行一个脚本或程序。在Shell中使用 crontab -e 命令可以启动一个编辑器用来编辑当前用户的 crontab文件:
0 6 * * * /usr/local/freeswitch/bin/fs_cli -x "originate user/1006 &playback(/tmp/wakeup.wav)"
后面的命令实际上是使用fs_cli的-x参数执行一个FreeSWITCH的API,它的操作很简单,即呼叫用户1006,接通后播放声音文件/tmp/wakeup.wav叫用户起床。
如果想要执行的业务逻辑比较复杂(如未接听的情况下重呼,或得接听了但不按“1”就重呼),可以将逻辑放到一个Lua脚本中,用以下命令调用:
0 6 * * * /usr/local/freeswitch/bin/fs_cli -x "luarun /tmp/wakeup.lua"
具体的Lua脚本内容就不举例了。当然,也可以写一个ESL应用,如/tmp/esl_wakeup,然后可以通过如下配置执行:0 6 * * * /tmp/esl_wakeup
总之实现定时呼逻辑应该不是很复杂的事。
5、freeswitch 源代码、编译
安装
官网文档:https://developer.signalwire.com/freeswitch/FreeSWITCH-Explained/Installation/
安装
- 1 Conventions
- 2 Selecting a Version
- 3 Installation Methods
- 3.1 Debian 12 Package (FreeSWITCH 1.10)
- 3.2 Centos 7 Package (FreeSWITCH 1.10)
- 3.3 Windows
- 3.4 Debian 12 Bookworm Source
- 3.5 macOS
- 3.6 OpenBSD
- 3.7 Smartos
- 3.8 Windows
- 4 Updating Binaries
- 5 Deprecated Instructions
- 5.1 Debian 7 Source
- 5.2 CentOS 6 Source
- 5.3 Unix Variants
- 5.4 Mac OS X
- 5.5 Windows
源码目录说明
FreeSWITCH 的源代码目录中,
- src 目录中包含了绝大部分的源代码;
- libs 目录下是一些第三方的库和模块,如 libs/sofia-sip 就是 Nokia 的 SIP 库。
- mod 目录(src/mod) 是模块相关的源码
相关说明
SWITCH APR
FreeSWITCH 在设计时就是跨平台的,它使用了跨平台较好的APR库。APR 出身于 Apache 的代码。Apache 是网络上非常流行的Web服务器软件,其代码是公认的写得比较好的。APR 的主要目的是为应用提供一个可移植的、平台无关的层。它的底层在不同平台上调用不同的库和函数来向上提供诸如文件系统访问、网络编程、进程和线程管理以及共享内存等一致的功能接口。除了跨平台支持外,APR的核心还提供系统内存、数据结构、线程、互斥锁等各种资源的管理和抽象等。
APR实用库(APR-UTIL,或者APU)是APR项目的另一个库。它在APR基础上使用统一标准的编程接口,提供了一部分功能函数集。APU并不是在每一个平台上都有一个单独的模块,但是它为某些其他常用的资源一个类似的方法,这些资源包括Base64编码、MD5/SHA1加密、UUID以及队列(Queue)管理等。
FreeSWITCH为了防止潜在的命名空间冲突等因素,对所有使用到的APR函数又进行了一些封装,这样所有的核心函数就都有了一致的命名空间“switch_”。这些封装在switch_apr.c里实现。
FreeSWITCH 采用了 APR 的代码风格及约定,非常易于使用。所以如果熟悉APR的话,看FreeSWITCH的源代码就容易多了。当然,熟悉了 FreeSWITCH 的源代码也会熟悉 APR。
命名空间
在APR和APR-UTIL中,所有的公开接口都使用了字符串前缀“apr_”(数据类型和函数)和“APR_”(宏)。与此类似,在FreeSWITCH中,所有的核心接口函数也都使用了“switch_”前缀的函数和“SWITCH_”前缀的宏。在APR命名空间中,也大量使用了二级命名空间,如“apr_socket_”等。同理在FreeSWITCH中,也有类似的如“switch_file_”、“switch_core_session_”等二级及三级命名空间。
声明的宏
APR使用类似于APR_DECLARE的宏进行声明,例如:
APR_DECLARE(apr_status_t) apr_initialize(void);
在很多的平台上,这是一个空声明,并且扩展为:
apr_status_t apr_initialize(void);
但在某些平台上,如在Windows的Visual C++平台上,需要使用特有的、非标准的关键字,例
如“_dllexport”、“__stdcall”等来允许其他的模块使用一个函数,这些宏就需要扩展以适应这些需要的关键字。
与APR类似,在FreeSWITCH中,大部分使用SWITCH_DECLARE或SWITCH_DECLARE_DATA之类的声明。
另外,FreeSWITCH中不同类型的模块也都有专用的声明,如声明Application的SWITCH_STANDARD_APP、声明Dialplan的SWITCH_STANDARD_DIALPLAN以及声明API的SWITCH_STANDARD_API等。
大部分的宏以及常量、枚举等都在switch_types.h中定义。
apr_status_t 和 返回值
在APR中广泛采用的一个约定是:函数返回一个状态值,用来为调用者指示成功或者返回一个错误代码。这个类型便是apr_status_t,它是在apr_errno.h中定义的整数值。因此一个APR函数的常见原型就是:APR_DECLARE(apr_status_t) apr_do_something(...function args...);
返回值应当在逻辑上进行判断,并进行相应的错误处理。返回值APR_SUCCESS意味着成功。FreeSWITCH也定义了类似的返回值,并以SWITCH_SUCCESS对应APR_SUCCESS。这里注意一点,SWITCH_SUCCESS对应该的枚举值为0,因此常见的错误是:
另外,有些函数会返回一个字符串(char*或者const char*)、一个void*或者void,这些函数就被认为没有失败条件或者在发生错误时返回一个空指针。
内存池
APR使用内存池来方便对内存的管理。大家都知道,C语言中的内存管理是“臭名昭著”的。而在APR中,通过使用内存池,用户在申请内存时可以不用时刻记着释放申请到的内存,可以在用完后一起释放,这极大地方便了内存管理,并能防止产生大量内存碎片。
实际上,APR中大部分的函数及资源严重依赖于内存池,如创建一个Socket需要内存池,创建一个Thread也需要内存池。这种情况听起来似乎有些过分,甚至其作者都认为这些操作显式地依赖于内存池是个巨大错误 [3],并希望能在2.0时解决这个问题。
内存池一般设计用于小的内存分配,如果要申请几兆字节的内存,那么不建议在内存池中申请 。
除了这些之外,APR在使用起来还是相当方便的。在APR中通常认为从内存池中申请的内存分配永远不会失败。这个假设成立的原因在于如果内存分配失败,那么系统是不可恢复的,任何错误处理都将失败。
其他
SWITCH APR还包装了APR中的字符串处理、文件管理、队列、互斥锁、Socket、线程库等。
main 函数
C语言程序的执行都是从 main 开始执行的,FreeSWITCH 也不例外。打开 src/switch.c,搜索main 就可以找到 main 函数。
它的主要作用就是解析从命令行带来的各种参数,然后把一些重要参数记到一个switch_core_flag_t 结构体中。默认系统会启动在前台。在Linux平台上,如果执行的时候提供了“-nc”参数,则在UNIX类系统上会通过fork系统调用将服务启动到后台(最终调用fork);在Windows平台上,则是通过FreeConsole WINAPI实现的。
总之,不管是在前台还是后台,它在初始化一些环境并设置好系统相关的路径后,就执行并调用switch_core_init_and_modload 函数加载各种模块。具体加载哪个模块则依赖于安装目录中的配置文件 conf/autoload_configs/modules.conf 中的设置。
加载完所有模块后,系统核心进入 switch_core.c: 的 switch_core_runtime_loop,对于后台启动的实例来讲,它基本什么都不做,在 Windows 平台上,执行 WaitForSingleObject 以等待服务终止;在UNIX类平台上,就是无限循环;对于从前台启动的系统,它会进入switch_console_loop 以启动一个控制台接收用户的链盘输入并打印系统运行的信息(命令输出和日志等)。
switch_console_loop 函数在 switch_console.c 定义。它使用跨平台的 libedit 库用于接收用户的按键并在控制台上打印信息。在第1176行启动了一个新线程执行 console_thread 函数。
在 console_thread 中(第1044行),也是一个循环用于接收用户输入。如果用户输入一条命令,则在检查命令的合法性后将命令放入命令历史记录(第1075行),以备以后再执行时可以使用键盘上的箭头键翻查命令历史。然后,在第1076行调用 switch_console_process 执行输入的命令并返回结果。
switch_console_process(第134行)又调用了switch_console_execute(第348行), 后者最终在第392行调用核心提供的 switch_api_execute(switch_loadable_module.c:2282)执行输入的命令。
status = switch_api_execute(cmd, arg, NULL, istream);
如果用户在命令行上输入sofia status,则上述命令执行的结果就是:
status = switch_api_execute("sofia", "status", NULL, istream);
上述命令的执行结果将存放到istream中,最终会在某处被取出并打印到命令行上。
可加载模块
main函数中 FreeSWITCH 已经启动并可以接收和执行命令了。下面看下FreeSWITCH中的可加载模块是如何被加载的。FreeSWITCH的核心代码非常紧凑,大部分实际的功能都是由外围的模块实现和扩展的。
switch_core_init_and_modload 函数负责初始化和加载各种模块。它是在 switch_core.c:2084 中定义的。在该文件的2110行,完成一些初始化后它就调用switch_loadable_module_init进行模块的初始化,该函数是在switch_loadable_module.c:1747中定义的。在第1761行到1770行定义了各种不同平台上的动态库的扩展名,如在Windows上动态链接库的扩展名是.dll、在Mac上是.dylib、在其他各种UNIX类系统上是.so。
在第1772行,初始化了一个结构体变量loadable_modules,它是一个可加载模块的容器。
通过 loadable_modules 的定义(switch_loadable_module.c:59)可以看出,它主要定义了各种哈希表(hash)。将来,新加载的各种模块将统一由不同的哈希表管理,如mod_sofia将被记入endpoint_hash,mod_g729将被记入codec_hash等。
此外,它还使用互斥(mutex)来防止多线程访问。pool是一个内存池。在接下来的1773行就紧接着初始化了这个内存池:switch_core_new_memory_pool(&loadable_modules.pool);
第1780~1798行则初始化了所有的hash及mutex。其中有些hash的键(Key)是区分大小写的,用switch_core_hash_init初始化;有些则是大小写无关的,用switch_core_hash_init_nocase进行初始化。这些数据结构初始化时都需要一个内存池,由此可以看出内存池的重要性
系统加载完以后,就开始加载各种模块了。在1802~1803行,首先加载的是CORE_SOFTTIMER_MODULE和CORE_PCM_MODULE两个模块,这两个模块是直接在核心代码中实现的,因而比较特殊。
switch_loadable_module_load_module("", "CORE_SOFTTIMER_MODULE", SWITCH_FALSE, &err);
switch_loadable_module_load_module("", "CORE_PCM_MODULE", SWITCH_FALSE, &err);
暂且不深入研究这两个模块是如何加载的,继续往下走。第1806行,switch_xml_open_cfg()将打开XML配置文件中的modules.conf(参见1753行)部分(默认在安装目录conf/autoload_configs/modules.conf.xml中配置),经过一个for循环(1809行)依次取得需要加载的模块的名字,并最终在第1824行执行switch_loadable_module_load_module_ex加载它们。
然后,用同样的方法尝试加载post_load_modules.conf(参见第754行)中配置的模块(1839行起)。
另外,如果上面两个配置文件中都没有找到可加载的模块(1867),则尝试加载所有模块(1872~1897行)。
无论如何,模块加载完毕后,将执行switch_loadable_module_runtime函数(第1902行)
接下来,我们看一下模块是怎么被加载的。模块加载最终是由1476行的switch_loadable_module_load_module_ex实现的。它会首先计算与欲加载的模块对应的文件名,并在第1515行检查loadable_modules.module_hash这个哈希表来判断该模块是否已被加载,如果未被加载,则在第1519行调用switch_loadable_module_load_file将该模块加载到内存。
switch_loadable_module_load_file是在第1328行定义的。在第1356行和第1360行,它会多次尝试使用switch_dso_open来打开相应的模块的动态链接库(switch_dso_open在switch_dso.c:35中定义。在Windows平台上,它将使用LoadLibraryEx来打开相应的.dll库,在Mac和Linux上,它将使用dlopen函数打开相应的.so文件)。
动态库打开后,通过下面的代码找到动态库里的符号表(第1379行)。执行到1402行时,如果符号表加载成功,则将该符号表赋值给mod_interface_functions变量。
mod_interface_functions是一个switch_loadable_module_function_table结构的结构体,它是在switch_type.h:2228中定义的:
该结构体定义了几个指向函数的指针,分别是load、shutdown和runtime。如果被加载的模块中实现了这些函数,则这些指针指向相关的函数入口,如果没有实现,就是NULL。
每个模块都必须实现load函数,它一般用于模块的初始化操作。因而在第1403行,load函数的指针被赋值给load_func_ptr这个变量。load_func_ptr = mod_interface_functions->load;
第1411行,load函数将被执行:status = load_func_ptr(&module_interface, pool);
load函数的原型是使用SWITCH_MODULE_LOAD_FUNCTION(name)这个宏来定义的(switch_types.h:2211),该宏展开的结果就是:switch_status_t mod_xx_load( switch_loadable_module_interface_t **module_interface, switch_memory_pool_t *pool)
因而,如果该函数执行成功,将返回SWITCH_STATUS_SUCCESS,并且会初始化一个module_interface指针。然后,在第1424行初始化一个module变量。
module变量是一个如下所示的结构(在第44行定义)。它定义了该模块的一些参数,其中,成员module_interface就用于存放刚刚在第1411行初始化的module_interface指针。
struct switch_loadable_module {
char *key;
char *filename;
int perm;
switch_loadable_module_interface_t *module_interface;
switch_dso_lib_t lib;
switch_module_load_t switch_module_load;
switch_module_runtime_t switch_module_runtime;
switch_module_shutdown_t switch_module_shutdown;
switch_memory_pool_t *pool;
switch_status_t status;
switch_thread_t *thread;
switch_bool_t shutting_down;
};
从第1451行开始,对module中的各成员赋值。最后,在第1463行初始化new_module指针,返回到调用该函数的地方,模块加载成功。
module->pool = pool;
module->filename = switch_core_strdup(module->pool, path);
module->module_interface = module_interface;
module->switch_module_load = load_func_ptr;
...
*new_module = module;
总之,模块加载的流程就是,首先找到模块对应的动态库文件,然后打开并找到符号表,接下来执行模块中的load函数。另外,如果模块定义了runtime及shutdown函数,也将一并记录到module结构的switch_module_runtime及switch_module_shutdown成员变量中。
switch_loadable_module_load_file 执行完毕后,得到了一个new_module指针,并返回到第1519行。紧接着在第1520行执行switch_loadable_module_process函数,它使用模块的文件名(file)和我们新得到的new_module结构作为参数传入。
} else if ((status = switch_loadable_module_load_file(path, file, global, &new_module)) == SWITCH_STATUS_SUCCESS) {
if ((status = switch_loadable_module_process(file, new_module))
switch_loadable_module_process函数是在第133行定义的,在第138行,有如下语句:
new_module->key = switch_core_strdup(new_module->pool, key);
第138行初始化new_module的key,它是一个字符串,实际上就是传入的文件名。switch_core_strdup用于制作一个key的副本(duplicate),它需要的内存是从内存池中申请的,因而后续不需要明确释放,在模块卸载时直接释放掉内存池就行了。
在第140行通过互斥的mutex来锁定全局的loadable_modules结构,并在第141行向其中的loadable_modules.module_hash哈希表中插入该模块,以记录该模块被加载了。
switch_mutex_lock(loadable_modules.mutex);
switch_core_hash_insert(loadable_modules.module_hash, key, new_module);
第143行进行判断,如果被加载的模块实现了一个endpoint_interface,则在第150行将它记录到loadable_modules.endpoint_hash中。
if (new_module->module_interface->endpoint_interface) {
...
switch_core_hash_insert(loadable_modules.endpoint_hash, ptr->interface_name, (const void *) ptr);
第151行产生一个模块加载的事件——SWITCH_EVENT_MODULE_LOAD,并于第156行发送出去:
if (switch_event_create(&event, SWITCH_EVENT_MODULE_LOAD) ==
SWITCH_STATUS_SUCCESS) {
...
switch_event_fire(&event);
同理,如果该模块也实现了其他的interface(在同一模块中可以实现多个interface,如第163行的codec_interface,第220行的dialplan_interface等),则都记录到相应的哈希表中,产生相关的事件,并针对不同的interface类型可能还有不同的检查和其他处理等。
if (new_module->module_interface->codec_interface) {
...
if (new_module->module_interface->dialplan_interface) {
至此,模块就加载成功了,最后到第551行会将锁定的临界区的资源解锁,并返回成功的状态码。
switch_mutex_unlock(loadable_modules.mutex);
return SWITCH_STATUS_SUCCESS;
不过,模块加载成功并不表示所有工作已完成。上面的函数返回后又回到第1521行。接下来,判断如果新加载的模块定义了runtime函数,则启动一个新的线程,执行该runtime函数。
if (new_module->switch_module_runtime) {
new_module->thread = switch_core_launch_thread(switch_loadable_module_exec, new_module, new_module->pool);
}
runtime函数是循环的执行的,即只要runtime函数不返回SWITCH_STATUS_TERM,则它就会被再次执行,直到该模块被卸载为止,参见第99~101行。
for (restarts = 0; status != SWITCH_STATUS_TERM && !module->shutting_down; restarts++)
{ status = module->switch_module_runtime(); }
至此,模块加载的工作才算完成了。FreeSWITCH进入正常运行阶段。
模块的结构
在 src/mod/sdk/autotools/src 目录下有一个mod_example.c,其描述了一个最精简的模块的结构。其他模块可以在这个基础上修改。该文件只是一个例子,有些语句默认是注释掉的,在需要的时候可以打开。
#include <switch.h>
/*
SWITCH_MODULE_SHUTDOWN_FUNCTION(mod_example_shutdown);
SWITCH_MODULE_RUNTIME_FUNCTION(mod_example_runtime);
*/
SWITCH_MODULE_LOAD_FUNCTION(mod_example_load);
SWITCH_MODULE_DEFINITION(mod_example, mod_example_load, NULL, NULL);
SWITCH_MODULE_LOAD_FUNCTION(mod_example_load)
{
/* connect my internal structure to the blank pointer passed to me */
*module_interface = switch_loadable_module_create_module_interface(pool, modname);
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_NOTICE, "Hello World!\n");
/* indicate that the module should continue to be loaded */
return SWITCH_STATUS_SUCCESS;
}
/*
Called when the system shuts down
SWITCH_MODULE_SHUTDOWN_FUNCTION(mod_example_shutdown);
{
return SWITCH_STATUS_SUCCESS;
}
*/
/*
If it exists, this is called in it's own thread when the module-load completes
If it returns anything but SWITCH_STATUS_TERM it will be called again automaticly
SWITCH_MODULE_RUNTIME_FUNCTION(mod_example_runtime);
{
while(looping)
{
switch_yield(1000);
}
return SWITCH_STATUS_TERM;
}
*/
/* For Emacs:
* Local Variables:
* mode:c
* indent-tabs-mode:nil
* tab-width:4
* c-basic-offset:4
* End:
* For VIM:
* vim:set softtabstop=4 shiftwidth=4 tabstop=4 noet expandtab:
*/
它首先装入switch.h,使得它可以引用 FreeSWITCH 核心中的公用函数( 即所谓的Core Public API)。然后使用3个宏(在switch_types:第2211~2213行定义)分别声明了3个函数:mod_example_shutdown、mod_example_runtime、mod_example_load。其中,只有 load 函数是必需的,因此其他两个默认是注释掉的。上面只是对3个函数的前向声明。真正让这3个函数起作用的行是第41行。它的作用是告诉核心,在加载该模块时,就要回调本模块的 mod_example_load函数进行一些初始化操作(即 switch_loadable_module.c
如果把其他两个都用上,我们可以这样写:SWITCH_MODULE_DEFINITION(mod_example, mod_example_load, mod_example_shutdown, mod_example_runtime);
这样的话,当模块被加载的时候就会回调load、启动一个新线程运行runtime,并在模块被卸载的时候执行shutdown函数。当该模块被加载时(如在FreeSWITCH控制台上执行mod_example),则回调其下面的函数。该函数在参数中会传过来一个空指针(实际上一个个双重指针)——module_interface,我们需要初始化这个指针(第46行)。在module_interface初始化完成后打印一条日志(第48行),并返回SWITCH_STATUS_SUCCESS值(第51行)以表示初始化成功。如果初始化失败(如不能连接数据库、不能申请相关资源等,都可能导致初始化失败),则可以返回SWITCH_STATUS_FALSE或其他错误值。
模块卸载时的回调函数,其可以用于断开数据库连接、释放内存、清理相关现场等。在此,我们的模块没有申请什么资源,因而直接返回成功(第58行)。
如果runtime函数存在,则系统核心会启动一个新线程来调用该函数。在这里,可以是一个无限循环(如第67行,当然要记住给无限循环终止条件,否则该模块就不能卸载了),也可以在执行一段时间后返回一个状态值,只要返回值不是SWITCH_STATUS_TERM,该函数就会被再次调用。
上述就是在FreeSWITCH中可加载模块的大体结构。后面会看到编写一个新模块的实际例子。
Session 和 Channel
在FreeSWITCH核心中,与通话最相关的部分莫过于Session和Channel了。FreeSWITCH是一个B2BUA, 因此,它参与通话的每一条腿(Leg)都是一个Channel。而Session则比Channel更高级一些,它用于描述一次会话,也就是说,虽然Session与Channel总是一一对应的,但前者管的事更多一些。也可以这样认为,Session更关注于控制信令层,而Channel更关注于媒体层。
每当有一个电话到来时,或者每次从FreeSWITCH中发起一路通话时,便建立一个Session(同时生成一个Channel)。用于标志Session的是一个struct switch_core_session的结构体,定义如下:( src/include/private/switch_core_pvt.h )
struct switch_core_session {
switch_memory_pool_t *pool;
switch_thread_t *thread;
switch_thread_id_t thread_id;
switch_endpoint_interface_t *endpoint_interface;
switch_size_t id;
switch_session_flag_t flags;
switch_channel_t *channel;
switch_io_event_hooks_t event_hooks;
switch_codec_t *read_codec;
switch_codec_t *real_read_codec;
switch_codec_t *write_codec;
switch_codec_t *real_write_codec;
switch_codec_t *video_read_codec;
switch_codec_t *video_write_codec;
switch_codec_implementation_t read_impl;
switch_codec_implementation_t real_read_impl;
switch_codec_implementation_t write_impl;
switch_codec_implementation_t video_read_impl;
switch_codec_implementation_t video_write_impl;
switch_audio_resampler_t *read_resampler;
switch_audio_resampler_t *write_resampler;
switch_mutex_t *mutex;
switch_mutex_t *stack_count_mutex;
switch_mutex_t *resample_mutex;
switch_mutex_t *codec_init_mutex;
switch_mutex_t *codec_read_mutex;
switch_mutex_t *codec_write_mutex;
switch_thread_cond_t *cond;
switch_mutex_t *frame_read_mutex;
switch_thread_rwlock_t *rwlock;
switch_thread_rwlock_t *io_rwlock;
void *streams[SWITCH_MAX_STREAMS];
int stream_count;
char uuid_str[SWITCH_UUID_FORMATTED_LENGTH + 1];
void *private_info[SWITCH_CORE_SESSION_MAX_PRIVATES];
switch_queue_t *event_queue;
switch_queue_t *message_queue;
switch_queue_t *signal_data_queue;
switch_queue_t *private_event_queue;
switch_queue_t *private_event_queue_pri;
switch_thread_rwlock_t *bug_rwlock;
switch_media_bug_t *bugs;
switch_app_log_t *app_log;
uint32_t stack_count;
switch_buffer_t *raw_write_buffer;
switch_frame_t raw_write_frame;
switch_frame_t enc_write_frame;
uint8_t raw_write_buf[SWITCH_RECOMMENDED_BUFFER_SIZE];
uint8_t enc_write_buf[SWITCH_RECOMMENDED_BUFFER_SIZE];
switch_buffer_t *raw_read_buffer;
switch_frame_t raw_read_frame;
switch_frame_t enc_read_frame;
uint8_t raw_read_buf[SWITCH_RECOMMENDED_BUFFER_SIZE];
uint8_t enc_read_buf[SWITCH_RECOMMENDED_BUFFER_SIZE];
switch_codec_t bug_codec;
uint32_t read_frame_count;
uint32_t track_duration;
uint32_t track_id;
switch_log_level_t loglevel;
uint32_t soft_lock;
switch_ivr_dmachine_t *dmachine[2];
switch_plc_state_t *plc;
switch_media_handle_t *media_handle;
uint32_t decoder_errors;
switch_core_video_thread_callback_func_t video_read_callback;
void *video_read_user_data;
switch_core_video_thread_callback_func_t text_read_callback;
void *text_read_user_data;
switch_io_routines_t *io_override;
switch_slin_data_t *sdata;
switch_buffer_t *text_buffer;
switch_buffer_t *text_line_buffer;
switch_mutex_t *text_mutex;
const char *external_id;
};
可以看出,在switch_core_session中有一个指向channel的指针 switch_channel_t *channel;
之所以在private(私有的)下面定义, 是因为它不想让FreeSWITCH核心之外的应用知道 Session 中的细节。也就是说,其他系统如果使用 FreeSWITCH 的库,即使在 FreeSWITCH 内部的模块中,也看不到 Session 内部的东西。如果一段代码需要知道与 Session 相关的 Channel, 则只能用 switch_core_session_get_channel(session) 函数从 session 变量中取得,而不能直接调用session- >channel。如果这样说读者还不是太明白的话,看一看该文件前面的注释。
Session 是由 switch_core_session_request_uuid 函数生成的。
在该函数中,它会检查是使用现有的内存池(第2319行)还是创建一个新的内存池(第2322行)。然后在该内存池上为session结构体变量申请内存空间(第2325行),并将它的pool成员变量指向该内存池(第2326行)。这样我们就有了一个Session了,并且,以后与该Session有关的内存申请都可以在该内存池中申请。该内存池会在Session消亡时释放,因而在很大程度上方便了内存管理。
第2330行又在该内存池中又立即为与session所对应的Channnel申请了内存,并在第2334行对Channel进行了初始化,同时将Channel的当前状态设为CS_NEW。
如果在调用该函数时提供了一个UUID,则使用它(第2344行),否则自动生成一个(第2346行),它用于标志一个Channel。接下来,就可以设置通道变量了,如第2350~2351行。
接下来,进行初始化与Session相关的各种变量、申请相关内存、初始化Mutex、锁、队列等操作。最后,将与该Session对应的UUID记录到一个核心的哈希表中(第2381行),至此,Session的初始化基本上就结束了。
然后,第1876行的switch_core_session_thread_launch函数会被调用,来启动一个新的线程。
由于启动一个新的线程是比较费时的操作,因而在系统内部维护了一个线程池。在该函数第1888行,判断核心参数是否启用线程池(默认启用),如果是,则就在第1889行把该Session推到线程池队列中去。否则就在第1906行启动一个新线程,执行switch_core_session_thread函数(当然在线程池队列中找到一个可用的线程后,也会执行该函数)。
switch_core_session_thread是在第1555行定义的。它有两个传入参数,一个是当前线程的指针thread,另一个是一个无类型的(void*)指针obj,该obj实际上就是我们的Session指针,因此在第1557行,初始化了一个session变量并指向与obj指针同样的地址。在进行一些初始化操作后,便执行1565行的switch_core_session_run函数。
转了这么一大圈,我们终于找到了关键的地方。switch_core_session_run实际上是一个状态机。该状态机的定义在switch_types.h中,它是一个枚举类型的定义,内容如下:
switch_core_session_run函数是在switch_core_state_machine.c:414中定义的。该函数主要的功能就是执行一个循环,只要该Session所对应的Channel的状态不是CS_DESTROY,它就会一直循环。在循环体内使用了一些switch…case语句,用于决定在不同的状态执行哪些代码段或函数,部分代码如下。其中,有一些关键的代码段被提取出来,放到一个名为STATE_MACRO宏中执行了,而该宏就是状态机中最关键的部分。
不深入研究了。总之,你只需要知道在Channel的核心状态机上可以安装回调函数,并在状态发生变化时得到回调。如果对细节特别感兴趣的读者也可以使用“gcc-E”命令将该源文件中的宏展开看一看。
从这一段的代码我们知道,Session与Channel是息息相关的。初始化了Session之后,就有了Channel,而状态机全部都是在Channel上实现的(其中CS_INIT中的CS便是Channel State的意思)。当然,核心中也定义了很多专门对Channel操作的函数,大部分都是在switch_channel.c中实现的。这些函数的名称和代码看起来都很直观,在这里我们就不多讲了,等到后面用到的时候再个别进行说明。
SWITCH IVR
大部分媒体处理逻辑都是在switch_ivr_*.c中实现的,其中多个源代码实现了不同的switch_ivr逻辑,如switch_ivr_async.c进行异步处理、switch_ivr_bridge.c处理话路桥接等。在此,我们先来看一个简单的echo应用。关于echo App我想大家都已经很熟悉了,我们知道它的作用就是将收到的媒体(音频或视频)原样再发回去。下面我们就看一看它是怎么实现的。
在通话执行到echo App时,将最终执行到switch_ivr_async.c:629定义的switch_ivr_session_echo函数。由于echo应用是需要媒体的,如果在执行echo时电话还没有应答(如在SIP应用中还没有收到或发送“200 OK”),则它会在第636行调用switch_channel_pre_answer试图在电话应答之前建立媒体连接(如果在SIP应用中将发送带SDP的183消息以尝试建立媒体连接)。当然,这是一个小的细节,我们继续往下看。
在该函数中,执行一个while循环,只要该Channel是正常的(由switch_channel_ready判断,它会检查一系列参数,在Channel正常建立时将返回真,在挂机或出现其他错误的情况下返回假),便会一直循环。然后,调用核心的函数switch_core_session_read_frame从该Channel中读取一帧的数据( 这里的一帧,在SIP应用中就是一个RTP包中的数据,如可能是20毫秒的音频数据)。接下来在通过一个宏来判断读到的数据是否有效,如果无效就跳出循环。如果数据有效,就继续进行。处理该Channel上相关的事件,如检查 DTMF 等。在收到 DTMF 的情况下会调用相关的回调函数。
下面可以看到它又调用了switch_core_session_write_frame,即将收到的数据写回了Session中,然后这些音频数据就会发到远端。当然,如果该Channel上有视频的话,它也会进行相关的处理,我们暂时忽略视频的处理代码。
总之,该函数主要的功能就是调用switch_core_session_read_frame读取音频数据,并通过switch_core_session_write_frame写回去
Core IO
switch_core_session_read_frame用于读数据,而switch_core_session_write_frame用于写数据。这两个函数是在switch_core_io.c中定义的。它们都非常长
系统核心的IO操作屏蔽了底层的数据流读、写(收、发)细节,各种需要处理的媒体的应用只需要调用核心的IO函数进行数据的读、写操作,而不用考虑底层的不同。同时,这种架构使得增加一种新的Endpoint非常容易——只需要增加一个Endpoint的逻辑结构,安装相应的回调函数,并调用更底层的驱动程序或者协议库进行媒体流读、写即可。
Core Media
Core Media用于在核心进行媒体协商和处理。这些代码原来是在mod_sofia模块中,但后来为了增加WebRTC的支持,把这一部分代码独立出来,放到了switch_core_media.c中。它目前主要是处理使用SDP描述的媒体(如基于RTP的媒体)。如果Endpoint中需要RTP媒体支持,则它可以在Session中建立一个媒体句柄,然后通过session- >media_handle来引用它。通过使用Core Media,可以隐藏一个SDP媒体协商及RTP处理的细节,使得开发基于RTP的媒体程序更加简单。
在Core Media中,switch_core_media_read_frame函数用于从底层的RTP中读一帧数据,其中,媒体的类型(type参数)可以是SWITCH_MEDIA_TYPE_AUDIO或SWITCH_MEDIA_TYPE_VIDEO(分别表示读音频数据和读视频数据)。在Core Media内部,有一个媒体引擎参数,它目前定义了音频和视频两个引擎组成的数组,在第1414行可以通过engines[type]找到所需要的媒体引擎。
当然,除了媒体读写以外,Core Media中还有switch_core_media_negotiate_sdp函数(用于媒体协商)、switch_core_media_activate_rtp函数(用于启动RTP收发)等
Core RTP
FreeSWITCH中的RTP媒体收、发都是在switch_rtp.c中实现的。我们在上一节提到过,在Core Media中, 会调用switch_rtp_zerocopy_read_frame来读取一帧数据。所谓zerocopy,就是不复制数据而直接返回一个数据指针。下面我们先来看一下这个函数。
switch_rtp_zerocopy_read_frame函数是在第5502行定义的,它的输入参数rtp_session是一个switch_rtp_t类型的指针,它唯一标志了一个RTP连接。第二个参数frame是一个switch_frame_t类型的指针,它用于存放读到的数据。它主要是在第5510行调用rtp_command_read从底层的Socket中读取数据,并用读到的数据去填充frame指针指向的结构体。
SWITCH XML
FreeSWITCH的配置文件严重依赖XML。FreeSWITCH对XML的解析是在switch_xml中实现的。
如果某个程序需要从XML中读取配置数据,则它会调用switch_xml_open_cfg函数首先来打开一个XML节点。该函数是在第2392行定义的。在该函数内部,它会在第2400行调用switch_xml_locate去查找相关的XML节点,并返回相关的XML结构指针。
SWITCH Event
FreeSWITCH中有一些功能是事件驱动的。另外,事件也是FreeSWITCH内部与外部进行数据交换的载体。当FreeSWITCH中发生状态改变或者代码执行到某个阶段时,都会触发一些事件。同时,另外一些感兴趣的模块也可以订阅这些事件,以便在收到相应事件时执行相应的动作。从某种意义上说,这种事件机制与我们上面讲过的回调函数和钩子想要达到的效果是一样的,不同的是,事件采用“Pub/Sub”(即发布/订阅机制,又称生产者/消费者模型)建立的是一种更松的耦合关系,使用起来更方便、更自由。另外,外部的第三方系统也可以通过系统提供的接口订阅到事件,从而可以更容易地集成。
在系统初始化时,首先调用switch_event_init函数进行事件系统的初始化,该函数是在switch_event.c:659中定义的,它会初始化事件系统所需的内存池、哈希表、Mutex、队列等。
Core Codec 和 Core File
Core Codec和Core File。之所以把两者放到一起讲,是因为他们比较类似——没有太多的业务逻辑,只是对不同的编解码和文件格式的抽象和封装。
在Core Codec中,提供了初始化(init)、编码(encode)、解码(decode)、释放(destroy)等函数的抽象,如我们在20.3.8节提到的switch_core_codec_encode和switch_core_codec_decode函数,它们都是在一种编码(codec)与另一种编码(other_codec)间转换。输入参数decoded_data表示未编码的(或者说是以L16线性编码的)数据缓冲区,而decoded_data_len则是数据的长度。同理,encoded_data和encoded_data_len则是编码后的数据缓冲区和长度。如果某种编码有相应的实现代码,则它会向核心注册codec->implementation->encode和codec->implementation->decode回调函数,所以在下面这两个函数中就直接调用这些回调函数进行编码或解码(如第736和第780行)。
总之,在核心中,就是通过这样的抽象与回调机制实现了媒体编解码接口(Codec Interface)、文件接口(File Interface)以及我们前面提到的终点接口(Endpoint Interface),还有我们在本章没有涉及但以前讲过的拨号计划接口(Dialplan Interface)、API接口(API Interface)、App接口(Application Interface)等等。
首次 编译
Free-SWITCH在开发中使用经典的gcc、Makefile及automake、autoconf、libtool等GNU工具链,因而在各种平台上都很容易进行编译。
FreeSWITCH主要是用C和C++编写的。编译C语言的程序一般需要gcc,如下命令会编译test.c并生成一个可以执行的二进制程序:gcc test.c -o test
当源代码文件数量过多时,一行一行地执行gcc就比较累了。因此,可以编写简单的Shell脚本或Makefile实现。Makefile是Make工具使用的文件,它除了能定义源文件到目标文件的编译方法外,还能定义这些文件的依赖关系。通过检查这些依赖关系,如果在下次编译时源文件没有修改过,则可以不用重复编译,因而可以大大加快编译速度。
不同平台上的工具链是不一样的,在Linux等开源平台上一般使用gcc,而在其他商业的UNIX系统上往往都有各厂商自己的编译工具链。为了屏蔽这些不同,一种称为automake的工具出现了。通过编写configure脚本,定义一些宏,可以在编译前自动检测当前的平台环境和工具链,以生成适当的Makefile。
当工程更大的时候,写configure脚本也是很累人的,因而又有人发明了automake和autoconf,通过定义更简单的宏(m4宏),可以自动生成configure脚本。
总之,通过这些工具和跨平台的宏定义便可以在不同的平台上生成不同的Makefile,进而可以进行编译。生成Makefile的总体流程如图
如果是从Git仓库中克隆的源代码,要进行编译,则需要先执行一下bootstrap.sh。它会执行一些初始化操作,生成configure文件。./bootstrap.sh
如果是直接下载的源代码Tar包,则不需要这一步,因为源代码在进行tar操作之前就已经执行过该步骤了。
接下来,执行configure,它会生成Makefile:./configure
configure有很多参数,其中比较常用的是prefix参数,用于将FreeSWITCH安装到指定的目录下(FreeSWITCH 默认的安装目录是/usr/local/freeswitch),如:
./configure --prefix=/usr/local/freeswitch2 ./configure --prefix=/opt/freeswitch
configure执行完毕后,将产生Makefile,以及一个modules.conf文件。modules.conf用于控制在编译阶段要自动编译哪些模块。如果你需要这些模块,则可以编辑该文件,并去掉前面的“#”号注释,如:
$ head modules.conf
#applications/mod_abstraction
#applications/mod_avmd
#applications/mod_blacklist
#applications/mod_callcenter
#applications/mod_cidlookup
applications/mod_cluechoo
applications/mod_commands
applications/mod_conference
#applications/mod_curl
applications/mod_db
如果不知道哪些模块是干什么的,可以暂且不管这个文件。到以后也可以再单独编译某些模块。接下来,执行make,它将根据Makefile进行编译:make
编译成功后,执行如下命令将程序安装到相应的位置:make install
注意,需要确认要安装的目标位置有写入的权限,如果这些命令都是以root执行的,那你不会遇到权限的问题,但如果你是以普通用户执行的,就可能遇到权限的问题。所以,如果有权限的问题,可以尝试用root进行安装:sudo make install
也可以通过如下方案以普通用户的身份安装,如以freeswitch用户安装,假设你现在登录的用户就是 freeswitch:
sudo mkdir /usr/local/freeswitch
sudo chown freeswitch /usr/local/freeswitch
make install
增量 编译
有时候修改了源文件,需要再次编译。在没有修改autoconf、automake相关的编译规则的话,直接执行make就行了:make
也可以直接执行如下命令:make install
make会检查全部的规则,并决定哪些需要重新编译,这还是比较耗时的。如果你知道自己修改了哪些模块,可以直接编译该模块,如:
make mod_sofia
make mod_sofia-install
使用这种方法也可以编译默认没有编译过的模块,如mod_shout模块提供MP3录、放音的支持,它默认是不被编译的,可以用以下命令安装:make mod_shout-install
当然,在大多数情况下也可以直接进入相关的模块目录下,执行make,如:cd src/mod/endpoints/mod_sofia make install
如果你改了核心的代码,则可以执行如下命令仅编译安装核心部分:make core-install
最佳 实践
下列命令编译最新的 master 版本到默认位置:
git clone git://git.freeswitch.org/freeswitch.git freeswitch-master
cd freeswitch-master
./bootstrap.sh && ./configure && make && make install
编译1.2 版本并安装到 /usr/local/freeswitch-1.2:
git clone git://git.freeswitch.org/freeswitch.git freeswitch-1.2
cd freeswitch-1.2
git checkout v1.2.stable
./bootstrap.sh && ./configure --prefix=/usr/local/freeswitch-1.2
make && make install
通过这种方式,以后在维护多个分支时就不会混乱了,而且如果有必要的话,也可以同时在一台主机上同时启动不同版本的FreeSWITCH实例。
6、FreeSWITCH 源代码 分析
任何项目、任何代码,从不熟悉到熟悉,总要有一个过程。代码的作者在开发的时候,代码是一行一行写成的,也是一步一步调试成功的,因此整个程序的结构全部在心里。当作为一个“外来人”去看代码时,就好像只看一栋盖好的大楼,想去了解其结构和建设过程一样,自然要困难得多。不过可以从 main 函数开始大致了解了总的框架结构。
不管干什么事情,从熟悉的地方入手,往往比较容易。下面就从 FreeSWITCH 模块对源代码进行分析。主要对 3个最有代表性的模块的源代码进行了深入剖析,以 echo、answer 等 App 作为突破口,一步一步深入跟踪,理清代码的执行流程,了解了各种回调函数的含义及触发时机。同时,也对从模块启动、网络监听、来话的接收、Session的生成、各种状态的转移直到应答等全部的流程进行了跟踪和梳理。
mod_dptools 模块
该模块包含了系统绝大部分的App,其中就包括我们熟悉的 echo(回声)和 answer(应答)
echo(回声)
先来找echo。通过使用全文搜索工具搜索源代码,我们很快就在 mod_dptools.c 中找到了一个函数定义 echo_function,只有三行代码:
从代码中可以看出,echo_function 这个函数是用 SWITCH_STANDARD_APP 宏来定义的。接着跟踪这个宏的定义,发现它是在 switch_types.h 定义的。
已经知道,每一路通话(一条腿)均有一个Session(即这里的session变量),每个App都是跟Session相关的,因而FreeSWITCH在调用每个App时,均会把当前的Session作为参数传入(一个session指针)。由于echo App没有参数,因而这里的data就是空字符串。当然,如果你在Dialplan中传入参数,如进行操作:<application action="echo" data="some data"/>
那么,这里的char*data的值就是some data,只不过我们在此并不需要用到该参数,因而直接忽略掉了。echo-function 函数就直接调用核心提供的switch_ivr_session_echo函数,将收到的RTP包原样发回去。switch_ivr_session_echo函数我们在20.3.7节已经详细介绍了,这里就不重复了。
至此,是不是觉得整个呼叫流程一下子就串起来了?当然,如果还是没有这种感觉,继续往下看。
继续在 mod_dptools.c 文件中找 echo_function,会发现下面一行:
它的作用是将我们刚刚定义的echo_function加到app_interface里(即核心的Application Interface指针)。SWITCH_ADD_APP也是一个宏,它是在switch_loadable_modules.c 行定义的:
#define SWITCH_ADD_APP(app_int, int_name, short_descript, \
long_descript, funcptr, syntax_string, app_flags) \
for (;;) { \
app_int = (switch_application_interface_t *) \
switch_loadable_module_create_interface(*module_interface, \
SWITCH_APPLICATION_INTERFACE); \
app_int->interface_name = int_name; \
app_int->application_function = funcptr; \
app_int->short_desc = short_descript; \
app_int->long_desc = long_descript; \
app_int->syntax = syntax_string; \
app_int->flags = app_flags; \
break; \
}
这个宏定义得非常巧妙,它使用了一个无限的for循环,但由于该循环的最后一条语句是break,因此它只会执行一次。该循环跟Linux内核中的“do{...}while(0)”有异曲同工之妙
所以一个SWITCH_ADD_APP相当于使用switch_loadable_module_create_interface函数创建了一个SWITCH_APPLICATION_INTERFACE类型的接口(即我们所说的Application Interface)变量app_interface,然后给它赋予合适的值。大部分参数都是一些描述信息或帮助字符串,最重要的是下面两行,其确定了echo这个app_interface与我们定义的echo_function的对应关系。
app_interface->interface_name = "echo";
app_interface->application_function = echo_function;
因而,通过SWITCH_ADD_APP这个宏,相当于给系统核心添加了一个echo App,它对应源代码中的 echo_function。这样每当系统执行到Dialplan中的echo App时,便通过这里的对应关系找到相应的函数入口, 进而执行echo_function函数。
answer(应答)
用同样的方法可以找到 answer_function
跟 echo_function 类似,该函数也是使用 SWITCH_STANDARD_APP 定义的。因为一个Session对应一个Channel。所以通过 switch_core_session_get_channel 函数便可以找当前与 Session 对应的 Channel,函数定义了一个 arg 指针,它指向answer的参数data。如果arg(即传过来的data)为空字符串( zstr函数用于判断空字符串),则尝试查一下该Channel上有没有answer_flags 这个通道变量,如果有(其中switch_stristr类似于标准的stristr,不区分大小写),则判断该参数中是否包含“is_conference”,如果包含,则在该Channel上设置一个CF_CONFERENCE标志(该标志主要用于RFC4575/RFC4579描述的会议系统)。最后,在调用核心的函数switch_channel_answer来对该Channel进行应答。switch_channel_answer 函数实际上是一个宏,在此使用一个宏的作用就是往函数中传入调用者的源文件名和行号信息,以便在日志中打印的文件名和行号是实际上调用该函数处的文件名和行号,而不是该函数实际定义处的行号(否则没有什么实际意义)。该宏展开后实际上是调用 switch_channel.c 中的switch_channel_perform_answer 函数。在该函数中,它会首先初始化一个msg变量,该变量是switch_core_session_message_t 类型的,用于定义一条消息(Message)。然后,初始化消息的内容,并将消息发送出去,消息(Message)是与Core Event类似的另外一种消息传递(调用)方式,与Core Event不同的是,消息的发送总是同步进行的,因此这里的 perform_receive_message实际上是直接调用各模块中接收消息的回调函数,
如果消息发送成功,就将该Channel的状态置为已经应答的状态。
实际上,如果这里的Channel是一个SIP通话的话,FreeSWITCH中的mod_sofia Endpoint模块便会调用底层的Sofia-SIP协议栈(libsofia)给对方发送“200 OK”的SIP消息。
answer_function也是由SWITCH_ADD_APP宏安装到核心中去的。
set (设置)
FreeSWITCH 中大量使用通道变量控制通话(Channel)的行为。设置通道变量的操作是由下面的set App实现的。该函数相当简单,因为它直接调用了另外一个函数 base_set。
SWITCH_STANDARD_APP(set_function)
{
base_set(session, data, SWITCH_STACK_BOTTOM);
}
base_set 其实会被多个函数调用,在此我们只关心它被 set_function 调用的情况。为了更直观,通过实际的例子来说明。假设在 Dialplan 中使用如下配置:
<action application="set" data="dialed_extension=$1"/>
其中,$1 为前面的正则表达式的匹配结果,它是一个变量(我们假设它的值为1001):
static void base_set (switch_core_session_t *session, const char *data, switch_stack_t stack)
{
char *var, *val = NULL;
const char *what = "SET";
switch (stack) {
case SWITCH_STACK_PUSH:
what = "PUSH";
break;
case SWITCH_STACK_UNSHIFT:
what = "UNSHIFT";
break;
default:
break;
}
if (zstr(data)) {
switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session), SWITCH_LOG_ERROR, "No variable name specified.\n");
} else {
switch_channel_t *channel = switch_core_session_get_channel(session);
char *expanded = NULL;
var = switch_core_session_strdup(session, data);
if (!(val = strchr(var, '='))) {
val = strchr(var, ',');
}
if (val) {
*val++ = '\0';
if (zstr(val)) {
val = NULL;
}
}
if (val) {
expanded = switch_channel_expand_variables(channel, val);
}
switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session), SWITCH_LOG_DEBUG, "%s %s [%s]=[%s]\n",
what, switch_channel_get_name(channel), var, expanded ? expanded : "UNDEF");
switch_channel_add_variable_var_check(channel, var, expanded, SWITCH_FALSE, stack);
if (expanded && expanded != val) {
switch_safe_free(expanded);
}
}
}
switch_core_session_strdup 将字符串复制一份。该函数是在session上进行操作的,它会使用该session的内存池申请字符串空间,因而申请以后的内存无需明确释放。为什么要重新复制一份字符串呢?是因为接下来的操作会修改该字符串的内存,因而复制一份可以避免破坏原来的字符串所占的内存空间。到此 var变量的值就是“dialed_extension=$1”了。
接下来判断字符串是否包含等号,在我们的例子里有等号,因此val指向等号所在的内存位置,也可以说val指针所指的字符串值为“=$1”。
如果val非空,则将val所指的位置写入“\0”(即C语言中的字符串结束符),并将val指针向后移动一个字节,此时它的值就是“$1”了。同时,由于将原字符串中的等号替换改成了“\0”,因此,var所指向的字符串的值也相当于变短了,此时var的值为“dialed_extension”。
继续判断如果val为非空(因为已经移动了指针,所以要重新判断),则执行将val指针中的$1变量替换为它的实际的值。在这里,我们将在expanded变量中得到实际的值是“1001”。
调用函数在Channel上设置我们指定的新变量:
最后不要忘记,expanded 指针所指向的内存是动态申请的,因此一定要释放内存,以避免引起内存泄漏。
switch_channel_add_variable_var_check 函数到底都干了些什么。该函数定义于 switch_channel.c。它在第1407行先对临界区加锁,以防止其他并发的线程同时修改。然后,经过一系列的判断和检查,如果最终所有检查都通过(第1417行),则在第1418行调用switch_event_add_header_string函数将通道变量添加到channel->variables中去。它实际上是向一个switch_event_t类型的结构体中添加数据,所以这里可以看到,channel->variables在内部是使用switch_event_t来存储的。这也不奇怪,因为通道变量本来就是一对“键/值”对(varname和value)。
当然,永远不要忘了释放锁:switch_mutex_unlock(channel->profile_mutex);
至此,set 函数就全部剖析完了。通过它设置的通道变量,以后也可以通过switch_channel_get_variable再取出来。当然,这就是另外的事情了。
bridge (桥接)
看一下 bridge 这个 App,从某种意义上讲,它属于 FreeSWITCH 的核心功能,比较有代表性。
bridge App 是由 audio_bridge_function 函数完成的。
该函数比较复杂,尽量挑简单的部分说。首先,该App在Dialplan中的使用方法一般是:
<action application="bridge" data="user/1001"/>
因而,该函数中的 data 参数便是一个指向字符串“user/1001”的指针。在第3052行,首先检查该字符串的有效性。如果它为空字符串,那就没有必要继续进行了,直接返回(return,第3053行)即可。跳过很多if…else假设,调用核心的 switch_ivr_originate 函数发起一个新的呼叫。switch_ivr_originate 函数是在 switch_ivr_originate.c 定义,该函数比较长,不深入研究,看下参数。
switch_ivr_originate 函数中,session 就是指当前的 Session,即呼入的那条腿(a-leg),我们执行到此处,调用该函数创建另一条腿(b-leg)。因而,第二个参数peer_session就是新建立的Session。由于我们在该函数执行完成后,需要知道peer_session指针的值,因此这里我们传入的是指针变量的地址(相当于一个双重指针)。同理,我们也需要在呼叫失败时得到呼叫原因(cause),因此把它作为第三个变量。第四个参数便是我们提供的呼叫字符串(data)的指针,在本例中该字符串的值是“user/1001”。
其他的参数我们就没必要看了,大部分都是空指针。在此,由于我们传入了当前的session指针,因此该函数在执行的时候就有参照物了。比如,它会将a-leg(当前session)中的主叫号码(effective_caller_id_number)作为主叫号码去呼叫b-leg等。当然,b-leg也不是仅参照a-leg,如果b-leg的对端回了呼叫进展消息(如SIP 180或183消息),则a-leg也能听到相关的提示音。
如果b-leg的对端应答了,或者在呼叫进展过程中返回了媒体消息(如SIP中的183消息),则上述的switch_ivr_originate函数就会返回。在接下来的
switch_channel_t *peer_channel = switch_core_session_get_channel(peer_session);
将得到新的Channel(b-leg对应的Channel—— peer_channel)。
如果我们在呼叫时使用的是Proxy Media模式,则执行函数仅进行信令级的桥接,否则(正常情况)就执行多线程的桥接函数 switch_ivr_multi_threaded_bridge。
接下来看 switch_ivr_multi_threaded_bridge 函数。它是在 switch_ivr.c 实现的。它首先在初始化了两个 switch_ivr_bridge_data_t 类型的变量 a_leg 和 b_leg,用于存放与两条腿相关的私有数据。它首先在peer_channel(即b-leg)上安装一些状态回调函数,当b-leg的状态发生变化时,将调用相关的回调函数。
switch_channel_add_state_handler(peer_channel, &audio_bridge_peer_state_handlers);
然后产生一个CHANNEL_BRIDGE事件,并发送出去。
接下来,分别在a-leg和b-leg上产生一个SWITCH_MESSAGE_INDICATE_BRIDGE消息(Message,用于标志该Channel已经被桥接了),分别发送给这两个leg
将一个 b_leg 数据指针确定的私有数据绑定到 b-leg上(使用私有数据与设置通道变量类似,但后者只能是字符串值,而前者可以绑定为任意值)。然后,将 b-leg 的状态设为媒体交换的状态(CS_EXCHANGE_MEDIA)。这时候,b-leg的状态发生了变化,因而会回调在上面设置的回调函数。
执行audio_bridge_thread函数,并将一个a_leg数据指针传入。该数据指针包含a-leg的一些信息。
audio_bridge_thread(NULL, (void *) a_leg);
它将阻塞的执行一直到bridge结束,因此,我们可以倒回来看b-leg上的回调函数了。
在 audio_bridge_on_exchange_media函数中,可以看到,它通过switch_channel_get_private取出了该Channel上的一个私有数据。该私有数据里面存储了与bridge相关的b-leg上的数据,有了它以后,我们就可以执行audio_bridge_thread函数了。
至此,我们可以看到,a-leg和b-leg分别在自己的线程中执行了audio_bridge_thread函数(这个很重要,我们的思维现在并行化了,即下面讲的所有代码都是在两条腿上在两个线程中并行执行的),并且在该函数中,它们分别传入了自己所在的那条腿上的switch_ivr_bridge_data_t结构的数据。
在该函数中,它首先将传入的数据从obj指针赋值给一个data指针,并将data指针中的session成员变量赋值给session_a。注意,到了这里,session_a就不一定是a-leg了,而是只在当前线程中的那条腿。即,如果在a-leg中调用该函数,它就是a-leg;如果在b-leg中调用该函数,它就是b-leg。同理,第237行的session_b变量也不一定是b-leg,而是与本条腿相对的那条腿(桥接中的另一条腿)。注意,该腿相关的session_b变量不是直接传入的指针,而是传入了一个Channel的UUID(data->b_uuid),因此我们需要使用 switch_core_session_locate来取得与该UUID对应的Session的指针session_b。至此,在两个线程中分别都有了当前的Session和另一个Session的信息了
第256~257行分别取出与它们对应的Channel。然后,就是一个无限循环(第341行),在该循环中,不停地在当前的Session(session_a)中读取一帧媒体数据(第547行),然后写入另一个Session(session_b,第565行)。这就实现了媒体 的交换,也是bridge App的全部秘密。当然,第546行的注释可能更简洁直观一些——从一个Channel中读取音频并写入另一个Channel。
witch_core_session_locate通过一个UUID获得了session_b的指针。而该函数在返回指针的同时会将当前的Session加锁,以防止产生竞争条件(Race Condition)。因此,在任何时候使用switch_core_session_locate函数并获得了非空的指针时,在指针使用完成后都需要明确解锁,
switch_core_session_rwunlock(session_b);
当然,当上面的audio_bridge_thread函数完成后,后续还有很多事情要做,如发送CHANNEL_UNBRIDGE事件、检查所有相关的after_bridge(桥接后的)变量(我们常用的hangup_after_bridge变量就是在这里检查的) 等,
Endpoint Interface
在mod_dptools模块中,实现了一些常用的“假”的Endpoint Interface。之所以说是“假”的,是因为它们并没有像mod_sofia那样既有底层的协议驱动,又有媒体收、发处理,而是为了简化某些操作,或者为了在某些特殊的情况下使用一些一致的命令或接口而实现的。比如,我们常用的user就是一个Endpoint。一般来说,一个Endpoint都会提供一个用于外呼的呼叫字符串,我们对于user提供的呼叫字符串已经非常熟悉了,如在命令行和
Dialplan中我们经常使用如下的呼叫字符串:
originate user/1000 &echo
<action application="bridge" data="user/1000" />
这里面的user就是由user Endpoint实现的。该Interface的指针是在第3879行声明的一个全局变量。
switch_endpoint_interface_t *user_endpoint_interface;
switch_io_routines_t 类型的结构体,用于定义回调函数。可以看出,由于该Endpoint很简单,它只定义了一个outgoing_channel回调函数。该回调函数将在有人使用user呼叫字符串时(如执行originate和bridge时)被调用。
回调函数的定义中输入参数中将包含一个outbound_profile,它的成员变量destination_number即是被叫号码。复制该被叫号码并赋值给user指针。
user = strdup(outbound_profile->destination_number);
然后获取domain的值,如果呼叫字符串中未包含domain(如user/1000@192.168.1.2就包含了domain,而user/1000则未包含)
接下来,从XML用户目录中查找该用户,并继续尝试找到dial-string配置参数:
如果该呼叫字符串是在bridge中使用的,则判断成立(即说明有a-leg),否则说明是个单腿的呼叫(originate)。然后根据不同的情况进行相关的设置,并得到一个 d_dest(的地址(如sip:1000@192.168.1.100:7890等)。
随后调用switch_ivr_originate去呼叫该地址。
user Endpoint是一个最简单的Endpoint。它目前仅支持SIP呼叫(理论上它还可以扩展支持其他的),实际的呼叫流程还要转到实际的mod_sofia Endpoint上进行处理
模块 框架
一个模块主要是由 load、runtime 和 shutdown 回调函数组成的。mod_dptools 当然也不例外。
mod_dptools 模块的 load 函数是将在模块被加载的时候执行。以看出它实现了包括API Interface、App Interface、Dialplan Interface在内的多个Interface。
在初始化了一系列的内存池及其他数据结构后,在向核心注册该模块。
向核心绑定(订阅)了一个SWITCH_EVENT_PRESENCE_PROBE事件的回调函数,即每当系统中产生该事件后,都会执行回调函数pickup_pres_event_handler。
另外,它还实现了一些EndpointInterface:
可以看到,inline Dialplan也是在该模块中实现的
总之,在该函数的最后,返回 SWITCH_STATUS_SUCCESS 表明该模块加载成功:
mod_dptoots模块没有runtime函数。其shutdown函数也很简单,该模块没有太多要清理的资源,它只需要向核心取消先前绑定的事件回调函数:
上述就是mod_dptoots模块的大体框架。可以尝试在 FreeSWITCH 控制台上使用 “reload mod_dptools” 命令查看与这些代码相关的日志输出,并对照实际的代码看下。
mod_commands 模块
在mod_commands中,实现了大部分的API命令,如常用的version、status、originate等。
模块 框架
定义了一个 switch_api_interface_t 类型的指针,用于实现 API Interface,向核心注册本模块,向核心注册相关命令实现的回调函数。
然后,它使用switch_console_set_complete添加命令补全信息,以便用户在控制台上输入命令时可以使用Tab键进行补全。
switch_console_set_complete("add show calls");
switch_console_set_complete("add show channels");
最后,函数返回 SWITCH_STATUS_NOUNLOAD。与其他模块返回SWITCH_STATUS_SUCCESS 不同,这里的返回值表示该模块是无法被卸载的(由于 unload 命令本身是在该模块内实现的)。
originate
originate命令是在originate_function中实现的,使用 SWITCH_STANDARD_API进行声明。该声明也是一个在switch_types.h 行定义的一个宏
展开结果可以看出,该宏有3个输入参数:第一个是输入的命令参数;第二个是一个session,但由于大多数的API命令都跟Session无关,因此该参数一般是一个空指针;第三个参数是一个stream,它是一个流,写入该流中的数据(命令输出)将可作为命令的结果返回。
由于originate命令的参数众多,因此它使用一个switch_separate_string对命令字符串进行分隔。该函数将分割后的结果放到一个argv数组中,并返回数组中参数的个数argc(在这一点上,类似于C语言中经典的main函数的参数)
在对输入参数进行分析后,调用 switch_ivr_originate 发起一个呼叫。可以看到,bridge App 也是调用了该函数发起呼叫,但不同的是,在这里它的第一个参数是一个空指针(NULL),因而这是一个单腿的呼叫。
另外一个与 bridge App 中调用方法不同的地方在于,在这里它的大部分参数都不是空指针,因而可以在外呼的同时指定其他参数,如超时(timeout)、主叫名称(cid_name)、主叫号码(cid_num)等。
如果在发起呼叫时使用“&”指定了一个App,如originate user/1000&echo命令,则它在对方接听后(严格来说是收到媒体后,如收到SIP 183消息后),即开始执行 app_name(如echo)所指定的函数。
否则就转移到相应的 Dialplan。如用户输入“originate user/1000 9196 XML default”命令则执行下面的代码。switch_ivr_session_transfer(caller_session, exten, dp,
调用输出流stream的write_function输出命令的反馈信息,如“+OK UUID”。:stream->write_function(stream, "+OK %s\n",
使用switch_ivr_originate所产生的Session也是加锁的,因而,我们也要明确地释放它:switch_core_session_rwunlock(caller_session);
mod_sofia 模块
mod_sofia是FreeSWITCH中最大的一个模块,也是最重要的一个模块。所有的SIP通话都是从它开始和终止的,因而分析该模块的源代码是很有参考意义的。
mod_sofia 模块非常庞大而且复杂,它实现了 SIP 注册、呼叫、Presence、SLA 等一系列的 SIP 特性。在此抓住一条主线,仅研究 SIP 呼叫有关的代码,以避免又陷入庞大代码的海洋。
模块 加载
还是把 mod_sofia 模块的 load 函数作为入口,它是在 mod_sofia.c 实现的
定义了一个 api_interface 指针,用于往核心中添加 API。它将一个全局变量mod_sofia_globals清零。该全局变量在整个模块内是有效的,它用于记录一些模块级的数据和变量。然后,在进行一定的初始化后,它在第5447行将全局变量的一个running成员变量设为1,标志该模块是在运行的。
启动一个消息处理线程,用于SIP消息的处理。sofia_msg_thread_start(0);
调用config_sofia函数来从XML中读取该模块的配置并启动相关的Sofia Profile。
向核心注册本模块。初始化一个新的Endpoint,接着指定该新的Endpoint的名字及绑定相关的回调函数
Sofia 的加载及通话建立
来看一下Sofia(即我们的SIP服务)到底是从哪里加载的,通话的建立是从哪里开始的,又是如何进行的。
关于 Sofia 的加载,它就隐藏在 config_sofia 函数中。该函数是在 sofia.c 定义。该函数非常长,它解析XML配置文件,初始化与Profile相关的变量的数据结构,并启动相关的Profile。
launch_sofia_profile_thread 启动一个新线程,并在新线程中执行sofia_profile_thread_run,同时将profile作为输入参数。
在新线程中得到profile指针的值。然后调用 nua_create 函数建立一个UA(User Agent)。nua_create 是 Sofia-SIP 库提供的函数,它将启动一个UA,监听相关的端口(如5060),并等待SIP消息到来。一旦收到SIP请求,它便会回调sofia_event_callback回调函数,该回调函数中将带着对应的 profile 作为回调参数。
到此为止,SIP 服务已经启动了,就等着接收SIP消息了。
SIP消息的接收
对SIP事件的处理是在单独的线程(组)中执行的。进行事件处理的线程是在模块加载时从sofia_msg_thread_start 函数开始的。该函数定义于sofia.c,它首先会启动一个新线程,并在以后根据CPU的数量以及当前的需要决定启动多少个消息处理线程。新的事件处理线程中将执行sofia_msg_thread_run 函数。
继续跟踪,就发现sofia_msg_thread_run 使用一个无限循环,不断地从消息队列中取出一条消息(事件),然后使用 sofia_process_dispatch_event函数发送出去。
继续跟踪 sofia_process_dispatch_event 发现调用一个回调函数,继续往下跟踪,找到our_sofia_event_callback 的定义后,可以看到它确实是在处理SIP消息了。在 switch语句的各个分支中,我们可以看到许多以nua_r_和nua_i_开头的SIP event,其中,前者表示收到一条响应(Response)消息,而后者表示收到一条请求消息。
集中精力看INVITE消息,如果收到INVITE消息,case条件成立,则说明是一个re-INVITE消息,否则,则说明是一个新的INVITE消息,调用sofia_handle_sip_i_invite处理。
在 sofia_handle_sip_i_invite中,将更深入解析INVITE消息,对Session的相关内容进行更新,如果需要对来话进行认证,还需要给对方发送SIP 407消息进行挑战认证等。
SIP 状态机
在Sofia-SIP底层,也实现了一个状态机,在SIP通话的不同阶段使用不同的状态进行表示和处理。因而在SIP状态发生改变时,它便向上层上报状态变化事件,这些状态变化事件也是在SIP事件的形式上报的,因而会经过跟上述的INVITE消息类似的回调过程一直到回调同一个回调函数our_sofia_event_callback。
由于 sofia_handle_sip_i_state 函数有太多的状态和情况需要处理,因此也非常长。
下面让我们拿起一个SIP电话,拨打9196,很快就可以在日志中看到如下的信息:
[DEBUG] sofia.c:5861 ... Channel entering state [received][100]
从上一条日志可以看出,在第5861行打印了一条日志,表示我们的状态机进入了收到INVITE消息后发送100 Trying消息的阶段(代码略)。而接着下一条日志则告诉我们Channel的状态从CS_NEW变成了CS_INIT。
[DEBUG] sofia.c:6116 ... State Change CS_NEW -> CS_INIT
有了上述信息,我们就可以在sofia.c的第6116行很快找到该日志对应的代码了。只要满足一定的条件,在该行就会把Channel的状态变为CS_INIT,然后Channel的核心状态机就会回调相关的状态回调函数了。
Channel 状态机
只要Channel的状态一变成CS_INIT,FreeSWITCH核心的状态机代码就会负责处理各种状态变化了,因而各Endpoint模块就不需要再自己维护状态机了。也就是说,在一个Endpoint模块,首先要有一定的机制用于初始化一个Session(对应一个Channel,它的初始状态将为CS_NEW),然后在适当的时候把该Channel的状态变成CS_INIT,剩下的事就基本不用管了。
当然,这里说的是“基本”而不是“绝对”。一般来说,还是要在Endpoint模块中跟踪Channel状态机的变化,这就需要靠在核心状态机上注册相应的回调函数实现,如 Sofia Channel 的状态机的回调定义。
IO 例程
与Channel状态机回调相比,Endpoint 模块中更重要的是IO例程的回调。IO例程主要提供媒体数据的输入输出(IO)功能。IO 例程的回调函数是由一个 switch_io_routines_t 类型的结构体变量设置的,该变量的定义
outgoing_channel 回调是在有外呼请求的时候(如,执行“originate sofia/gateway/...”时)被回调执行的。在此我们再来看一下mod_sofia中的outgoing_channel有何不同。
该模块的 outgoing_channel 回调
它也是初始化了一个新的 Session(nsession),然后初始化了一个新的 tech_pvt 用于存放私有数据。从outbound_profile中复制被叫号码,得到对应的Channel(nchannel)。如果该外呼是由bridge 发起的,则还会有a-leg存在,
nchannel = switch_core_session_get_channel(nsession);
因而将得到与a-leg对应的Channel,新生成的nchannel即是b-leg。
之后,将 tech_pvt 与 nsession 关联进来。
sofia_glue_attach_private(nsession, profile, tech_pvt, dest);
可以看出,新的Channel nchannel的状态变为了CS_INIT。然后,该Channel便进入正常的呼叫流程了。接下来,核心的状态机会接管后面的状态变化,如将状态机置为CS_ROUTING,然后进行路由查找(即查找Dialplan),最后进入CS_EXECUTE状态,执行在Dialplan中找到的各种App等。
if (switch_channel_get_state(nchannel) == CS_NEW) {
switch_channel_set_state(nchannel, CS_INIT);
}
当代码中某处调用switch_core_session_read_frame试图读取一帧音频数据时,就会执行read_frame回调函数,该回调函数由于将大部分功能都移动到核心的Core Media代码中去了,因而非常简单,它主要就是调用核心的 switch_core_media_read_frame 从底层的RTP中读取音频数据。写数据的情况与此差不多,write_frame回调函数 调用了Core Media中的函数switch_core_media_write_frame通过RTP将音频数据发送出去。
视频的回调函数read_video_frame和write_video_frame与此差不多。
最后,receive_message 回调,
static switch_status_t sofia_receive_message(switch_core_session_t *session,
switch_core_session_message_t *msg)
{
...
switch (msg->message_id) {
...
switch (msg->message_id) {
...
case SWITCH_MESSAGE_INDICATE_ANSWER:
status = sofia_answer_channel(session);
两个 switch语句用于判断收到的各种消息,并进行相应的处理。在收到SWITCH_MESSAGE_INDICATE_ANSWER 消息时,它将调用 sofia_answer_channel 对当前通话进行应答。应答将会向对方发送SIP 200 OK消息。
nua_respond(tech_pvt->nh, SIP_200_OK, ...
调用 Sofia-SIP 底层库实现
回到 answer App 可以看到它调用了核心的 switch_answer_channel 函数,并在switch_channel.c发送了一个 SWITCH_MESSAGE_INDICATE_ANSWER 消息,因而 sofia_receive_message 函数被回调,并最终向对方的SIP终端发送200 OK消息。
至此,我们所有的呼叫流程就全部都串起来了,我们对源代码的分析也到此结束了
7、FreeSWITCH 二次开发
修改 FreeSWITCH 的源代码,开发一个新的模块,然后编译、运行。
编解码 模块 大致实现方法
测试 VP8 视频编码时,FreeSWITCH 当时还不支持VP8,因而需要自己添加支持。在FreeSWITCH 中写一个新模块很简单。而且,由于FreeSWITCH中的视频模块不支持转码,因而大部分回调函数什么也不做。
首先在FreeSWITCH源代码目录中 src/mod/codecs 下创建 mod_vp8 目录,并在里面创建mod_vp8.c 文件,然后,找一个类似的编解码模块,并把它里面的内容复制过来稍加修改即可。当时发现与VP8最像的模块是 mod_theora,因此就直接复制了 mod_threora.c 里面的内容。修改后的 mod_vp8.c 内容如下。
首先,是include和模块声明。在该模块中,只需要load函数。
#include <switch.h>
SWITCH_MODULE_LOAD_FUNCTION(mod_vp8_load);
SWITCH_MODULE_DEFINITION(mod_vp8, mod_vp8_load, NULL, NULL);
在编解码模块中,当在核心中初始化一个编码时,首先回调的就是init,即这里的switch_vp8_init函数。该函数在此要做的事情不多,基本上直接返回了成功值——SWITCH_STATUS_SUCCESS。
static switch_status_t switch_vp8_init(switch_codec_t *codec, switch_codec_flag_t flags, const switch_codec_settings_t *codec_settings)
{
...
return SWITCH_STATUS_SUCCESS;
}
如果在调用该模块进行编码时或解码时,将调用这里的encode或decode函数。由于我们并不支持视频的编、解码,因此直接返回SWITCH_STATUS_FALSE。实际上,由于核心本身不支持编解码,因而永远也不会回调到这里。
最后要释放编解码器的回调函数destroy:
其实该模块最重要的就是load函数了。在load函数中,第82行初始化了一个codec_interface,它是一个switch_codec_interface_t类型的指针,说明我们想要创建一个Codec Interface。第84行就紧接着创建了它。第85行,将该codec_interface安装到核心中去。
在该codec_interface上增加了一个实现(Implementation),并增加了实现的回调函数。具体的参数定义我们在此就不多讲了,总之它定义了4个回调函数,即我们上面讲过的init、encode、decode和destroy(参见20.3.13节的内容)。虽然有些回调函数什么也不做,不过我们也最好写上它们,以便跟现有的其他模块中的代码一致。最后,返回SWITCH_STATUS_SUCCESS以标明模块加载成功(第102行)。
后来,Anthony在做WebRTC时,还用同样的方法增加了red和ulpfec视频编码,不过那就是后话了(见第91行和第96行)。
有了上述的目录和模块实现文件后,在核心进行configure的时候将会自动生成一个Makefile,不过,我们也可以先自己写一个Makefile用于测试。在Makefile中加入如下内容后就可以编译该模块了。其中第1行指定FreeSWITCH源代码的主目录,第2行装入通用的模块编译规则:
BASE=../../../..
include $(BASE)/build/modmake.rules
然后,直接在当前目录下可以使用如下命令编译安装:make install
接下来就可以在FreeSWITCH中直接加载使用了。
- mod_vp8:在1.6中已被
mod_vpx
代替。 - mod_vpx:VP8、VP9 编解码模块。
mod_vpx.c 源码
/*
* FreeSWITCH Modular Media Switching Software Library / Soft-Switch Application
* Copyright (C) 2005-2015, Anthony Minessale II <anthm@freeswitch.org>
*
* Version: MPL 1.1
*
* The contents of this file are subject to the Mozilla Public License Version
* 1.1 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
* http://www.mozilla.org/MPL/
*
* Software distributed under the License is distributed on an "AS IS" basis,
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
* for the specific language governing rights and limitations under the
* License.
*
* The Original Code is FreeSWITCH Modular Media Switching Software Library / Soft-Switch Application
*
* The Initial Developer of the Original Code is
* Seven Du <dujinfang@gmail.com>
* Portions created by the Initial Developer are Copyright (C)
* the Initial Developer. All Rights Reserved.
*
* Contributor(s):
*
* Anthony Minessale II <anthm@freeswitch.org>
* Seven Du <dujinfang@gmail.com>
* Sam Russell <sam.h.russell@gmail.com>
*
* mod_vpx.c -- VP8/9 Video Codec, with transcoding
*
*/
#include <switch.h>
#ifdef SWITCH_HAVE_YUV
#ifdef SWITCH_HAVE_VPX
#include <vpx/vpx_encoder.h>
#include <vpx/vpx_decoder.h>
#include <vpx/vp8cx.h>
#include <vpx/vp8dx.h>
#include <vpx/vp8.h>
// #define DEBUG_VP9
#ifdef DEBUG_VP9
#define VPX_SWITCH_LOG_LEVEL SWITCH_LOG_ERROR
#else
#define VPX_SWITCH_LOG_LEVEL SWITCH_LOG_DEBUG1
#endif
#define SLICE_SIZE SWITCH_DEFAULT_VIDEO_SIZE
#define KEY_FRAME_MIN_FREQ 250000
#define CODEC_TYPE_ANY 0
#define CODEC_TYPE_VP8 8
#define CODEC_TYPE_VP9 9
typedef struct my_vpx_cfg_s {
char name[64];
int lossless;
int cpuused;
int token_parts;
int static_thresh;
int noise_sensitivity;
int max_intra_bitrate_pct;
vp9e_tune_content tune_content;
vpx_codec_enc_cfg_t enc_cfg;
vpx_codec_dec_cfg_t dec_cfg;
switch_event_t *codecs;
} my_vpx_cfg_t;
#define SHOW(cfg, field) switch_log_printf(SWITCH_CHANNEL_LOG_CLEAN, SWITCH_LOG_INFO, " %-28s = %d\n", #field, cfg->field)
static void show_config(my_vpx_cfg_t *my_cfg, vpx_codec_enc_cfg_t *cfg)
{
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, " %-28s = %s\n", "name", my_cfg->name);
switch_log_printf(SWITCH_CHANNEL_LOG_CLEAN, SWITCH_LOG_INFO, " %-28s = %d\n", "decoder.threads", my_cfg->dec_cfg.threads);
SHOW(my_cfg, lossless);
SHOW(my_cfg, cpuused);
SHOW(my_cfg, token_parts);
SHOW(my_cfg, static_thresh);
SHOW(my_cfg, noise_sensitivity);
SHOW(my_cfg, max_intra_bitrate_pct);
SHOW(my_cfg, tune_content);
SHOW(cfg, g_usage);
SHOW(cfg, g_threads);
SHOW(cfg, g_profile);
SHOW(cfg, g_w);
SHOW(cfg, g_h);
SHOW(cfg, g_bit_depth);
SHOW(cfg, g_input_bit_depth);
SHOW(cfg, g_timebase.num);
SHOW(cfg, g_timebase.den);
SHOW(cfg, g_error_resilient);
SHOW(cfg, g_pass);
SHOW(cfg, g_lag_in_frames);
SHOW(cfg, rc_dropframe_thresh);
SHOW(cfg, rc_resize_allowed);
SHOW(cfg, rc_scaled_width);
SHOW(cfg, rc_scaled_height);
SHOW(cfg, rc_resize_up_thresh);
SHOW(cfg, rc_resize_down_thresh);
SHOW(cfg, rc_end_usage);
SHOW(cfg, rc_target_bitrate);
SHOW(cfg, rc_min_quantizer);
SHOW(cfg, rc_max_quantizer);
SHOW(cfg, rc_undershoot_pct);
SHOW(cfg, rc_overshoot_pct);
SHOW(cfg, rc_buf_sz);
SHOW(cfg, rc_buf_initial_sz);
SHOW(cfg, rc_buf_optimal_sz);
SHOW(cfg, rc_2pass_vbr_bias_pct);
SHOW(cfg, rc_2pass_vbr_minsection_pct);
SHOW(cfg, rc_2pass_vbr_maxsection_pct);
SHOW(cfg, kf_mode);
SHOW(cfg, kf_min_dist);
SHOW(cfg, kf_max_dist);
SHOW(cfg, ss_number_layers);
SHOW(cfg, ts_number_layers);
SHOW(cfg, ts_periodicity);
SHOW(cfg, temporal_layering_mode);
if (my_cfg->codecs) {
switch_event_header_t *hp;
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "======== Codec specific profiles ========\n");
for (hp = my_cfg->codecs->headers; hp; hp = hp->next) {
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, " %-28s = %s\n", hp->name, hp->value);
}
}
}
/* http://tools.ietf.org/html/draft-ietf-payload-vp8-10
The first octets after the RTP header are the VP8 payload descriptor, with the following structure.
#endif
0 1 2 3 4 5 6 7
+-+-+-+-+-+-+-+-+
|X|R|N|S|R| PID | (REQUIRED)
+-+-+-+-+-+-+-+-+
X: |I|L|T|K| RSV | (OPTIONAL)
+-+-+-+-+-+-+-+-+
I: |M| PictureID | (OPTIONAL)
+-+-+-+-+-+-+-+-+
L: | TL0PICIDX | (OPTIONAL)
+-+-+-+-+-+-+-+-+
T/K:|TID|Y| KEYIDX | (OPTIONAL)
+-+-+-+-+-+-+-+-+
VP8 Payload Header
0 1 2 3 4 5 6 7
+-+-+-+-+-+-+-+-+
|Size0|H| VER |P|
+-+-+-+-+-+-+-+-+
| Size1 |
+-+-+-+-+-+-+-+-+
| Size2 |
+-+-+-+-+-+-+-+-+
| Bytes 4..N of |
| VP8 payload |
: :
+-+-+-+-+-+-+-+-+
| OPTIONAL RTP |
| padding |
: :
+-+-+-+-+-+-+-+-+
*/
#ifdef _MSC_VER
#pragma pack(push, r1, 1)
#endif
#if SWITCH_BYTE_ORDER == __BIG_ENDIAN
typedef struct {
unsigned extended:1;
unsigned reserved1:1;
unsigned non_referenced:1;
unsigned start:1;
unsigned reserved2:1;
unsigned pid:3;
unsigned I:1;
unsigned L:1;
unsigned T:1;
unsigned K:1;
unsigned RSV:4;
unsigned M:1;
unsigned PID:15;
unsigned TL0PICIDX:8;
unsigned TID:2;
unsigned Y:1;
unsigned KEYIDX:5;
} vp8_payload_descriptor_t;
typedef struct {
unsigned have_pid:1;
unsigned have_p_layer:1;
unsigned have_layer_ind:1;
unsigned is_flexible:1;
unsigned start:1;
unsigned end:1;
unsigned have_ss:1;
unsigned zero:1;
} vp9_payload_descriptor_t;
typedef struct {
unsigned n_s:3;
unsigned y:1;
unsigned g:1;
unsigned zero:3;
} vp9_ss_t;
typedef struct {
unsigned t:3;
unsigned u:1;
unsigned r:2;
unsigned zero:2;
} vp9_n_g_t;
typedef struct {
unsigned temporal_id:3;
unsigned temporal_up_switch:1;
unsigned spatial_id:3;
unsigned inter_layer_predicted:1;
} vp9_p_layer_t;
#else /* ELSE LITTLE */
typedef struct {
unsigned pid:3;
unsigned reserved2:1;
unsigned start:1;
unsigned non_referenced:1;
unsigned reserved1:1;
unsigned extended:1;
unsigned RSV:4;
unsigned K:1;
unsigned T:1;
unsigned L:1;
unsigned I:1;
unsigned PID:15;
unsigned M:1;
unsigned TL0PICIDX:8;
unsigned KEYIDX:5;
unsigned Y:1;
unsigned TID:2;
} vp8_payload_descriptor_t;
typedef struct {
unsigned zero:1;
unsigned have_ss:1;
unsigned end:1;
unsigned start:1;
unsigned is_flexible:1;
unsigned have_layer_ind:1;
unsigned have_p_layer:1;
unsigned have_pid:1;
} vp9_payload_descriptor_t;
typedef struct {
unsigned zero:3;
unsigned g:1;
unsigned y:1;
unsigned n_s:3;
} vp9_ss_t;
typedef struct {
unsigned zero:2;
unsigned r:2;
unsigned u:1;
unsigned t:3;
} vp9_n_g_t;
typedef struct {
unsigned inter_layer_predicted:1;
unsigned spatial_id:3;
unsigned temporal_up_switch:1;
unsigned temporal_id:3;
} vp9_p_layer_t;
#endif
typedef union {
vp8_payload_descriptor_t vp8;
vp9_payload_descriptor_t vp9;
} vpx_payload_descriptor_t;
#define kMaxVp9NumberOfSpatialLayers 16
typedef struct {
switch_bool_t has_received_sli;
uint8_t picture_id_sli;
switch_bool_t has_received_rpsi;
uint64_t picture_id_rpsi;
int16_t picture_id; // Negative value to skip pictureId.
switch_bool_t inter_pic_predicted; // This layer frame is dependent on previously
// coded frame(s).
switch_bool_t flexible_mode;
switch_bool_t ss_data_available;
int tl0_pic_idx; // Negative value to skip tl0PicIdx.
uint8_t temporal_idx;
uint8_t spatial_idx;
switch_bool_t temporal_up_switch;
switch_bool_t inter_layer_predicted; // Frame is dependent on directly lower spatial
// layer frame.
uint8_t gof_idx;
// SS data.
size_t num_spatial_layers;
switch_bool_t spatial_layer_resolution_present;
uint16_t width[kMaxVp9NumberOfSpatialLayers];
uint16_t height[kMaxVp9NumberOfSpatialLayers];
// GofInfoVP9 gof;
} vp9_info_t;
#ifdef _MSC_VER
#pragma pack(pop, r1)
#endif
#define __IS_VP8_KEY_FRAME(byte) !(((byte) & 0x01))
static inline int IS_VP8_KEY_FRAME(uint8_t *data)
{
uint8_t S;
uint8_t DES;
uint8_t PID;
DES = *data;
data++;
S = DES & 0x10;
PID = DES & 0x07;
if (DES & 0x80) { // X
uint8_t X = *data;
data++;
if (X & 0x80) { // I
uint8_t M = (*data) & 0x80;
data++;
if (M) data++;
}
if (X & 0x40) data++; // L
if (X & 0x30) data++; // T/K
}
if (S && (PID == 0)) {
return __IS_VP8_KEY_FRAME(*data);
} else {
// if (PID > 0) switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "PID: %d\n", PID);
return 0;
}
}
#define IS_VP9_KEY_FRAME(byte) ((((byte) & 0x40) == 0) && ((byte) & 0x0A))
#define IS_VP9_START_PKT(byte) ((byte) & 0x08)
#ifdef WIN32
#undef SWITCH_MOD_DECLARE_DATA
#define SWITCH_MOD_DECLARE_DATA __declspec(dllexport)
#endif
SWITCH_MODULE_LOAD_FUNCTION(mod_vpx_load);
SWITCH_MODULE_SHUTDOWN_FUNCTION(mod_vpx_shutdown);
SWITCH_MODULE_DEFINITION(CORE_VPX_MODULE, mod_vpx_load, mod_vpx_shutdown, NULL);
struct vpx_context {
int debug;
switch_codec_t *codec;
int is_vp9;
vp9_info_t vp9;
vpx_codec_iface_t *encoder_interface;
vpx_codec_iface_t *decoder_interface;
unsigned int flags;
switch_codec_settings_t codec_settings;
unsigned int bandwidth;
vpx_codec_enc_cfg_t config;
switch_time_t last_key_frame;
vpx_codec_ctx_t encoder;
uint8_t encoder_init;
vpx_image_t *pic;
switch_bool_t force_key_frame;
int fps;
int format;
int intra_period;
int num;
int partition_index;
const vpx_codec_cx_pkt_t *pkt;
vpx_codec_iter_t enc_iter;
vpx_codec_iter_t dec_iter;
uint32_t last_ts;
switch_time_t last_ms;
vpx_codec_ctx_t decoder;
uint8_t decoder_init;
int decoded_first_frame;
switch_buffer_t *vpx_packet_buffer;
int got_key_frame;
int no_key_frame;
int got_start_frame;
uint32_t last_received_timestamp;
switch_bool_t last_received_complete_picture;
uint16_t last_received_seq;
int need_key_frame;
int need_encoder_reset;
int need_decoder_reset;
int32_t change_bandwidth;
uint64_t framecount;
switch_memory_pool_t *pool;
switch_buffer_t *pbuffer;
switch_time_t start_time;
switch_image_t *patch_img;
int16_t picture_id;
};
typedef struct vpx_context vpx_context_t;
#define MAX_PROFILES 100
struct vpx_globals {
int debug;
uint32_t max_bitrate;
uint32_t rtp_slice_size;
uint32_t key_frame_min_freq;
uint32_t dec_threads;
uint32_t enc_threads;
my_vpx_cfg_t *profiles[MAX_PROFILES];
};
struct vpx_globals vpx_globals = { 0 };
static my_vpx_cfg_t *find_cfg_profile(const char *name, switch_bool_t reconfig);
static void parse_profile(my_vpx_cfg_t *my_cfg, switch_xml_t profile, int codec_type);
static switch_status_t init_decoder(switch_codec_t *codec)
{
vpx_context_t *context = (vpx_context_t *)codec->private_info;
//if (context->decoder_init) {
// vpx_codec_destroy(&context->decoder);
// context->decoder_init = 0;
//}
if (context->flags & SWITCH_CODEC_FLAG_DECODE && !context->decoder_init) {
vpx_codec_dec_cfg_t cfg = {0, 0, 0};
vpx_codec_flags_t dec_flags = 0;
vp8_postproc_cfg_t ppcfg;
my_vpx_cfg_t *my_cfg = NULL;
vpx_codec_err_t err;
if (context->is_vp9) {
my_cfg = find_cfg_profile("vp9", SWITCH_FALSE);
} else {
my_cfg = find_cfg_profile("vp8", SWITCH_FALSE);
}
if (!my_cfg) return SWITCH_STATUS_FALSE;
cfg.threads = my_cfg->dec_cfg.threads;
if ((err = vpx_codec_dec_init(&context->decoder, context->decoder_interface, &cfg, dec_flags)) != VPX_CODEC_OK) {
switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(codec->session), SWITCH_LOG_ERROR,
"VPX decoder %s codec init error: [%d:%s:%s]\n",
vpx_codec_iface_name(context->decoder_interface),
err, vpx_codec_error(&context->decoder), vpx_codec_error_detail(&context->decoder));
return SWITCH_STATUS_FALSE;
}
context->last_ts = 0;
context->last_received_timestamp = 0;
context->last_received_complete_picture = 0;
context->last_received_seq = 0;
context->decoder_init = 1;
context->got_key_frame = 0;
context->no_key_frame = 0;
context->got_start_frame = 0;
// the types of post processing to be done, should be combination of "vp8_postproc_level"
ppcfg.post_proc_flag = VP8_DEBLOCK;//VP8_DEMACROBLOCK | VP8_DEBLOCK;
// the strength of deblocking, valid range [0, 16]
ppcfg.deblocking_level = 1;
// Set deblocking settings
vpx_codec_control(&context->decoder, VP8_SET_POSTPROC, &ppcfg);
if (context->vpx_packet_buffer) {
switch_buffer_zero(context->vpx_packet_buffer);
} else {
switch_buffer_create_dynamic(&context->vpx_packet_buffer, 512, 512, 0);
}
}
return SWITCH_STATUS_SUCCESS;
}
static int CODEC_TYPE(const char *string)
{
if (!strcmp(string, "vp8")) {
return CODEC_TYPE_VP8;
} else if (!strcmp(string, "vp9")) {
return CODEC_TYPE_VP9;
}
return CODEC_TYPE_ANY;
}
static void parse_codec_specific_profile(my_vpx_cfg_t *my_cfg, const char *codec_name)
{
switch_xml_t cfg = NULL;
switch_xml_t xml = switch_xml_open_cfg("vpx.conf", &cfg, NULL);
switch_xml_t profiles = cfg ? switch_xml_child(cfg, "profiles") : NULL;
// open config and find the profile to parse
if (profiles) {
switch_event_header_t *hp;
for (hp = my_cfg->codecs->headers; hp; hp = hp->next) {
// switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "%s: %s\n", hp->name, hp->value);
if (!strcmp(hp->name, codec_name)) {
switch_xml_t profile;
for (profile = switch_xml_child(profiles, "profile"); profile; profile = profile->next) {
const char *name = switch_xml_attr(profile, "name");
if (!strcmp(hp->value, name)) {
parse_profile(my_cfg, profile, CODEC_TYPE(codec_name));
}
}
}
}
}
if (xml) switch_xml_free(xml);
}
static switch_status_t init_encoder(switch_codec_t *codec)
{
vpx_context_t *context = (vpx_context_t *)codec->private_info;
vpx_codec_enc_cfg_t *config = &context->config;
my_vpx_cfg_t *my_cfg = NULL;
vpx_codec_err_t err;
char *codec_name = "vp8";
if (context->is_vp9) {
codec_name = "vp9";
}
if (!zstr(context->codec_settings.video.config_profile_name)) {
my_cfg = find_cfg_profile(context->codec_settings.video.config_profile_name, SWITCH_FALSE);
}
if (!my_cfg) {
my_cfg = find_cfg_profile(codec_name, SWITCH_FALSE);
}
if (!my_cfg) return SWITCH_STATUS_FALSE;
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "config: %s\n", my_cfg->name);
if (context->is_vp9) {
my_cfg->enc_cfg.g_profile = 0; // default build of VP9 only support 0, TODO: remove this
}
if (my_cfg->codecs) {
parse_codec_specific_profile(my_cfg, codec_name);
}
if (vpx_globals.debug) show_config(my_cfg, &my_cfg->enc_cfg);
if (!context->codec_settings.video.width) {
context->codec_settings.video.width = 1280;
}
if (!context->codec_settings.video.height) {
context->codec_settings.video.height = 720;
}
if (context->codec_settings.video.bandwidth == -1) {
context->codec_settings.video.bandwidth = 0;
}
if (context->codec_settings.video.bandwidth) {
context->bandwidth = context->codec_settings.video.bandwidth;
} else {
context->bandwidth = switch_calc_bitrate(context->codec_settings.video.width, context->codec_settings.video.height, 1, 15);
}
if (context->bandwidth > vpx_globals.max_bitrate) {
switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(codec->session), SWITCH_LOG_WARNING, "REQUESTED BITRATE TRUNCATED FROM %d TO %d\n", context->bandwidth, vpx_globals.max_bitrate);
context->bandwidth = vpx_globals.max_bitrate;
}
switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(codec->session), SWITCH_LOG_NOTICE,
"VPX encoder reset (WxH/BW) from %dx%d/%u to %dx%d/%u\n",
config->g_w, config->g_h, config->rc_target_bitrate,
context->codec_settings.video.width, context->codec_settings.video.height, context->bandwidth);
context->pkt = NULL;
context->start_time = switch_micro_time_now();
*config = my_cfg->enc_cfg; // reset whole config to current defaults
config->g_w = context->codec_settings.video.width;
config->g_h = context->codec_settings.video.height;
config->rc_target_bitrate = context->bandwidth;
if (context->is_vp9) {
if (my_cfg->lossless) {
config->rc_min_quantizer = 0;
config->rc_max_quantizer = 0;
}
}
if (context->encoder_init) {
if ((err = vpx_codec_enc_config_set(&context->encoder, config)) != VPX_CODEC_OK) {
switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(codec->session), SWITCH_LOG_ERROR,
"VPX encoder %s codec reconf error: [%d:%s:%s]\n",
vpx_codec_iface_name(context->encoder_interface),
err, vpx_codec_error(&context->encoder), vpx_codec_error_detail(&context->encoder));
return SWITCH_STATUS_FALSE;
}
} else if (context->flags & SWITCH_CODEC_FLAG_ENCODE) {
if (vpx_globals.debug) {
switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(codec->session), SWITCH_LOG_INFO, "VPX encoder %s settings:\n", vpx_codec_iface_name(context->encoder_interface));
show_config(my_cfg, config);
}
if ((err = vpx_codec_enc_init(&context->encoder, context->encoder_interface, config, 0 & VPX_CODEC_USE_OUTPUT_PARTITION)) != VPX_CODEC_OK) {
switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(codec->session), SWITCH_LOG_ERROR,
"VPX encoder %s codec init error: [%d:%s:%s]\n",
vpx_codec_iface_name(context->encoder_interface),
err, vpx_codec_error(&context->encoder), vpx_codec_error_detail(&context->encoder));
return SWITCH_STATUS_FALSE;
}
context->encoder_init = 1;
vpx_codec_control(&context->encoder, VP8E_SET_TOKEN_PARTITIONS, my_cfg->token_parts);
vpx_codec_control(&context->encoder, VP8E_SET_CPUUSED, my_cfg->cpuused);
vpx_codec_control(&context->encoder, VP8E_SET_STATIC_THRESHOLD, my_cfg->static_thresh);
if (context->is_vp9) {
if (my_cfg->lossless) {
vpx_codec_control(&context->encoder, VP9E_SET_LOSSLESS, 1);
}
vpx_codec_control(&context->encoder, VP9E_SET_TUNE_CONTENT, my_cfg->tune_content);
} else {
vpx_codec_control(&context->encoder, VP8E_SET_NOISE_SENSITIVITY, my_cfg->noise_sensitivity);
if (my_cfg->max_intra_bitrate_pct) {
vpx_codec_control(&context->encoder, VP8E_SET_MAX_INTRA_BITRATE_PCT, my_cfg->max_intra_bitrate_pct);
}
}
}
return SWITCH_STATUS_SUCCESS;
}
static switch_status_t switch_vpx_init(switch_codec_t *codec, switch_codec_flag_t flags, const switch_codec_settings_t *codec_settings)
{
vpx_context_t *context = NULL;
int encoding, decoding;
encoding = (flags & SWITCH_CODEC_FLAG_ENCODE);
decoding = (flags & SWITCH_CODEC_FLAG_DECODE);
if (!(encoding || decoding) || ((context = switch_core_alloc(codec->memory_pool, sizeof(*context))) == 0)) {
return SWITCH_STATUS_FALSE;
}
memset(context, 0, sizeof(*context));
context->flags = flags;
codec->private_info = context;
context->pool = codec->memory_pool;
if (codec_settings) {
context->codec_settings = *codec_settings;
}
if (!strcmp(codec->implementation->iananame, "VP9")) {
context->is_vp9 = 1;
context->encoder_interface = vpx_codec_vp9_cx();
context->decoder_interface = vpx_codec_vp9_dx();
} else {
context->encoder_interface = vpx_codec_vp8_cx();
context->decoder_interface = vpx_codec_vp8_dx();
}
if (codec->fmtp_in) {
codec->fmtp_out = switch_core_strdup(codec->memory_pool, codec->fmtp_in);
}
context->codec_settings.video.width = 320;
context->codec_settings.video.height = 240;
switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(codec->session), SWITCH_LOG_DEBUG, "VPX VER:%s VPX_IMAGE_ABI_VERSION:%d VPX_CODEC_ABI_VERSION:%d\n",
vpx_codec_version_str(), VPX_IMAGE_ABI_VERSION, VPX_CODEC_ABI_VERSION);
if (!context->is_vp9) {
context->picture_id = 13; // picture Id may start from random value and must be incremented on each frame
} else {
context->vp9.picture_id = 13;
}
return SWITCH_STATUS_SUCCESS;
}
static switch_status_t consume_partition(vpx_context_t *context, switch_frame_t *frame)
{
vpx_payload_descriptor_t *payload_descriptor;
uint8_t *body, *c = NULL;
uint32_t hdrlen = 0, payload_size = 0, max_payload_size = 0, start = 0, key = 0;
switch_size_t remaining_bytes = 0;
switch_status_t status;
if (!context->pkt) {
if ((context->pkt = vpx_codec_get_cx_data(&context->encoder, &context->enc_iter))) {
start = 1;
if (!context->pbuffer) {
switch_buffer_create_partition(context->pool, &context->pbuffer, context->pkt->data.frame.buf, context->pkt->data.frame.sz);
} else {
switch_buffer_set_partition_data(context->pbuffer, context->pkt->data.frame.buf, context->pkt->data.frame.sz);
}
}
}
if (context->pbuffer) {
remaining_bytes = switch_buffer_inuse(context->pbuffer);
}
if (!context->pkt || context->pkt->kind != VPX_CODEC_CX_FRAME_PKT || !remaining_bytes) {
frame->datalen = 0;
frame->m = 1;
context->pkt = NULL;
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "writing 0 bytes\n");
return SWITCH_STATUS_SUCCESS;
}
key = (context->pkt->data.frame.flags & VPX_FRAME_IS_KEY);
#if 0
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "flags: %x pts: %lld duration:%lu partition_id: %d\n",
context->pkt->data.frame.flags, context->pkt->data.frame.pts, context->pkt->data.frame.duration, context->pkt->data.frame.partition_id);
#endif
/* reset header */
*(uint8_t *)frame->data = 0;
payload_descriptor = (vpx_payload_descriptor_t *) frame->data;
memset(payload_descriptor, 0, sizeof(*payload_descriptor));
if (context->is_vp9) {
hdrlen = 1; /* Send VP9 with 1 byte REQUIRED header. */
} else {
hdrlen = 4; /* Send VP8 with 4 byte extended header, includes 1 byte REQUIRED header, 1 byte X header and 2 bytes of I header with picture_id. */
}
body = ((uint8_t *)frame->data) + hdrlen;
if (context->is_vp9) {
payload_descriptor->vp9.start = start;
if (1) {
// payload_descriptor->vp9.have_p_layer = key; // key?
payload_descriptor->vp9.have_pid = 1;
if (payload_descriptor->vp9.have_pid) {
if (context->vp9.picture_id > 0x7f) {
*body++ = (context->vp9.picture_id >> 8) | 0x80;
*body++ = context->vp9.picture_id & 0xff;
hdrlen += 2;
} else {
*body++ = context->vp9.picture_id;
hdrlen++;
}
}
if (key) {
vp9_ss_t *ss = (vp9_ss_t *)body;
payload_descriptor->vp9.have_ss = 1;
payload_descriptor->vp9.have_p_layer = 0;
ss->n_s = 0;
ss->g = 0;
ss->y = 0;
ss->zero = 0;
body++;
hdrlen++;
if (0) { // y ?
uint16_t *w;
uint16_t *h;
ss->y = 1;
w = (uint16_t *)body;
body+=2;
h = (uint16_t *)body;
body+=2;
*w = (uint16_t)context->codec_settings.video.width;
*h = (uint16_t)context->codec_settings.video.height;
hdrlen += (ss->n_s + 1) * 4;
}
} else {
payload_descriptor->vp9.have_p_layer = 1;
}
}
}
if (!context->is_vp9) {
payload_descriptor->vp8.start = start;
payload_descriptor->vp8.extended = 1; /* REQUIRED header. */
payload_descriptor->vp8.I = 1; /* X header. */
payload_descriptor->vp8.M = 1; /* I header. */
c = ((uint8_t *)frame->data) + 2;
*c++ = (context->picture_id >> 8) | 0x80;
*c = context->picture_id & 0xff;
payload_descriptor->vp8.L = 0;
payload_descriptor->vp8.TL0PICIDX = 0;
payload_descriptor->vp8.T = 0;
payload_descriptor->vp8.TID = 0;
payload_descriptor->vp8.Y = 0;
payload_descriptor->vp8.K = 0;
payload_descriptor->vp8.KEYIDX = 0;
}
/*
Try to split payload to packets evenly(with largest at the end) up to vpx_globals.rtp_slice_size,
(assume hdrlen constant across all packets of the same picture).
It keeps packets being transmitted in order.
Without it last (and thus the smallest one) packet usually arrive out of order
(before the previous one)
*/
max_payload_size = vpx_globals.rtp_slice_size - hdrlen;
payload_size = remaining_bytes / ((remaining_bytes + max_payload_size - 1) / max_payload_size);
if (remaining_bytes <= payload_size) {
switch_buffer_read(context->pbuffer, body, remaining_bytes);
context->pkt = NULL;
frame->datalen = hdrlen + remaining_bytes;
frame->m = 1;
// increment and wrap picture_id (if needed) after the last picture's packet
if (context->is_vp9) {
context->vp9.picture_id++;
if ((uint16_t)context->vp9.picture_id > 0x7fff) {
context->vp9.picture_id = 0;
}
} else {
context->picture_id++;
if ((uint16_t)context->picture_id > 0x7fff) {
context->picture_id = 0;
}
}
status = SWITCH_STATUS_SUCCESS;
} else {
switch_buffer_read(context->pbuffer, body, payload_size);
frame->datalen = hdrlen + payload_size;
frame->m = 0;
status = SWITCH_STATUS_MORE_DATA;
}
if (frame->m && context->is_vp9) {
payload_descriptor->vp9.end = 1;
}
return status;
}
static switch_status_t reset_codec_encoder(switch_codec_t *codec)
{
vpx_context_t *context = (vpx_context_t *)codec->private_info;
if (context->encoder_init) {
vpx_codec_destroy(&context->encoder);
}
context->last_ts = 0;
context->last_ms = 0;
context->framecount = 0;
context->encoder_init = 0;
context->pkt = NULL;
return init_encoder(codec);
}
static switch_status_t switch_vpx_encode(switch_codec_t *codec, switch_frame_t *frame)
{
vpx_context_t *context = (vpx_context_t *)codec->private_info;
int width = 0;
int height = 0;
uint32_t dur;
int64_t pts;
vpx_enc_frame_flags_t vpx_flags = 0;
switch_time_t now;
vpx_codec_err_t err;
if (frame->flags & SFF_SAME_IMAGE) {
return consume_partition(context, frame);
}
if (context->need_encoder_reset != 0) {
if (reset_codec_encoder(codec) != SWITCH_STATUS_SUCCESS) {
return SWITCH_STATUS_FALSE;
}
context->need_encoder_reset = 0;
}
if (frame->img->d_h > 1) {
width = frame->img->d_w;
height = frame->img->d_h;
} else {
width = frame->img->w;
height = frame->img->h;
}
if (context->codec_settings.video.width != width || context->codec_settings.video.height != height) {
context->codec_settings.video.width = width;
context->codec_settings.video.height = height;
reset_codec_encoder(codec);
frame->flags |= SFF_PICTURE_RESET;
context->need_key_frame = 3;
}
if (!context->encoder_init) {
if (init_encoder(codec) != SWITCH_STATUS_SUCCESS) {
return SWITCH_STATUS_FALSE;
}
}
if (context->change_bandwidth) {
context->codec_settings.video.bandwidth = context->change_bandwidth;
context->change_bandwidth = 0;
if (init_encoder(codec) != SWITCH_STATUS_SUCCESS) {
return SWITCH_STATUS_FALSE;
}
}
now = switch_time_now();
if (context->need_key_frame > 0) {
// force generate a key frame
if (!context->last_key_frame || (now - context->last_key_frame) > vpx_globals.key_frame_min_freq) {
switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(codec->session), VPX_SWITCH_LOG_LEVEL,
"VPX encoder keyframe request\n");
vpx_flags |= VPX_EFLAG_FORCE_KF;
context->need_key_frame = 0;
context->last_key_frame = now;
}
}
context->framecount++;
pts = (now - context->start_time) / 1000;
//pts = frame->timestamp;
dur = context->last_ms ? (now - context->last_ms) / 1000 : pts;
if ((err = vpx_codec_encode(&context->encoder,
(vpx_image_t *) frame->img,
pts,
dur,
vpx_flags,
VPX_DL_REALTIME)) != VPX_CODEC_OK) {
switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(codec->session), SWITCH_LOG_ERROR, "VPX encode error [%d:%s:%s]\n",
err, vpx_codec_error(&context->encoder), vpx_codec_error_detail(&context->encoder));
frame->datalen = 0;
return SWITCH_STATUS_FALSE;
}
context->enc_iter = NULL;
context->last_ts = frame->timestamp;
context->last_ms = now;
return consume_partition(context, frame);
}
static switch_status_t buffer_vp8_packets(vpx_context_t *context, switch_frame_t *frame)
{
uint8_t *data = frame->data;
uint8_t S;
uint8_t DES;
// uint8_t PID;
int len;
if (context->debug > 0) {
switch_log_printf(SWITCH_CHANNEL_LOG, context->debug,
"VIDEO VPX: seq: %d ts: %u len: %u %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x mark: %d\n",
frame->seq, frame->timestamp, frame->datalen,
*((uint8_t *)data), *((uint8_t *)data + 1),
*((uint8_t *)data + 2), *((uint8_t *)data + 3),
*((uint8_t *)data + 4), *((uint8_t *)data + 5),
*((uint8_t *)data + 6), *((uint8_t *)data + 7),
*((uint8_t *)data + 8), *((uint8_t *)data + 9),
*((uint8_t *)data + 10), frame->m);
}
DES = *data;
data++;
S = (DES & 0x10);
// PID = DES & 0x07;
//switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "DATA LEN %d S BIT %d PID: %d\n", frame->datalen, S, PID);
if (DES & 0x80) { // X
uint8_t X = *data;
//switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "X BIT SET\n");
data++;
if (X & 0x80) { // I
uint8_t M = (*data) & 0x80;
//switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "I BIT SET\n");
data++;
if (M) {
//switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "M BIT SET\n");
data++;
}
}
if (X & 0x40) {
//switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "L BIT SET\n");
data++; // L
}
if (X & 0x30) {
//switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "T/K BIT SET\n");
data++; // T/K
}
}
if (!switch_buffer_inuse(context->vpx_packet_buffer) && !S) {
if (context->got_key_frame > 0) {
context->got_key_frame = 0;
context->got_start_frame = 0;
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG2, "packet loss?\n");
}
return SWITCH_STATUS_MORE_DATA;
}
if (S) {
switch_buffer_zero(context->vpx_packet_buffer);
context->last_received_timestamp = frame->timestamp;
#if 0
if (PID == 0) {
key = __IS_VP8_KEY_FRAME(*data);
}
#endif
}
len = frame->datalen - (data - (uint8_t *)frame->data);
//switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "POST PARSE: DATA LEN %d KEY %d KEYBYTE = %0x\n", len, key, *data);
if (len <= 0) {
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "Invalid packet %d\n", len);
return SWITCH_STATUS_RESTART;
}
if (context->last_received_timestamp != frame->timestamp) {
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG2, "wrong timestamp %u, expect %u, packet loss?\n", frame->timestamp, context->last_received_timestamp);
switch_buffer_zero(context->vpx_packet_buffer);
return SWITCH_STATUS_RESTART;
}
switch_buffer_write(context->vpx_packet_buffer, data, len);
return SWITCH_STATUS_SUCCESS;
}
// https://tools.ietf.org/id/draft-ietf-payload-vp9-01.txt
static switch_status_t buffer_vp9_packets(vpx_context_t *context, switch_frame_t *frame)
{
uint8_t *data = (uint8_t *)frame->data;
uint8_t *vp9 = (uint8_t *)frame->data;
vp9_payload_descriptor_t *desc = (vp9_payload_descriptor_t *)vp9;
int len = 0;
if (context->debug > 0) {
switch_log_printf(SWITCH_CHANNEL_LOG, frame->m ? SWITCH_LOG_ERROR : SWITCH_LOG_INFO,
"[%02x %02x %02x %02x] m=%d len=%4d seq=%d ts=%u ssrc=%u "
"have_pid=%d "
"have_p_layer=%d "
"have_layer_ind=%d "
"is_flexible=%d "
"start=%d "
"end=%d "
"have_ss=%d "
"zero=%d\n",
*data, *(data+1), *(data+2), *(data+3), frame->m, frame->datalen, frame->seq, frame->timestamp, frame->ssrc,
desc->have_pid,
desc->have_p_layer,
desc->have_layer_ind,
desc->is_flexible,
desc->start,
desc->end,
desc->have_ss,
desc->zero);
}
vp9++;
if (desc->have_pid) {
#ifdef DEBUG_VP9
uint16_t pid = 0;
pid = *vp9 & 0x7f; //0 bit is M , 1-7 bit is pid.
#endif
if (*vp9 & 0x80) { //if (M==1)
vp9++;
#ifdef DEBUG_VP9
pid = (pid << 8) + *vp9;
#endif
}
vp9++;
#ifdef DEBUG_VP9
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "have pid: %d start=%d end=%d\n", pid, desc->start, desc->end);
#endif
}
if (desc->have_layer_ind) {
#ifdef DEBUG_VP9
vp9_p_layer_t *layer = (vp9_p_layer_t *)vp9;
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "temporal_id=%d temporal_up_switch=%d spatial_id=%d inter_layer_predicted=%d\n",
layer->temporal_id, layer->temporal_up_switch, layer->spatial_id, layer->inter_layer_predicted);
#endif
vp9++;
if (!desc->is_flexible) {
vp9++; // TL0PICIDX
}
}
//When P and F are both set to one, indicating a non-key frame in flexible mode
if (desc->have_p_layer && desc->is_flexible) { // P & F set, P_DIFF
if (*vp9 & 1) { // N
vp9++;
if (*vp9 & 1) { // N
vp9++;
if (*vp9 & 1) {
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "Invalid VP9 packet!");
switch_buffer_zero(context->vpx_packet_buffer);
goto end;
}
}
}
vp9++;
}
if (desc->have_ss) {
vp9_ss_t *ss = (vp9_ss_t *)(vp9++);
#ifdef DEBUG_VP9
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "have ss: %02x n_s: %d y:%d g:%d\n", *(uint8_t *)ss, ss->n_s, ss->y, ss->g);
#endif
if (ss->y) {
int i;
for (i=0; i<=ss->n_s; i++) {
#ifdef DEBUG_VP9
int width = ntohs(*(uint16_t *)vp9);
int height = ntohs(*(uint16_t *)(vp9 + 2));
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "SS: %d %dx%d\n", i, width, height);
#endif
vp9 += 4;
}
}
if (ss->g) {
int i;
uint8_t ng = *vp9++; //N_G indicates the number of frames in a GOF
for (i = 0; ng > 0 && i < ng; i++) {
vp9_n_g_t *n_g = (vp9_n_g_t *)(vp9++);
vp9 += n_g->r;
}
}
}
if (vp9 - data >= frame->datalen) {
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG1, "Invalid VP9 Packet %" SWITCH_SSIZE_T_FMT " > %d\n", vp9 - data, frame->datalen);
switch_buffer_zero(context->vpx_packet_buffer);
goto end;
}
if (!switch_buffer_inuse(context->vpx_packet_buffer)) { // start packet
if (!desc->start) {
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "got invalid vp9 packet, packet loss? waiting for a start packet\n");
goto end;
}
}
len = frame->datalen - (vp9 - data);
switch_buffer_write(context->vpx_packet_buffer, vp9, len);
end:
#ifdef DEBUG_VP9
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "buffered %d bytes, buffer size: %" SWITCH_SIZE_T_FMT "\n", len, switch_buffer_inuse(context->vpx_packet_buffer));
#endif
return SWITCH_STATUS_SUCCESS;
}
static switch_status_t switch_vpx_decode(switch_codec_t *codec, switch_frame_t *frame)
{
vpx_context_t *context = (vpx_context_t *)codec->private_info;
switch_size_t len;
vpx_codec_ctx_t *decoder = NULL;
switch_status_t status = SWITCH_STATUS_SUCCESS;
int is_start = 0, is_keyframe = 0, get_refresh = 0;
if (context->debug > 0 && context->debug < 4) {
vp9_payload_descriptor_t *desc = (vp9_payload_descriptor_t *)frame->data;
uint8_t *data = frame->data;
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "%02x %02x %02x %02x m=%d start=%d end=%d m=%d len=%d\n",
*data, *(data+1), *(data+2), *(data+3), frame->m, desc->start, desc->end, frame->m, frame->datalen);
}
if (context->is_vp9) {
is_keyframe = IS_VP9_KEY_FRAME(*(unsigned char *)frame->data);
is_start = IS_VP9_START_PKT(*(unsigned char *)frame->data);
if (is_keyframe) {
switch_log_printf(SWITCH_CHANNEL_LOG, VPX_SWITCH_LOG_LEVEL, "================Got a key frame!!!!========================\n");
}
if (context->last_received_seq && context->last_received_seq + 1 != frame->seq) {
switch_log_printf(SWITCH_CHANNEL_LOG, VPX_SWITCH_LOG_LEVEL, "Packet loss detected last=%d got=%d lost=%d\n", context->last_received_seq, frame->seq, frame->seq - context->last_received_seq);
if (is_keyframe) switch_buffer_zero(context->vpx_packet_buffer);
}
context->last_received_seq = frame->seq;
} else { // vp8
is_start = (*(unsigned char *)frame->data & 0x10);
is_keyframe = IS_VP8_KEY_FRAME((uint8_t *)frame->data);
}
if (!is_keyframe && context->got_key_frame <= 0) {
context->no_key_frame++;
//switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_WARNING, "no keyframe, %d\n", context->no_key_frame);
if (context->no_key_frame > 50) {
if ((is_keyframe = is_start)) {
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG1, "no keyframe, treating start as key. frames=%d\n", context->no_key_frame);
}
}
}
if (context->debug > 0 && is_keyframe) {
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "GOT KEY FRAME %d\n", context->got_key_frame);
}
if (context->need_decoder_reset != 0) {
vpx_codec_destroy(&context->decoder);
context->decoder_init = 0;
status = init_decoder(codec);
context->need_decoder_reset = 0;
}
if (status != SWITCH_STATUS_SUCCESS) goto end;
if (!context->decoder_init) {
status = init_decoder(codec);
}
if (status != SWITCH_STATUS_SUCCESS) goto end;
if (!context->decoder_init) {
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_WARNING, "VPX decoder is not initialized!\n");
return SWITCH_STATUS_FALSE;
}
decoder = &context->decoder;
// switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "len: %d ts: %u mark:%d\n", frame->datalen, frame->timestamp, frame->m);
// context->last_received_timestamp = frame->timestamp;
context->last_received_complete_picture = frame->m ? SWITCH_TRUE : SWITCH_FALSE;
if (is_start) {
context->got_start_frame = 1;
}
if (is_keyframe) {
switch_set_flag(frame, SFF_IS_KEYFRAME);
if (context->got_key_frame <= 0) {
context->got_key_frame = 1;
context->no_key_frame = 0;
} else {
context->got_key_frame++;
}
} else if (context->got_key_frame <= 0) {
if ((--context->got_key_frame % 200) == 0) {
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG1, "Waiting for key frame %d\n", context->got_key_frame);
}
get_refresh = 1;
if (!context->got_start_frame) {
switch_goto_status(SWITCH_STATUS_MORE_DATA, end);
}
}
status = context->is_vp9 ? buffer_vp9_packets(context, frame) : buffer_vp8_packets(context, frame);
if (context->dec_iter && (frame->img = (switch_image_t *) vpx_codec_get_frame(decoder, &context->dec_iter))) {
switch_goto_status(SWITCH_STATUS_SUCCESS, end);
}
// switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "====READ buf:%ld got_key:%d st:%d m:%d\n", switch_buffer_inuse(context->vpx_packet_buffer), context->got_key_frame, status, frame->m);
len = switch_buffer_inuse(context->vpx_packet_buffer);
//if (frame->m && (status != SWITCH_STATUS_SUCCESS || !len)) {
//switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "WTF????? %d %ld\n", status, len);
//}
if (status == SWITCH_STATUS_SUCCESS && frame->m && len) {
uint8_t *data;
int corrupted = 0;
vpx_codec_err_t err;
switch_buffer_peek_zerocopy(context->vpx_packet_buffer, (void *)&data);
context->dec_iter = NULL;
err = vpx_codec_decode(decoder, data, (unsigned int)len, NULL, 0);
if (err != VPX_CODEC_OK) {
switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(codec->session), (context->decoded_first_frame ? SWITCH_LOG_ERROR : VPX_SWITCH_LOG_LEVEL),
"VPX error decoding %" SWITCH_SIZE_T_FMT " bytes, [%d:%s:%s]\n",
len, err, vpx_codec_error(decoder), vpx_codec_error_detail(decoder));
if (err == VPX_CODEC_MEM_ERROR) {
switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(codec->session), SWITCH_LOG_WARNING, "VPX MEM ERROR, resetting decoder!\n");
context->need_decoder_reset = 1;
}
switch_goto_status(SWITCH_STATUS_RESTART, end);
} else {
if (!context->decoded_first_frame) context->decoded_first_frame = 1;
}
if (vpx_codec_control(decoder, VP8D_GET_FRAME_CORRUPTED, &corrupted) != VPX_CODEC_OK) {
switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(codec->session), SWITCH_LOG_WARNING, "VPX control error!\n");
switch_goto_status(SWITCH_STATUS_RESTART, end);
}
if (corrupted) {
frame->img = NULL;
#ifdef DEBUG_VP9
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "corrupted!!\n");
#endif
} else {
frame->img = (switch_image_t *) vpx_codec_get_frame(decoder, &context->dec_iter);
#ifdef DEBUG_VP9
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "decoded: %dx%d\n", frame->img->d_w, frame->img->d_h);
#endif
}
switch_buffer_zero(context->vpx_packet_buffer);
if (!frame->img) {
//context->need_decoder_reset = 1;
context->got_key_frame = 0;
context->got_start_frame = 0;
status = SWITCH_STATUS_RESTART;
}
}
end:
if (status == SWITCH_STATUS_RESTART) {
switch_buffer_zero(context->vpx_packet_buffer);
//context->need_decoder_reset = 1;
context->got_key_frame = 0;
context->got_start_frame = 0;
//switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_WARNING, "RESET VPX\n");
}
if (!frame->img || status == SWITCH_STATUS_RESTART) {
status = SWITCH_STATUS_MORE_DATA;
}
if (context->got_key_frame <= 0 || get_refresh) {
switch_set_flag(frame, SFF_WAIT_KEY_FRAME);
}
if (frame->img && (codec->flags & SWITCH_CODEC_FLAG_VIDEO_PATCHING)) {
switch_img_free(&context->patch_img);
switch_img_copy(frame->img, &context->patch_img);
frame->img = context->patch_img;
}
return status;
}
static switch_status_t switch_vpx_control(switch_codec_t *codec,
switch_codec_control_command_t cmd,
switch_codec_control_type_t ctype,
void *cmd_data,
switch_codec_control_type_t atype,
void *cmd_arg,
switch_codec_control_type_t *rtype,
void **ret_data)
{
vpx_context_t *context = (vpx_context_t *)codec->private_info;
switch(cmd) {
case SCC_VIDEO_RESET:
{
int mask = *((int *) cmd_data);
if (mask & 1) {
context->need_encoder_reset = 1;
}
if (mask & 2) {
context->need_decoder_reset = 1;
}
}
break;
case SCC_VIDEO_GEN_KEYFRAME:
context->need_key_frame = 1;
break;
case SCC_VIDEO_BANDWIDTH:
{
switch(ctype) {
case SCCT_INT:
context->change_bandwidth = *((int *) cmd_data);
break;
case SCCT_STRING:
{
char *bwv = (char *) cmd_data;
context->change_bandwidth = switch_parse_bandwidth_string(bwv);
}
break;
default:
break;
}
}
break;
case SCC_CODEC_SPECIFIC:
{
const char *command = (const char *)cmd_data;
if (ctype == SCCT_INT) {
} else if (ctype == SCCT_STRING && !zstr(command)) {
if (!strcasecmp(command, "VP8E_SET_CPUUSED")) {
vpx_codec_control(&context->encoder, VP8E_SET_CPUUSED, *(int *)cmd_arg);
} else if (!strcasecmp(command, "VP8E_SET_TOKEN_PARTITIONS")) {
vpx_codec_control(&context->encoder, VP8E_SET_TOKEN_PARTITIONS, *(int *)cmd_arg);
} else if (!strcasecmp(command, "VP8E_SET_NOISE_SENSITIVITY")) {
vpx_codec_control(&context->encoder, VP8E_SET_NOISE_SENSITIVITY, *(int *)cmd_arg);
}
}
}
break;
case SCC_DEBUG:
{
int32_t level = *((uint32_t *) cmd_data);
context->debug = level;
}
break;
default:
break;
}
return SWITCH_STATUS_SUCCESS;
}
static switch_status_t switch_vpx_destroy(switch_codec_t *codec)
{
vpx_context_t *context = (vpx_context_t *)codec->private_info;
if (context) {
switch_img_free(&context->patch_img);
if ((codec->flags & SWITCH_CODEC_FLAG_ENCODE)) {
vpx_codec_destroy(&context->encoder);
}
if ((codec->flags & SWITCH_CODEC_FLAG_DECODE)) {
vpx_codec_destroy(&context->decoder);
}
if (context->pic) {
vpx_img_free(context->pic);
context->pic = NULL;
}
if (context->vpx_packet_buffer) {
switch_buffer_destroy(&context->vpx_packet_buffer);
context->vpx_packet_buffer = NULL;
}
}
return SWITCH_STATUS_SUCCESS;
}
static void init_vp8(my_vpx_cfg_t *my_cfg)
{
vpx_codec_enc_config_default(vpx_codec_vp8_cx(), &my_cfg->enc_cfg, 0);
my_cfg->dec_cfg.threads = vpx_globals.dec_threads;
my_cfg->enc_cfg.g_threads = vpx_globals.enc_threads;
my_cfg->static_thresh = 100;
my_cfg->noise_sensitivity = 1;
my_cfg->cpuused = -6;
my_cfg->enc_cfg.g_profile = 2;
my_cfg->enc_cfg.g_timebase.num = 1;
my_cfg->enc_cfg.g_timebase.den = 1000;
my_cfg->enc_cfg.g_error_resilient = VPX_ERROR_RESILIENT_PARTITIONS;
my_cfg->enc_cfg.rc_resize_allowed = 1;
my_cfg->enc_cfg.rc_end_usage = VPX_CBR;
my_cfg->enc_cfg.rc_target_bitrate = switch_parse_bandwidth_string("1mb");
my_cfg->enc_cfg.rc_min_quantizer = 4;
my_cfg->enc_cfg.rc_max_quantizer = 63;
my_cfg->enc_cfg.rc_overshoot_pct = 50;
my_cfg->enc_cfg.rc_buf_sz = 5000;
my_cfg->enc_cfg.rc_buf_initial_sz = 1000;
my_cfg->enc_cfg.rc_buf_optimal_sz = 1000;
my_cfg->enc_cfg.kf_max_dist = 360;
}
static void init_vp9(my_vpx_cfg_t *my_cfg)
{
vpx_codec_enc_config_default(vpx_codec_vp9_cx(), &my_cfg->enc_cfg, 0);
my_cfg->dec_cfg.threads = vpx_globals.dec_threads;
my_cfg->enc_cfg.g_threads = vpx_globals.enc_threads;
my_cfg->static_thresh = 1000;
my_cfg->cpuused = -8;
my_cfg->enc_cfg.g_profile = 0;
my_cfg->enc_cfg.g_lag_in_frames = 0;
my_cfg->enc_cfg.g_timebase.den = 1000;
my_cfg->enc_cfg.g_error_resilient = VPX_ERROR_RESILIENT_PARTITIONS;
my_cfg->enc_cfg.rc_resize_allowed = 1;
my_cfg->enc_cfg.rc_end_usage = VPX_CBR;
my_cfg->enc_cfg.rc_target_bitrate = switch_parse_bandwidth_string("1mb");
my_cfg->enc_cfg.rc_min_quantizer = 4;
my_cfg->enc_cfg.rc_max_quantizer = 63;
my_cfg->enc_cfg.rc_overshoot_pct = 50;
my_cfg->enc_cfg.rc_buf_sz = 5000;
my_cfg->enc_cfg.rc_buf_initial_sz = 1000;
my_cfg->enc_cfg.rc_buf_optimal_sz = 1000;
my_cfg->enc_cfg.kf_max_dist = 360;
my_cfg->tune_content = VP9E_CONTENT_SCREEN;
}
static my_vpx_cfg_t *find_cfg_profile(const char *name, switch_bool_t reconfig)
{
int i;
for (i = 0; i < MAX_PROFILES; i++) {
if (!vpx_globals.profiles[i]) {
vpx_globals.profiles[i] = malloc(sizeof(my_vpx_cfg_t));
switch_assert(vpx_globals.profiles[i]);
memset(vpx_globals.profiles[i], 0, sizeof(my_vpx_cfg_t));
switch_set_string(vpx_globals.profiles[i]->name, name);
if (!strcmp(name, "vp9")) {
init_vp9(vpx_globals.profiles[i]);
} else {
init_vp8(vpx_globals.profiles[i]);
}
vpx_globals.profiles[i]->token_parts = switch_core_cpu_count() > 1 ? 3 : 0;
return vpx_globals.profiles[i];
}
if (!strcmp(name, vpx_globals.profiles[i]->name)) {
if (reconfig) {
memset(vpx_globals.profiles[i], 0, sizeof(my_vpx_cfg_t));
switch_set_string(vpx_globals.profiles[i]->name, name);
}
return vpx_globals.profiles[i];
}
}
return NULL;
}
#define UINTVAL(v) (v > 0 ? v : 0);
#define _VPX_CHECK_ERR(fmt, ...) switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_WARNING, "VPX config param \"%s\" -> \"%s\" value \"%s\" " fmt "\n", profile_name, name, value, __VA_ARGS__)
#define _VPX_CHECK_ERRDEF(var, fmt, ...) _VPX_CHECK_ERR(fmt ", leave default %d", __VA_ARGS__, var)
#define _VPX_CHECK_ERRDEF_INVL(var) _VPX_CHECK_ERRDEF(var, "%s", "is invalid")
#define _VPX_CHECK_ERRDEF_NOTAPPL(var) _VPX_CHECK_ERRDEF(var, "%s", "is not applicable")
#define _VPX_CHECK_MIN(var, val, min) do { int lval = val; if (lval < (min)) _VPX_CHECK_ERRDEF(var, "is lower than %d", min); else var = lval; } while(0)
#define _VPX_CHECK_MAX(var, val, max) do { int lval = val; if (lval > (max)) _VPX_CHECK_ERRDEF(var, "is larger than %d", max); else var = lval; } while(0)
#define _VPX_CHECK_MIN_MAX(var, val, min, max) do { int lval = val; if ((lval < (min)) || (lval > (max))) _VPX_CHECK_ERRDEF(var, "not in [%d..%d]", min, max); else var = lval; } while(0)
static void parse_profile(my_vpx_cfg_t *my_cfg, switch_xml_t profile, int codec_type)
{
switch_xml_t param = NULL;
const char *profile_name = profile->name;
vpx_codec_dec_cfg_t *dec_cfg = NULL;
vpx_codec_enc_cfg_t *enc_cfg = NULL;
dec_cfg = &my_cfg->dec_cfg;
enc_cfg = &my_cfg->enc_cfg;
for (param = switch_xml_child(profile, "param"); param; param = param->next) {
const char *name = switch_xml_attr(param, "name");
const char *value = switch_xml_attr(param, "value");
int val;
if (!enc_cfg || !dec_cfg) break;
if (zstr(name) || zstr(value)) continue;
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "%s: %s = %s\n", my_cfg->name, name, value);
val = atoi(value);
if (!strcmp(name, "dec-threads")) {
_VPX_CHECK_MIN(dec_cfg->threads, switch_parse_cpu_string(value), 1);
} else if (!strcmp(name, "enc-threads")) {
_VPX_CHECK_MIN(enc_cfg->g_threads, switch_parse_cpu_string(value), 1);
} else if (!strcmp(name, "g-profile")) {
_VPX_CHECK_MIN_MAX(enc_cfg->g_profile, val, 0, 3);
#if 0
} else if (!strcmp(name, "g-timebase")) {
int num = 0;
int den = 0;
char *slash = strchr(value, '/');
num = UINTVAL(val);
if (slash) {
slash++;
den = atoi(slash);
if (den < 0) den = 0;
}
if (num && den) {
enc_cfg->g_timebase.num = num;
enc_cfg->g_timebase.den = den;
}
#endif
} else if (!strcmp(name, "g-error-resilient")) {
char *s = strdup(value);
if (s) {
vpx_codec_er_flags_t res = 0;
int argc;
char *argv[10];
int i;
argc = switch_separate_string(s, '|', argv, (sizeof(argv) / sizeof(argv[0])));
for (i = 0; i < argc; i++) {
if (!strcasecmp(argv[i], "DEFAULT")) {
res |= VPX_ERROR_RESILIENT_DEFAULT;
} else if (!strcasecmp(argv[i], "PARTITIONS")) {
res |= VPX_ERROR_RESILIENT_PARTITIONS;
} else {
_VPX_CHECK_ERR("has invalid token \"%s\"", argv[i]);
}
}
free(s);
enc_cfg->g_error_resilient = res;
}
} else if (!strcmp(name, "g-pass")) {
if (!strcasecmp(value, "ONE_PASS")) {
enc_cfg->g_pass = VPX_RC_ONE_PASS;
} else if (!strcasecmp(value, "FIRST_PASS")) {
enc_cfg->g_pass = VPX_RC_FIRST_PASS;
} else if (!strcasecmp(value, "LAST_PASS")) {
enc_cfg->g_pass = VPX_RC_FIRST_PASS;
} else {
_VPX_CHECK_ERRDEF_INVL(enc_cfg->g_pass);
}
} else if (!strcmp(name, "g-lag-in-frames")) {
_VPX_CHECK_MIN_MAX(enc_cfg->g_lag_in_frames, val, 0, 25);
} else if (!strcmp(name, "rc_dropframe_thresh")) {
_VPX_CHECK_MIN_MAX(enc_cfg->rc_dropframe_thresh, val, 0, 100);
} else if (!strcmp(name, "rc-resize-allowed")) {
_VPX_CHECK_MIN_MAX(enc_cfg->rc_resize_allowed, val, 0, 1);
} else if (!strcmp(name, "rc-scaled-width")) {
_VPX_CHECK_MIN(enc_cfg->rc_scaled_width, val, 0);
} else if (!strcmp(name, "rc-scaled-height")) {
_VPX_CHECK_MIN(enc_cfg->rc_scaled_height, val, 0);
} else if (!strcmp(name, "rc-resize-up-thresh")) {
_VPX_CHECK_MIN_MAX(enc_cfg->rc_resize_up_thresh, val, 0, 100);
} else if (!strcmp(name, "rc-resize-down-thresh")) {
_VPX_CHECK_MIN_MAX(enc_cfg->rc_resize_down_thresh, val, 0, 100);
} else if (!strcmp(name, "rc-end-usage")) {
if (!strcasecmp(value, "VBR")) {
enc_cfg->rc_end_usage = VPX_VBR;
} else if (!strcasecmp(value, "CBR")) {
enc_cfg->rc_end_usage = VPX_CBR;
} else if (!strcasecmp(value, "CQ")) {
enc_cfg->rc_end_usage = VPX_CQ;
} else if (!strcasecmp(value, "Q")) {
enc_cfg->rc_end_usage = VPX_Q;
} else {
_VPX_CHECK_ERRDEF_INVL(enc_cfg->rc_end_usage);
}
} else if (!strcmp(name, "rc-target-bitrate")) {
_VPX_CHECK_MIN(enc_cfg->rc_target_bitrate, switch_parse_bandwidth_string(value), 1);
} else if (!strcmp(name, "rc-min-quantizer")) {
_VPX_CHECK_MIN_MAX(enc_cfg->rc_min_quantizer, val, 0, 63);
} else if (!strcmp(name, "rc-max-quantizer")) {
_VPX_CHECK_MIN_MAX(enc_cfg->rc_max_quantizer, val, 0, 63);
} else if (!strcmp(name, "rc-undershoot-pct")) {
if (codec_type == CODEC_TYPE_VP9) {
_VPX_CHECK_MIN_MAX(enc_cfg->rc_undershoot_pct, val, 0, 100);
} else {
_VPX_CHECK_MIN_MAX(enc_cfg->rc_undershoot_pct, val, 0, 1000);
}
} else if (!strcmp(name, "rc-overshoot-pct")) {
if (codec_type == CODEC_TYPE_VP9) {
_VPX_CHECK_MIN_MAX(enc_cfg->rc_overshoot_pct, val, 0, 100);
} else {
_VPX_CHECK_MIN_MAX(enc_cfg->rc_overshoot_pct, val, 0, 1000);
}
} else if (!strcmp(name, "rc-buf-sz")) {
_VPX_CHECK_MIN(enc_cfg->rc_buf_sz, val, 1);
} else if (!strcmp(name, "rc-buf-initial-sz")) {
_VPX_CHECK_MIN(enc_cfg->rc_buf_initial_sz, val, 1);
} else if (!strcmp(name, "rc-buf-optimal-sz")) {
_VPX_CHECK_MIN(enc_cfg->rc_buf_optimal_sz, val, 1);
} else if (!strcmp(name, "rc-2pass-vbr-bias-pct")) {
_VPX_CHECK_MIN_MAX(enc_cfg->rc_2pass_vbr_bias_pct, val, 0, 100);
} else if (!strcmp(name, "rc-2pass-vbr-minsection-pct")) {
_VPX_CHECK_MIN(enc_cfg->rc_2pass_vbr_minsection_pct, val, 1);
} else if (!strcmp(name, "rc-2pass-vbr-maxsection-pct")) {
_VPX_CHECK_MIN(enc_cfg->rc_2pass_vbr_maxsection_pct, val, 1);
} else if (!strcmp(name, "kf-mode")) {
if (!strcasecmp(value, "AUTO")) {
enc_cfg->kf_mode = VPX_KF_AUTO;
} else if (!strcasecmp(value, "DISABLED")) {
enc_cfg->kf_mode = VPX_KF_DISABLED;
} else {
_VPX_CHECK_ERRDEF_INVL(enc_cfg->kf_mode);
}
} else if (!strcmp(name, "kf-min-dist")) {
_VPX_CHECK_MIN(enc_cfg->kf_min_dist, val, 0);
} else if (!strcmp(name, "kf-max-dist")) {
_VPX_CHECK_MIN(enc_cfg->kf_max_dist, val, 0);
} else if (!strcmp(name, "ss-number-layers")) {
_VPX_CHECK_MIN_MAX(enc_cfg->ss_number_layers, val, 0, VPX_SS_MAX_LAYERS);
} else if (!strcmp(name, "ts-number-layers")) {
if (codec_type == CODEC_TYPE_VP8) {
_VPX_CHECK_MIN_MAX(enc_cfg->ts_number_layers, val, 0, VPX_SS_MAX_LAYERS);
} else if (codec_type == CODEC_TYPE_VP9) {
_VPX_CHECK_MIN_MAX(enc_cfg->ts_number_layers, val, enc_cfg->ts_number_layers, enc_cfg->ts_number_layers); // lock it
} else {
_VPX_CHECK_ERRDEF_NOTAPPL(enc_cfg->ts_number_layers);
}
} else if (!strcmp(name, "ts-periodicity")) {
if (codec_type == CODEC_TYPE_VP9) {
_VPX_CHECK_MIN_MAX(enc_cfg->ts_periodicity, val, enc_cfg->ts_periodicity, enc_cfg->ts_periodicity); // lock it
} else {
_VPX_CHECK_MIN_MAX(enc_cfg->ts_periodicity, val, 0, 16);
}
} else if (!strcmp(name, "temporal-layering-mode")) {
if (codec_type == CODEC_TYPE_VP9) {
_VPX_CHECK_MIN_MAX(enc_cfg->temporal_layering_mode, val, enc_cfg->temporal_layering_mode, enc_cfg->temporal_layering_mode); // lock it
} else {
_VPX_CHECK_MIN_MAX(enc_cfg->temporal_layering_mode, val, 0, 3);
}
} else if (!strcmp(name, "lossless")) {
if (codec_type == CODEC_TYPE_VP9) {
_VPX_CHECK_MIN_MAX(my_cfg->lossless, val, 0, 1);
} else {
_VPX_CHECK_ERRDEF_NOTAPPL(my_cfg->lossless);
}
} else if (!strcmp(name, "cpuused")) {
if (codec_type == CODEC_TYPE_VP8) {
_VPX_CHECK_MIN_MAX(my_cfg->cpuused, val, -16, 16);
} else {
_VPX_CHECK_MIN_MAX(my_cfg->cpuused, val, -8, 8);
}
} else if (!strcmp(name, "token-parts")) {
_VPX_CHECK_MIN_MAX(my_cfg->token_parts, switch_parse_cpu_string(value), VP8_ONE_TOKENPARTITION, VP8_EIGHT_TOKENPARTITION);
} else if (!strcmp(name, "static-thresh")) {
_VPX_CHECK_MIN(my_cfg->static_thresh, val, 0);
} else if (!strcmp(name, "noise-sensitivity")) {
if (codec_type == CODEC_TYPE_VP8) {
_VPX_CHECK_MIN_MAX(my_cfg->noise_sensitivity, val, 0, 6);
} else {
_VPX_CHECK_ERRDEF_NOTAPPL(my_cfg->noise_sensitivity);
}
} else if (!strcmp(name, "max-intra-bitrate-pct")) {
if (codec_type == CODEC_TYPE_VP8) {
_VPX_CHECK_MIN(my_cfg->max_intra_bitrate_pct, val, 0);
} else {
_VPX_CHECK_ERRDEF_NOTAPPL(my_cfg->max_intra_bitrate_pct);
}
} else if (!strcmp(name, "vp9e-tune-content")) {
if (codec_type == CODEC_TYPE_VP9) {
if (!strcasecmp(value, "DEFAULT")) {
my_cfg->tune_content = VP9E_CONTENT_DEFAULT;
} else if (!strcasecmp(value, "SCREEN")) {
my_cfg->tune_content = VP9E_CONTENT_SCREEN;
} else {
_VPX_CHECK_ERRDEF_INVL(my_cfg->tune_content);
}
} else {
_VPX_CHECK_ERRDEF_NOTAPPL(my_cfg->tune_content);
}
}
} // for param
}
static void parse_codecs(my_vpx_cfg_t *my_cfg, switch_xml_t codecs)
{
switch_xml_t codec = NULL;
if (!codecs) {
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "no codecs in %s\n", my_cfg->name);
return;
}
codec = switch_xml_child(codecs, "codec");
if (my_cfg->codecs) {
switch_event_destroy(&my_cfg->codecs);
}
switch_event_create(&my_cfg->codecs, SWITCH_EVENT_CLONE);
for (; codec; codec = codec->next) {
const char *name = switch_xml_attr(codec, "name");
const char *profile = switch_xml_attr(codec, "profile");
if (zstr(name) || zstr(profile)) continue;
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "codec: %s, profile: %s\n", name, profile);
switch_event_add_header_string(my_cfg->codecs, SWITCH_STACK_BOTTOM, name, profile);
}
}
static void load_config(void)
{
switch_xml_t cfg = NULL, xml = NULL;
my_vpx_cfg_t *my_cfg = NULL;
memset(&vpx_globals, 0, sizeof(vpx_globals));
vpx_globals.max_bitrate = switch_calc_bitrate(1920, 1080, 5, 60);
vpx_globals.rtp_slice_size = SLICE_SIZE;
vpx_globals.key_frame_min_freq = KEY_FRAME_MIN_FREQ;
xml = switch_xml_open_cfg("vpx.conf", &cfg, NULL);
if (xml) {
switch_xml_t settings = switch_xml_child(cfg, "settings");
switch_xml_t profiles = switch_xml_child(cfg, "profiles");
if (settings) {
switch_xml_t param;
for (param = switch_xml_child(settings, "param"); param; param = param->next) {
const char *profile_name = "settings"; // for _VPX_CHECK_*() macroses only
const char *name = switch_xml_attr(param, "name");
const char *value = switch_xml_attr(param, "value");
if (zstr(name) || zstr(value)) continue;
if (!strcmp(name, "debug")) {
vpx_globals.debug = atoi(value);
} else if (!strcmp(name, "max-bitrate")) {
_VPX_CHECK_MIN(vpx_globals.max_bitrate, switch_parse_bandwidth_string(value), 1);
} else if (!strcmp(name, "rtp-slice-size")) {
_VPX_CHECK_MIN_MAX(vpx_globals.rtp_slice_size, atoi(value), 500, 1500);
} else if (!strcmp(name, "key-frame-min-freq")) {
_VPX_CHECK_MIN_MAX(vpx_globals.key_frame_min_freq, atoi(value) * 1000, 10 * 1000, 3000 * 1000);
} else if (!strcmp(name, "dec-threads")) {
int val = switch_parse_cpu_string(value);
_VPX_CHECK_MIN(vpx_globals.dec_threads, val, 1);
} else if (!strcmp(name, "enc-threads")) {
int val = switch_parse_cpu_string(value);
_VPX_CHECK_MIN(vpx_globals.enc_threads, val, 1);
}
}
}
if (profiles) {
switch_xml_t profile = switch_xml_child(profiles, "profile");
for (; profile; profile = profile->next) {
switch_xml_t codecs = switch_xml_child(profile, "codecs");
const char *profile_name = switch_xml_attr(profile, "name");
my_vpx_cfg_t *my_cfg = NULL;
if (zstr(profile_name)) continue;
my_cfg = find_cfg_profile(profile_name, SWITCH_TRUE);
if (!my_cfg) continue;
parse_profile(my_cfg, profile, CODEC_TYPE(profile_name));
parse_codecs(my_cfg, codecs);
} // for profile
} // profiles
switch_xml_free(xml);
} // xml
if (vpx_globals.max_bitrate <= 0) {
vpx_globals.max_bitrate = switch_calc_bitrate(1920, 1080, 5, 60);
}
if (vpx_globals.rtp_slice_size < 500 || vpx_globals.rtp_slice_size > 1500) {
vpx_globals.rtp_slice_size = SLICE_SIZE;
}
if (vpx_globals.key_frame_min_freq < 10000 || vpx_globals.key_frame_min_freq > 3 * 1000000) {
vpx_globals.key_frame_min_freq = KEY_FRAME_MIN_FREQ;
}
my_cfg = find_cfg_profile("vp8", SWITCH_FALSE);
if (my_cfg) {
if (!my_cfg->enc_cfg.g_threads) my_cfg->enc_cfg.g_threads = 1;
if (!my_cfg->dec_cfg.threads) my_cfg->dec_cfg.threads = switch_parse_cpu_string("cpu/2/4");
}
my_cfg = find_cfg_profile("vp9", SWITCH_FALSE);
if (my_cfg) {
if (!my_cfg->enc_cfg.g_threads) my_cfg->enc_cfg.g_threads = 1;
if (!my_cfg->dec_cfg.threads) my_cfg->dec_cfg.threads = switch_parse_cpu_string("cpu/2/4");
}
}
#define VPX_API_SYNTAX "<reload|debug <on|off>>"
SWITCH_STANDARD_API(vpx_api_function)
{
if (session) {
return SWITCH_STATUS_FALSE;
}
if (zstr(cmd)) {
goto usage;
}
if (!strcasecmp(cmd, "reload")) {
const char *err;
my_vpx_cfg_t *my_cfg;
int i;
switch_xml_reload(&err);
stream->write_function(stream, "Reload XML [%s]\n", err);
load_config();
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, " %-26s = %d\n", "rtp-slice-size", vpx_globals.rtp_slice_size);
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, " %-26s = %d\n", "key-frame-min-freq", vpx_globals.key_frame_min_freq);
for (i = 0; i < MAX_PROFILES; i++) {
my_cfg = vpx_globals.profiles[i];
if (!my_cfg) break;
if (!strcmp(my_cfg->name, "vp8")) {
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "Codec: %s\n", vpx_codec_iface_name(vpx_codec_vp8_cx()));
} else if (!strcmp(my_cfg->name, "vp9")) {
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "Codec: %s\n", vpx_codec_iface_name(vpx_codec_vp9_cx()));
} else {
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "Codec: %s\n", my_cfg->name);
}
show_config(my_cfg, &my_cfg->enc_cfg);
}
stream->write_function(stream, "+OK\n");
} else if (!strcasecmp(cmd, "debug")) {
stream->write_function(stream, "+OK debug %s\n", vpx_globals.debug ? "on" : "off");
} else if (!strcasecmp(cmd, "debug on")) {
vpx_globals.debug = 1;
stream->write_function(stream, "+OK debug on\n");
} else if (!strcasecmp(cmd, "debug off")) {
vpx_globals.debug = 0;
stream->write_function(stream, "+OK debug off\n");
}
return SWITCH_STATUS_SUCCESS;
usage:
stream->write_function(stream, "USAGE: %s\n", VPX_API_SYNTAX);
return SWITCH_STATUS_SUCCESS;
}
SWITCH_MODULE_LOAD_FUNCTION(mod_vpx_load)
{
switch_codec_interface_t *codec_interface;
switch_api_interface_t *vpx_api_interface;
memset(&vpx_globals, 0, sizeof(struct vpx_globals));
load_config();
/* connect my internal structure to the blank pointer passed to me */
*module_interface = switch_loadable_module_create_module_interface(pool, modname);
SWITCH_ADD_CODEC(codec_interface, "VP8 Video");
switch_core_codec_add_video_implementation(pool, codec_interface, 99, "VP8", NULL,
switch_vpx_init, switch_vpx_encode, switch_vpx_decode, switch_vpx_control, switch_vpx_destroy);
SWITCH_ADD_CODEC(codec_interface, "VP9 Video");
switch_core_codec_add_video_implementation(pool, codec_interface, 99, "VP9", NULL,
switch_vpx_init, switch_vpx_encode, switch_vpx_decode, switch_vpx_control, switch_vpx_destroy);
SWITCH_ADD_API(vpx_api_interface, "vpx",
"VPX API", vpx_api_function, VPX_API_SYNTAX);
switch_console_set_complete("add vpx reload");
switch_console_set_complete("add vpx debug");
switch_console_set_complete("add vpx debug on");
switch_console_set_complete("add vpx debug off");
/* indicate that the module should continue to be loaded */
return SWITCH_STATUS_SUCCESS;
}
SWITCH_MODULE_SHUTDOWN_FUNCTION(mod_vpx_shutdown)
{
int i;
for (i = 0; i < MAX_PROFILES; i++) {
my_vpx_cfg_t *my_cfg = vpx_globals.profiles[i];
if (!my_cfg) break;
if (my_cfg->codecs) {
switch_event_destroy(&my_cfg->codecs);
}
free(my_cfg);
}
return SWITCH_STATUS_SUCCESS;
}
#endif
#endif
/* For Emacs:
* Local Variables:
* mode:c
* indent-tabs-mode:t
* tab-width:4
* c-basic-offset:4
* End:
* For VIM:
* vim:set softtabstop=4 shiftwidth=4 tabstop=4 noet:
*/
从头开始写一个模块
从头实现一个综合性模块,实现自己的Dialplan、自己的App以及自己的API。
示例:完整 的 自定义模块
:https://blog.csdn.net/xxm524/article/details/125964014
示例:一个完整 的 自定义模块
#include <switch.h>
SWITCH_MODULE_SHUTDOWN_FUNCTION(mod_myapp_shutdown);
SWITCH_MODULE_LOAD_FUNCTION(mod_myapp_load);
//模块定义,分别是模块加载、模块卸载
SWITCH_MODULE_DEFINITION(mod_myapp, mod_myapp_load, mod_myapp_shutdown, NULL);
SWITCH_STANDARD_APP(myapp_function);
//模块加载
SWITCH_MODULE_LOAD_FUNCTION(mod_myapp_load)
{
switch_application_interface_t *app_interface;
*module_interface = switch_loadable_module_create_module_interface(pool, modname);
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_WARNING, "myapp mod loaded.\n");
SWITCH_ADD_APP(app_interface, "myapp", "myapp", "myapp", myapp_function, "", SAF_NONE);
return SWITCH_STATUS_SUCCESS;
}
//模块卸载
SWITCH_MODULE_SHUTDOWN_FUNCTION(mod_myapp_shutdown)
{
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_WARNING, "myapp shutdown\n");
return SWITCH_STATUS_SUCCESS;
}
//myapp 执行函数
SWITCH_STANDARD_APP(myapp_function)
{
switch_status_t status;
if (session == NULL)
return;
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_WARNING, "myapp function start\n");
return;
}
可以看到自定义模块最少要实现3个标准接口:
- 模块加载 SWITCH_MODULE_LOAD_FUNCTION(mod_myapp_load)
- 模块卸载 SWITCH_MODULE_SHUTDOWN_FUNCTION(mod_myapp_shutdown)
所以,模块加载和卸载的完整定义如下:
- 模块加载:switch_status_t mod_myapp_load(switch_loadable_module_interface_t **module_interface, switch_memory_pool_t *pool)
- 模块卸载:switch_status_t mod_myapp_unload(void)
自定义模块"app"的创建
这里是关键,真正的可使用的app是这个接口实现的:
switch_application_interface_t *app_interface;
SWITCH_ADD_APP(app_interface, "myapp", "myapp", "myapp", myapp_function, "", SAF_NONE);
和模块的创建和卸载一样,app接口也是一个宏定义:
参数说明:
参数 | 解释 |
---|---|
app_int | app接口句柄 |
int_name` | app名称 |
short_descript | app短描述 |
long_descript | app长描述 |
funcptr | app回调函数 |
syntax_string | app格式字符串 |
app_flags | app标志 |
app 回调函数
SWITCH_STANDARD_APP(myapp_function)
{
switch_status_t status;
if (session == NULL)
return;
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_WARNING, "myapp function start\n");
return;
}
和上面一样先看下宏定义:#define SWITCH_STANDARD_APP(name) static void name (switch_core_session_t *session, const char *data)
参数说明:
app_int | app接口句柄 |
---|---|
session | 会话session |
data | app参数,每个app都可以传递参数 |
app 回调函数可以实现业务特定功能,如:
- 启动业务线程
- 添加media bug,获取通话语音流
- 实现业务循环,比如启动ASR/TSS
在拨号计划中加入自定义模块 app
将如下代码加入到 FreeSwitch目录conf/dialplan/default.xml
<extension name="myapp-diaplan">
<condition field="destination_number" expression="^123$">
<action application="answer"/>
<action application="myapp"/>
<action application="echo" data=""/>
<action application="hangup" data=""/>
</condition>
</extension>
说明:
destination_number
拨号计划表达式字符串,即拨打“123”号码,就可以进行condition拨号配置answer
接听
-myapp
这就是前面我们创建的自定义模块里面的app
-echo
回音app,由于myapp不是阻塞函数,所以要用echo程序以阻塞通话,达到通话不中断目的
-hangup
挂断
通话测试
启动FreeSwitch,使用Yate注册1000号码,先看下自定义模块myapp
加载输出:
拨号命令:originate user/1000 123
123 是前面拨号计划配置表达字符串。
最后输出:
一个简单的模块
模块名命名为 mod_book,因为脱离 FreeSWITCH 源代码的环境单独存放。所以可以在任何喜欢的目录下创建目录 mod_book,然后在里面创建 mod_book.c :
/* Book Example: Dialplan/App/API Author: Seven Du */
#include <switch.h>
SWITCH_MODULE_LOAD_FUNCTION(mod_book_load);
SWITCH_MODULE_DEFINITION(mod_book, mod_book_load, NULL, NULL);
SWITCH_STANDARD_DIALPLAN(book_dialplan_hunt)
{
switch_caller_extension_t* extension = NULL;
switch_channel_t* channel = switch_core_session_get_channel(session);
if (!caller_profile) {
caller_profile = switch_channel_get_caller_profile(channel);
}
switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session), SWITCH_LOG_INFO,
"Processing %s <%s>->%s in context %s\n",
caller_profile->caller_id_name, caller_profile->caller_id_number,
caller_profile->destination_number, caller_profile->context);
if ((extension = switch_caller_extension_new(session, "book", "book")) == 0) {
abort();
}
switch_caller_extension_add_application(session, extension,
"log", "INFO Hey, I'm in the book");
switch_caller_extension_add_application(session, extension,
"book", "FreeSWITCH - The Definitive Guide");
return extension;
}
SWITCH_STANDARD_APP(book_function)
{
const char* name;
if (zstr(data)) {
name = "No Name";
}
else {
name = data;
}
switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session),
SWITCH_LOG_INFO, "I'm a book, My name is: %s\n", name);
}
SWITCH_STANDARD_API(book_api_function)
{
const char* name;
if (zstr(cmd)) {
name = "No Name";
}
else {
name = cmd;
}
stream->write_function(stream, "I'm a book, My name is: %s\n", name);
return SWITCH_STATUS_SUCCESS;
}
SWITCH_MODULE_LOAD_FUNCTION(mod_book_load)
{
switch_dialplan_interface_t* dp_interface;
switch_application_interface_t* app_interface;
switch_api_interface_t* api_interface;
*module_interface = switch_loadable_module_create_module_interface(pool, modname);
// 向核心注册自定义的 Dialplan, 并设置回调函数 book_dialplan_hunt
SWITCH_ADD_DIALPLAN(dp_interface, "book", book_dialplan_hunt);
// 向核心注册自定义的 app, 并设置回调函数 book_function
SWITCH_ADD_APP(app_interface, "book", "book example", "book example", book_function, "<name>", SAF_SUPPORT_NOMEDIA);
// 向核心注册自定义的 api, 并设置回调函数 book_api_function
SWITCH_ADD_API(api_interface, "book", "book example", book_api_function, "[name]");
return SWITCH_STATUS_SUCCESS;
}
可以看出,这是最简单的模块。下面创建一个Makefile,内容如下(注意,这里的BASE变量引用的是一个绝对路径,它就是 FreeSWITCH 源代码的路径:
BASE=/usr/src/freeswitch
include $(BASE)/build/modmake.rules
然后,直接在当前目录执行 make install,该模块就安装好了。接着到 FreeSWITCH 控制台上加载该模块,从日志输出中可以看到我们的模块已经加载好了:
freeswitch> load mod_book
[CONSOLE] switch_loadable_module.c:1464 Successfully Loaded [mod_book]
添加 Dialplan
为了使模块更有用,需要增加一些功能。在此就实现一个自己的 Dialplan Interface,仍然叫它book。设置一个回调函数,并实现
SWITCH_ADD_DIALPLAN(dp_interface, "book", book_dialplan_hunt);
当 FreeSWITCH 执行到该回调函数时,说明有一路电话进入了路由(ROUTING)阶段,查找 Dialplan,返回对应的 Extension(或里面的Action),以后在后续 Channel 进入执行阶段时(EXECUTE),执行相关的 App。初始化一个extension,往该 extension 上增加了一个App。在此并没有进行任何查找,而是直接硬编码了一个 log 的App。最后,返回生成的extension。
再次执行 make install,并在FreeSWITCH控制台上使用 reload mod_book 重新加载模块。然后可以快速使用如下命令实验一下该Dialplan的效果:
freeswitch> originate user/1006 9999 book
9999 就是 Extension,book 就是Dialplan的名字;而Context由于省略了,默认就是default。因此,可以在日志中看到如下的“绿色的行”,并且也可以看到我们增加的App也如期执行了(输出了对应的日志)
[INFO] mod_book.c:17 Processing <0000000000>->9999 in context default
[INFO] mod_dptools.c:1595 Hey, I'm in the book
至此 Dialplan应该可以正常工作了。我们可以在XML Dialplan里转向它:
<action application="trasfer" data="9999 book default" />
也可以在Sofia Profile中(如internal)直接使用它,配置如下:
<param name="dialplan" value="book"/>
<param name="context" value="default"/>
当然这个Dialplan 功能还不是很强大,有待于进一步加强。
Dialplan 它就是负责找到一组 App,保证以后 FreeSWITCH 后续能执行这些 App。
添加 App
向核心注册自定义的 app, 并设置回调函数 book_function
SWITCH_ADD_APP(app_interface, "book", "book example", "book example", book_function, "<name>", SAF_SUPPORT_NOMEDIA);
实现该回调函数。该函数的参数将从 data 指针中传过来,如果为空就指定一个默认的名字;否则,就把传入的参数作为书的名字。
重新编译加载后,再执行一次,日志如下(可以看出 App已经执行了):
[INFO] mod_dptools.c:1595 Hey, I'm in the book
[INFO] mod_book.c:45 I'm a book, My name is: FreeSWITCH - The Definitive Guide
添加 API
写一个API也是很容易的。该API的名字还是叫 book。
将API注册到核心,并设置回调函数book_api_function:
SWITCH_ADD_API(api_interface, "book", "book example", book_api_function, "[name]");
重新编译并加载该模块后,在日志中看到如下的输出,它分别增加了以book为名称的Dialplan、Application 和 API Function。
[NOTICE] switch_loadable_module.c:227 Adding Dialplan 'book'
[NOTICE] switch_loadable_module.c:269 Adding Application 'book'
[NOTICE] switch_loadable_module.c:315 Adding API Function 'book'
然后,我们在 FreeSWITCH 控制台上就可以执行 book 命令了:
freeswitch> book
I'm a book, My name is: No Name
freeswitch> book FreeSWITCH - The Definitive Guide
I'm a book, My name is: FreeSWITCH - The Definitive Guide
使用 libfreeswitch
FreeSWITCH 不仅有完善的可加载模块支持,而且它的库 libfreeswitch 也可以被连接到其他系统中去,使得其他系统立即具有所有的 FreeSWITCH 中的功能。
其实,FreeSWITCH的源代码中,也自带一些例子,其中就包括使用libfreeswitch的例子。
自己写一个软交换机
尝试写一个软交换机。这里并不是一切都从头写,而是利用现有的 libfreeswitch 库,把它集成到我们的系统中来。
假设已有一个系统,该系统的功能非常强大。不过理解,把系统精简到了最简单的程序。
int main(int argc, char **argv) {
printf("Hello, MySWITCH is running ...\n");
return 0;
}
下面将 libfreeswitch 集成到系统中,对main函数做了一些改变,具体实现代码如下。
- 装入 switch.h头文件,以便能引用里面的函数;
- 设置一个flags标志,让它使用核心数据库;
- 定义一个console变量并设为true;
- 设置一些默认的全局参数;
- 初始化并加载模块;
- 进入控制台循环。
#include <switch.h>
int main(int argc, char** argv)
{
switch_core_flag_t flags = SCF_USE_SQL;
switch_bool_t console = SWITCH_TRUE;
const char *err = NULL;
printf("Hello, MySWITCH is running ...\n");
switch_core_set_globals();
switch_core_init_and_modload(flags, console, &err);
switch_core_runtime_loop(!console);
return 0;
}
通过这几行就实现了一个功能强大的交换机,它具有 FreeSWITCH 全部的功能。Makefile 编译:
FS = /usr/local/freeswitch
INC = -I$(FS)/include
LIB = -L$(FS)/lib
all: myswitch myrtp
myswitch: myswitch.c
gcc -o myswitch -ggdb $(INC) $(LIB) -lfreeswitch myswitch.c
myrtp: myrtp.c
gcc -o myrtp -ggdb $(INC) $(LIB) -lfreeswitch myrtp.c
clean:
rm -rf myswitch myswitch.so myswitch.dSYM myrtp myrtp.so myrtp.dSYM
上面的 Makefile 最开始的3行定义了3个变量。其中 INC 和 LIB 分别指定头文件和库文件的参数。使用 gcc 进行编译,输出可执行文件为 myswitch;为了调试方便,在编译时使用 -ggdb 加入符号表;- lfreeswitch 作用为连接 libfreeswitch.so 库文件(在Mac上为“freeswitch.dylib”),最后的 myswitch.c 为源文件名。执行 make 即可进行编译,编译完成后运行结果如下。由结果可以看到其为与 FreeSWITCH 中类似的日志,由此证明,一个强大的软交换机诞生了。
./myswitch
Hello, MySWITCH is running ...
...
使用 libfreeswitch 的库函数
程序的功能是从本地音频文件中读取数据,然后用PCMU进行编码,并通过RTP发送出去。将源文件命名为 myrtp.c。在main函数的最开始,定义并初始化了很多变量。
#include <switch.h>
int main(int argc, char *argv[])
{
switch_bool_t verbose = SWITCH_TRUE;
const char *err = NULL;
const char *fmtp = "";
int ptime = 20;
const char *input = NULL;
int channels = 1;
int rate = 8000;
switch_file_handle_t fh_input = { 0 };
switch_codec_t codec = { 0 };
char buf[2048];
switch_size_t len = sizeof(buf)/2;
switch_memory_pool_t *pool = NULL;
int blocksize;
switch_rtp_flag_t rtp_flags[SWITCH_RTP_FLAG_INVALID] = { 0 };
switch_frame_t read_frame = { 0 };
switch_frame_t write_frame = { 0 };
switch_rtp_t *rtp_session = NULL;
char *local_addr = "127.0.0.1";
char *remote_addr = "127.0.0.1";
switch_port_t local_port = 4444;
switch_port_t remote_port = 6666;
char *codec_string = "PCMU";
int payload_type = 0;
switch_status_t status;
if (argc < 2) goto usage;
input = argv[1];
if (switch_core_init(SCF_MINIMAL, verbose, &err) != SWITCH_STATUS_SUCCESS) {
fprintf(stderr, "Cannot init core [%s]\n", err);
goto end;
}
switch_core_set_globals();
switch_loadable_module_init(SWITCH_FALSE);
switch_loadable_module_load_module("", "CORE_SOFTTIMER_MODULE", SWITCH_TRUE, &err);
switch_loadable_module_load_module("", "CORE_PCM_MODULE", SWITCH_TRUE, &err);
if (switch_loadable_module_load_module((char *) SWITCH_GLOBAL_dirs.mod_dir,
(char *) "mod_sndfile", SWITCH_TRUE, &err) != SWITCH_STATUS_SUCCESS) {
fprintf(stderr, "Cannot init mod_sndfile [%s]\n", err);
goto end;
}
/* initialize a memory pool */
switch_core_new_memory_pool(&pool);
fprintf(stderr, "Opening file %s\n", input);
if (switch_core_file_open(&fh_input, input, channels, rate,
SWITCH_FILE_FLAG_READ | SWITCH_FILE_DATA_SHORT, NULL) != SWITCH_STATUS_SUCCESS) {
fprintf(stderr, "Couldn't open %s\n", input);
goto end;
}
if (switch_core_codec_init(&codec,
codec_string, fmtp, rate, ptime, channels,
SWITCH_CODEC_FLAG_ENCODE, NULL, pool) != SWITCH_STATUS_SUCCESS) {
fprintf(stderr, "Couldn't initialize codec for %s@%dh@%di\n", codec_string, rate, ptime);
goto end;
}
blocksize = len = (rate * ptime) / 1000;
switch_assert(sizeof(buf) >= len * 2);
fprintf(stderr, "Frame size is %d\n", blocksize);
switch_rtp_init(pool);
rtp_flags[SWITCH_RTP_FLAG_IO] = 1;
rtp_flags[SWITCH_RTP_FLAG_NOBLOCK] = 1;
rtp_flags[SWITCH_RTP_FLAG_DEBUG_RTP_WRITE] = 1;
rtp_flags[SWITCH_RTP_FLAG_USE_TIMER] = 1;
rtp_session = switch_rtp_new(local_addr, local_port,
remote_addr, remote_port,
payload_type, rate / (1000 / ptime), ptime * 1000,
rtp_flags, "soft", &err, pool);
if (!switch_rtp_ready(rtp_session)) {
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "Can't setup RTP session: [%s]\n", err);
goto end;
}
signal(SIGINT, NULL); /* allow break with Ctrl+C */
while (switch_core_file_read(&fh_input, buf, &len) ==
SWITCH_STATUS_SUCCESS) {
char encode_buf[2048];
uint32_t encoded_len = sizeof(buf);
uint32_t encoded_rate = rate;
unsigned int flags = 0;
if (switch_core_codec_encode(&codec, NULL, buf, len*2, rate,
encode_buf, &encoded_len, &encoded_rate, &flags) != SWITCH_STATUS_SUCCESS) {
fprintf(stderr, "Codec encoder error\n");
goto end;
}
len = encoded_len;
write_frame.data = encode_buf;
write_frame.datalen = len;
write_frame.buflen = len;
write_frame.rate= 8000;
write_frame.codec = &codec;
switch_rtp_write_frame(rtp_session, &write_frame);
status = switch_rtp_zerocopy_read_frame(rtp_session, &read_frame, 0);
if (status != SWITCH_STATUS_SUCCESS &&
status != SWITCH_STATUS_BREAK) {
goto end;
}
len = blocksize;
}
end:
switch_core_codec_destroy(&codec);
if (fh_input.file_interface) switch_core_file_close(&fh_input);
if (pool) switch_core_destroy_memory_pool(&pool);
switch_core_destroy();
return 0;
usage:
printf("Usage: %s input_file\n\n", argv[0]);
return 1;
}
- 第 36 行初始化 libfreeswitch 的内核,这里使用了SCF_MINIMAL选项,它将启动最小配置(因为这里不需要完整的FreeSWITCH)
- 第41行,设置一些全局的参数。
- 第42行初始化可加载模块的设置,这里使用了SWITCH_FALSE参数不让它自动加载模块,而是在后面手工加载。第43~44行就加载了两个核心的模块,它们都是在核心中实现的,CORE_SOFTTIMER_MODULE是一个时钟模块,用于定时;CORE_PCM_MODULE即PCM编解码模块,用于PCMU/PCMA编解码。由于我们这里只用到PCMU,因此,其他编解码模块就不需要加载了,否则需要手工加载对应的编解码模块
- 第47行加载了mod_sndfile 模块,读取音频文件
- 第53行初始化一个内存池。
- 第57行调用 switch_core_file_open打开输入的音频文件。其参数的值都在main函数的开始部分定义了。其中,channels为声道的数量,rate为采样率,
- 第63行初始化PCMU编解码codec。音频数据从文件中读出来后,都是以L16编码实现的线性编码,后面我们需要把它们转成PCMU。其中,rate为采样率、ptime为打包时间、channels为声道数。SWITCH_CODEC_FLAG_ENCODE标志说明我们只需要用到该编码器的编码器,即不需要用它解码。
- 可以根据采样率和打包时间算出一个数据包的长度len和需要的内存空间blocksize( 第78行),在此我们使用的PCMU编码的数据长度就是8000×20/1000=160,即每个RTP包有160个字节的数据,而原始读取来的数据由于是使用16位的存储,因此每个数据有2字节,所以实际原始数据的长度是160×2字节=320字节。
- 在第74行初始化系统RTP环境,然后初始化一个RTP的标志参数;
- 第76行表示它支持输入输出;
- 第77行表示采取非阻塞的方式发送;
- 第78行表示允许调试,它将在日志中打印调试信息;
- 第79行指定使用时钟,以更好地定时。
- 第81~84行初始化一个rtp_session,它的参数包含了RTP中必要的参数:本地IP地址和端口、远程IP地址和端口、负载类型(Payload Type)、采样率以及打包间隔等。另外,soft是一个定时器的名字,它是核心提供的定时器。
- libfreeswitch 默认会捕获各种信号,因此,我们在第99行将信号捕获回调设为空值,以后我们在调试的时候随时可以按“Ctrl+C”终止程序。
- 无限循环一直从文件中读取数据。我们每次只读取一帧(len)大小的数据,数据将读到buf缓冲区中(第93行)。然后,在第100~101行,将读到的数据进行编码,编码后的数据将存储到encode_buf中,数据长度可以在encodec_len中得到。
- 将数据编码成PCMU以后,我们就可以把它打包成一个数据帧(frame)。下面就是设置该数据帧的各种参数:第107行,设置帧数据的地址指向我们新编码的数据;第108行设置数据的长度;第109行设置缓冲区的长度;第110行设置采样率;第111行设置该数据帧的编解码;然后在第112行将该数据帧发送出去。
- 第114行,我们尝试从该rtp_session中读取一帧数据。其实,由于没人给我们发送数据,它将于20毫秒后超时,进入下一次循环。当然,在进入下一次循环前我们要重置len的值(第121行),以避免len的值可能在某些场合下更改为其他的值从而引起的错误。
- 如果在前面遇到错误,或前面读文件的循环退出(如,读到文件尾),则代码会执行到第124行。后面第125行会释放编解码器;第126行关掉文件接口;第127行释放内存池;并于第128行释放整个libfreeswitch的核心资源,程序结束。
将上述程序编译运行后,便可以看到它从本地的4444端口向外(6666端口)发送RTP数据了。由于数据长度为160字节,加上12字节的RTP包头,因而日志中显示的一共是172字节。部分日志如下:
$ ./myrtp /wav/test.wav
Opening file /wav/test.wav
Frame size is 160
[DEBUG] switch_rtp.c:3047 Starting timer [soft] 160 bytes per 20ms
W NoName b= 172 127.0.0.1:4444 127.0.0.1:6666 127.0.0.1:4444 pt=0 ts=160 m=1
W NoName b= 172 127.0.0.1:4444 127.0.0.1:6666 127.0.0.1:4444 pt=0 ts=320 m=0
W NoName b= 172 127.0.0.1:4444 127.0.0.1:6666 127.0.0.1:4444 pt=0 ts=480 m=0
如果在运行时,不想要FreeSWITCH打印日志,可以在第7行将verbose调为SWITCH_FALSE。也可以尝试修改其他参数。
调试 跟踪
在编译过程时,使用 "-ggdb" 选项,可以让 GCC 在生成的可执行程序中写入相关的符号表。设置 ulimit 环境,以允许系统产生内核转储文件(core dump)。
核转储文件的大小限制(unlimited即无限制,默认是0)设置:ulimit -c unlimited
设置完成后,可以使用“ulimit-a”命令验证。然后,重新运行程序,产生 core dump 文件。
在Linux系统上,core dump文件一般是在当前目录中产生
这样,当程序执行报错时,就可以使用 GDB 进行调试:gdb -core /cores/core.75886
$ gdb -core /cores/core.75886
GNU gdb 6.3.50-20050815 (Apple version gdb-1824)...
Reading symbols for shared libraries . done
#0 apr_palloc (pool=0x0, size=72) at apr_pools.c:603
603 if (pool->user_mutex) apr_thread_mutex_lock(pool->user_mutex);
可以看到调用堆栈中的第“#0”层有一个pool变量是空指针(0x0即NULL),这可能是我们遇到问题的原因。但上述的命令并没有列出详细的调用栈,apr_pools.c是APR底层的库,因而我们还是需要从更上层查找问题。接着输入“bt”命令(Back Trace),我们看到了详细的调用栈,原来在调堆栈的第“#2”层调用switch_rtp_init时pool指针就是空指针。
(gdb) bt
#0 apr_palloc (pool=0x0, size=72) at apr_pools.c:603
#1 0x0000000108db8b55 in apr_thread_mutex_create (mutex=0x108f146d0, flags=1, pool=0x0) at
thread_mutex.c:50
#2 0x0000000108d53373 in switch_rtp_init (pool=0x0) at switch_rtp.c:1328
#3 0x0000000108cce7e0 in main (argc=2, argv=0x7fff56f32760) at myrtp.c:74
查找源文件,发现该pool在任何地方都没有初始化,因而它是一个空指针。所以,增加了第53行的对内存池初始化的函数后,一切就都正常了。
这里错误比较明显,因而很容易发现。而在实际编程开发时,可能遇到一些更隐秘的错误,不容易直接从Back Trace中看到结果。那就要配合在代码中添加日志打印语句,或临时注释掉一些语句等手段来调试了。有时候,直接使GDB连接(attach)到正在运行的进程上,再进行调试、添加断点等,也是比较有效的调试方法。调试程序是一门细活,也需要有一定的耐心和经验。