freeswitch 权威指南 --- 实战篇

FreeSWITCH 常见问题:http://aizuda.com/article/1089440
FreeSWITCH案例大全:https://www.freeswitch.org.cn/books/case-study/
FreeSWITCH参考手册:https://www.freeswitch.org.cn/books/references/

1、freeswitch 调试、排错

sip "请求、响应" 状态

SIP Messages:http://www.sip.org.cn/wiki/index.php?title=SIP_Messages

状态码介于100和199之间的响应被看作是"1xx response",任何状态码介于 200和 299的响应看作是一个"2xx response"响应,以此类推。以第一个数字为划分类别,SIP/2.0支持了六个级别的状态响应码:

  • 1xx: Provisional – 临时应答。请求收到的响应码,表示是临时响应,会继续处理此请求;
            100 Trying 正在处理中
            180 Ringing 振铃
            181 call being forwarder 呼叫正在前向
            182 queue 排队
            183 session progress 会话进行
  • 2xx: Success – 会话成功。成功收到处理流程,理解,接受了处理流程;
            200 OK 会话成功
  • 3xx: Redirection – 重定向。需要进一步的流程处理来完成此请求;
        300 multiple 多重选择
        301 moved permanently 永久移动
        302 moved temporaily 临时移动
        305 use proxy 用户代理
        380 alternative service 替代服务
  • 4xx: Client Error – 请求失败。此请求中包含错误语法或不能满足服务器的要求;
            400 bad request 错误请求
            401unauthorized 未授权
            402 payment required 付费要求
            403 forbidden 禁止
            404 not found 未发现
            405 method no allowed 方法不允许
            406 not acceptable 不可接受
            407 proxy authentication required 代理需要认证
            408 request timeout 请求超时
            410 gone 离开
            413 request entity too large 请求实体太大
            414 request-url too long 请求URL太长
            415 unsupported media type 不支持的媒体类型
            416 unsupported url scheme 不支持的URL计划
            420 bad extension 不良扩展
            421 extension required 需要扩展
            423 interval too brief 间隔太短
            480 temporarily unavailable 临时失效
            481 call/transaction does not exist 呼叫/事务不存在
            482 loop detected 发现环路
            483 too many hops 跳数太多
            484 address incomplete 地址不完整
            485 ambiguous 不明朗
            486 busy here 这里忙
            487 request terminated 请求终止
            488 not acceptable here 这里请求不可接受
            491 request pending 未决请求
            493 undecipherable 不可辨识
  • 5xx: Server Error – 服务器失败。服务器端不能满足一个明确有效请求;
            500 server internal error 服务器内部错误
            501 not implemented 不可执行
            502 bad gateway 坏网关
            503 service unavailable 服务无效
            504 server time-out 服务器超时
            505 version not supported 版本不支持
            513 message too large 消息太大
  • 6xx: Global Failure – 全局性错误。任何服务器都不能满足此请求流程。
            600 busy everywhere 全忙
            603 decline 丢弃
            604 does not exist anywhere 不存在
            606 not acceptable 不可接受

SIP 应答消息状态码格式如下:类型 状态码 状态说明

1、临时应答(类型) 100(状态码) Trying 正在处理中(状态说明)

图 2

一般 排错 流程

  • 端口有没有被占用
  • 防火墙 (iptables、ufw 等) 有没有关闭
  • 打 9196 看是否能听到回声。9196 是默认的回音测试(echo)功能。使用 echo 一般用于简单测试双向语音的情况,即 FreeSWITCH 作为一个“回音壁”,会等待客户端将 RTP 包发过来然后原样返回。如果由于防火墙或某种原因导致 FreeSWITCH 收不到RTP包,则它也就没有数据可发了。
  • 在某些情况下,会只有单向的语音流,这种情况称为单通。测试单通的情况,可以用uuid_debug_media 对“收”和“发”分别测试。如果没法执行命令的情况下,也可以使用更直观的方法进行测试。例如,出于简单起见,先测试“发”。拨打默认的 9664,FreeSWITCH就会播放保持音乐,如果对方听不到的话,就需要在对方的电脑或话机上抓包看 RTP 包是否到达。如果没有到达,说明网络有问题,如果到达了还没有声音,那就可能是对方终端的问题,或扬声器或耳机的问题。然后,倒过来测试是否能 “收” 到 RTP流。先建一个 Dialplan 路由到record App 进行录音,录音完成后听一下录音文件是否完整即可判断是否正确收到RTP流。录音的 Dialplan 设置如下:<action application="record" data="/tmp/test.wav"/> 或者直接使用如下回拨命令进行录音:freeswitch> originate user/1002 &record(/tmp/test.wav)  如果一切正常,说明 其中一端没有问题了。再用同样的方法解决另一端的问题。保证两端都没有问题后,再尝试直接从 一端 呼叫 另一端,看 bridge 后是否还有其他问题。一步一步,直到问题解决。注意:没有媒体流或者媒体流不正常也可能是收到SIP消息错误引起的。
  • 呼叫其他号码,如果呼叫到达还是出错,查看是不是 Dialplan 设置的问题。如果呼叫没到达,并且确认防火墙没有问题后,可以通过抓包看有没有RTP流到达或从服务器发出。
  • 查看日志,日志中一般有挂机原因(Hangup Cause),比如 CALL_REJECTED 一般代表呼叫被拒绝,可能是认证的用户名或密码不对。在日志中,“警告”(WARNING)和“错误”(ERROR)是级别比较高的日志,“调试”(DEBUG)是级别比较低的日志,但其能显示更多的细节。在FreeSWITCH控制台上,可以通过 console loglevel debug 命令打开 DEBUG 级别的日志。
  • 如果呼叫需要经过一个网关出去,看看呼叫的后半段有没有问题。示例:originate sofia/gateway/gw1/Bob &echo。gw1为呼叫Bob使用的网关的名字。上述命令是阻塞的,因而在Bob对应的网关没有任何反应的时候(不回任何SIP消息),可能会阻塞较长时间。当FreeSWITCH控制台被阻塞时,将不能输入任何命令。如果要解决这个问题,可以打开另一个终端,用fs_cli连接FreeSWITCH,然后使用show channels找到对应的Channel, 并且用uuid_kill<channel UUID>将该Channel释放。当然,为了避免产生这种阻塞的情况,可以提前使用bgapi,如:bgapi originate sofia/gateway/gw1/Bob &echo  。bgapi 可以使 originate 在后台(新的线程中)执行,因而不会阻塞控制台。
  • 除了看日志以外,FreeSWITCH 也支持现场抓包,
    freeswitch> sofia profile internal siptrace on
    freeswitch> sofia profile external siptrace on
    可以开启不同Profile的SIP抓包。当a-leg和b-leg分别在不同的Profile上时可以分别来看。当然,初学者可能不知道实际的SIP消息应该走哪个Profile,这时可以使用以下命令打开所
    有 Profile 的抓包:
    freeswitch> sofia global siptrace on
    当然,也可以随时关闭抓包:
    freeswitch> sofia global siptrace off
  • 如果怀疑是 SIP 协议栈底层的问题 ( 可能是SIP兼容性的问题或FreeSWITCH本身的BUG),就要打开底层协议栈的调试信息。开启Sofia协议栈底层的调试器的方法是使用以下命令:
    freeswitch> sofia loglevel all 9
    freeswitch> sofia loglevel all 0    关闭所有底层的调试
    Sofia 层的日志信息会有很多,如果你比较熟悉的话(一般是开发者),也可以仅打开某一类的日志,如:freeswitch> sofia loglevel nua 9
    具体可以参考 sofia 命令的帮助信息,如直接输入sofia 命令不带任何参数会显示一个帮助,里面可以看到下面一行:sofia loglevel <all|default|tport|iptsec|nea|nta|nth_client|nth_server|nua|soa| sresolv|stun> [0-9]    开某一类型的日志

小技巧:有时候开启测试以及 SIP Trace 后可能会收到大量的消息,不停滚动屏幕会令人很难定位到有问题的消息。如果在调试时遇到这类问题时,一般会使用 fs_cli 连接到 FreeSWITCH,开启相应的DEBUG级别和跟踪,打个测试电话,然后使用 Ctrl+D快捷键或 /exit命令退出,这样就可以避免屏幕滚动。

uuid_debug_media

uuid_debug_media 可以调试媒体流。uuid_debug_media API 是一个很有用但常常被忽略的命令。首先,在 FreeSWITCH 控制台上可以使用 show channels 命令来找到当前通话的 Channel 的 UUID:freeswitch> show channels

然后使用 uuid_debug_media 调试媒体相关的信息并查看调试输出。命令的第一个参数是 Channel 的 UUID,接下来是调试的方向。方向有 read 和 write 两种(即读和写,也即收和发,都是相对于 FreeSWITCH 而言的,下同),也可以使用 both 参数表示双向都调试。最后一个参数是 on 或 off,分别表示打开或关闭调试。

命令:freeswitch> uuid_debug_media b4fae306-3f78-4d91-bcde-a2e95b0f9c1d both on

  • R(Read) 表示:接收
  • W(Write) 表示:发送
  • b=172 表示 收(发) 数据的字节数,它等于160字节的PCMU语音净荷(20ms)加上12个字节的RTP包头。
  • 25714 (本地端口) 和 49294(远端端口号)
  • pt=0 说明Payload Type(载荷类型)为0,即PCMU编码的语音数据。
  • ts为时间戳,可以看出它在收和发方向上分别是以160递增的。
  • m 为 RTP 的 Marker位,一般是0

如果能看到 R 和 W 说明正确收发媒体流。如果还是听不到声音,那可能需要更深入地检查了。

使用外部抓包工具

虽然在FreeSWITCH内部也可以抓包,但如果系统中有大量的呼叫,使用 FreeSWITCH 抓包就有些 "看不过来"。这时候,就要使用外部的抓包工具。外部抓包工具可以对IP包进行更全面的分析。

现有的大部分工具都是基于 libpcap 实现的,它们在不同的场景下各有优势。由于抓包是一种非常底层的操作,因此在大多数平台上通常需要用root或管理员用户来执行。大部分也都支持将抓包结果存储在本地PCAP格式的文件中,这种格式的文件后续可以使用其他工具(如 rtpplay 和Wireshark 等)进行重放和分析。

  • ngrep 主要的特点是类似于UNIX grep,在文本界面下实时看起来比较方便。
  • pcapsipdump 是专门针对 SIP 的抓包工具,在抓包的同时能将不同的通话分别存放到不同文件中。

在使用时,可根据需要选用合适的工具。

tcpdump

tcpdump 是经典的抓包工具。可以指定端口、网卡、输出格式之类的,并且可以保存在文件里。它支持使用 用户的ip地址过滤,用户的 ip 可以用 sofia staus 查出来。也可以抓取所有RTP包来进行分析,因为RTP包是有端口范围(16384~32768)的,所以根据端口范围tcpdump即可。

示例:只抓取 5060 端口上的SIP包:

tcpdump -nq -s 0 -A -vvv -i eth0 port 5060

其中,-n、-q表示不进行域名翻译及减少输出内容;-s 0表示不限制包长,即争取抓最大的长度;-A表示以ASCII方式输出,这样用眼看起来比较直观;-v表示显示的详细程度,“v”越多则越详细;-i表示使用指定的网卡;port 5060表示过滤器,这里我们只关心5060端口上的SIP包。

可以使用 -w 将结果写入文件中,如下列命令将结果写入/tmp/dump.pcap文件中:

tcpdump -nq -s 0 -i eth0 -w /tmp/dump.pcap port 5060

当还需要分析RTP流时,可以将port 5060简单改成udp,这样就可以抓取所有的UDP包:

tcpdump -nq -s 0 -i eth0 -w /tmp/dump.pcap udp

只抓某个用户的包,可以根据用户的IP地址来进行过滤。需要先找到用户实际的IP地址,通过如下命令可以找到用户1002的Contact地址:sofia status profile internal reg 1002 然后就可以只针对该用户抓包了(假设从Contact地址中知道它的IP和端口号分别是192.168.7.5和65272)

tcpdump -i eth0 -s 0 -A host 192.168.7.5 and port 65272

抓取所有的 RTP 包进行分析。在FreeSWITCH中,默认的端口号范围是16384~32768,

tcpdump -i eth0 -w /tmp/sip-rtp.pcap "udp and (port 5060 or port 5080 or portrange 16384- 32768)"

使用or(或)定义了多个过滤器参数,它分别代表抓5060、5080以及端口范围在16384至32768之间的包。

Wireshark

Wireshark 是最强大的抓包工具,能识别主流的各种通信协议,可以很方便地分析各种协议。

抓包:菜单栏 ---> 捕获 ---> 选项,勾选 WLAN网卡。如果不知道选择哪个网卡,可以查看网卡上使用的 IP (或者看那个网卡有流量,就选择哪个网卡),选择网卡后,点击 Start,启动抓包。

Wireshark 也可以过滤 VoIP 通话:菜单栏 ---> 电话 ---> VoIP。

示例:1234 呼叫 1000,接通后随便聊几句,然后挂断。

挂断后,点击 wireshark 中的停止抓包,然后查看 VoIP 呼叫:菜单栏 ---> 电话 ---> VoIP ,就可以看到所有的VoIP呼叫,如图:

点击 "播放流" 可以播放通话过程中的对话。

点击 "流 序列" 可以看到详细的 SIP 呼叫流程。

也可以用于分析 RTP包:菜单栏 ---> 电话 ---> RTP

除此之外,还可以使用 Graph(图标) 按钮生成某一路流相关的统计图,

如果图表显示比较规律,那一般说明是比较好的,如果生成的图表不是很规律,那就说明可能是网络上有丢包、延迟或抖动等。

现在可以按照上面流程试着打几个正常的或者不正常的电话,比较一下它们的异同。

显示 过滤

根据来源IP地址过滤,如:ip.src == 192.168.1.143

根据目的地址过滤,如:ip.dst == 192.168.1.143

多种条件组合:ip.src == 192.168.1.127 && ip.dst == 192.168.1.143 && udp.port == 5060

示例:ip.src == 192.168.1.127 && ip.dst == 192.168.1.143 && udp.port == 5060

如果 Wireshark 和 FreeSWITCH 在同一台机器上,可以直接使用 Wireshark 抓包:选择一个网卡后,单击 Start 开始抓包了。这时候,可以打一个视频电话(默认的9196号码),此时可以看到SIP包以及许多RTP的包。这时候就可以挂断电话了。电话挂断后,我们可以停止抓包,否则数据量大了以后看起来就比较麻烦。

另外,为了方便以后分析,也可以将抓到的数据包保存起来,一般保存为PCAP文件。

如果想抓远程Linux 服务器上的包,而一般远程机器没有或不打开图形界面,则可以在 Linux上安装 tcpdmp 或 Wireshark 的命令行版本 tshark。Wireshark 可以打开 tcpdump 或 pcapsipdump 抓下来的 pcap包。

分析 SIP包

当抓到很多包,在过滤栏中输入sip,便可以只显示所有的SIP包。可以通过鼠标定位到INVITE消息,

Wireshark 有专门分析VoIP通话的工具。在主菜单中,选择 Telephony ---> VoIP Calls 可以看到所有的 VoIP呼叫。选择一路呼叫并单击Flow按钮,就可以看到详细的SIP呼叫流程。可以打几个正常的或者不正常的电话,按照这个流程试一下。

Flow 按钮的旁边有一个 Play 按钮,单击这个按钮以后再单击 Decode 按钮可以将音视频流的数据进行解码,并可以播放音频流里的声音。注意,如果音频不是以 PCMU 或 PCMA 格式编码的,则不能播放。

分析 RTP包

在 Filter 栏中输入 RTP 就可以看到所有的RTP包。详细的分析可以使用 Telephony ---> RTP --->  Show All Streams 显示所有的 RTP 流,先选择一路音频流,通过单击 Find Reverse 按钮,可以找到并选择反向的流。然后单击 Analysis 就可以看到两路 RTP 流的详细分析,

有时候,可以为了跟踪一路通话的语音问题而从一个通话的中间开始抓包。这时候,由于缺少 SIP的上下文信息,所有抓包的内容显示为 UDP( 即如果没有 SIP包的话,Wireshark 就认不出这些UDP 包是 RTP 包了)。如果出现这样的情况,需要找到类似 RTP 的包。比方说,如果知道呼叫是 PCMU 编码的,那么它的包长可能是 214字节( IP+UDP包头42字节加上RTP包的172字节 )。找到这类包以后,可以点击某一个包,然后从右键菜单中选择 "Decode As...",进而选择RTP,就可以将同类的包用 RTP 包的格式进行解码了。

tshark

tshark 是 Wireshark 的命令行版,比 tcpdump 能识别更多的协议。使用方法与 tcpdump 类似,如可以直接在命令行上运行 tshark

其中 Capturing on eth0 表示从第一块网卡上抓包,它等价于以下命令:tshark -i eth0

将抓包结果写入pcap文件中:tshark -i eth0 -w /tmp/dump.pcap

直观地查看收发的SIP包:tshark host 192.168.1.9 and port 5060

ngrep

https://github.com/jpr5/ngrep

ngrep工具是 grep 命令的网络版,ngrep 用于抓包,并可以通过正则表达式,过滤、获取指定样式的数据包。能识别TCP、UDP和ICMP协议,理解 bpf 的过滤机制。使用 ngrep 抓包可以确定数据包是否已经到了某个服务模块,从而定位是哪个部分的问题。

从抓包结果中可以很清楚地看出数据包的传输方向以及SIP协议的具体内容。它可以提供类似于FreeSWITCH 内部的抓包输出的显示方式,显示比较直观。

常用的选项组合是:ngrep -p -q -W byline port 5060

  • -p 表示不使用混杂模式;
  • -q 表示使用安静模式,仅输出包头和相关包的载荷(如果有的话);
  • -W 表示选择一种显示输出方式,对于基本文本的协议来说,使用 byline 可以按换行符进行换行,显示比较直观;
  • port 5060 是一个过滤器,即只看关心的包。

除了 host、port 这样的过滤器外,ngrep 还支持针对内容的过滤。这在比较繁忙的服务器上(比如有大量SIP包的情况下)比较有用。下面的命令可以过滤仅含有 139xxxxxxxx(可能是个手机号码)字符串的内容:ngrep -p -q '139xxxxxxxx' -W byline port 5060

sngrep

sngrep 是控制台可用的实时可视化sip抓包工具。最好用的 sip 可视化抓包工具。如果是只针对sip信令的抓包,则 sngrep 更专业好用。

https://github.com/irontec/sngrep

https://wdd.js.org/opensips/tools/sngrep/

ngrep
  [-hVcivNqrD]
  [-IO pcap_dump]
  [-d dev]
  [-l limit]
  [-k keyfile]
  [-LH capture_url]
  [<match expression>]
  [<bpf filter>]

  • -h --help: 显示帮助信息
  • -V --version: 显示版本信息
  • -d --device: 指定抓包的网卡
  • -I --input: 从pacp文件中解析sip包
  • -O --output: 输出捕获的包到pacp文件中
  • -c --calls: 仅显示invite消息
  • -r --rtp: Capture RTP packets payload 捕获rtp包
  • -l --limit: 限制捕获对话的数量
  • -i --icase: 使大小写不敏感
  • -v --invert: 颠倒(不太明白)
  • -N --no-interface: Don’t display sngrep interface, just capture
  • -q --quiet: Don’t print captured dialogs in no interface mode
  • -D --dump-config: Print active configuration settings and exit
  • -f --config: Read configuration from file
  • -R --rotate: Rotate calls when capture limit have been reached.
  • -H --eep-send: Homer sipcapture url (udp:X.X.X.X:XXXX)
  • -L --eep-listen: Listen for encapsulated packets (udp:X.X.X.X:XXXX)
  • -k --keyfile: RSA private keyfile to decrypt captured packets

sngrep 有四个页面,每个页面都有一些不同的快捷键。

  1. 呼叫列表页面
  2. 呼叫流程页面
  3. 原始呼叫信息页面
  4. 信息对比页面

pcapsipdump

pcapsipdump 能将不同通话IP包存到不同的文件里,其中某一路通话所有的 SIP 和 RTP数据都存到同一个文件中。这样在同时有大量通话的时候抓包分析比较有用。

使用方法:pcapsipdump -i eth0 -d /tmp/sipdump/

将抓到的包文件都以PCAP格式存放到/tmp/sipdump这个目录中。后续可以使用其他工具针对个别的呼叫进行分析。这在服务器话务量比较高的时候比较有用。

2、originate 命令示例

在调试过程中 originate 命令是经常使用的一个命令,通过它几乎可以了解到系统中所有的知识。

尖括号 是必选参数,方括号 是可选参数。originate 命令用于 从FreeSWITCH中向 "外" 发起一个呼叫,这里的 "外" 就是 <call url> 指定的呼叫字符串(Dial String)

  • 第1个参数:呼叫字符串
  • 第2个参数:exten(可以认为是一个分机号)
  • 第3个参数:它是Dialplan的类型,如果不设置,默认就是XML。
  • 第4个参数:是 Dialplan 的 Context,对于 inline Dialplan,它会忽略 Context,而对于XML则是有效的

第1个参数:呼叫字符串

originate user/1000 &echo                   呼叫 1000 进行 回声测试

user/1000 就是呼叫字符串。它表示从本地的注册用户中查找该用户的联系地址。呼叫字符串的格式是 “类型/参数/参数”,其中第一部分是字符串的类型。随便输入一个错误的字符串会出现什么:

CHAN_NOT_IMPLEMENTED表示这种Channel的类型没有实现,因而这不是一个合法的字符串,无法创建这种类型的Channel并进行呼叫。

系统到底提供了多少 Channel 类型 ?一般来说,每种 Endpoint 都会提供相应的呼叫字符串,每一种呼叫字符串的类型都属于一个 Endpoint Interface,其中一些类型又类似于一种 “高级” 的呼叫字符串,如 user 和 group。这些呼叫字符串看起来像一个虚拟的 Endpoint Interface,它们最终会解析到底层的 sofia Endpoint Interface。

  • 逗号(,)分割 呼叫字符串:可以 "同振(同时震动)"。
    示例:originate user/1000,user/1001 &echo        同时拨打两通电话,同时呼叫1000和1001,两个话机都会振铃,哪个先接听则接通哪个,另一路会自动挂断,这种呼叫方式称为“同振”。
  • 竖线符号(|)分割 呼叫字符串,可以 "顺振(顺序震动)"。
    示例:originate user/1000|user/1001 &echo  一通 呼不通,就打第二通

第2个参数:exten (分机号)

originate 的第2个参数是一个 exten(可以认为是一个分机号),或者是一个“&”符号加上App。

  • 当是 &app 时,整个命令的作用是向外发起一个呼叫,建立一个Channel,对方接听后在本端执行一个App
  • 当是 exten(分机号) 时,FreeSWITCH 向外发起一个呼叫并等待对方接听后,FreeSWITCH就与对方建立了一个Channel,Channel 的远端当然是接听电话的用户。在本端与App的情形不同的是,它会将 Channel 转入 Dialplan 去路由,路由要查找的目的地就是 exten。也就是说,在用户接听后,这种情况跟用户呼入的处理是一样的,都是进入Dialplan进行路由,
    下面的两条命令基本上是等价的:
            originate user/1000 9196     后面接路由并走 dialplan,这个例子就是走 9196 的路由。
            originate user/1000 &echo

第3个参数:Dialplan (呼叫计划)

第三个参数是 Dialplan,如果不设置,默认就是 XML。

originate user/1000 echo inline        指定 inline 模式

等价

originate user/1000 &echo                   呼叫 1000 进行 回声测试

第4个参数:Dialplan 的 Context

对于 inline Dialplan,它会忽略Context,而对于XML则是有效的:

originate user/1000 1001 XML public      在1000接听后会进入public Dialplan查找路由。

    对于 xml 格式的 dialplan 可以指定 context

第 5、6 参数:修改 主叫 "名称、号码"

接下来的两个参数分别是

  • 主叫名称(cid_name,Caller ID Name)
  • 主叫号码(cid_number,Caller IDNumber)。

FreeSWITCH发起呼叫时默认使用的主叫号码是 0000000000。可以在发起呼叫时自行指定主叫号码,而不使用默认值,如:

sofia profile internal siptrace on       指定只对 "内部profile (UA)" 呼叫开启抓包

sofia profile external siptrace on      指定只对 "外部profile (UA)" 呼叫开启抓包

当a-leg和b-leg分别在不同的Profile上时可以分别来看,避免消息太多造成干扰。

如果不知道实际的 SIP消息走哪个Profile,可以使用以下命令打开所有Profile的抓包

sofia global siptrace on     打开所有Profile的抓包

使用 sofia global siptrace on 开启全局抓包后,

执行呼叫:originate user/1234 &echo XML default 'Seven Du' 7777   指定主叫号码为7777,呼叫1234

可以看到,会更改 From 字段和 Remote-Party-ID 字段。

sofia global siptrace off   关闭所有Profile的抓包

第7个参数:超时时间

设置超时时间,这个超时不是指对方不接听的超时,而是对方不回复 100 Trying 的超时时间,一般 表示 ip地址不可达。

sofia profile internal siptrace on
bgapi originate sofia/internal/1000@192.168.42.28 &echo XML default 'Seven Du' 7777 10

在 FreeSWITCH 发出 INVITE 消息后,由于没有收到100 Trying 回复,于是 在1秒后重发INVITE消息,如果还收不到则于2秒、4秒后重发,由 于我们指定了10秒超时,因此该呼叫于10秒后失败,返回 NO_ANSWER。默认的 originate 命令是阻塞的,如果执行上述命令,则无法输入其 他命令或取消该呼叫。一般用 bgapi 避免阻塞。

防止命令阻塞

originate 命令是阻塞的,因此如果执行上述命令,则无法输入其他命令或取消该呼叫。解决这一问题有两个办法:

  • 使用 bgapi,如 bgapi originate sofia/......
  • 开启另外一个 fs_cli 客户端,使用 show channels 找到该呼叫的 UUID,然后执行 uuid_kill;或者,直接执行 hupall 挂断所有通话。
  • 注意:如果在Event Socket方式下使用originate发起呼叫,一般要使用bgapi来避免阻塞,如:freeswitch> bgapi originate user/1000 &echo

使用 通道变量

通道变量可以影响呼叫行为。在 orignate 时也可以使用通道变量。

通道变量是加在呼叫字符串上的

通过使用通道变量,下列命令也能改变主号名称和号码:

freeswitch> originate {origination_caller_id_name='Seven Du',origination_caller_id_number=7777}user/1000 &echo

多个通道变量之间使用逗号隔开,有时候有的通道变量里会有逗号,可能造成冲突,因而下面的命令都达不到你想要的效果:

freeswitch> originate {absolute_codec_string=G729,PCMU}user/1000 &echo
freeswitch> originate {absolute_codec_string='G729,PCMU'}user/1000 &echo

上述命令的本意是在呼叫时在SDP里向对方提供 G729 和 PCMU 编码,但执行命令后却在日志中看到PCMU被丢掉了,按 F8 打开调试,在 SDP 中可以看到只有 G729

所以,需要对 Codec 字符串里的逗号进行转义,可以使用一个反斜杠来进行转义,如:

freeswitch> originate {absolute_codec_string=G729\,PCMU}user/1000 &echo

或者,使用 "^^" 进行转义,用别的符号代替逗号分隔符,如:

freeswitch> originate {absolute_codec_string=^^:G729:PCMU}user/1000 &echo

"^^" 后面跟冒号表示以后要用冒号代替逗号,FreeSWITCH 在遇到冒号时就会把它用逗号替换回来,这样就可以避免因为逗号冲突而导致错误。

有时候,这些通道变量参数可能是程序里自动生成的,因而计算何时该用逗号可能要多费一些代码,所以 FreeSWITCH 也支持将不同的变量写到不同的大括号中,如:

freeswitch> originate {var1=1}{var=2}{var3=3}user/1000 &echo

还有一类通道变量是在方括号中定义的,称为局部通道变量。它唯一作用于某一条腿。在先呼叫某一用户的办公分机时,如果超时,则通过网关gw1呼叫其手机号,如果再超时,则通过网关gw2呼叫:

freeswitch> originate {var1=1}[leg_timeout=10]user/1000|[leg_timeout=20]sofia/gateway/gw1/1380000000|[leg_timeout=20]sofia/gateway/gw2/1380000000

其中,leg_timeout 只应用于靠近它的那一条腿上,它用于定义呼叫超时,即等待对方返回媒体(如183或200)的超时时间。而var1=1是全局的,每条腿都会有这个变量。

通过使用局部的通道变量,就可以为不同的腿(不同的网关或不同的呼叫路径)定义不同的参数,如这里使用leg_timeout为不同的腿定义不同的超时时间。

early media (振铃或者彩铃)

振铃或者彩铃被称为 early media,一般对方返回这个就可以认为 originate 成功。

通常呼叫本地的IP话机或软电话,对方回的是180 Ring,同样情况下如果通过网关呼叫PSTN用户的话,则 PSTN 网络往往会返回 Early Media,也就是前期的振铃音或彩铃。PSTN 在呼叫失败时也会返回 Early Media,如 "您拨的电话忙,请稍后再拨……" 等。Early Media 一般是用183消息指示的。因而,一旦对方返回了183,FreeSWITCH 就认为 originate 成功了。

从这里可以看出,"在执行originate时,originate命令会一直等待对方接听才返回这种说法不是很准确",严格来说,originate 命令是在收到媒体指示就返回的,如收到 SIP 中的 183 或 200 消息。一般来说,软电话不会返回 183,因而该命令直到软电话接听(即收到200时)才返回,但在通过外部网关拨打 PSTN 电话的时候往往不是如此,PSTN 网络中大多数情况下会在电话应答前返回回铃音(即Eearly Media),然后相连的网关设备就会向FreeSWITCH回复带SDP的183消息,以建立媒体通路。

有时候,有些Early Media对我们没有意义,如我们在主动外呼的应用中(如电话自动催费)希望用户真正接听以后才对他放音,或进入IVR,这时怎么办?有一个参数可以帮我们做到,如下:

freeswitch> originate {ignore_early_media=true}sofia/gateway/gw/13800000000 &playback(/tmp/test.wav) 

通过指定 ignore_early_media 变量,FreeSWITCH 在发起呼叫时会忽略对方返回的 Early Media,直到等到收到正常的应答信号(如200 OK)才返回。

总结:originate 命令是在收到媒体指示就返回的(而非接听才返回),如收到SIP中的183或200消息,可以配置成 originate时忽略 early media,以保证用户真的接听了才往下一步走。

bridge 命令

bridge 的底层其实跟 originate 是同一个函数,区别只是在于参数不同,bridge 多传了一个现存session 的参数。

示例:使用 originate 命令发起一个呼叫,接通1000和1001两个号码:

freeswitch> originate user/1000 &bridge(user/1001)

originate 命令发起呼叫时,先建立一个Channel,然后呼叫1000,1000接听后,在该Channel上(它相当于a-leg)执行 bridge App,bridge 会再建立另一个Channel(即b-leg)来呼叫1001。此时,1000 和1001 这两个 Channel 在信令上建立了桥接关系。1001 接听后,bridge 会把它们的媒体也桥接起来,此时进入正常通话。

bridge 中的 Early Media

在 bridge 的时候,

  • 场景 1:如果 b-leg 返回 180,则由于 a-leg 已经接听( a-leg 已经处于 answered 状态,即处于正常通话状态),因此 FreeSWITCH 无法为 a-leg 发送 180 Ringing 消息,因而 a-leg 在 b-leg 振铃期间将听不到任何声音。
  • 场景 2:如果 b-leg 返回 SIP 的 183 Session Progress,则 FreeSWITCH 也会向 a-leg 发 183 Session Progress,因此可以将 b-leg 上收到的媒体发到 a-leg 上,a-leg 就听到了回铃音(或彩铃、呼叫失败提示音等)。

为了解决 场景 1 的问题,可以让FreeSWITCH作为一个中间人,为a-leg在这种情况下产生一个假的回铃音,实现方法是在 bridge 之前设置一个 transfer_ringback 变量。

freeswitch> originate {transfer_ringback=local_stream://moh}user/1000 &bridge(user/1001)

这时 a-leg 接听后,如果 b-leg 振铃,a-leg 将能听到假的回铃音。 这里假的回铃音是在收到 b-leg的180时开始播放的。

有时候,等待收到 b-leg 的180 Ringing也需要很长时间,因而另外一个参数 instant_ringback 可以让 bridge 立即播放回铃音:

freeswitch> originate {instant_ringback=true}{transfer_ringback=local_stream://moh}user/1000 &bridge(user/1001)

以上的参数(transfer_ringback,instant_ringback)都是用在a-leg已经接听的情况,即回呼的情况

bridge 中的主叫号码

主叫号码的显示(俗称来电显示)

使用 FreeSWITCH 发起呼叫,并更改 主叫号码(下面是将主叫号码设为7777):

freeswitch> originate {origination_caller_id_number=7777}user/1000 &echo

示例:使用 bridge 呼叫 b-leg :freeswitch> originate {originattion_caller_id_number=7777}user/1000 &bridge(user/1001)

由 FreeSWITCH 发现 a-leg 是一个回呼,当 a-leg 接听后,再作为主叫去呼叫 b-leg 时,会进行主叫号码翻转。在日志中我们就可以看到如下的行:

翻转后a-leg的主叫号码就变成了1000,它去桥接1001时,显示的主叫号码1000是正确的。

但是有的时候想改变1000这个主叫号码,比方说想把它变成8888,那么可以使用如下的方法(注意,这里不关心a-leg在接到电话时显示什么主叫号码,所以省略一些参数,命令行短了一些):freeswitch> originate user/1000 &bridge({origination_caller_id_number=8888}user/1001

与 originate 类似,bridge 的参数也是一个呼叫字符串,因而它也可以使用一样的通道普量以控制主叫号码的显示。

如果直接在 b-leg 上设置变量不方便,那么改变 b-leg 上显示的主叫号码还可以在a-leg上做文章。

freeswitch> originate {effective_caller_id_number=8888}user/1000 &bridge(user/1001)

结论:

  • effective_caller_id_number 变量设置在a-leg上,但影响b-leg的主叫号码显示(俗称来电显示)
  • origination_caller_id_number 可以设置到a-leg,也可以设置到 b-leg上,它将影响本 leg 的来电显示。

呼叫1000和1001两个号码,在1000上显示7777,在1001上显示8888。下面两种方法是等价的:

  • freeswitch> originate {originatioin_caller_id_number=7777}user/1000 &bridge({origination_caller_id_number=8888}user/1001)
  • freeswitch> originate {originatioin_caller_id_number=7777}{effective_caller_id_number=8888}user/1000 &bridge(user/1001)

呼叫 工作流程

分析一个呼叫在 FreeSWITCH 里面到底是怎么工作的。

假设用的是默认的配置,并从 1000 呼叫 1001。则把 1000 称为主叫,把 1001 称为 被叫。

1000 的 SIP 话机作为 UAC 会发送 INVITE 请求到 FreeSWITCH 的5060端口(图中的①),也就是到达 mod_sofia 的 internal 这个 Profile 所配置的UAS(见conf/sip_profiles/internal.xml)。

该 UAS 收到正确的 INVITE 后会返回 100响应码,表示我收到你的请求了,稍等片刻。该UAS 中对所有收到的 INVITE 都要进行鉴权(因为auth-calls=true)。它会先检查ACL(访问控制列表。一般用于IP鉴权)。默认ACL检查是不通过的,因此就会走到密码鉴权阶段,SIP 没有发明新的鉴权方式,而是使用与HTTP协议中一样的Digest Auth [1]进行鉴权。其特点就是密码并不在网络上传输,因而比较安全。一般是UAS回复401,然后UAC重新发送带鉴权信息的INVITE。UAS收到后,便将鉴权信息提交到上层的FreeSWITCH代码,FreeSWITCH就会到Directory(用户目录)中查找相应的用户(图中的②),找到conf/directory/default/1000.xml文件中配置的用户信息,并根据其中配置的密码信息进行鉴权。如果鉴权不通过则回送 403 Forbidden 等错误信息会话结束。如果鉴权通过FreeSWITCH 就取到了用户的信息,比较重要的是 user_context,在例子中它的值为default。

接下来电话进入路由(Routing)阶段,开始查找 Dialplan(图中的③)。由于该用户的 Context 是default,因此路由就从 default 这个 Dialplan 查起(由conf/dialplan/default.xml定义)。Context 的对应关系如图所示。

找到 1001 这个用户,并执行 bridge user/1001。这里 user/1001称为呼叫字符串,它会再次查找 Directory,找到 conf/directory/default/1001.xml 里配置的参数(图中的④)。由于这里1001 是被叫,因此它会进一步查找直到找到 1001 实际注册的位置。实际上,它实际的注册位置是用 dial-string 参数来表示的,由于所有用户的规则都一样,因此该参数被放到conf/directory/default.xml中,在该文件中可以看到如下设置:

其中,最关键的是 sofia_contact 这个 API调用,它会查找数据库,找到1001实际注册的Contact地址,并返回真正的呼叫字符串。如我机器上执行如下命令可以返回1001注册的Contact地址:

freeswitch> sofia_contact 1001@192.168.1.109
sofia/internal/sip:1001@192.168.1.109:51641

找到呼叫字符串后,FreeSWITCH又启动另外一个会话(图中的⑤)作为一个 UAC 给1001发送INVITE请求(图中的⑥),如果1001摘机,则1001向FreeSWITCH回送SIP 200 OK消息,FreeSWITCH 再向1000回SIP 200 OK,通话开始。

整个过程中,FreeSWITCH 是一个B2BUA,上面的过程建立了一对通话,其中有两个Channel。

在 FreeSWITCH 的默认配置中,external.xml 对应的 Profile 是不鉴权的,因此凡是送到 5080 端口的 INVITE 都不需要鉴权。

  • FreeSWITCH作为一个客户端,若要添加一个网关,则该网关会被放到sip_profiles/external/的XML文件中,它就会被包含到sip_profiles/external.xml中。它向其他的服务器注册时,其Contact地址就是IP:5080,如果有来话,对方的服务器就会把INVITE送到它的5080端口(跟上面我们呼叫1001时把INVITE送到51641端口是同样的道理)。
  • 大部分SIPUA允许你直接把 INVITE 送到任意一个端口,这一般用在中继方式对接的情况。

在 external.xml 中 auth-calls=false,所以,当它收到 INVITE 时就不会进行鉴权了,而是直接对来话进行路由。而路由需要一个 Context,这个 Context 从哪里来呢?仔细看一下external.xml,里面有这样一条:<param name="context" value="public"/> 所以路由就会查找 public 这个 Dialplan,而其中只定义了很少的 Extension,也就是说不经过认证的 INVITE 请求一般只能做比较少的事情,如只处理来话。

internal.xml 里的 context 值为什么也是 public ?

internal 这个Profile 中有 auth-calls=true 这一参数。对于一路来话而言,如果经过了认证,就会找到一个用户,然后就会使用用户目录中的 user_context,而不是使用这个 Profile 中的context。即 用户目录中的 user_context 比 Profile 中的 context 参数优先级要高

那么,这个 Profile 中的 context 什么时候用呢?

如果你正确配置了ACL的话,对有些IP地址发来的INVITE请求可以不鉴权,那么自然就不知道是哪个用户打来的电话,因此就无法找到对应的user_context,那就只能使用Profile提供的context了。当然,你可以根据需要把这个context改成任意值,只要你配置了相应的Dialplan进行路由就行了。

对于没有经过鉴权的呼叫,同样可以有主、被叫号码,只不过主叫号码是不可信的,对方可以指定显示任意号码。

总结:

主叫先发100 INVITE到freeswitch的5060内部端口,internal会回复100表示收到,然后是鉴权阶段,鉴权阶段会查找用户,校验密码。

然后电话到路由阶段,开始查找dialplan,根据用户的context(user_context字段)开始查找对应的dialplan。然后会找到 被叫用户,获取他的注册地址(使用 sofia_contact这个API 获取)。

找到被叫用户之后,freeswitch会给被叫用户发INVITE请求,如果被叫用户摘机,则给freeswitch发 200ok,freeswitch再给主叫发200ok,通话开始。

这里注意在5080端口的通话都没有鉴权步骤。

而路由的context来源于external.xml里的配置。

<param name="context" value="public"/>

如果是已经注册,有鉴权步骤的用户,那么以用户指定的context为准,如果没有鉴权步骤,freeswitch就不会去找注册用户,则以external.xml里配置的context为准。

值得一提的是,对于没有经过鉴权的呼叫,同样可以有主、被叫 号码,只不过主叫号码是不可信的,对方可以指定显示任意号码。

dialplan xml 解析

<extension name="incoming_call">
	<condition field="destination_number" expression="^1234$">
		<action application="answer" data=""/>
		<action application="sleep" data="1000"/>
		<action application="ivr" data="welcome"/>
	</condition>
</extension>

呼叫目标号码 "1234" 时,自动接听电话,并在1秒后播放欢迎语音(通过 "ivr" 应用程序实现)。就可以听到配置的IVR菜单了。注意,在实际应用中,为了能接受外部来的呼叫,可能要把这里的 1234 改成实际的 DID(Direct Inbound Dial)号码。

  • <extension>:定义一个扩展(extension),即一个电话呼入的处理逻辑。
  • name="incoming_call":扩展的名称,可能是用于标识并区分不同的扩展。
  • <condition>:定义一个条件,用于判断是否应该执行下面的一系列操作。
  • field="destination_number" expression="^1234$":设置条件的字段为 destination_number,并且要求其值为正则表达式 ^1234$ 匹配的内容。即如果用户拨打1234时,就会在 Dialplan 中路由到这里。
  • <action>:定义一个操作,即在满足条件时执行的动作。三个action,分别指定接下来要执行的动作。
  • application="answer" data="":执行 answer 应用程序,用于接听呼入电话。就是 执行answer对来话进行应答(必须先应答才能向对方播放声音);
  • application="sleep" data="1000":执行 sleep 应用程序,使当前线程暂停1000毫秒(1秒)。使用sleep暂停一秒,以防止由于声音通路没建立好(有时候,特别是当主叫与被叫之间的链路比较复杂时,声音链路的建立需要一个过程)而丢失声音;
  • application="ivr" data="welcome":执行 ivr 应用程序,并传递 welcome 作为数据。

3、FreeSWITCH 图形用户界面

图形用户界面就是通常所说的 GUI(Graphic User's Interface)。对于开发者和极客们来讲,使用命令行配置和手工修改配置文件是最理想的选择。但对于 FreeSWITCH 新手或维护人员来讲,可能更喜欢一个图形界面来帮助理解和使用 FreeSWITCH。

FreeSWITCH官方的 wiki 上列举了一些现有的 GUI 产品,有开源的也有商业的,它们各有特点,但到目前为止还没有一个占统治地位的GUI产品。

freeswitch 官方列举的 GUI:Freeswitch GUI | FreeSWITCH Documentation (signalwire.com)

github 上 freeswitch gui:https://github.com/search?q=freeswitch+gui&type=repositories

介绍几款典型的开源GUI产品

FusionPBX

FusionPBX 是使用 PHP 开发的 FreeSWITCH GUI。支持Linux、Windows、BSD、Mac OS X等多种操作系统,并支持PostgreSQL、MySQL、SQLite等数据库。它是通过将数据存储到数据库中,并修改本地的XML配置文件实现的。支持无限的分机、IVR、呼叫组、传真、会议、队列、呼叫前转、点击拨号,以及实时呼叫状态显示等诸多特性,使用起来比较方便。

更多的介绍请参考:http://www.fusionpbx.com

blue.box

blue.box是2600Hz团队开发的一款开源的GUI产品,使用PHP/MySQL技术开发。它最初的开发是基于著名的Asterisk的GUI FreePBX,号称是FreePBX V3。它也是使用修改本地XML配置文件并重新加载的方式工作。除支持普通的用户、分机、路由管理外,它还支持多语言和多租户。 由于2600Hz团队近几年一直把精力放在基于Erlang的云平台上,所以blue.box的开发一度中断。不过,在ClueCon 2013上,2600Hz团队又宣布了该项目的重生,新的开发计划将基于Python。更多的介绍请参考:https://2600hz.org/

github:https://github.com/2600hz

FreeSWITCH Portal

上述的各种 GUI 产品都或多或少依赖于 Apache/Nginx 等 Webserver、PHP 等开发语言及PostgreSQL/MySQL 等数据库,因此,配置 GUI 本身就需要一定的知识和基础。FreeSWITCH Portal 内置于 FreeSWITCH,提供了开箱即用(Out of the Box)功能,它仅依赖于mod_xml_rpc模块提供的内置于 FreeSWITCH 的 Webserver。该模块是默认编译的,但不加载。在 FreeSWITCH 控制台上执行命令加载模块:

freeswitch> load mod_xml_rpc

如果想让该模块随着FreeSWITCH启动而自动加载,可以在conf/autoload_configs/modules.xml中将模块的注释去掉,去掉注释后的配置如下:<load module="mod_xml_rpc"/>

使用浏览器打开 http://127.0.0.1:8080/portal

用户名:freeswitch

密码:works  (密码可以在conf/autoload_configs/xml_rpc.conf.xml中配置)

系统主菜单分为以下几个部分:

  • Users:显示所有用户。
  • Calls:显示当前的呼叫状态,如果选中“Auto Update”复选框,则表示它是实时更新的。
  • Channels:显示当前所有Channel的状态,它是实时更新的。
  • Show:调用系统的show命令显示很多FreeSWITCH内部的东西,如注册信息、已加载的模块、App、API等。

如果看到绿色的 “Socket Connected”,则表示Websocket连接正常。注意,目前Websocket是不认证的, 因此可能有安全性问题,如果你在生产系统中使用,请注意安全。

如果看到右上角红色的“Socket Disconnected”也不用担心,那是因为你的系统不支持Websocket,修改xml_rpc.conf.xml,将“enable-websocket”参数设为true就可以了。

<param name="enable-websocket" value="true"/>

如果你的系统不支持Websocket,你也不会损失太多功能。数据的更新会自动变成通过Ajax轮循方式获取。

另外,如果你是一个开发者的话,相信你比较关心系统的事件。在fs_cli中讲过订阅和查看事件的方法,但是好多人会让 FreeSWITCH 动辄数百行的“大”事件吓倒。在 FreeSWITCH Porta l里还留了一个“后门”,可以允许用户用更好的方法查看系统事件。

很多浏览器(如Chrome或FireFox)都具有网页调试功能(FireFox上有著名的 FireBug 插件)。这里以 Chome 为例来说明。

在Chrome浏览器页面上右击,单击最下方的“审查元素”(Inspect Elements),即可在页面下方打开调试窗口。在调试窗口中切换到Console页,输入:event("ALL");

即可订阅全部事件。如果这时候打个电话,就可以看到调试窗口中输出各种事件的名字,就知道打一个电话大体都会产生哪些事件了。也可以做一下“慢动作”,顺便观察不同时间产生的不同事件,如振铃时产生什么事件、接听时产生什么事件、挂机后又产生什么事件等。

另外,通过打开全局的调试事件的选项可以看到更详细的事件信息,

在调试窗口中输入:global_debug_evnet = true;

如果不想看详细事件了,则把上述变量改成false即可,或直接刷新页面。

当然,任何时候都可以通过api或bgapi函数来直接执行FreeSWITCH的API命令,如:

api("status");
api("sofia status");
bgapi("originate user/1000 &echo");

FreeSWITCH Portal致力于简单、实用、易用。About 页面上列出了将要完成(Todo)的功能

YouPBX

YouPBX (基于Django开发):https://github.com/JoneXiong/YouPBX

freeswitch-gui (基于PHP开发):https://github.com/muzi-long/freeswitch-gui

需要安装 php:PHP For Windows: Binaries and sources Releases

和 Composer (PHP 用来管理依赖(dependency)关系的工具):Composer 安装与使用 | 菜鸟教程

Composer 官网:Composer中文网 / Packagist中国全量镜像

4、FreeSWITCH 基本功能

批量创建用户

FreeSWITCH 的 conf/directory/default 目录下存放用户配置文件,每个用户对应一个 XML 文件,FreeSWITCH 默认提供了1000~1019 这20个用户,如果要手工创建另外一个用户,如 1020,只需以 1000.xml 为模板,将该文件中的内容复制到 1020.xml,然后把1020.xml 文件中的所有出现 1000 的地方全部替换成 1020 即可

除了手工的复制和替换外,还可以在 UNIX 系统上的 Shell 中使用下列命令完成:

sed -e "s/1000/1020/" 1000.xml > 1020.xml

其中 sed 是 UNIX 系统上经典的流文件编辑器,使用 sed 的“s”命令将1000.xml文件中所有出现1000的地方都替换成1020,然后将命令的输出重定向(大于号是Shell中的重定向操作符)到1020.xml文件中。

要创建 1020~1039这20个用户,具体的Shell命令如下:

for i in `seq 1020 1039`; do sed -e "s/1000/$i/" 1000.xml > $i.xml ; done

window 下通过 bat 脚本:

for /L %%i in (1020, 1 1039) do sed -e "s/1000/%%i" 1000.xml > %%i.xml

IVR(电话语音菜单

IVR(Interactive Voice Response,交互式语音响应)实际上就是 "电话语音菜单"。

FreeSWITCH 默认的配置已包含了一个功能齐全的例子。

随便拿起一个分机,拨 5000,就可以听到菜单提示了。

当然,默认的提示是英文的,大意是说欢迎来到 FreeSWITCH

  • 按 1 进入FreeSWITCH会议;
  • 按 2 进入回音(echo)程序(这时候可以听到自己的回音);
  • 按 3 会听到等待音乐(Music on Hold,MOH);
  • 按 4 转到FreeSWITCH开发者Brian West的SIP电话上;
  • 按 5 会听到一只猴子的尖叫;
  • 按 6 进入下级菜单;
  • 拨 9 重听;
  • 拨 1000~1019 之间的号码则会转到对应分机。

真正的菜单配置信息是放到一对 "<menus></menus>" 标签中,每一对 "<menus></menus>" 标签描述一个菜单。每个菜单有唯一的名字(name),以便在拨号计划(Dialplan)中引用(作为ivr App 的参数)。被装入的 XML 文件最外层一般都有一对 <include> 标签,以保证 XML 文档的完整性。

默认 IVR 简介

系统默认提供了一个名叫 demo_ivr 的菜单。最初的语音提示(greet-long 及 greet-short)是用Phrase 实现的。Phrase 是用 XML 定义的一些短语,虽然最终也是播放相关的声音文件,但在多语言系统中会更灵活。在此先不讨论Phrase,可以简单地认为它就是一个声音文件。其他各属性的含义都很直观,也不多介绍了。这里重点来看一下各菜单项。这些菜单项的配置如下:

可以看出,菜单选项大多都是根据用户按键使用 menu-exec-app 执行相应的 App。

  • 按 1,则会执行 bridge,连接到 FreeSWITCH 官方的电话会议平台;
  • 按 2,则执行 transfer,转移到 XML Dialplan 的 default Context 中的 9196 这个 extension 进行处理;
  • 按 3,则会转到9664(保持音乐);按4转到9191(ClueCon);
  • 按 5 转到enum Dialplan进行处理;
  • 按 6  使用 menu-sub 进入一个下级菜单。menu-sub 表示会执行一个下级菜单。其实下级菜单所有的配置项目与主菜单基本上都是一样的,不同的是,在下级菜单中(此处是demo_ivr_submenu),可以使用 menu-top 来返回上级菜单。默认的例子中下级菜单只包含一个到上级菜单的菜单项,它表示按“*”号键就可以返回主菜单。该菜单项的配置如下:<entry action="menu-top" digits="*"/>

使用 XML 方式配置 IVR 菜单的知识基本上只有上面介绍的这么多。不过,通过设置多级菜单,以及与 Dialplan 相配合,根据不同的情况进行跳转,可以实现一些相对复杂的功能。当然,如果这些还不够灵活和强大,后面也会讲到使用嵌入式脚本或 Event Socket 实现更灵活的菜单。

如果在试验过程中遇到非预期的情况,可以打开 FreeSWITCH 控制台,按 F8键 或 使用“console loglevel debug”命令将日志调到DEBUG级别,在打电话时便能看到详细的执行过程。根据日志中的信息结合自己的使用经验就应该能比较容易定位到问题了。

创建自定义菜单

首先创建一个 XML 配置文件 conf/ivr_menus/welcome.xml 内容如下:

<include>
    <menus>
        <menu name="welcome"
            greet-long="welcome.wav"
            greet-short="welcom_short.wav"
            invalid-sound="ivr/ivr-that_was_an_invalid_entry.wav"
            exit-sound="voicemail/vm-goodbye.wav"
            timeout="15000"
            max-failures="3"
            max-timeouts="3"
            inter-digit-timeout="2000"
            digit-len="4">
        <entry action="menu-exec-app" digits="0" param="transfer 1000 XML default"/>
        <entry action="menu-exec-app" digits="/^(10[01][0-9])$/" param="transfer $1 XML default"/>
        </menu>
    </menus>
</include>

在上述配置中,首先,指定菜单的名字(name)是 welcome,其他各项的含义如下:

  • greet-long:指定最开始的欢迎音,即最开始播放的“您好,欢迎致电某某公司,请直拨分机号,查号请拨0”的语音,该语音文件默认的位置应该是在/usr/local/freeswitch/sounds目录下。应该事先把声音文件录好(可以使用Windows系统自带的“录音机”或其他录音软件实现,后面我们还会讲到其他录音的方法),在本例子中,我们都使用相对路径,当然也可以使用绝对路径,如/root/ivr/welcome.wav)。另外,PSTN交换机都是使用PCM编码的,如果与PSTN对接的话,将声音文件存储为单声道、 8000Hz的格式,能获得较好的音质并占用较少的系统资源。当然,对于其他采样率的声音文件,FreeSWITCH也能自动转换,只是在Log中会有相关的采样率不匹配的警告,初学者可以暂时不必理会。
  • greet-short:该项指定一个简短的提示音。如果用户长时间没有按键,则应重新提示拨号,但重新提示应该简短,比如直接说“请直拨分机号,查号请拨0”,而不用再把公司的欢迎广告再重复播放一遍。所以,可以把这么一个声音文件录制到welcome_short.wav中。
  • invalid-sound:如果用户按错了键,则会使用该提示。如果使用“make sounds-install”命令安装了声音文件,则该文件应该是默认存在的。只是它是英文的,如果需要中文的提示,可以自己录一个,并修改这里的路径使其指向自己录制的声音文件。
  • exit-sound:指定最后菜单退出时(一般是超时没有按键)的声音,默认会提示“Good Bye”。
  • timeout:指定超时时间(毫秒),即多长时间没有收到按键就超时,播放其他提示音。
  • max-failures:为容忍用户按键错误的次数。如果用户的按键与下面配置的正则表达式不匹配(即没有找到相关的菜单项),就认为是错误的。
  • max-timeouts:即最大超时次数。
  • inter-digit-timeout:为两次按键的最大间隔(毫秒)。如用户拨分机号1001时,假设拨了10,等3秒,然后再按01,这时系统实际收到的号码为10(后面的01超时后没有收到),则会播放invalid-sound指定的声音文件以提示错误。
  • digit-len:说明菜单项的长度,即最大收号位数。在本例中,用户分机号长度为4位,因此我们使用4。

该菜单中有两个菜单项(Entry)

  • 第一个是在用户按 0(digits="0")时,通过 menu-exec-app 执行一个 App。在此处它执行 transfer,将来话转到 default Dialplan 中进行路由,并会最终转到分机1000。如果来电用户知道被叫的分机号,则可以直接拨分机号,而不用经过人工转接,以节约时间。
  • 第二个菜单项中的 digits 正则表达式 "/^(10[01][0-9])$/" 会匹配用户输入的1000~1019之间的分机,也是转到 default Dialplan 中进行路由,并最终转到对应的分机上。如果来电用户按其他按键,则由于找不到匹配的菜单项进而提示错误( invalid-sound 指定的声音),并提示用户重新输入。

以上菜单设定好后,在控制台中执行 reloadxml(或按F6键)使配置生效。

测试(呼叫 1001,接听后进入 ivr 菜单):originate user/1001 &ivr(welcome)  测试成功后,就可以配置 Dialplan 把用户来话转接到菜单,在 Dialplan 中加入一个 extension(注意,需要加到正确的 Dialplan Context 中,如果不确定加到哪个 Context 中,可以在 default 和 public 中都加上会比较保险):

<extension name="incoming_call">
	<condition field="destination_number" expression="^1234$">
		<action application="answer" data=""/>
		<action application="sleep" data="1000"/>
		<action application="ivr" data="welcome"/>
	</condition>
</extension>

呼叫目标号码 "1234" 时,自动接听电话,并在1秒后播放欢迎语音(通过 "ivr" 应用程序实现)。就可以听到配置的IVR菜单了。注意,在实际应用中,为了能接受外部来的呼叫,可能要把这里的 1234 改成实际的 DID(Direct Inbound Dial)号码。

  • <extension>:定义一个扩展(extension),即一个电话呼入的处理逻辑。
  • name="incoming_call":扩展的名称,可能是用于标识并区分不同的扩展。
  • <condition>:定义一个条件,用于判断是否应该执行下面的一系列操作。
  • field="destination_number" expression="^1234$":设置条件的字段为 destination_number,并且要求其值为正则表达式 ^1234$ 匹配的内容。即如果用户拨打1234时,就会在 Dialplan 中路由到这里。
  • <action>:定义一个操作,即在满足条件时执行的动作。三个action,分别指定接下来要执行的动作。
  • application="answer" data="":执行 answer 应用程序,用于接听呼入电话。就是 执行answer对来话进行应答(必须先应答才能向对方播放声音);
  • application="sleep" data="1000":执行 sleep 应用程序,使当前线程暂停1000毫秒(1秒)。使用sleep暂停一秒,以防止由于声音通路没建立好(有时候,特别是当主叫与被叫之间的链路比较复杂时,声音链路的建立需要一个过程)而丢失声音;
  • application="ivr" data="welcome":执行 ivr 应用程序,并传递 welcome 作为数据。

按时间进行路由

场景:

  • 在上班时间,路由到一个IVR,该IVR在报完欢迎语后,可以引导转到人工总机接电话;
  • 在下班后,来电就转到另外一个IVR,工作全部由电脑自动处理。

可以构造如下 Dialplan:

<extension name="time_based_ivr">
	<condition wday="2-6" hour="8:30-17:30">
		<action application="ivr" data="ivr_day"/>
		<anti-action application="ivr" data="ivr_night"/>
	</condition>
</extension>

可以看到,这里的测试条件(Condition)与以前的不同。以前大部分以 destination_number 为测试条件,而在这里,有两个测试条件,一个是wday,一个是hour,这两个测试条件是逻辑“与”的关系。其中,wday表示星期(星期日的值为“0”),一般上班的时间都是周一至周五,因而这里wday的值用“2~ 6”表示;同时,这里的hour表示小时,即工作日的8:30至17:30为上班时间。

在有呼叫到达后,如果系统时间在该条件定义的范围内,则执行在后续的action中定义的App。这里,只有一个action,它只是使用ivr App将呼叫转入白天应该播放的IVR(即ivr_day)。如果定义的条件不在这个范围内,FreeSWITCH就会执行anti-action指定的App,它也是执行ivr,不过这次它的参数是另一个IVR—— ivr_night。

但是上面的例子没有限制被叫号码,相当于任何被叫号码都会转到 IVR,而实际上只希望有人打我们的DID号码时才转入IVR,所以我们的配置一般会比上面的更复杂一些。

首先,写一个extension,它会根据不同的时间执行不同的动作,如:

<extension name="time_based_ivr" continue="true">
	<condition wday="2-6" hour="8:30-17:30">
		<action application="set" data="ivr=ivr_day" inline="true"/>
		<anti-action application="set" data="ivr=ivr_night" inline="true/>
	</condition>
</extension>

与前面的 extension 不同的是,此处的 extension 中使用了 “continue=true” 属性。保证在 Dialplan 解析的时候解析完此处会继续往下进行。如果当前的时间可以匹配这里的测试条件(工作日上班时间),便会执行set,设置ivr变量的值为ivr_day;否则,则将ivr变量的值设为ivr_night。

上面的action和anti-action都设置了inline属性,它的值为true表示该动作会立即生效,以便后续的Dialplan使用此处设置的 ivr 变量来进行条件判断。虽然后续并没有使用它来进行条件判断,但这里使用该参数仍然是一个好习惯。

总之,在路由阶段检查过这一段Dialplan后,变量ivr的值不是等于 ivr_day 就是等于 ivr_night,具体如何取决于来电的时间。另外,由于该 extension 的定义中还有“continue=true”参数,因而,Dialplan 的解析过程会继续往下执行。如果我们在它后面建立如下的Dialplan,它将通过被叫号码匹配到来话的DID,然后,执行answer进行应答,并执行ivr转到刚才我们设置的ivr变量指定的IVR。

<extension name="ivr">
	<condition field="destination_number" expression="^DID$">
		<action application="answer" data=""/>
		<action application="ivr" data="${ivr}"/>
	</condition>
</extension>

这样,其他的被叫号码可以忽略这里的时间检查及ivr变量照常进行路由,不受这里的时间条件的影响。当然,要完成完整的例子还需要配置实现 ivr_day 和 ivr_night 两个不同的IVR,

配置中文语音提示

FreeSWITCH 中默认的提示音都是英文的,在中文社区网友的帮助下,有了中文的语音包,虽然现在看来有些过时,但有总比没有好。这些语音包可以在下面的地址找到:http://wiki.freeswitch.org/wiki/Language_Files#Chinese。

FreeSWITCH 默认的声音文件放在 sounds 目录,不同语种及嗓音的文件以不同的目录分类存放。

基中,en/us表示语种,即美国英语;callie是个人名,即这些录音是由callie录的。下面的子目录分别是声音文件的分类,如 ascii 代表 ASCII 字符、digits 代表数字等。当然,使用 tree 命令可能看起来更直观一些。每个子目录中又都有 8000 这个子目录,它说明这些声音文件是以 8000Hz 的采样率存放的。

使用 sound_prefix

在 FreeSWITCH 的配置文件中,有一个 sound_prefix 变量用于定义声音文件的具体路径,它在vars.xml 中是这样定义的:

<X-PRE-PROCESS cmd="set" data="sound_prefix=$${sounds_dir}/en/us/callie"/>

所以,改变该变量的值也能改变大部分声音文件的参考位置。例如,可以将其指向我们中文的语音文件路径:

<X-PRE-PROCESS cmd="set" data="sound_prefix=$${sounds_dir}/zh/cn/link"/>

当然,也可以在 Dialplan 中针对每一个 Channel 进行改变,如:

<action application="set" data="sound_prefix=$${sounds_dir}/zh/cn/link"/>

另外,该变量也可以设置到用户目录中,当特定的用户拨打电话时就能使用该变量。如,我们在1002.xml中可以加入以下设置:

<variable name="sound_prefix" value="$${sounds_dir}/zh/cn/link"/>

然后设置如下测试 Dialplan:

<extension name="lang">
	<condition field="destination_number" expression="^1234$">
		<action application="answer"/>
		<action application="playback" data="digits/1.wav"/>
		<action application="playback" data="digits/2.wav"/>
		<action application="playback" data="digits/3.wav"/>
	</condition>
</extension>

当1002拨打1234时,便能听到 "一二三",但其他用户拨打时仍会听到英文的“one two three”。

使用 Phrase(短语) 框架

FreeSWITCH 中大部分的语音提示,如 IVR 和 Voicemail 等,为了支持多语言,都是使用 Phrase来实现的。为了屏蔽各种不同语言提示的差异性,FreeSWITCH 实现了Phrase(短语)框架。简单来说,通过使用它,可以将不同语言的日期、时间、货币及数字等以相同语法表示,并可以在必要时结合 TTS 实现更强大的语音提示。

在默认的 IVR 配置中就使用了Phrase。如在ivr_menus/demo_ivr.xml中,greet-long 的配置如下:

greet-long="phrase:demo_ivr_main_menu"

该 Phrase 是在 conf/lang/en/demo/demo_ivr.xml 中定义的一个宏,该宏定义的前几行代码如下:

<macro name="demo_ivr_main_menu" pause="100">
	<input pattern="(.*)">
		<match>
			<action function="play-file" data="ivr/ivr-welcome_to_freeswitch.wav"/>
			<action function="play-file" data="ivr/ivr-this_ivr_will_let_you_test_features.wav"/>
			<action function="play-file" data="ivr/ivr-you_may_exit_by_hanging_up.wav"/>

其中,name属性定义了该宏的名称,以便在其他地方引用;pause表示在每个action(下面讲到)之间暂停多长时间(毫秒),它匹配(match)指定的模式(pattern)后(我们这里不讨论match和pattern),便执行下面的动作(action)。本例中的动作都是一系列的play-file,即播放后面的data指定的声音文件。

这些声音文件的具体路径是在conf/lang/en/en.xml中定义的,实际上,demo_ivr.xml 也是由该文件使用 INCLUDE 装入的。en.xml 的部分内容如下:

<language name="en" say-module="en"
sound-prefix="$${sounds_dir}/en/us/callie"
tts-engine="cepstral" tts-voice="callie">
	<phrases>
		<macros>
			<X-PRE-PROCESS cmd="include" data="demo/*.xml"/>

可以看出,en.xml 定义了一种语言(language),该语言的名字(name)为en;该语言是由 say-module指定的en模块(在mod_say_en模块中实现)支持的;所有支持它的声音文件路径都由sound-prefix指定;如果用到TTS,后面还有tts-engine和tts-voice参数(我们将在11.7节研究TTS)。

然后,在该语言中,定义了一些phrase标签,在phrase中又定义了一些宏(macros),具体的宏都是由后续的XML配置文件定义的,如上面的IVR中使用的宏就是在demo_ivr.xml中定义的。

更多关于Phrase的知识我们就不多介绍了,有兴趣的读者不妨拨打9386试一下。在FreeSWITCH默认的配置中拨打9386将播放一些有趣的声音,这些声音的定义都是在funny_prompts这个Phrase中实现的,读者可以通过这里所学的知识及跟踪FreeSWITCH的日志看是否能找到它们。另外,我们将在11.8.3节再讲一个实际的Phrase Macro的例子。读者也可以参考http://wiki.freeswitch.org/wiki/Speech_Phrase_Management进行更深入的学习。

中文 Phrase 配置

下面以增加中文支持为例来看一下如何修改这些配置文件。

FreeSWITCH 默认没有中文的语音文件配置,因此需要自己添加。中文与英文的配置大同小异,将英文的复制一份,并在它的基础上进行修改。使用如下命令复制整个目录到 zh,复制完成后,中文配置文件的基本框架已经完成了,下面修改 zh.xml,将其中的name和say-module都修改为zh,并把sound-prefix修改为中文录音文件的路径,修改后的结果如下:

<language name="zh" say-module="zh"
sound-prefix="$${sounds_dir}/zh/cn/link"
tts-engine="mod_tts_commandline" tts-voice="Ting-Ting">

关于 tts-engine 和 tts-voice 两个参数先设置到这里。改好这些配置框架以后,就可以告诉FreeSWITCH 将新建的中文配置文件包括进来了。在conf/freeswitch.xml中,可以看到如下的行:<X-PRE-PROCESS cmd="include" data="lang/en/*.xml"/>

上面是装入英文配置文件的配置,只需要在该行后面加入如下的行让它也装入中文的配置:

<X-PRE-PROCESS cmd="include" data="lang/zh/*.xml"/>

重启 FreeSWITCH,中文语言的配置就基本准备好了。

Say:"说" 中文

另外,FreeSWITCH中的 Say 接口(Interface)可以通过一些预先录制的声音文件“说”出一些常用的词语组合,如日期、时间、货币、数字等。它相当于一种限制级的TTS实现(之所以说是限制级,就是因为它虽然非常灵活,但只能读特定范围内的词)。

Say接口支持各种语言,如果需要支持中文语言,就需要mod_say_zh模块。该模块默认是不被编译也不被加载的,因此我们需要先编译它。到 FreeSWITCH 的源代码目录中,执行如下命令即可编译安装:make mod_say_zh-install

然后在 FreeSWITCH 控制台上加载该模块:freeswitch> load mod_say_zh

为了让FreeSWITCH在启动时能自动加载该模块,需要在conf/autoload_configs/modules中将行 <load module="mod_say_zh"/> 的注释去掉,以让 FreeSWITCH 自动加载该模块。

使用中文语音提示

中文支持准备就绪后,为了能播放中文提示,还需要在Dialplan中指定language或(和)default_language通道变量。比如,我们修改默认的5000对应的Dialplan,加入以下配置:<action application="set" data="language=zh"/>

完整的 Dialplan 如下:

<extension name="ivr_demo">
	<condition field="destination_number" expression="^5000$">
		<action application="set" data="language=zh"/>
		<action application="answer"/>
		<action application="sleep" data="2000"/>
		<action application="ivr" data="demo_ivr"/>
	</condition>
</extension>

另外,该变量也可以加到用户目录中。比如,我们可以在 1002 的用户配置文件1002.xml的variables标签中添加如下配置,让该语言仅对1002用户生效:

<variable name="language" value="zh"/>
<variable name="default_language" value="zh"/>

也可以将上述配置添加到 conf/directory/default.xml 中(也就是添加到 variables 标签中),以使该配置对本域中的所有用户都生效。

总之,不管使用哪种配置,拿起对应的分机拨打5000,就应该能听到中文的语音提示了。

录音 record 

在实际使用过程中,经常需要对通话进行录音。FreeSWITCH支持多种录音方式,也支持多种格式的录音文件。

单腿 record 录音 (阻塞 录音)

在创建 IVR 或比较复杂的呼叫流程时,经常需要一些录音文件,除了使用 Windows 录音机或其他软件外,还可以直接通过 FreeSWITCH 来实现。

执行命令:originate user/1000 &record(d:\welcome.wav)

呼叫1000,1000接听后即可以直接讲话并录音。由于这种录音方式仅涉及一条腿(leg,即一个Channel),因而称为 单腿录音。

另外,也可以在 Dialplan 中录音(与上述命令是等价的):

<extension name="record">
	<condition field="destination_number" expression="^rec(.*)$">
		<action application="answer"/>
		<action application="playback" data="tone_stream://%(100,1000,800)"/>
		<action application="record" data="/tmp/$1.wav"/>
	</condition>
</extension>

设置 Dialplan,呼叫一个 rec 开头的号码,如 recwelcome,就可以开始录音并将录音文件保存到 /tmp/welcome.wav 中,同样也可以通过拨打其他的号码,录不同名称的声音文件。另外,为了在录音前给个提示,可以在 record 前先用 playback 播放一段提示音,如“请在嘀声后开始录音”。在本例中,这里仅用 playback 播放了一个“嘀”声,该声音是用 tone_stream 产生的。后面会讲到 tone_stream。有的软电话可以呼叫含有字母的被叫号码。如果不支持呼叫含有字母的号码,也可以把这里的 rec 换成其他的数字,如 123。然后,呼叫 1231000 就可以将录音存到 /tmp/1000.wav。在这种录音方式中,由于只有一个 Channel,所以录音文件是单声道的。我们可以直接用下面的方式使用 playback 来测试播放刚才的录音:

播放 录音

freeswitch> originate user/1000 &playback(/tmp/welcome.wav)

命令呼叫 1000,1000 接听后就可以听到刚刚录制的声音了。

对两条腿的通话进行录音

一路正常的通话通常由两个Channel组成,两个Channel使用一个相同的channeL_uuid。而对于每个Channel 都可以单独录音。在一个Channel中语音有两个方向。对于SIP客户端或话机而言,两个方向分别为“说”和“听”,对FreeSWITCH而言,则分别为“读”(read,r)和“写”(write,w)。

以下API命令可以对正在进行的通话录音(需要事先知道相关Channel的UUID):

uuid_record <channel_uuid> start /tmp/record.wav

如果决定不录音了,则可以使用以下命令停止录音:

uuid_record <channeL_uuid> stop /tmp/record.wav

该录音文件会包含两个声道,以后要使用 playback 播放这种录音时,会在日志中看到如下的警告信息:freeswitch> originate user/1000 &playback(/tmp/record.wav) 这条信息表示,该文件有两个声道,但playback仅是对一个Channel而言的,它仅支持一个声道。因而,FreeSWITCH会先将两个声道混音,变成一个声道后再播放。该警告一般是无害的。只是,由于在播放时会进行混音,会多占用一些CPU,因而在高并发的场合,可以使用一些工具事先将声音文件混为一个声道。如我们可以使用sox命令行进行混音:sox record.wav -c 1 record-1.wav  其中,“-c”指定Channel的数量。这里用的是“-c 1”,就是说将原来的声音文件混成一个声道。可以使用以下ls命令,从中看到混音后的声音文件大小比原来的大约小了一半(从136364到了68204):ls -l /tmp/record*.wav

另外,也可以使用sox附带的play命令在操作系统命令行上播放声音文件,并查看一些详细的信息,如(注意,该命令在声卡驱动不正常时是不能使用的):play /tmp/record.wav

可以看到,上述输出中声音编码(Encoding)是有符号的(Signed)PCM数据;有两个16bit的声道(Channel);采样率(Samplerate)是8000Hz,文件的长度是4.26秒。读者也可以对比一下它与上面单声道录音文件的区别。

实际上,录音使用的是FreeSWITCH中的Media Bug功能,声音媒体数据通过一个“三通”流到录音文件中。FreeSWITCH支持同时创建多个Media Bug,因而可以同时录制多个声音文件,这在需要截取不同时间段的录音时非常有用,如可以在不同的时间点分别执行下列命令:

uuid_record <channel_uuid> start /tmp/1.wav
uuid_record <channel_uuid> start /tmp/2.wav
uuid_record <channel_uuid> start /tmp/3.wav

可以随时用对应的stop指令停止某个录音,也可以使用特殊的文件名“all”同时停止所有录音,如:

uuid_record <channel_uuid> stop all

当然,如果在录音过程中电话挂断了,所有录音也就自动停止了。

record_session (非阻塞 录音)

另外,在Dialplan中,也可以通过record_session达到类似的效果,如:

<extension name="record">
	<condition field="destination_number" expression="^(100[0-9])$">
		<action application="record_session" data="/tmp/record-$1.wav"/>
		<action application="bridge" data="user/$1"/>
	</condition>
</extension>

其中,在执行 bridge 前使用 record_session 开始对当前的 Channel 进行录音。与 uuid_record 不同的是,record_sesison 是一个App,而前者是一个API。相同的是两者在执行时都会为当前的Channel添加一个Media Bug,因而可以实时录音。另外,与record比起来,

  • record_session 是非阻塞的,它可以作用于单腿的呼叫也可以用于桥接(Bridge)的呼叫;
  • record 是阻塞的,它会阻塞直到录音完成,即只能录单腿的呼叫。

立体声

通过使用相关参数,可以录制立体声,即可以将通话的两个人的声音分别存在两个声道里。该特性非常有用。笔者最早在做网络远程一对一教学时,就曾使用了该技术。通过将通话中的老师和学生的声音录在不同的声道里,可以在事后很方便地检查出他们对话的内容,并有针对性地提出建议以提高教学质量。另外,我们也可以很方便地将不同的声道分离到不同的声音文件中,进行更高级的自动声音识别和分析。

通过事先将 RECORD_STEREO 通道变量设置为 true 可以在录音时直接录成立体声。

在 Dialplan 中使用 record_session 时的设置如下:

<action application="set" data="RECORD_STEREO=true"/>

<action application="record_session" data="/tmp/record-$1.wav"/>

当然,也可以在使用 uuid_record 录音前使用 uuid_setvar 设定该通道变量,从而录制成立体声的声音文件。这种方式与上述方式等价,具体命令如下:

freeswitch> uuid_setvar <channel_uuid> RECORD_STEREO true

freeswitch> uuid_record <channel_uuid> start /tmp/record.wav

录音相关的通道变量

除了上面讲到的 RECORD_STEREO外,还有一些与录音相关的通道变量,下面简单介绍一下(注意这些变量是区分大小写的):

  • RECORD_HANGUP_ON_ERROR:默认值为false,如果设置为true则会在录音失败时(如文件不可写入、格式错误等)挂断电话。
  • RECORD_WRITE_ONLY:只录“写”方向的录音。这里的方向是相对FreeSWITCH而言的,即FreeSWITCH发出,也就是对端能够听到的声音。
  • RECORD_READ_ONLY:只录“读”方向的录音,即FreeSWITCH“听”到的录音。
  • RECORD_STEREO:立体声录音。
  • RECORD_STEREO_SWAP:同立体声录音,但将左、右声道互换。
  • RECORD_ANSWER_REQ:默认值为false,如果设为true,则在应答前执行了record_session或uuid_record就暂时不录音,而等到通话被应答后才开始录音。可以防止录上不必要的Early Media(如回铃音或彩铃等)。
  • RECORD_BRIDGE_REQ:与RECORD_ANSWER_REQ类似,只是在当前的通道与其他通道桥接了之后才开始录音。
  • RECORD_APPEND:在默认情况下,如果指定的录音文件已经存在,再次录音就会覆盖该文件。将该选项设为true可以向已经存在的文件后面追加录音。可用于电话中断重新建立后重新录音时。
  • record_sample_rate:可以在录音时指定采样率,以进行实时的转码。合法的采样率在源代码中使用switch_is_valid_rate(rate)宏定义,可能的取值有8000、12000、16000、24000、32000、11025、22050、44100、48000,单位为Hz。
  • enable_file_write_buffering:该变量的默认值为true,即默认先写到一个内存缓冲区中,可以大大减小对存储设备(如硬盘)的访问次数,防止产生IO瓶颈。默认的缓冲区大小是65536字节。
  • RECORD_MIN_SEC:最小录音秒数。如果录音时长小于该数(默认是3秒),则认为是无效的录音,并删除录音文件。
  • RECORD_INITIAL_TIMEOUT_MS:如果从录音开始检测不到声音,则在指定的毫秒后超时,停止录音。
  • RECORD_FINAL_TIMEOUT_MS:如果在录音过程中检测不到声音,则在指定的毫秒后超时,停止录音。
  • RECORD_SILENCE_THRESHOLD:静音能量阈值,即能量小于该值认为是静音。默认值为200。
  • 其他:还有一些变量可以设置声音文件的元信息,名字都很直观,在此就不多介绍了。这些变量有RECORD_TITLE、RECORD_COPYRIGHT、RECORD_SOFTWARE、RECORD_ARTIST、RECORD_COMMENT、RECORD_DATE等。

两条腿 原生格式 录音

为了最大限度地节省系统资源,可以将声音录制成原生(Native)格式。这种格式是由mod_native_file 模块提供的。如果在录音时不提供录音文件的扩展名,就可以以原生的格式录音,例如,下列命令会将录音文件录成 /tmp/test.PCMU(假设Channel使用的Codec是PCMU):

freeswitch> originate user/1000 &record(/tmp/test)

对于一个两条腿的通话,也可以使用 uuid_record 进行原生方式的录音,如:

freeswitch> uuid_record b40dec66-2f47-480d-88bb-335a025d856e start /tmp/test

[INFO] mod_native_file.c:94 Opening File [/tmp/test-in.PCMU] 8000hz

[INFO] mod_native_file.c:94 Opening File [/tmp/test-out.PCMU] 8000hz

[DEBUG] switch_core_media_bug.c:532 Attaching BUG to sofia/internal/ 1001@123.130.116.42

从日志中可以看出,由于该通话有两条腿,而原生的格式不支持混音,因此只能将双向的声道录到两个不同的文件中,如上面的“-in”和“-out”分别代表“入”和“出”。

使用这种录音方式,一个最大的好处是可以直接支持G729编码。G729是受专利保护的编解码器,因而FreeSWITCH中不包含开源的编解码实现。通过使用原生格式录音,不需要进行编码和解码,因而就可以绕开这种限制。另外,使用这种方式也不用在FreeSWITCH中进行编解码处理,因而也会减少对系统CPU的使用。

如果Channel使用G729编码,则原生格式的录音文件扩展名将是.G729。这种格式的文件可以再使用其他的G729工具进行处理,我们就不多介绍了。在本节结束之前,我们再来讲一下PCM格式的文件的处理。PCM 格式的原生文件可以使用sox软件中的play命令播放,如:play -e u-law -r 8000 -t raw test-in.PCMU

注意,由于原生格式的录音文件中不包含录音数据的元信息(即编码、采样率、声道等参数),因而需要在播放时指定。在上述命令中,“-e”指定了编码为“u-law”,即μ律;“-r”参数指定采样率为8000Hz;-t raw说明以原生格式播放。

当然,也可以使用如下的sox命令将该原生格式的文件转换成WAV文件:

sox -e u-law -r 8000 -t raw test-in.PCMU test-in.wav sox -e u-law -r 8000 -t raw test-out.PCMU test-out.wav

也可以进一步将上述两路文件合成一个文件。下列两条命令都可以将两个声音文件合成到一个中去,其中前者将进行混音,而后者将合成立体声的。

sox test-in.wav test-out.wav -m test.wav sox test-in.wav test-out.wav -M test.wav

更多的使用方法请参阅有关资料。

放音 playback

playback 的参数

下面试 Dialplan 设置可以播放声音文件/tmp/test.wav:

<action application="playback" data="/tmp/test.wav"/>

其中,/tmp/test.wav是一个文件名作为playback的参数。实际上,playback的参数范围比这个广得多,最典型的,playback的参数是一些“音频源”(以后,也可能有视频源),这些音频源大部分是由Format定义的。这些文件接口一般会有打开(Open)和关闭(Close)、读(Read)和写(Write)等属性。其中,playback会从文件中“读”,而上面讲到的录音(record),实际上也使用这些文件接口,它会往文件中“写”。这些文件接口有以下几类。

声音文件

对大部分声音文件的支持都是在 mod_sndfile 模块中实现的。该模块直接调用了 libsndfile 库,因而 libsndfile 库所支持的文件 FreeSWITCH 都能支持,典型的如 WAV、AU、AIFF、VOX 等。

<action application="playback" data="/tmp/test.wav"/>
<action application="playback" data="/tmp/test.au"/>
<action application="playback" data="/tmp/test.aiff"/>

libsndfile 不支持的声音文件则由其他模块实现的。如 mod_shout 模块实现了对 MP3 文件的支持。该模块默认是不编译的,# make mod_shout-install,去掉注释在源代码目录中编译安装:

然后,在 FreesSWITCH 中使用下列命令加载该模块:freeswitch> load mod_shout

然后就可以播放 MP3 文件了:<action application="playback" data="/tmp/test.mp3"/>

当然,该模块也支持播放远程SHOUTcast服务器上的MP3文件,如:

<action application="playback" data="shout://shoutcase-server/test.wav"/>

另外,Playback也可以直接播放由mod_native_file模块支持的原生文件,只要在播放时不带扩展名。FreeSWITCH会自己查找与本Channel语音编码一致的文件。在以下设置中,如果Channel使用PCMU编码,则播放test.PCMU,如果Channel使用G729编码,则会播放test.G729:<action application="playback" data="/tmp/test"/>

在FreeSWITCH控制台上使用 "show file" 命令可以列出哪些模块都实现了哪些文件类型的支持,

local_stream

local_stream 是在 mod_local_stream 中实现的。该模块实现了一些 Stream,即“流”。它与文件类似,不同的是,每个流在整个系统中只有一个实例,但可以同时被多个 Channel 读取。这样,当系统中有成千上万个 Channel 时,便能节省很多系统资源。由于流的这个特性,使得它非常适合做保持音乐(Music on Hold,MOH)。比如 FreeSWITCH 默认设置中的 9664 号码就是用下列配置播放了一个流:<action application="playback" data="local_stram://moh"/>,代码中的流的名字是“local_stream://moh”,它是在 conf/autoload_configs/local_stream.conf.xml 中定义的。可以在配置文件中找到如下的配置:

可以看出,上述代码中的流的名字是 moh/8000,FreeSWITCH 支持多种采样频率,如果当前Channel为8000Hz,名字为 local_stream://moh 便可以自动对应到 moh/8000 这个流。同理,如果是 16000Hz的Channel, 就会找到 moh/16000 定义的流。

其后面的 path 定义了该流所需的声音文件的路径,它指定一个目录,在 mod_local_stream 模块加载时会预先加载所有声音文件。有时候,如果在最初安装 FreeSWITCH 时忘了安装声音文件,该流便不能生效,在使用时会遇到类似如下的错误:

[WARNING] mod_local_stream.c:469 Unknown source moh, trying 'default'
[ERR] mod_local_stream.c:478 Unknown source default

在 FreeSWITCH 中使用流时,如果找不到指定的流,则会尝试使用 default 流代替;如果default流也不存在,则会显示“Unknown source default”错误,即在前面看到的出错信息。

出现上述错误一般是在安装FreeSWITCH时忘了安装相关的声音文件,可以尝试到源代码目录中执行如下命令以安装默认的声音文件:

# make moh-install

执行命令重新加载模块使之生效:freeswitch> reload mod_local_stream

silence_stream

silence_stream是一个静音流,它是在 mod_tone_stream中实现的。使用方法如下:

<action application="playback" data="silence_stream://2000"/>

其中,2000是指定播放的时间,单位是毫秒,即上述配置会播放2秒的静音。

在Dialplan中,有一个sleep App在某种程度上可以达到与playback同样的效果。如下列命令会使当前Channel的执行暂停2秒,表现在对端就是2秒内听不到任何声音:

<action application="sleep" data="2"/>

但两者还是有区别的。slience_stream会播放静音,也就是说,仍然会有RTP数据发送,只是RTP中携带的声音数据中音量全部为0;而sleep会中断RTP流的传输。有时候,中断RTP流的传输后可能会造成对方功能失常,在这种情况下,就可以播放slience_stream以代替sleep。

如果在silence_stream中指定时长为0,则默认为无限长,如下列配置会一直播放静音:

<action application="playback" data="silence_stream://0"/>

使用上面介绍的silence_stream播放的静音称为绝对静音,因为它产生的静音音量绝对为0。但有时候,在电话中听起来,绝对静音与电话断了没什么区别,因而电话的另一端的人有时会误以为电话断了,进而可能会错误

地挂断电话。为避免这种情况,最好能像普通电话一样能产生轻微的“嘶嘶”声,这样对方就知道电话还是通的。这种“嘶嘶”声音量不大,恰好能被听到,又不招人讨厌,一般被称为“舒适噪声”(Comfortable Noice,CN)。

可以在silence_stream中使用逗号加上第二个参数让它产生舒适噪声,如下面的配置将产生时长为2秒的舒适噪声:

<action application="playback" data="silence_stream://2000,1400"/>

其中,1400为舒适噪声的参数值,该参数的取值范围是1~10000,值越小噪声的音量越大,通常使用的参数值为600~1400。在不同的场景下具体哪个值合适,读者可以自己试一下。另外,如果将该参数值设为“–1”,则它会产生绝对静音。

tone_stream

tone_stream 是一个铃流,它是在 mod_tone_stream 模块中实现的。它可以使用TGML语言生成各种信号音。如我国的回铃音信号为450Hz的信号音,通断时间为1秒通,4秒断。所以,可以用如下参数产生回铃音信号(其中,1000、4000单位均为毫秒):

<action application="playback" data="tone_stream://%(1000,4000,450)"/>

美国的回铃音信号是440Hz和480Hz交替的,通断时间分别为2000毫秒和4000毫秒:

<action application="playback" data="tone_stream://%(2000,4000,440,480)"/>

而英国的回铃音更复杂一点,读者可以思考一下它的含义:

<action application="playback"data="tone_stream://%(400,200,400,450);%(400,2000,400,450)"/>

conf/vars.xml中还定义了一些其他国家的信号音。

除此之外,也可以在后面再附加别的参数,如loops=3表示循环产生3次信号音:

<action application="playback" data="tone_stream://%(1000,4000,450);loops=3"/>

如果loops=-1,则表示无限循环。更多的参数我们就不多介绍了,读者可以参考http://wiki.freeswitch.org/wiki/TGML进行进一步的学习。

file_string

file_string文件接口原先是在mod_file_string 模块中实现的,后来被合并到了mod_dptools中。它相当于一种更高级的文件格式。通过它,可以将多个文件串联起来,放到同一个playback命令中播放,如下列配置将会顺序播放三个文件:

<action application="playback"data="file_string:///tmp/file1.wav!/tmp/file2.wav!/tmp/file3.wav"/>
其中,“!”为多个文件之间的分隔符。如果文件名中含有该符号,也可以通过playback_delimiter通道变量来改变它的默认值。例如,在下列配置中将多个文件的分隔符变成“|”(注意,这时“!”就可以作为文件名的一部分,如file3!.wav):

<action application="set" data="playback_delimiter=|"/>
<action application="playback"
data="file_string:///tmp/file1.wav|/tmp/file2.wav|/tmp/file3!.wav"/>

另外,也可以指定多个文件间的播放时间间隔,例如下列设置将时间间隔设为500毫秒:

<action application="set" data="playback_sleep_val=500"/>

<action application="playback" data="file_string://file1.wav!file2.wav"/>

其他接口与参数(vlc、http、say、speak、phrase )

mod_vlc 模块实现了一个 vlc 的文件接口。VLC 是一个跨平台的多媒体播放器,它能播放主流音、视频文件。而 libvlc 则是从 VLC 中独立出来的一个库,mod_vlc 便使用了该库支持各种 libvlc 能支持的文件类型。如下列配置可以使用 mod_vlc 中的 vlc 文件接口播放一个本地文件:

<action application="playback" data="vlc:///tmp/test.mp3"/>

也可以播放视频文件中的音频部分:

<action application="playback" data="vlc:///tmp/test.mp4"/>

也可以播放远程HTTP服务器上的文件:

<action application="playback" data="vlc://http://192.168.0.2/test.mp3"/>

当然,另外有一个 mod_httapi 模块直接实现了一个HTTP接口(可能需要事先编译加载),与vlc接口不同的是,它可以将远程文件缓存到本地,使用方法如下:

<action application="playback" data="http://192.168.0.2/test.mp3"/>

除了各种文件接口外,在 playback 的参数中添加 “say:” 前缀也可以直接调用 TTS 功能,如:

<action application="playback" data="say:tts_commandline:Ting-Ting:使用FreeSwitch" />

或(下列配置与上一行是等价的):

<action application="set" data="tts_engine=tts_commandline"/>
<action application="set" data="tts_voice=Ting-Ting"/>
<action application="playback" data="say:欢迎使用FreeSWITCH"/>

当然,上述的playback与下面的 speak 也是等价的,如:

<action application="speak" data="tts_commandline|Ting-Ting|欢迎阅读FreeSWITCH"/>

另外,也可以在playback中使用“phrase:”前缀播放Phrase宏,如:

<action application="playback" data="phrase:demo_ivr_main_menu"/>

它等价于:

<action application="phrase" data="demo_ivr_main_menu"/>

那么,既然有等价的 speak 和 phrase App,为什么要在 playback 中实现这种带前缀的格式呢?

答案是:它有助于用在其他类似 playback 的地方,如在 IVR 的配置中,默认 IVR 的配置中的欢迎音就是用带有 phrase:前缀的 Phrase 宏实现的。参考配置:greet-long="phrase:demo_ivr_main_menu"  另外,这种格式也可以直接用于 read 或 play_and_get_digits App 中需要一个声音文件参数的地方,如:

<action application="set" data="tts_engine=tts_commandline"/>
<action application="set" data="tts_voice=Ting-Ting"/>
<action application="read" data="1 1 say:请选择提示音,1为普通话,2为英语"/>

关于playback的参数的例子可以自己多多练习,找到最适合自己的方法。

循环播放

有些文件接口类型本身就支持循环播放,如各种 Stream 的实现有的天生就是循环的,有的可以用参数控制实现循环。而对于单纯的声音文件,则一般无法实现循环,如果要多次播放,则可以多次调用playback,或使用file_string实现。

另外,系统也提供了 endless_playback 和 loop_playback 两个 App 用于多次播放某个文件。顾名思义,前者会无限循环地播放一个声音文件,后者会播放一个声音文件并重复播放指定的次数。比如,不断播放test.wav直至挂机的实现如下:

<action application="endless_playback" data="/tmp/test.wav"/>

循环播放test.wav文件3次后停止播放的命令如下:

<action application="loop_playback" data="+3 /tmp/test.wav"/>

Say

FreeSWITCH给Say模块增加中文语音
https://www.cnblogs.com/garvenc/p/add_zh_support_to_say_module_for_freeswitch.html

除了 playback 外,FreeSWITCH 中还有其他放音的方法。Say 是一个接口(Interface),它通过定义一个统一的接口屏蔽了语言的多样性。Say 也是一个 App,它可以通过特定的语法,使用预先录好的声音读出日期、时间等,并通过 Say 接口支持多语言(语种)。我们先来看一个例子。下面的例子将不使用 TTS,而直接使用系统预先录好的文件读出 "One Two Three Four"(注意,这里我们选的是英语的发音):

<action application="say" data="en number iterated 1234"/>

其中,参数 en 代表语种,这里是英语;number 说明要播放的数据的类型,这里表示的是数字;iterated是指定播放的方式,这里说明读数字要逐个读出;最后的1234即实际要读的内容。

实际上,Say会根据一定的规则找到1.wav、2.wav、3.wav、4.wav并播放出来。

Say接口能读的数据类型如表11-1所示,但注意并不是所有语言都实现了所有类型的读法。

在确定了数据类型后,具体的读法有以下三种(以数字27为例):

  • PRONOUNCED:按数字语音语出,如英文“Twenty seven”,中文“二十七”;
  • ITERATED:逐个读出,如英文“Two,Seven”,中文“二、七”;
  • COUNTED:以序数词方式读出,如英文“Twenty senventh”,中文“第二十七”。

了解了这些以后,来看下面几个例子:

<action application="say" data="en NUMBER ITERATED 1234"/>
<action application="say" data="en NUMBER PRONOUNCED 1234"/>
<action application="say" data="en CURRENCY PRONOUNCED 1234.56"/>
<action application="say" data="en CURRENT_DATE PRONOUNCED ${strepoch()}"/>

由于上面这一组使用的是英文,因此电话路由到这里后将依次读出:

  • One two three four
  • One thousand two hundred thirty-four
  • One thousand two hundred thirty-four dollars and fifty-six cents
  • October nine,two thousand thirteen

其中${strepoch()}将自动计算当前时间,所以这里最后一项日期读出的结果取决于当前的时间。

接着来看下一组:

<action application="say" data="zh NUMBER ITERATED 1234"/>
<action application="say" data="zh NUMBER PRONOUNCED 1234"/>
<action application="say" data="zh CURRENCY PRONOUNCED 1234.56"/>
<action application="say" data="zh CURRENT_DATE PRONOUNCED ${strepoch()}"/>

这一组使用中文,将依次读出:

  • 一二三四
  • 一千二百三十四
  • 一千二百三十四元五十六分
  • 二零一三年十月九日星期三

对比一下可以看出,中文与英文除了日期中多了个星期三外,还有一个很特别的地方,就是货币的读法是非常不符合中文的习惯。

就这个问题,笔者曾与原模块的作者交流过。他说如果改成我们的习惯以后,可能会破坏其他地区中文的朗读习惯。笔者想了想,他说的确实有道理。比方说,在美国的人,即使说中文,但他们指的货币也是美元,可能仍会按照美元的习惯读“五十六分”而不是读“五毛六”。

后来,又依照zh这个接口,增加了zh_CN接口,让它符合我们中国人的习惯。具体的实现代码及补丁我们后面会讲到。在这里,如果要实现配置和使用这个接口,只需要仿照前面讲的将

conf/lang/zh/zh.xml 文件复制一份到conf/lang/zh/zh_CN.xml,并将文件中language标签的name属性改成“zh_CN”就可以了,如:<language name="zh_CN" ...

然后,重新启动FreeSWITCH,下面的Say就可以说地道的中文了。如下面的配置读出的结果是“一千二百三十四元五角六分”。

<action application="say" data="zh_CN CURRENCY PRONOUNCED 1234.56"/>

当为了“好玩”,还增加了“厘”的读音。如下面的配置将会读出“一千二百三十四元五角六分七厘八”:

<action application="say" data="zh_CN CURRENCY PRONOUNCED 1234.5678"/>

当然如果小数点后有更多的位数,也可以原样读出。不过好玩归好玩,它的“忍耐力”也是有限的,实际使用时应该事先将数字截取到合情合理的精度。

最后,再讲一个例子。有的读者可能想,能否让它将“1234.56”读成“一千二百三十四点五六元”呢?据笔者所知,目前现成实现是没有的,而且在NUMBER的读法中也没有读小数的功能。不过,我们可以变通一下,用下列配置结合playback达到我们的目的。这样做虽然不是很完美,但总算能达到我们要的效果。具体的原理读者可以自己思考一下,在此就不多讲了。

<action application="say" data="zh NUMBER PRONOUNCED 1234"/> <action application="playback" data="digits/point.wav"/> <action application="say" data="zh NUMBER ITERATED 56"/> <action application="playback" data="currency/yuan.wav"/>

TTS (Text To Speech 语音合成)

TTS(Text To Speech)是将文本转换成语音的一项技术,因而又称为语音合成(Synthesis)。

mod_flite 是FreeSWITCH基于Flite语音合成引擎的一个TTS模块。目前,该模块仅支持英文。不过可以先拿它来做个实验,学习一下 FreeSWITCH 中 TTS 的用法。

首先,到FreeSWITCH源代码目录中使用下列命令编译安装TTS模块:

# make mod_flite-install

然后,到FreeSWITCH中加载TTS模块:freeswitch> load mod_flite

加载TTS模块后,就可以使用speak App来调用它了。我们先来试一下以下命令:

originate user/1000 &speak('flite|kal|Hello, Welcome to FreeSWITCH')

上述命令呼叫1000,并执行speak。其中speak的参数是使用竖线隔开的。第一个参数flite表示TTS引擎的名字;第二个参数kal表示一种嗓音(Voice),一般是一个人名。mod_flite目前支持awb、kal、rms、slt四种嗓音。执行上述命令,在分机1000振铃并接听后,将可以听到“Hello,Welcome to FreeSWITCH”。

也可以做以下 Dialplan 进行上述测试:

<extension name="TTS">
	<condition field="destination_number" expression="^1234$">
		<action application="answer"/>
		<action application="speak" data="flite|rms|Hello, Welcome to FreeSWITCH"/>
	</condition>
</extension>

配置好上述Dialplan后,拨打1234,也能听到同样的声音。除了在speak的参数中指定TTS引擎和嗓音参数外,也可以通过通道变量指定。如下列配置与上述Dialplan是等价的(我们在11.6.1节也讲过这种等待关系):

<action application="set" data="tts_engine=flite"/>
<action application="set" data="tts_voice=kal"/>
<action application="speak" data="Hello, Welcome to FreeSWITCH"/>

mod_tts_commandline。许多TTS软件都有命令行版本,可以使用命令行来执行TTS功能。mod_tts_commandline模块可以调用这些命令,生成一个声音文件,进而播放这个声音文件,从而达到利用其他TTS软件执行TTS功能的目的。

使用 Linux 上的TTS功能

使用 windows 上的TTS功能

 使用Windows TTS API从文本生成语音

https://www.cnblogs.com/garvenc/p/use_windows_tts_api_to_generate_voice_from_text.html

使用 MAC 上的TTS功能

ASR (自动语音识别)

TTS、ASR 可以使用各个大厂提供的接口,例如:google、微软、阿里、腾讯、百度、字节 等。

呼叫失败时播放语音提示

在呼叫失败的情况下向主叫用户播放语音提示。在传统的电话网络中,在呼叫失败的情况下交换机会给主叫用户一个有意义的语音提示,如“您拨打的电话正在通话中,请稍后再拨……”或“您拨打的电话暂时无法接通……”之类。在FreesSWITCH中也可以做到。

在默认的配置中,对本地用户的路由都是在conf/dialplan/default.xml中的Local_Extension部分配置的。在呼叫失败时,会将电话转到voicemail(语音信箱),提示用户可留言。为了能给主叫用户一个语音提示,我们可以在电话进入语音信箱之前做点文章,让它播放一个语音提示(当然播放完毕后也可以不进入语音信箱而直接挂机)。

找到 <extension name="Local_Extension"> 部分的最后几行:

<action application="bridge" data="user/${dialed_extension}@${domain_name}"/>
<action application="answer"/>
<action application="sleep" data="1000"/>
<action application="bridge" data="loopback/app=voicemail:default ${domain_name}
${dialed_extension}"/>

其中,第一个bridge用于呼叫被叫号码,如果呼叫失败,则Dialplan会继续往下执行,执行顺序是:

  • 应答(answer);表示已经准备好了,用于接听呼入电话。就是 执行answer对来话进行应答(必须先应答才能向对方播放声音);
  • “睡”一会(sleep,即暂停1秒钟,其中1000的单位是毫秒);使当前线程暂停1000毫秒(1秒)。使用sleep暂停一秒,以防止由于声音通路没建立好(有时候,特别是当主叫与被叫之间的链路比较复杂时,声音链路的建立需要一个过程)而丢失声音;
  • 进入语音信箱(voicemail)。

到这里,只需要在最后一个 bridge 之前加入下面一行:

<action application="playback" data="/tmp/${originate_disposition}.wav"/>

重新打电话试一下,如果被叫忙(或拒接),则originate_disposition变量的值就会是USER_BUSY(有的终端会返回CALL_REJECTED),而如果用户没注册就是USER_NOT_REGISTERED之类的。通过增加上面一行,笔者在拨打一个不存在的用户时,看到如下的日志:

EXECUTE sofia/internal/1002@192.168.7.5 playback(USER_NOT_REGISTERED.wav)

[ERR] mod_sndfile.c:202 Error Opening File [/tmp/USER_NOT_REGISTERED.wav][System error : No such file or directory.]

上面的日志提示找不到指定的文件。当然这个问题很好解决,我们录一个放上去就可以了,内容可以是“您拨打的电话暂时无法接通……”。当然,对于其他情况,如用户忙,我们也可以录一段提示并放到/tmp/USER_BUSY.wav文件中。

不过,呼叫失败的原因可能有很多,我们不可能录上所有的声音文件,此时有两种方法可以解决这个问题。

1)使用一个Lua(或其他语言)脚本。

我们可以不增加刚才playback一行的配置(即上一小节提到的在最后一个bridge之前加入的代码),而是增加如下的行:<action appliction="lua" data="/tmp/xxx.lua"/>

在Lua脚本中可以取到originate_disposition变量,从而可以使用if...then...else之类的逻辑选择播放各种声音文件(即只针对有限几种常用的情况播放特定的声音,其他的一律播放同一个文件)。关于Lua开发的知识我们将在第16章讲到,在这里就不多讲了。

2)当然,如果不想使用Lua,也可以使用Dialplan配置做到。实际上,FreeSWITCH的Dialplan功能是非常强大的,你只需要使用如下配置将呼叫转到播放不同声音文件的Dialplan Extension:

<action application="transfer" data="play-cause-${originate_disposition}"/>

然后创建如下Dialplan Extension,以匹配上面转移的目的地:

<extension name="Local_Extension_play-cause">
	<condition field="destination_number" expression="^play-cause-USER_BUSY$">
		<action application="playback" data="/tmp/user-busy.wav"/>
	</condition>
</extension>
<extension name="Local_Extension_play-cause">
	<condition field="destination_number" expression="^play-cause-USER_NOT_REGISTERED$">
		<action application="playback" data="/tmp/user-not-registered.wav"/>
	</condition>
</extension>
<extension name="Local_Extension_play-cause">
	<condition field="destination_number" expression="^play-cause(.*)$">
		<action application="log" data="WARNING hangup cause: $1"/>
		<!-- 对于其他各种情况,播放该文件 -->
		<action application="playback" data="/tmp/sounds/unknown-error.wav"/>
	</condition>
</extension>

这里的Dialplan配置即根据转移的目的地参数的不同播放不同的声音文件。读者可根据前面的知识自行分析。

使用 TTS

这里用TTS做语音提示。基于 Phrase Macro 功能来实现各种提示音。在conf/lang/zh/demo目录下创建以下脚本,可以将文件命名为 bookdemo.xml,内容如下:

<include>
	<macro name="USER_BUSY">
		<input pattern="(.*)">
			<match>
				<action function="speak-text" data="分机 $1 正在通话, 请稍后再拨"/>
			</match>
		</input>
	</macro>
	<macro name="USER_NOT_REGISTERED">
		<input pattern="(.*)">
			<match>
				<action function="speak-text" data="分机"/>
				<action function="say" data="$1" method="iterated" type="number"/>
				<action function="speak-text" data="暂时无法接通, 请稍后再拨"/>
			</match>
		</input>
	</macro>
</include>

可以看出,上面配置了两个宏(Macro),第一个用于在用户忙的时候播放;第二个在用户未注册的时候播放。其中,name为Macro的名字,pattern会使用正则表达式匹配输入的参数(类似于Dialplan中的正则表达式)。在这里,我们会将被叫号码作为输入参数。也就是说,如果呼叫分机1009失败,就会播放“分机1009正在通话,请稍后再拨”。当然,我们在第二个Macro中使用了Say来播放被叫号码,只是为了演示另外一种实现方式。

配置好上述文件后,在FreeSWITCH控制台上执行reloadxml使之生效。然后,在Dialplan中,就可以将11.8.1节的playback的参数换写成以下这样:

<action application="playback" data="phrase:${originate_disposition}:$1"/>

其中,以“phrase:”开头的文件参数表示这里要播放一个Phrase Macro,冒号后面跟的是参数,即我们在Dialplan正则表达式匹配中获得的被叫号码。如果被叫是1009,并且在被叫忙的情况下,上述配置展开后最终的结果是这样的:

<action application="playback" data="phrase:USER_BUSY:1009"/>

当然,也可以在Dialplan中直接使用phrase App,下面的代码与上述配置是等价的(不同的是,这里参数的分隔符是逗号):

<action application="phrase" data="${originate_disposition},$1"/>

执行reloadxml后,再次拨打一个失败的电话试一下,就可以听到用TTS功能播放的提示音了。

当然,有了Phrase Macro,我们的配置方式就更灵活了,如我们可以只用一个Phrase Macro来搞定所有的情况。先配置如下Dialplan:

<action application="phrase" data="HangupCause,${originate_disposition}"/>

为了配合上述Dialplan,我们需要在Phrase Macro中配置一个名为HangupCause的Macro,并将originate_disposition作为参数传入。然后我们再来配置该Macro,同一个Macro中配置多个input可以针对不同的参数输入执行不同的动作(action),具体如下:

<macro name="HangupCause">
	<input pattern="USER_BUSY">
		<match>
			<action function="speak-text" data="你拨打的电话正在通话中,请稍后再拨"/>
		</match>
	</input>
	<input pattern="USER_NOT_REGISTERED">
		<match>
			<action function="speak-text" data="你拨打的电话不在线,请稍后再拨"/>
		</match>
	</input>
	<input pattern="(.*)">
		<match>
			<action function="speak-text" data="你拨打的电话暂时无法接通,请稍后再拨"/>
		</match>
	</input>
</macro>

可以看出,该Phrase Macro对前两种情况播放不同的语音提示,对以后所有的情况都播放同一个语音提示。当然,最后这一个例子在播放提示时没有播放被叫号码。如果也想在提示音中包含被叫号码,可以与originate_disposition参数一起传到Phrase Macro中,使用一个分隔符隔开(如“!”),这样就可以再用类似“^(.*)!(.*)$”这样的正则表达式将两个参数取出来了。

原理

当然,在被叫失败后播放我们上面指定的提示音或TTS是有一个前提的——在Dialplan的第一个bridge之前要有以下两行:

<action application="set" data="hangup_after_bridge=true"/>

<action application="set" data="continue_on_fail=true"/>

其中,第一行的作用是:在一个成功的bridge之后,如果被叫用户挂断电话,我们也没有必要再向主叫用户播放提示音了,因此可以直接挂机(Hangup After Bridge的字面意思就是“在Bridge成功后挂断电话”)。当然,这一行也可以没有,这就需要在后面检查originate_disposition变量值的时候,发现如果是“NORMAL_CLEARING”(表示呼叫正常释放),再决定是否播放相关语音文件或挂机。第二行的作用是:如果呼叫失败(fail,如空号,拒接等),让Dialplan继续往下执行;否则(值为false的情况)Channel在第一次呼叫(bridge)失败时就挂机了。该变量的值除了是笼统的“true”以外,还可是各种用逗号分隔开的挂断原因值。例如,下面的配置表示只有遇到用户忙或无应答这两种情况才播放语音,其他的就直接挂机。

<action application="set" data="continue_on_fail=USER_BUSY,NO_ANSWER"/>

常见的挂断原因取值除了上述代码中的外还有NORMAL_TEMPORARY_FAILURE(普通临时失败)、TIMEOUT(超时)、NO_ROUTE_DESTINATION(空号/无法路由)等几种。更详细的失败原因可以参见http://wiki.freeswitch.org/wiki/Hangup_causes。

总之,通过上述两个参数就可以在呼叫失败的时候让Dialplan正常往下进行,进而有机会检查上一次呼叫失败的原因,并播放相关的语音提示。

实现呼叫前转业务

在传统的电话系统中,有一项基础的增值业务称为呼叫前转。它的使用方法是:通过拨打一个特定的功能码(一般是“*57*”)登记欲转移到的电话号码,以后所有呼叫都会转移到该号码上。具体来讲,如分机 1002是 Alice 办公室的电话,由于她临时需要到另一个办公室工作,而另一个办公室有一部电话1007,因此,Alice 在1002上拨打*57*1007#就可以登记呼叫前转业务,这样如果以后有人呼叫1002,就会自动转移到1007上,这样 Alice 就不会错过任何电话了。当她从另一个办公室回来时,她可再在1002上拨打#57#取消该业务,以后再有来话1002就会直接振铃了,而不会再发生转移了。

在传统的交换机上,需要为1002分机开通一个权限的,然后该分机才可以拨打*57*或#57#进行登记和取消。当然,我们在这里先不考虑这些权限问题,认为所有用户都有该权限就可以了。

为了实现业务登记,先建立以下路由:

<extension name="call_forward_unconditional_set">
	<condition field="destination_number" expression="^\*57\*([^#]*)#?$">
		<action application="hash" data="insert/${domain_name}-cfwdu/${username}/$1"/>
		<action application="answer"/>
		<action application="playback" data="你登记的业务已经成功.wav"/>
	</condition>
</extension>

可以看出,正则表达式“^\*57\*([^#]*)#?”会匹配“*57*1007#”,并将电话号码存储在变量$1中。这里,考虑到有些话机无法输入末尾的#号键(有些话机会认为按#表示号码输入完毕,因而送给FreeSWITCH的被叫号码不包含#号),我们通过正则表达式中的“?”把它标志为可选的。也就是说,如果用户拨打“*57*1007”也会匹配到这里。

在电话到达后,我们使用hash App将该信息存储在系统内存中的一个哈希表中。hash的参数是以“/”隔开的几项:insert表示向哈希表中插入数据;${domain_name}-cfwdu表示一个域(realm),该域是我们自己定义的,其中的cfwdu是无条件呼叫前转(Call Forward Unconditional)的英文简写形式,用于标志我们这里的呼叫前转业务;${username}是一个键(key),在本例子中,它就是主叫用户1002;$1是该键的值(value),在此它等于1007。

当然,登记成功后,我们不会忘记使用playback播放一个友好的语音提示。

通过下面Dialplan Extension,可以在1002拨打#57#时将登记的信息删除:

<extension name="call_forward_unconditional_unset">
	<condition field="destination_number" expression="^#57#?$">
		<action application="hash" data="delete/${domain_name}-cfwdu/${username}"/>
		<action application="answer"/>
		<action application="playback" data="你登记的业务已经取消.wav"/>
	</condition>
</extension>

同样,我们也不要求被叫号码最后必须有#号,就算没有上面的正则表达式也会匹配#57。可以看出,hash App的参数由insert换成了delete,表示我们要删除一个键。

在实现了登记的删除后,我们就在正常的路由中进行检查了。来看下面的Dialplan设置:

<extension name="call_forward_unconditional_check" continue="true">
	<condition>
		<action application="info" data=""/>
		<action application="set" inline="true" data="cfwdu=${hash(select/${domain_name}-cfwdu/${destination_number})}"/>
	</condition>
</extension>

上述 Dialplan 定义了一个绝对的条件(因为<condition>标签没有其他属性),因而它总是会被匹配到。在电话路由到这里后,我们先使用info App在日志中打印一些调试信息,以方便查找问题。然后,通过set App设置一个cfwdu变量,该变量的值是从哈希表中使用select取出的。如果用*57*进行了登记,则表示它就有值,否则就是一个空字符串。在此,如果有人拨打1002,则这里哈希表的键就是以${destination_number}标志的,即1002。

注意,此处的inline="true"参数非常关键。有了它,这一行的set命令在Dialplan解析的阶段就会执行,这样到后面我们才能根据该变量来进行后续的条件判断。

在此,我们也可以用下面的hash API命令进行检查(192.168.7.5是笔者电脑上${domain_name}的值):freeswitch> hash select/192.168.7.5-cfwdu/1002

可以看出,我们确定可以在域192.168.7.5-cfwdu中通过1002这个键找到1007这个值(当然,前提是1002已经拨打过“*57*1007#”进行登记)。然后,继续向下路由,直到下面的Dialplan Extension:

<extension name="call_forward_unconditional_check2" continue="true">
	<condition field="${cfwdu}" expression="^10[01][0-9]">
		<action application="bridge" data="user/${cfwdu}"/>
	</condition>
</extension>

该Extension检查的条件是cfwdu这个变量的值(就是我们上一步中set的),如果它的值匹配一个分机号(登记后它将匹配1007),我们就执行bridge,它的参数在本例中将是user/1007,因而电话就“转移”到分机1007上去了。如果不匹配,Dialplan就会继续向下执行,跟没有登记是一样的。

在本例中,我们用了内存中的哈希表来存储登记的号码,如果FreeSWITCH重启,这些值都会消失。如果希望在重启后也能保存这些值,可以使用持久化的数据库存储。mod_db实现了持久化的数据库存储,与hash类似,它提供了“db insert”、“db delete”、“db select”等。

详情请参见 http://wiki.freeswitch.org/wiki/Mod_db。

5、高级功能、实例配置

上面是 FreeSWITCH 的一些基本功能与配置实例,下面是一些比较高级的功能,如:

  • 呼叫中心应用
  • 多人会议
  • 与数据库的连接
  • 话单
  • 计费
  • 等。
     

mod_fifo 实现简单呼叫队列

在呼叫中心应用中,有一个很常用的功能 ACD(Automatic Call Distributioin,自动电话分配)。这里所说的呼叫中心都是客服型呼叫中心,即有一组 话务员(称为座席,英文称为Agent)在等待为用户服务。当有电话呼入时,先将用户放入一个队列进行排队(因为可能同时有大量用户呼入),然后按一定的策略选择一个空闲的座席为用户服务(将来话从队列中取出并与该座席通话)。这种通过一定策略为来话分配空闲座席的功能就称为ACD。

mod_fifo 模块实现了一些简单的 ACD 功能,下面我们来看一下。

呼叫停泊与取回

一般来说,普通的电话呼叫会使用 bridge App 来桥接两条腿。但在电话分配中,一般采用停泊与取回的方式进行电话搭接。顾名思义,停泊跟泊车类似,即有来话时,先将来话停靠在一个泊位上(一般还会放点音乐),然后通过某种算法找到一个空闲的座席,该座席拨打一个与该泊位相关的号码,就可以将该泊位上的电话取回,双方进行通话。

FreeSWITCH 默认的 Dialplan 中提供了使用这种方式进行电话分配的例子。先来看停泊的部分,Dialplan 设置如下:

拿起电话拨打 5900,就可以将电话停在一个泊位上。从上面的Dialplan中可以看出:首先,它使用set 设置了一个 fifo_music 的通道变量,该变量指定电话停靠在泊位上时播放的音乐。然后,使用fifo App将电话放入一个先入先出的队列中( fifo的意思就是First In,First Out,即先入先出),该队列的名字是 5900@${domain_name},一般来说 ${domain_name} 变量的值是 FreeSWITCH所在服务器的IP地址。参数in表示是入队。

当有电话呼入时,可以使用 fifo list 命令显示当前队列的状态,具体如下:

fifo list  执行结果

<fifo_report>
	<fifo name="manual_calls" consumer_count="0" caller_count="0" waiting_count="0" importance="0" outbound_per_cycle="0" outbound_per_cycle_min="0" ring_timeout="0" default_lag="0" outbound_priority="5" outbound_strategy="ringall">
		<outbound></outbound>
		<callers></callers>
		<consumers></consumers>
		<bridges></bridges>
	</fifo>
	<fifo name="cool_fifo@192.168.42.28" consumer_count="0" caller_count="0" waiting_count="0" importance="0" outbound_per_cycle="1" outbound_per_cycle_min="1" ring_timeout="60" default_lag="30" outbound_priority="5" outbound_strategy="ringall">
		<outbound></outbound>
		<callers></callers>
		<consumers></consumers>
		<bridges></bridges>
	</fifo>
</fifo_report>

fifo是一个“生产者 --- 消费者”模型,即来话(Caller)相当于生产者(Producer),而座席(Agent)则称为消费者(Consumer),它对来话进行服务即相当于“消费”生产者生产的内容。

接着看 Dialplan 的 unpark,它也是来自默认的配置:

可以看出,这里Dialplan的配置与前面入队时是类似的,所不同的是使用了out参数代表出队,另外有一个附加的nowait参数,它表示如果队列中没有电话在等待(可能这时候主叫已经等不及而挂机了),即没有人需要服务时,座席端的电话就没必要在这里等待了。当然,如果把该参数改成wait,或者不加该参数(默认值为wait),那么座席端会在这里继续等待,直到队列里来了一路通话,它便可以立即得到服务。

静态座席的配置

上面讲了fifo入队及出队的简单原理。一般情况下,fifo是不需要配置的,如果在呼叫中需要一个fifo,FreeSWITCH就会自动创建(如上面的fifo名字可以改成任意的其他字符串)。但有时候,我们也可能希望配置一些静态的座席用于呼叫分配。

mod_fifo 的配置文件是 conf/autoload_configs/fifo.conf.xml。它默认的配置中有一个 cool_fifo 的例子,

为了简单起见,自己配置一个fifo队列,命名为 book,具体如下:

<fifos>
	<fifo name="book" importance="0">
		<member timeout="60" simo="1" lag="5">user/1005</member>
		<member timeout="60" simo="1" lag="5">user/1006</member>
	</fifo>
</fifos>

其中,为book队列配置了两个member(1005和1006),每个member相当于一个座席。user/1005 为该 member 的呼叫字符串,即队列中有来话时怎么呼叫该座席;timeout表示呼叫超时值;simo 表示最大能服务的呼叫的数量;lag 表示该座席在接听一个呼叫后,隔多长时间(秒)才可以再接收下一个呼叫。

使用如下命令可以使刚才的配置生效:

freeswitch> reloadxml
freeswitch> fifo reparse

然后,可以使用 fifo list book 查看一下配置的结果。该命令显示的结果比我们配置的会多出一些参数,这里就不详细介绍了。可以使用如下Dialplan将来话路由到我们刚刚配置的fifo。

<extension name="fifo-test">
	<condition field="destination_number" expression="^(1234)$">
		<action application="set" data="hold_music=$${hold_music}"/>
		<action application="fifo" data="book in"/>
	</condition>
</extension>

当有用户拨打1234时,便进入我们刚才配置的fifo,听等待音乐。然后,1005和1006就会轮流振铃(笔者在试验中,拒接其中一个,另一个就振铃)。

当将队列中的来话分配到座席时,fifo使用轮循的方式分配,因而它是“公平”的,即不会出现某些座席“忙死”而其他座席“饿死” 的情况。

动态座席的配置

在实际的呼叫中心应用中,座席的配置往往是动态的,即一个话务员上班时,会执行一个“签入”的动作,说明他(她)上班了,准备好接电话了;而在中途休息时,或者下班时执行“签出”,这样队列里再有电话进来就不会分配到该座席了。

mod_fifo 提供了一个 fifo_member 命令可以动态增加和删除座席,命令再增加两个座席:

freeswitch> fifo_member add user/1007

freeswitch> fifo_member add user/1008

删除座席,则可以使用如下命令(注意,最后一个参数呼叫字符串必须与增加的时候严格相同):

freeswitch> fifo_member del user/1008

熟悉了这些命令后,就可以使用 Lua 脚本或 Event Socket 等与其他系统集成了,如可以在业务系统的网页上集成一些按钮,按这些按钮就可以进行签入或签出了。

当然,在一些简单的呼叫中心中可能没有业务系统,甚至没有条件做网页。在这种情况下,也可以用我们学过的知识,使用话机签入签出。如下面的Dialplan就可以做到:

<extension name="Agent Login">
	<condition field="destination_number" expression="^5902$">
		<action application="answer"/>
		<action application="set" data="result=${fifo_member(add book user/ ${user_name})}"/>
		<action application="playback" data="ivr/ivr-you_are_now_logged_in.wav"/>
	</condition>
</extension>
<extension name="Agent Logout">
	<condition field="destination_number" expression="^5903$">
		<action application="answer"/>
		<action application="set" data="result=${fifo_member(del book user/ ${user_name})}"/>
		<action application="playback" data="ivr/ivr-you_are_now_logged_out.wav"/>
	</condition>
</extension>

通过使用上述 Dialplan,就可以在上班时在自己话机上拨打5902进行签入,下班时拨打进行5903签出,它们都是使用了我们上面讲过的fifo_member API命令实现的。

offhook 座席

以上配置的座席中,不管是静态配置的还是动态配置的,都称为onhook(挂机)座席。这种座席的特点是当队列中来了电话后再去呼叫座席。而与之相对的另一种座席称为offhook(摘机)座席,这种座席会事先呼入队列并等待(一般听等待音乐),当有来电时就可以立即接听,省去了呼叫座席的时间,因而能更迅速为客户提供服务。这在比较繁忙的呼叫中心中也有助于提高效率。

其实我们把12.1.1节从队列中取回电话的例子稍作修改,就可以支持offhook座席。比如,我们建立下面的Dialplan,当座席呼叫5902时,就会执行fifo App,它会尝试从队列中取出一个呼叫。而此时如果队列中没有呼叫在等待服务,则由于此处我们使用了wait参数(对比一下12.1.1节中使用的是wait),因此该座席会一直停在这里等待,直到队列中有新的电话进入。该Dialplan的设置如下:

<extension name="unpark">
	<condition field="destination_number" expression="^5902$">
		<action application="answer"/>
		<action application="fifo" data="5900@${domain_name} out wait"/>
	</condition>
</extension>

当服务完成后,该座席就又变成等待状态,继续等待下一个呼叫的到来。

fifo

在mod_fifo中,有许多相关的通道变量可以改变它的行为。通过有效地使用这些通道变量,往往能配置出比较实用的功能。

如每个fifo都有10个优先级队列,在进行入fifo前,可以使用 fifo_priority 变量指定来话的优先级,高优先级的来话将排在队列的前面(默认的优先级是5):

<action application="set" data="fifo_priority=1" />

<action application="fifo" data="book in" />

在队列中有多个电话排队时,座席这一端也可以随意取出一个电话进行接听。当然,如果达到这种效果需要通过其他手段获取到队列中所有成员的UUID,如下面的设置中,我们在从队列中取出(out)电话前先把fifo_bridge_uuid变量设置为想取出的Channel的UUID,然后就可以取出与特定的UUID相关联的通话了:

<action application="set" data="fifo_bridge_uuid=9c3a1fee-fae2-4355-bceb-5064f2107983" /> <action application="fifo" data="book out" />

在下面的例子中,我们使用了很多的通道变量。该例子基于多年前笔者实现的一个呼叫中心应用。我们先看看Dialplan中的配置:

<action application="answer"/>
<action application="set" data="fifo_music=$${hold_music}"/>
<action application="set" data="tts_engine=tts_commandline"/>
<action application="set" data="tts_voice=Ting-Ting"/>
<action application="set" data="fifo_chime_list=say:1 1 2"/>
<action application="set" data="fifo_chime_freq=15"/>
<action application="set" data="fifo_orbit_exten=1007:45"/>
<action application="set" data="fifo_orbit_dialplan=XML"/>
<action application="set" data="fifo_orbit_context=default"/>
<action application="set" data="fifo_orbit_announce=say:"/>
<action application="set" data="fifo_caller_exit_key=2"/>
<action application="set" data="fifo_caller_exit_to_orbit=true"/>
<action application="set" data="fifo_override_announce=say:tts_commandline:Ting-Ting: ${caller_id_number}"/>
<action application="fifo" data="book in"/>

当有来电进来时,我们会将电话路由到这里的Dialplan处理。其中,第1行先对来话进行应答;第2行设置来话在队列中听到的保持音乐;第3行和第4行设置TTS引擎的参数(因为使用TTS比使用声音文件讲起来更方便一些);第5行设置一个声音,它会每隔15秒(在第6行设置)播放一次,以给用户比较好的体验 [2];第7行,设置

一个分机号,如果等待超时(冒号后面指定超时时间,即45秒),会转到该号码;第8行和第9行分别是与第7行配合的Dialplan和Context;第10行,设置一个声音,如果座席接听,则先给来话用户播放一段提示音;第11行,设置一个按键,配置第5行;第12行,实现在用户按了第11行的按键的情况下,转到第7行的号码上(否则的话将会退出fifo);第13行,是在座席接听电话后,在与来电客户通话前播放给座席的,这里我们播放了来电的号码[3];第14行,这里是最重要的,即将来话送入book这个fifo队列中(如果没有这一行的话上面所有的设置就都白费了)。

还有一些其他变量是在fifo是执行过程中自动产生的,如fifo_position表示来话在当前队列中的位置; fifo_serviced_by或fifo_serviced_by表示来话被哪个Channel(通过UUID可以进而找到相关的座席)服务了。

当然,该模块还有其他相关的变量,限于篇幅,我们就不一一讲解了。有需要的读者可以参阅http://wiki.freeswitch.org/wiki/Mod_fifo。

相关事件

当 fifo 相关的状态发生变化,会产生一些 Subclass 为 fifo::info 的 CUSTOM 事件。通过这些事件,可以获取 fifo 中的详细信息,如哪个座席为哪个来话进行了服务等。

事件消息中,有一个FIFO-Action消息头标志了事件实际的动作。在笔者的试验中,一通来话从入队到挂机,大致经过以下几个事件:

  • push:来话入队时产生。
  • pre-dial:呼叫座席之前产生。
  • post-dial:呼叫座席之后产生。
  • consumer_start:消费者(即座席)开始。
  • caller_pop:主叫出队。
  • consumer_pop:消费者出队。
  • bridge-consumer-start:消费者Channel开始bridge。
  • bridge-caller-start:主叫Channel开始bridge。
  • bridge-consumer-stop:消费者Channel停止bridge。
  • bridge-caller-stop:主叫Channel停止bridge。
  • consumer_stop:消费者停止。·channel-consumer-start:消费者Channel开始。
  • channel-consumer-stop:消费者Channel结束。
  • 具体的事件我们就不赘述了。

在 fifo 应用中,通话中的两条腿都是独立建立的,只有这样才能进行桥接(即bridge)。因而,在普通的通话中靠 a-leg 上的变量去影响b-leg的做法(如,使用export将变量设置到未来的b-leg上)在这里就不适用了。

那么,为了在 b-leg 上设置我们想要的通道变量,可以在向 b-leg 发起呼叫的呼叫字符串上做文章。如下面的设置可以在呼叫1007时将主叫号码改为7777:

<member>{origination_call_id_name=7777}user/1007</member>

也可以设置与其他业务系统中的座席工号进行关联,如:

<member>{my_worker_number=1234}user/1007</member>

当然,这里的座席成员除了是本地用户外,也可以是外部系统的号码,如通过某网关能打通的一个手机号:

<member>sofia/gateway/pstn/139xxxxxxxx</member>

对于PSTN网络来讲,它们一般会在呼叫进展阶段返回回铃音或彩铃(对应SIP中会收到183消息),这会让FreeSWITCH认为电话已经接通。因此,为了消除这种影响,一般来说会增加ignore_early_media参数,如:

<member>{ignore_early_media=true}sofia/gateway/pstn/139xxxxxxxx</member>

注意,FreeSWITCH在拨打PSTN网络中的电话之前是无法知道对方是否正常空闲的(也可能是无法接通或被叫忙),它只能按正常的策略去呼出。如果碰巧出现被叫忙或不在服务区之类的情况,PSTN网络中通常会播放相关的提示音,而不是正常的返回信令消息,这会令接续到下一个座席的时间延长。这是与PSTN网络中的电话对接固有的问题。在具体应用时可能还需要注意设置恰当的超时(timeout)等参数,在此我们就不多讲了。

mod_callcenter 实现呼叫中心

从入门到进阶-如何基于FreeSWITCH搭建呼叫中心平台:https://www.zhihu.com/tardis/zm/art/260476782

fifo 只是实现了一些先入先出的呼叫队列,与实际的呼叫中心应用相比,还是显得简单了点。而mod_callcenter 从名字上看就是一个与呼叫中心应用有关的模块。它实现的ACD队列更接近于实际的呼叫中心应用。

对于主叫用户来说,mod_callcenter 采用一种基于积分(score)策略的排队算法。用户在队列中每等待一秒,积分就自动加一 。在座席端,它实现了一个多级的梯队,允许具有不同技能的座席通过不同的优先级来对用户提供服务。

mod_callcenter 模块简介

与 mod_fifo 不同,mod_callcenter 模块并不是默认就编译安装的。因而首先要编译安装它。跟其他模块的安装方法类似,

FreeSWITCH源代码目录中使用以下命令编译安装:

# make mod_callcenter-install

然后到FreeSWITCH控制台上加载该模块:

freeswitch> load mod_callcenter

如果需要在FreeSWITCH启动时自动加载该模块,则可以编辑conf/autoload_configs/modules.conf.xml,去掉与该模块相关的行的注释,即如下一行:

<load module="mod_callcenter"/>

mod_callcenter中有以下基本概念:

  • 队列(Queue):用于标志一个队列,呼入的电话会在相关的队列中进行排队。
  • 座席(Agent):座席会事先登录到一个梯队中(后文会讲到),该梯队会与某个队列相关联,当关联的队列中有来话(排队)时,系统会根据一定的策略选择一个空闲的座席为来话客户服务(接听电话)。
  • 策略(Strategy):队列中座席的分配策略,即当队列中有来话时,如何选择一个空闲的座席。该模块实现了以下几种策略。
            ring-all:选择所有座席。即让所有座席振铃,哪个先接就选择哪个。
            long-idel-agent:选择空闲时间最长的座席。
            round-robin:轮循。
            top-down:按固定的顺序选择。
            agent-with-least-talk-time:总是选择通话时间最短的座席;
            agent-with-fewest-calls:总是选择接电话次数最少的座席;
            sequentially-by-agent-order:根据梯队和顺序选择;
            random:随机选择。
  • 座席状态:一个座席有两个状态标志,分别是Status和States。Status是一个座席逻辑上的状态,它有以下几种取值:
            Logged Out:退出服务状态。
            Available:可用状态,可以接电话。
            Available(On Demand):一种特殊的可用状态。
            On Break:座席已登录,但不可以接电话。
  • States是跟电话呼叫有关的状态,它有以下几种取值:
            Idle:空闲。
            Waiting:等待接受呼叫。
            Receiving:正在接受呼叫。
            In a queue call:当前正在一个队列呼叫中。
  • 座席类型:有callback和uuid-standby两种类型。前者是onhook座席,即如果有电话分配到该座席时,FreeSWITCH将会呼叫座席;而后者是offhook座席,即座席需要首先拨入FreeSWITCH建立一个呼叫,当有来话时则可以立即与座席桥接起来。
  • 梯队(Tier):梯队是将队列与座席连接起来的桥梁。另外,它也可以实现多级服务。例如,通过将不同的座席分配到不同的梯队中,可以实现当有电话进来时先连接到普通的座席,有必要时再转到专家座席这样的应用。

静态座席的配置

mod_callcenter 默认的配置文件是 conf/autoload_configs/callcenter.conf.xml,其中配置了一个support@default 队列,配置如下:

其中,strategy 参数指定了队列的分配方式,它的值(value)longest-idle-agent 说明要优先选择等待时间最长的座席分配。

配置一个座席,名字是1005@default,配置如下:

<agent name="1005@default" type="callback"
contact="[call_timeout=10]user/1005"
status="Available" max-no-answer="3" wrap-up-time="10"
reject-delay-time="10" busy-delay-time="60" />

在该座席的配置中,name指定座席的名字,它的值为1005@default,以后都可使用该名字引用这个座席; type指定座席的类型;contact为座席的呼叫字符串,用于在有来话时呼叫该座席;status为座席的初始状态; max-no-answer定义座席呼叫失败的次数,如果超过这个值,则将座席状态设为On Break;reject-delay-time为座席拒接后再次选到该座席的最短时长;busy-delay-time为如果该座席忙,则再次尝试选到该座席的最短时间。

同理我们可以按上面的例子再增加其他座席配置,如1006@default、1007@default等。

然后,还需要配置tier,以将座席与队列关联起来。如下面的配置将三个座席与队列support@default关联起来了。

<tier agent="1005@default" queue="support@default" level="1" position="1"/>
<tier agent="1006@default" queue="support@default" level="1" position="1"/>
<tier agent="1007@default" queue="support@default" level="1" position="1"/>

上述设置完成后,就可以重新加载模块使之生效了:freeswitch> reload mod_callcenter

然后,当有电话呼叫(或转移)到support时,就可以使用如下的Dialplan将电话转到该callcenter队列中了:

<extension name="Callcenter Example">
	<condition field="destination_number" expression="^support$">
		<action application="answer"/>
		<action application="callcenter" data="support@default"/>
	</condition>
</extension>

然后 callcenter 便会根据上面设置的规则去呼叫相关的座席。

动态管理队列和座席

mod_callcenter提供了一个callcenter_config的API命令,用于管理与该模块相关的各种资源。比如,可以使用如下命令手工将座席签入:

callcenter_config agent set status 1005@default 'Available'

当然,上述的签入命令用于座席已正确配置的基础上。通过把座席的状态变成Available,该座席就准备好接听电话了。下列命令将该座席的状态置为Logged Out,因此,就不会再有电话分配到该座席了。

freeswitch> callcenter_config agent set status 1005@default 'Logged Out'

可以用以下命令列出当前所有的座席:

freeswitch> callcenter_config agent list

当然,如果座席本身没有在配置文件中配置,也可以使用命令添加。比如,下列命令添加一个名字为1007@default的座席:

freeswitch> callcenter_config agent add 1007@default callback

当然,还需要用下列命令给它设置相关的参数,如contact:

freeswitch> callcenter_config agent set contact 1007@default user/1007

也可以用以下命令列出当前的队列:

freeswitch> callcenter_config queue list

或列出当前的梯队:

freeswitch> callcenter_config tier list

类似的命令还有很多,我们就不一一介绍了。当然,这些命令一般不会仅用手工执行。通过Event Socket或XMLRPC等接口调用这些命令,则可以做出很友好的界面,也可以很容易集成到其他业务系统中去。

offhook 座席

mod_callcenter也支持offhook座席。该模块的Wiki页面上有一个示例的Dialplan设置,我们把它稍微简化一下:

<extension>
<condition field="destination_number" expression="^(4099)$">
<action application="set" data="transfer_after_bridge=4099"/>
<action application="sleep" data="300"/>
<action application="set" data="r=${callcenter_config(agent set uuid ${caller_id_number}@default '${uuid}')}"/>
<action application="set" data="r=${callcenter_config(agent set type ${caller_id_number}@default 'uuidstandby')}"/>
<action application="set" data="r=${callcenter_config(agent 10 set status ${caller_id_number}@default'Available (On Demand)')}"/>
<action application="set" data="r=${callcenter_config(agent set state ${caller_id_number}@default'Waiting')}"/>
<action application="set" data="cc_warning_tone=tone_stream://%(15 200,0,500,600,700)"/>
<action application="answer" />
<action application="playback" data="$${hold_music}"/>
</condition>
</extension>

为了便于讲解,我们增加了行号。如果座席呼叫4099,则会匹配到第2行,并进入该Dialplan;第3行设置transfer_after_bridge参数,当该座席与来电通话(bridge)完毕后,会再转到4099上,重新等待;第4行加了一个sleep,防止执行得太快;第5行执行set,通过${API(...)}的方式调用callcenter_config API命令,它将设置本座席的uuid参数为当前Channel的UUID;第6行将该座席的类型设为uuid-standby,表示如果有来话就不需要呼叫该座席了,而直接找到前面设置的座席的uuid直接将来话和座席的Channel桥接(bridge)到一起就可以了;第7行设置座席的状态(Status)为可用状态;第8行设置座席的呼叫状态(State);第9行通过通道变量cc_warning_tone设置一个铃音,当有电话进来时,将向座席播放该铃音;第10行应答;第11行则播放保持音乐。

至此座席就一直在听音乐,直到有新的电话进来。

除了上面介绍的参数外,mod_callcenter还有好多配置参数,以及好多可用的命令可以配置不同的队列和运行方式。另外,有一些以cc_开头的通道变量(如上面我们讲到的cc_warning_tone)也会在某些时候发生作用。关于这些,读者可以多参考该模块的Wiki页面,在这里我们就不一一列举了。mod_callcenter在排队及电话分配的各个阶段都会产生一些事件,允许第三方的开发者通过Event Socket等接口监控队列的行为,并进行各种集成——如可以在Web界面上实时显示队列及座席的状态,将状态变化写入数据库供以后产生报表等。当然,要做成一个好的应用前端的开发,工作量还是不小的。

该模块提供的功能与FreeSWITCH中其他的模块功能相比,属于更偏上层(应用)的功能。一般来说类似这种功能的应用大家都是使用Event Socket等接口在外部进行开发,但若在FreeSWITCH内部开发则明显控制力更强一些。当然,不管是在内部开发还是外部开发,对于类似呼叫中心的应用,本模块的一些概念和业务逻辑都是很有参考价值的

数据库

FreeSWITCH内部使用关系型数据库记录一些实时的数据。这些数据有的是临时存储的,有的是持久存储的。关系型数据库很适合描述通信应用中的实体以及关系数据,而且它所支持的各种关系运算也极大简化了FreeSWITCH的代码量及开发难度。

FreeSWITCH 默认使用 SQLite 嵌入式数据库,因而不需要任何配置就能工作。默认的数据库存储位置是FreeSWITCH安装目录下的db目录,默认的核心数据库名称是core.db。我们可以先打开该数据库,看看里面到底有什么。

sqlite3 core.db        # sqlite3 命令用于打开SQLite数据库。
sqlite> .tables        # .tables命令会显示数据库中所有的表

各表的作用如下:

  • aliases:别名表,用于存储命令行别名。
  • basic_calls:一个视图,基于channels和calls表,提供基本的呼叫信息。
  • calls:呼叫表,在bridge的呼叫中,用于关联Channel表中的两条腿。
  • channels:存储所有当前的Channel。
  • complete:存储所有Tab Complete数据。
  • detailed_calls:一个视图,基于Channels和Calls,提供详细的呼叫信息。
  • interfaces:存储所有的Interface。
  • nat:存储当前的NAT映射关系。
  • recovery:在使用系统恢复功能功能时,该表存储所有呼叫的详细信息。
  • registrations:存储注册用户的信息。
  • tasks:当前的任务表,如heartbeat(心跳)、检测IP地址变化等。

FreeSWITCH 的 show 命令的大部分内容都是基于这些表的,如 show channels 即查询 Channels表。当 FreeSWITCH 中有通话时,可以使用如下 SQL 语句进行查询:

sqlite> select * from channels;

也可以使用.schema命令看表的结构,如下列命令就显示了calls表的结构:

sqlite> .schema calls

Sofia 数据库

除核心数据库外,很多外部模块也都使用数据库存储数据。最典型的就是 mod_sofia 模块。

mod_sofia 模块中的每一个 Profile 都使用一个单独的数据库,如可以通过下面命令看到每个数据库:cd /usr/local/freeswitch/db$ && ls sofia*

打开 sofia_reg_internal.db(即internal Profile对应的数据库),看看里面有什么:

$ sqlite3 sofia_reg_internal.db

sqlite> .tables

其中,SIP用户的注册信息都存储在sip_registrations表中。当然,该表与core.db中的registrations表中的有些数据是重复的。由于不止SIP用户需要注册,其他模块的用户也可能需要注册,核心数据库中存储的是一种统一的注册信息。

其他各表的功能简介如下:

  • sip_authentication:SIP认证相关。
  • sip_dialogs:SIP对话相关。
  • sip_presence:SIP呈现相关。
  • sip_shared_appearance_dialogs:SIP SLA相关,记录当前的Dialog。
  • sip_shared_appearance_subscriptions:SIP SLA相关,记录订阅信息。
  • sip_subscriptions:SIP订阅相关。

感兴趣的可以自己打开看一下。其他模块,如 mod_fifo、mod_callcenter等也有自己的数据库。通过直接访问这些数据库,也可以从里面取出有趣的数据,甚至还可以改变模块的运行规则。当然,这类似黑客行为,跟前面讲的在数据库上安装触发器差不多,不推荐使用。

SQLite在并发量特别大的情况下可能会是一个瓶颈,另外,在一些需要HA或负载均衡的场合,也需要一个外部的数据库。FreeSWITCH支持通过ODBC接口连接外部的数据库。最典型的外部数据库是PostgreSQL和MySQL,

视频通话

FreeSWITCH 也支持基于 SIP 的视频通话,下面看下基本的设置,以及视频转码与录像等高级话题。FreeSWITCH默认的配置文件中并没有对视频编解码的相关项,因而默认不支持视频呼叫。如果需要支持视频呼叫,只需要在配置文件中增加相关的视频编解码就可以了。

目前 FreeSWITCH 支持的视频编解码有H261、H263、H263-1998(H263+)、H263-2000(H263++)、H264、VP8等。具体应该使用哪种或哪几种编解码需要看SIP终端的支持。需要注意的是,与音频编解码不同,FreeSWITCH中的视频编解码目前仅支持透传,即 FreeSWITCH 仅将通话中一方的视频原样送到另一方去,而不做任何编码转换。这就要求进行视频通信的双方使用一致的编解码。

FreeSWITCH 支持的媒体编码默认是在 conf/vars.xml 中定义的,可以在该文件中找到类似下面的配置:

<X-PRE-PROCESS cmd="set" data="global_codec_prefs=G722,PCMU,PCMA,GSM"/>

<X-PRE-PROCESS cmd="set" data="outbound_codec_prefs=PCMU,PCMA,GSM"/>

以上两行分别定义了两个全局变量,它们的字面意思分别是全局的和出局的编解码首选项。假设我们要增加对 H264 和 VP8 编码的支持,则相应的配置如下:

<X-PRE-PROCESS cmd="set" data="global_codec_prefs=G722,PCMU,PCMA,GSM,GSM,H264,VP8"/>

<X-PRE-PROCESS cmd="set" data="outbound_codec_prefs=PCMU,PCMA,GSM,GSM,H264,VP8"/>

由于上述配置文件实际修改的是全局变量,一般来说需要重启 FreeSWITCH 才能使之生效。

另外,不需要重启FreeSWITCH使之生效的一个技巧——如果是在UNIX类平台上,可以给FreeSWITCH进程发送一个SIGHUP信号,让它重新解析全局变量(当然,FreeSWITCH在收到该信号后还会做其他的事,如执行日志轮转等)。

示例如下:kill -HUP <FreeSWITCH 进程号> 

注意,上述Shell命令只是保证FreeSWITCH正确解析了这些变量。实际上,这些变量的值是在Sofia Profile中引用的,因而,还需要在FreeSWITCH中执行以下命令来重读Sofia的配置,如:

freeswitch> sofia profile internal rescan

freeswitch> sofia profile external rescan

上面重新加载了 internal 和 external 两个 Profile 配置。当然,也可以用以下命令简单地重新加载

Sofia模块:freeswitch> reload mod_sofia

实际上,也可以不修改全局变量,而直接修改 Profile 的配置。如 internal.xml 默认的配置如下:

<param name="inbound-codec-prefs" value="$${global_codec_prefs}"/>

<param name="outbound-codec-prefs" value="$${global_codec_prefs}"/>

可以把视频编码附加到变量引用的后面,具体如下:

<param name="inbound-codec-prefs" value="$${global_codec_prefs},H264,VP8"/>

<param name="outbound-codec-prefs" value="$${global_codec_prefs},H264,VP8"/>

或直接改成如下形式:

<param name="inbound-codec-prefs" value="PCMA,PCMU,H264,VP8"/>

<param name="outbound-codec-prefs" value="PCMA,PCMU,H264,VP8"/>

如果出局和入局编解码相同,也可以直接使用同一个参数设置,如:

<param name="codec-prefs" value="$${global_codec_prefs}"/>

配置完毕后记得通过rescan或reload mod_sofia命令使之生效。可以使用如下命令查看是否生效(其中CODECS IN和CODECS OUT分别代表入局和出局时使用的编解码):

freeswitch> sofia status profile internal

配置了正确的视频编解码后,就可以在视频话机之间进行视频通话了。

视频录像与回放

FreeSWITCH 支持录音和视频通话,现在来讲讲录像。与录音相比,录像要复杂一些。录像数据要按一定的格式存储在文件中,而这些文件格式有好多种。不同的文件格式称为不同的容器(Container),在这些容器中,通常会包含多个音频轨道(Track)和视频轨道,有的还含有同步信息。目前,处理视频格式最好的开源软件就是 ffmpeg。FreeSWITCH 中实现了一个简单的 mod_fsv 模块,提供FreeSWITCH中的录像及回放支持。它不依赖于任何其他的视频处理库,而是自己定义了一种私有的格式,将音频轨道用 L16编码的数据保存,视频轨道则将整个RTP原始包都保存进去。在默认的 Dialplan 中,也提供了录像与回放的例子。拨打 9193 可以通过 record_fsv App 进行录像,Dialplan的设置如下:

<action application="record_fsv" data="/tmp/testrecord.fsv"/>

录像过程中,用户可以看到自己的视频也被原样echo了回来。录制完成后,就可以拨打9194播放刚刚录制的录像了,它是使用 play_fsv App 实现的。

<action application="play_fsv" data="/tmp/testrecord.fsv"/>

上面的录像只能针对单腿的呼叫进行。因为,对于桥接(bridge)的呼叫,需要类似音频的 Media Bug 方式支持录像。而标准的 FreeSWITCH 并不包含视频的 Media Bug 支持。

视频转码

在FreeSWITCH支持WebRTC之后,人们对视频转码的需求就越来越迫切了。因为在此之前,视频设备一般都支持H263或H264转码,很容易统一。但有了WebRTC以后,由于Google Chrome浏览器实现的WebRTC仅支持VP8,与原来的H26x的设备进行视频通信就成了问题,而原来的视频设备也不能很快地支持VP8。当然,Firefox浏览器也开始支持WebRTC了,并支持H264格式的视频,但WebRTC毕竟是Google提出来的标准,因而还是有大量的用户会使用Chrome。

通过相应的视频库或硬件的DSP芯片,也可以在FreeSWITCH中提供视频的转码。在FreeSWITCH的作者Anthony Minessale的支持下,目前笔者已经开始了这方面的研究,希望在不远的将来会有所突破。

多人电话会议

FreeSWITCH支持很强大的多人电话会议(又称音频会议)功能,在会议过程中,可以随时播放声音文件、对会议进行录音,也可以对任意成员进行禁言、禁听等操作。另外,它也支持简单的视频会议功能。

电话会议功能是在 mod_conference  这个模块中实现的

音频会议

先使用 FreeSWITCH 的默认配置对音频会议来做一个初步的体验。首先,拿起电话拨打 3000,就进入了一个会议。在会议中,每一个 Channel 都是会议的一个成员(Member),会议中的每个成员都是以 member_id 标志的,它是一个整数值。由于第一次进入会议时只有一个成员,会议系统会首先播放 “You are the only member in this conference”,即提示该会议中只有你一个成员。接下来可以用其他电话也呼入3000 进行电话会议。

使用 DTMF 按键进行控制

在会议中,各成员也可以随时通过DTMF按键来进行各种操作。按键对应的操作可以在系统配置文件中配置,默认的控制功能如表所示。

其中,比较常用的是0,它用于切换本成员静音与否。一般来说,在一个会议中,如果超过3个人同时讲话的话,就不容易分辨是谁在说话了;因此,如果某个成员长时间不发言,就可以自发地将自己端的电话静音,以保证会议的质量。另外,有时候有的会议成员的背景环境噪声比较大,如果不发言,也可以使用静音或调低说话音量。

Dialplan 将电话路由到会议

mod_conference 提供了一个conference App,因此,我们可以直接在Dialplan中使用它。下面的Dialplan 是 FreeSWITCH 提供的默认的 Dialplan:

<extension name="nb_conferences">
	<condition field="destination_number" expression="^(30\d{2})$">
		<action application="answer"/>
		<action application="conference" data="$1-${domain_name}@default"/>
	</condition>
</extension>
  • 正则表达式 ^(30\d{2})$ 匹配以30开头的4位数号码,所以,用户可以呼叫 3000~3099 之间的号码进入该 Dialplan。
  • 然后执行 answer 动作对来话应答(这步也可以省略,如果后续执行到 conference 的话它也可以自动应答),然后就可以执行到 conference 进入会议了。其中,conference的参数 $1-${domain_name}@default中引用了一些变量,domain_name 一般来说是一个IP地址(如192.168.1.7),如果用户呼叫的是3000,则上面的参数值就是3000-192.168.1.7@default。 其中,@之前的部分为会议的名字,之后的部分为会议的参数。在这里,只有一个参数default,它是一个会议的Profile。该Profile中定义了一个会议的基本参数和特性。

先不讨论 Profile 中参数的细节,接着看下面三个 Dialplan 配置(它们都是在默认Dialplan中的,为了节省篇幅,我们省去了其他不关心的内容):

可以与3000的那个对比一下,除了匹配的被叫号码不同外,进入会议的Profile也不同。

  • 拨打 31 开头的4位数电话号码就进入一个宽带(Wideband,16kHz)的会议;
  • 拨打 32 开头的4位数电话号码就进入一个超宽带(Ultra Wideband,32kHz)的电话会议;
  • 拨打 33 开头的4位数电话号码就进入一个CD音质(CD Quality,48kHz)的电话会议。

关于这些 Profile 的区别我们稍后会讲到。上面只讲了conference App的Profile参数,实际上还可以指定其他参数。conference的参数总的语法规则是<conference_name>@<profile>+<pin>+flags{<flag1>|<flag2}。其中,各参数也可以酌情省略。几种有代表性的写法如下:

  • 3000:3000是一个会议名称
  • 3000+1234:3000是会议名称,使用默认的Profile default, 1234是密码。所有成员在进入会议时都会提示输入密码。
  • 3000@default+1234:同上,只是明确指定了Profle的名字为default,当然也可以指定其他Profile的名字。
  • 3000@default+1234+flags{mute|waste}:在上面的基础上,又增加了mute和waste参数。
  • 3000@default+flags{endconf|moderator}:与上面的类似,只是参数换成了endconf和moderator。

通过上面的语法规则可以看到,会议的名称应该是普通字母加数字组成,不要有“@”、“+”之类的特殊字符。

使用 API 命令控制会议

除了conference App外,FreeSWITCH也提供了一个 conference API 命令用于对会议进行各种控制。在控制台上输入conference 命令不加任何参数可以显示一些命令帮助信息,如:

freeswitch> conference

上面列出了 conference 命令的参数格式,其中,第一个参数是会议的名称;紧接着后面的参数是一个子命令;子命令后面是子命令的参数。不过,上述帮助信息中没列出的是,其中的子命令“list”和“xml_list”前面也可以不需要会议名称,如果没有会议名称就默认列出系统中所有的会议,如:

freeswitch> conference list

freeswitch> conference xml_list

下面按帮助中列出的格式把各子命令简单介绍一下。

在这里都以会议名称为3000做例子,默认的Dialplan生成的会议是类似“3000-IP地址”这样的会议名称,为了简单起见可以将Dialplan中的“-${domain-name}”去掉(在本书的例子中我们大部分都去掉了,这样可以避免每个人的IP地址不同造成的差异)。

  • list 和 xml_list:列出会议中的成员。
  • energy:设置成员的能量值。FreeSWITCH具有静音检测功能,只有音量超过一定能量才能混音到会议桥中。它的参数首先是一个成员的ID(member_id),即修改哪个成员的能量,然后是实际能量值。如,ID为1的成员背景噪声比较大,将其的能量提升为500(默认值为300)。

freeswitch> conference 3000 energy 1 500

其中,成员的 ID 除了以整数形式存在的值外,还可以是下列特殊值:

  • all:所有成员
  • last:最后加入的成员
  • non_moderator:非会议主席的成员
  • volume_in:调整成员的输入音量,如下面的例子。

freeswitch> conference 3000 volume_in 1 1

  • volume_out:调整成员的输出音量。
  • play:向会议中播放声音文件。如向全体成员播放声音的配置如下。

freeswitch> conference 3000 play /tmp/test.wav

仅向ID为1的成员播放声音的配置如下。

freeswitch> conference 3000 play /tmp/test.wav 1

  • pause:暂停会议中声音文件的播放。
  • file_seek:声音文件的快进、快退。
  • say:使用TTS功能播放文本。如向会议中播放某一文本的配置如下。

freeswitch> conference 3000 say "Hello everyone"

  • saymember:仅向某个成员播放TTS。
  • stop:停止声音文件的播放。看下面的例子。

freeswitch> conference 3000 stop current #
freeswitch> conference 3000 stop all #
freeswitch> conference 3000 stop last

  • dtmf:向某个成员发送DTMF。
  • kick:踢出某个成员。
  • hup:挂断某成成员。
  • mute:将成员静音(不允许发言)。
  • tmute:切换成员的静音状态。
  • unmute:取消成员的静音。
  • deaf:禁听。
  • undeaf:取消禁听。
  • relate:使用该功能可以任意控制两个成员间是否可以听到彼此的声音,如下面的例子。

freeswitch> confernece 3000 relate 1 2 nohear
freeswitch> confernece 3000 relate 1 2 nospeak
freeswitch> confernece 3000 relate 1 2 clear

  • lock:锁定会议,别人无法加入。
  • unlock:将会议解锁。
  • agc:启用自动增益控制(Auto Gain Control)。
  • dial:从会议呼出。参数是一个呼叫字符串,如呼叫1000加入会议:

freeswitch> conference 3000 dial user/1000
也可以在呼出时指定主叫名称和号码,如以下命令将在1000的话机上显示来电信息为“Seven<7777>”。
freeswitch> conference 3000 dial user/1000 Seven 7777

  • bgdial:同dial,只是dial是阻塞的,bgdial是异步的,不阻塞当前命令的执行,类似bgapi。
  • transfer:将一个成员转到另一个会议中,如以下命令将成员1转到3001中。

freeswitch> conference 3000 transfer 3001 1

  • record:对会议录音,如下面的例子。

freeswitch> conference 3000 record /tmp/conference.wav

  • chkrecord:检查录音情况。
  • norecord:停止录音。
  • pause:暂停录音。
  • resume:恢复录音。
  • recording:如果在会议的Profile中设置了录音模板,则该子命令可以开始和停止录音。如下面的例子。

freeswitch> conference 3000 recording start #

freeswitch> conference 3000 recording stop #

freeswitch> conference 3000 recording check #

freeswitch> conference 3000 recording pause #

freeswitch> conference 3000 recording resume #

exit_sound:如果会议中有人退出,所有成员都可以听到一个声音,该命令可以修改这个声音。如下面的例子。

freeswitch> conference 3000 exit_sound on #

freeswitch> conference 3000 exit_sound off #

freeswitch> conference 3000 exit_sound none #

freeswitch> conference 3000 exit_sound file /tmp/a.wav #

  • enter_sound:如果会议中有成员进入则播放该声音,用法同上。
  • pin:修改会议的密码。
  • nopin:取消会议密码。
  • get:取得参数的值,这些参数有max_members(最大成员数)、sound_prefix(声音文件路径)、caller_id_name(主叫名称)、caller_id_number(主叫号码)等。
  • set:设置上述变量的值。
  • floor:将某成员设为floor,没什么用。
  • vid-floor:设置视频成员的floor。在视频会议中,所有人都看到持有floor的成员。
  • clear-vid-floor:取消视频成员的floor。

配置文件

会议的配置文件,它的默认位置是 conf/autoload_configs/conference.conf.xml。其中,首先配置了一组 DTMF 按键控制功能:

<caller-controls>
	<group name="default">
		<control action="mute" digits="0"/>
		<control action="deaf mute" digits="*"/>
		<control action="energy up" digits="9"/>
		<control action="energy equ" digits="8"/>
		<control action="energy dn" digits="7"/>
		<control action="vol talk up" digits="3"/>
		<control action="vol talk zero" digits="2"/>
		<control action="vol talk dn" digits="1"/>
		<control action="vol listen up" digits="6"/>
		<control action="vol listen zero" digits="5"/>
		<control action="vol listen dn" digits="4"/>
		<control action="hangup" digits="#"/>
	</group>
</caller-controls>

上述的配置也就是在前面看到的按键控制的配置参数,可以对比一下看看各参数与实际功能的对应关系。如果需要改变这些按键的功能,建议另外加一个组(group),然后就可以通过组名在后面的Profile中引用。接下来,就可以在配置文件中找到前面讲到的那几个Profile,如:

<profile name="default">
<profile name="wideband">
<profile name="ultrawideband">
<profile name="cdquality">

可以看到,这些 Profile 之间最主要的区别就是会议的采样率,不同音质的会议采样率不同。我们最常用的defalt Profile使用与PSTN一致的8000Hz的采样率。参数配置如下:

<param name="rate" value="8000"/>

此外,Profile的参数中还有一大堆的“-sound”参数,它们配置了在会议中不同时刻播放的声音。

如“muted-sound”是在用户被静音时播放给用户的,“unmuted-sound”则相反。有些用户不喜欢在加入会议时听到“You are the only person in this conference”,则可以把“alone-sound”改掉或注释掉。

“comfort-noise”是一个比较有用的参数。在多人会议中,如果没有人发言,则会议中会很安静,大家会误以为会议或者网络出了问题。因此,设置该参数可以产生舒适噪声。该参数的值可以根据需要调整,详情可以参见12.6.1节中的silence_stream部分。

“auto-record”参数可以设置一个录音模板,当会议室中有至少两个人时,便会自动开始录音。

其他的参数我们就不多介绍了,读者可以自己尝试一下或参考该模块相关的Wiki页面。

视频会议

配置好了视频编解码后,就可以拨打3000呼入默认的会议进行视频会议了。由于FreeSWITCH目前不能对视频进行转码,因而只能是所有人共同看其中一个人的视频图像。那么,具体应该看谁的呢?笔者经常开玩笑说:“谁的声音大就看谁的”。当然,这么说不完全准确。实际上,在会议的所有成员中,其中一个成员会有一个vid_floor标志,谁持有这个标志,大家就看到谁。这个标志是在会议中由FreeSWITCH根据发言者的声音能量值来动态分配的。一般来说,当有多个人轮流说话时,大家就轮流看到正在发言的人,在这种情况下它工作得很好。

当然,视频与音频不同,前者切换没有后者快。而且,在有的视频终端上,可能会出现“花屏”(即屏幕杂乱,出现马赛克之类的)现象。产生这种现象的原因是视频的传输和编码机制造成的。一般来说,视频是以一帧一帧的图像压缩后传送的 [3]。为了减少传送的数据量,则先传送一个完整的帧(称为关键帧 [4]),然后后面只传送后面的帧与前面帧的不同的部分(称为非关键帧或预测帧 [5])。由于每帧之间图像的差别不是很大,因而可以大大减少传送的数据量。但为了保证画面的清晰流畅以及减少由到丢包等引起的影响,每隔一段时间需要重新发一个关键帧。

所以,在视频会议中发生发言人的视频切换时,如果大家的终端正好错过了新的发言人的终端发出的关键帧,而此时只收到预测帧,在播放时就可能会出现花屏的现象。为了解决这个问题,FreeSWITCH支持在发生视频切换时主动请求一个关键帧。对于一些旧的SIP视频终端,一般支持picture_fast_update [6]请求;对于新的应用,如WebRTC,则会使用FIR [7]功能请求关键帧。

有时候,在会议中我们可能不希望FreeSWITCH自动改变当前看到的成员图像,而是希望手工控制。在FreeSWITCH 1.4中,新加入了一个vid-floor子命令,可以用它来控制当前哪个成员显示在大家面前。比如,在会议中,以下命令将大家看到的视频切换到member-id为3的成员:

conference 3000 vid-floor 3 force

注意其中的force参数,该参数是可选的。如果没有该参数,则只是临时切换到该成员,根据声音大小自动切换还是会出现;如果使用该参数,就固定永远显示这一个成员(当然如果该成员挂机,就又会变成自动选择模式)。

FreeSWITCH从1.4版开始也支持使用WebRTC参与会议。不过,由于FreeSWITCH不能进行视频转码,因而参与会议的成员必须同使用相同的编码才能相互看到。一般的视频话机都使用H263或H264编码,而Chrome浏览器中实现的WebRTC仅支持VP8编码。

多画面融屏

另一种比较常见的电话会议是融屏。即将多个成员的图像缩小,再合并到一个图像上,这样,如果会议室中的成员比较少(一般用4分屏或9分屏,再多了以后图像就很小了)时,就可以看到所有人的图像了。

FreeSWITCH内部的视频不能进行转码,因而也不支持这种应用。不过,可以通过将视频呼叫转发到其他的MCU(Multiple Control Unit)上面实现。

在2013年ClueCon大会上也演示了如何把FreeSWITCH做成一个MCU的示例。当然,该Demo是配合一个硬件板卡实现的,不是标准FreeSWITCH的组成部分。不过,在这里,我们也简单介绍一下其实现思路,或许对基于FreeSWITCH开发这方面应用的读者有借鉴意义。

由于考虑到软件的视频转码可能会有性能问题,正好有个合作伙伴也提供了一个DSP 用于测试,就花了一些时间写了个示例程序。

笔者拿到的DSP提供SDK,很快在其提供的示例代码基础上做了一个四分屏融屏程序。实现思路很简单,给视频芯片发过去四路RTP视频流,经过融合以后返回来一路RTP。为简单起见,我们都使用同一种视频编码—— H264。实现原理如图所示。

其中,我们可以看到四个视频终端的视频流都发送到FreeSWITCH,我们将它们转发到DSP上,然后控制DSP进行融屏,并将结果视频返回给FreeSWITCH,FreeSWITCH再将视频发送到各个视频终端上去。当然,为了使例子看起来更灵活,笔者还特意控制DSP芯片将融合后的视频也转发了一路到一个VLC播放器上播放。

为了实现该应用,笔者修改了mod_conference,增加了一些代码 [11]。在会议开始时,每加入一方就将其视频RTP流直接转发到DSP视频处理芯片上,它会把各路视频融合,并返回一路视频RTP流到FreeSWITCH,在FreeSWITCH中启动一个线程专门接收该路视频,并发送到所有视频终端上去。然后所有终端就都能看到所有与会者融合以后的画面,而混音功能仍由FreeSWITCH的mod_conference提供。

图12-2是笔者在芝加哥“彩排”时的截图。其中左上角的视频就是笔者;右上角是一个手机SIP客户端发过来的视频;右下角是从一个本地视频文件中播放的视频;左下角是空的,等待其他成员加入。

视频监控

有了视频会议及融屏的解决方案以后,就可以将视频监控也融入到FreeSWITCH中了。为了能与FreeSWITCH进行视频通信,最简单的方案就是找一个支持SIP协议的摄像机。笔者验证过一款Grandstream(潮流)高清IP摄像机,型号是GXV3615W。图12-3是该摄像机的SIP配置页面,可以看出,它跟普通的SIP话机配置方式基本上是一样的。

配置完成后,该摄像机也是以一个SIP客户端的方式注册到FreeSWITCH上。一般来说,它不会主动发起呼叫,但是我们可以从FreeSWITCH中呼叫它。摄像机在接收到呼叫后,会自动应答,所以,我们就可以简单呼叫一个电话号码查看该摄像机覆盖范围内的图像了。在一些智能家居类的应用中,通过呼叫一个电话号码即可以查看家里的情况。在FreeSWITCH中,就这么简单地实现了。

当然,也可以将该摄像机加入会议,如(所配置的账号为1010):conference 3000 dial user/1010

由于该摄像机一般不会说话,所以在FreeSWITCH视频会议中如果想看到它的话,就需要使用vid-floor功能强制把floor设置成它,如:conference 3000 vid-floor 2 force

当然,如果使用前面讲到的视频融屏方案,就很容易看到它了。

话单

作为一个运营平台,一定要有记录话单(Call Detail Record,CDR)的能力,以便为后续的计费提供数据支持。FreeSWITCH内部有几个可以记录话单的模块,我们来简单介绍一下。

mod_cdr_csv 模块可以记录CSV(Comma-Separated Values,即以逗号分隔的值)格式的话单,也可以在话单文件中嵌入SQL语句,方便后续直接导入到数据库中。该模块是默认加载的。

话单的格式是以模块定义的,默认的配置文件中定义了sql、example、snom、linksys、asterisk、opencdrrate等几个模板。默认使用的模板是example,它是一个CSV模板,具体如下:

<template name="example">
"${caller_id_name}","${caller_id_number}",
"${destination_number}","${context}","${start_stamp}",
"${answer_stamp}","${end_stamp}","${duration}","${billsec}",
"${hangup_cause}","${uuid}","${bleg_uuid}","${accountcode}",
"${read_codec}","${write_codec}"
</template>

其中,各字段都是通道变量的引用,在挂机的时候 FreeSWITCH 会将通道变量对应的值替换这里的变量。可以看到,里面包含主叫号码(caller_id_number)、被叫号码(destination_number)、起止时间(start_stamp、end_stamp)、呼叫时长(duration)、计费时长(billsec)等。其中呼叫时长是从呼叫开始到结束的总时间,而计费时长是从应答才开始计算的。如果使用sql模板,就可以直接产生SQL语句,模板格式如下(省略部分字段):

<template name="sql">INSERT INTO cdr VALUES ("${caller_id_name}",...

当然,也可以参照这些模板写自己的模板。可以在配置文件中使用以下参数配置使用的模板:

<param name="default-template" value="example"/>

默认情况下,只记录a-leg的话单,如果连b-leg也记,可以修改如下参数:

<param name="legs" value="ab"/>

话单文件默认存储在FreeSWITCH安装目录的log/cdr-csv目录中。其中有Master.csv记录了所有的话单,而每个独立的分机也有自己的话单文件。如果只需要记录总的话单,则可以在配置文件中配置如下参数:

<param name="master-file-only" value="true"/>

话单文件是持续写入的。一般来说,当处理话单时(如将话单导入数据库),需要一个“冻结”版本的话单文件,以防止丢失或产生重复话单。为了达到该目的,可以在FreeSWITCH中使用cdr_csv rotate命令让话单文件产生轮转。该命令会将当前话单文件改名为包含有日期时间标志的文件名,如Master.csv.2013-07-29-09-29- 41;并且重新打开一个新的话单文件继续写入。这时我们就可以用其他脚本或程序去处理旧的话单文件了。

也可以在Shell中按如下的方式执行该命令,如:

# fs_cli -x "cdr_csv rotate"

如果配置文件中的rotate-on-hup参数值为true,也可以使用下列命令给FreeSWITCH进程发HUP信号触发日志轮转:

# kill -HUP <FreeSWITCH ID> ID>

在UNIX类系统中,还可以把日志轮转加到crontab定时任务中,如:

0 * * * * fs_cli -x "cdr_csv rotate"

将上述配置加到crontab的配置中,可以每小时将话单轮转一次。当然,也可以在定时任务中直接配置一个处理话单的脚本,在脚本中调用话单轮转命令,并处理产生的话单文件。详细的处理过程我们就不多介绍了,请参考该模块相关的Wiki页面。

直接将话单写入数据库

通过mod_cdr_pg_csv 模块可以直接在FreeSWITCH中将话单写入PostgreSQL数据库。默认该模块是不自动安装的,因此,在使用前需要手工安装它。

首先,确保系统上安装了PostgreSQL的客户端库。如在Ubuntu Linux上,可以用如下命令安装:

# apt-get install libpq-dev libpq5

然后就可以按类似其他模块的安装方式来安装该模块了。到FreeSWITCH的源代码目录中,执行如下命令安装:

# make mod_cdr_pg_csv-install

该模块的配置文件是conf/autoload_configs/cdr_pg_csv.conf.xml,其中有一个参数是配置数据库的连接的,如:

<param name="db-info" value="host=127.0.0.1 dbname=freeswitch user=password=

connect_timeout=10" />

可以看出,它并没有像其他模块那样使用核心数据库的“pgsql://”字符串方式,而是直接使用的PostgreSQL标准的连接字符串。因为该模块是在核心数据库中加入PostgreSQL之前写的,所以这里的配置格式并不统一。

默认写入的数据库表的名称是“cdr”,也可以使用如下参数修改,如:

<param name="db-table" value="freeswitch_cdr"/>

配置文件中的“schema”标签配置了通道变量与数据库中字段的对应关系,可以根据这里的配置建立数据库表结构。默认的配置如下:

<schema>
	<field var="local_ip_v4"/>
	<field var="caller_id_name"/>
	<field var="caller_id_number"/>
	<field var="destination_number"/>
	<field var="context"/>
	<field var="start_stamp"/>
	<field var="answer_stamp"/>
	<field var="end_stamp"/>
	<field var="duration" quote="false"/>
	<field var="billsec" quote="false"/>
	<field var="hangup_cause"/>
	<field var="uuid"/>
	<field var="bleg_uuid"/>
	<field var="accountcode"/>
	<field var="read_codec"/>
	<field var="write_codec"/>
	<!-- <field var="sip_hangup_disposition"/> -->
	<!-- <field var="ani"/> -->
</schema>

可以看出,由于duration和billsec的值是整数值,因此对应的数据库中也应该使用整数(INTEGER)类型的字段,并且我们在SQL语句中不需要使用单引号,因此这里加了一个参数quote="false"。

默认数据库中话单的表结构中的字段名称与这里配置的名称是相同的,如果不相同,可以在<field>标签中增加column参数,以匹配在数据库中的字段名,如:

<field var="local_ip_v4" column="server_ip"/>

其他的配置参数与mod_cdr_csv中的类似,我们就不多讲了。配置完毕后就可以加载该模块将话单直接写入数据库了。

freeswitch> load mod_cdr_pg_csv

如果由于某种原因引起写入话单失败,则FreeSWITCH就会将这张话单存入一个本地的文件话单池中(默认位置在log/cdr-pg-csv目录中),以免丢失话单。在发生这种情况时,可以使用其他的工具将话单池中的话单取出,再导入数据库。

另外,如果你使用的是MySQL数据库,则目前FreeSWITCH中没有对应的模块,可以考虑使用我们下面介绍的方式实现。

使用 HTTP 服务器接收话单

除了上面介绍的方式外,FreeSWITCH也支持将话单写入远程的HTTP服务器。在那里,你可以使用任何喜欢的编程语言处理话单以及写入任何可能的数据库。有三个模块可以实现该功能:mod_xml_cdr、mod_json_cdr 以及 mod_format_cdr。前两者分别产生XML和JSON格式的话单,最后一个模块则可以配置产生XML或JSON格式的话单。

以 mod_xml_cdr 为例,在配置文件中配置如下url参数即可以将话单写入指定的Web服务器指定的地址上:<param name="url" value="http://localhost/cdr_curl/post.php"/>

如果有话单写入,在Web服务器端就会收到一个HTTP Post请求,请求中带有一个xml参数,它的值是一个XML文档。然后,在Web服务器上就可以取出这个参数,并用XML解析器解析里面的内容了。XML格式的话单内容非常详细(包括电话转接历史等),因而可以很容易在其中获取各种信息。

服务器处理完毕后,应该返回HTTP“200 OK”消息,然后FreeSWITCH就会释放相关的Channel。如果服务器没有任何反应,FreeSWITCH就会一直等待,从而产生很多僵尸Channel,所以一定要保证Web服务器能正常响应。当然,Web技术都发展了几十年了,这种解决方案已经很成熟了。

如果由于某种原因引起写入话单失败,服务器返回非“200 OK”的消息,则FreeSWITCH就会将这张话单存入一个本地的XML文件中(默认位置在log/xml_cdr目录中),以免丢失话单。

计费

严格来说,计费并不是 FreeSWITCH 要做的工作。FreeSWITCH 只需要忠实地产生正确的话单,实际的计费工作可以由专门的系统去处理。实际的电信运营商也是有专门的计费中心用于话费处理的(因为现在的情况更多的不是以时长(如分钟)计费,而是有各种形式的套餐,在交换机中是不可能知道五花八门的套餐信息的)。

不过,在这里还是简单介绍一下FreeSWITCH相关的计费方案,以供读者参考。

mod_nibblebill 是一个预付费的计费模块。可以在 FreeSWITCH 源代码目录中使用如下命令配置安装:# make mod_nibblebill-install

它需要配合一个外部数据库才能工作,外部数据库可以是通过ODBC或内置的PostgreSQL原生模式连接。本例中,笔者使用的是PostgreSQL数据库。我们先来创建一个数据库表用于存储账户:

pgsql> create table accounts (
id bigserial not null,
name varchar( 256 ),
cash double precision not null
);

插入一个账户,给我们自己充值100元钱:

pgsql> insert into accounts (name, cash) values ('Seven', 100);

然后,修改模块配置,指向我们的数据库:

<param name="odbc-dsn" value="pgsql://hostaddr=127.0.0.1 dbname=freeswitch"/>

查询一下数据库中的账户及余额:

pgsql> select * from accounts;

另外,我们也可以看出我们账户的id是“1”,后面我们将根据该id计费。

在用户目录中(如1002.xml),加入以下参数:

<variable name="nibble_rate" value="0.01"/>

<variable name="nibble_account" value="1"/>

其中,nibble_rate是每分钟的费率;nibble_account对应accounts表中的id字段的值,即对哪个账号进行计费。这样只要该用户打电话,它的费率就是每分钟1分钱。

当然,我们也可以改变,账号对应的字段,如,我们可以通过accounts表中的name字段引用账号:

<param name="db_column_account" value="name"/>

然后,上述的配置就可以改成这样了:

<variable name="nibble_rate" value="0.01"/>

<variable name="nibble_account" value="Seven"/>

配置完成后,加载该模块:

freeswitch> load mod_nibblebill

然后,以后每次分机1002打电话,它的账户上就每分钟扣1分钱。

当然,我们也可以根据不同的被叫号码设置不同的费率。如我们可以在Dialplan中设置,如果是长途呼叫,则每分钟0.3元:

<extension name="Toll Bill">
<condition field="destination_number" expression="^(0.*)$">
<action application="set" data="nibble_rate=0.3"/>
<action application="bridge" data="sofia/gateway/.....

为了保证计费安全,在用户通话过程中该模块将每隔一定时间就从账户上扣一次钱。比如,以下设置实现了每分钟写一次数据库:

<param name="global_heartbeat" value="60"/>

此外,该模块可以允许设置账号的最低余额、低余额提醒等。通过设置负数的最低余额或者往账号中预充一定的金额,也可以支持后付费的应用。

除了该模块外,还有一个开源的专门用于FreeSWITCH计费的产品——vBilling。这款产品笔者没有用过,有兴趣的读者也可以参考官方网站http://vbilling.org/,在此我们就不再赘述了。

6、FreeSWITCH 与 FreeSWITCH 对接

以前的交换机又叫交换局,因此呼出和呼入就常称为出局和入局。

启动 多个 FreeSWITCH 实例

在同一台主机启动多个FreeSWITCH实例:https://www.cnblogs.com/garvenc/p/start_multiple_freeswitch_instances_on_the_same_host.html

主 FreeSWITCH 跑主要业务,把带 Skype的FreeSWITCH启动到另一个实例(对于主的FreeSWITCH而言,它们就相当于两个SIP到Skype的转换网关),这样就避免了由于Skype模块崩溃影响到所有业务。当然,为了让Skype模块崩溃后也不影响使用Skype进行通话的学生,后来又启动了另一个带有Skype的FreeSWITCH实例。这样,正常情况下两个Skype网关可以平均分担Skype通话,一旦一台崩溃,所有的Skype话务就都由另外一台承担。起到了类似HA(High Availability,高可用)的效果这时就需要多个FreeSWITCH之间进行对接

FreeSWITCH 默认的配置文件在 /usr/local/freeswitch/conf 下。

  • 假设第一个实例已启动并正确运行。
  • 为了启动第二个 FreeSWITCH 实例,首先要复制一个新的环境(放到 freeswitch2 目录中,以下的操作都在该目录中):
            mkdir /usr/local/freeswitch2         新目录freeswitch2
            cp -R /usr/local/freeswitch/conf /usr/local/freeswitch2/    把旧的配置文件复制到新目录中
            mkdir /usr/local/freeswitch2/log
            mkdir /usr/local/freeswitch2/db
            ln -sf /usr/local/freeswitch/sounds /usr/local/freeswitch2/sounds
  • 修改端口号,防止发生冲突:第一个要修改的就是 Event Socket 的端口号,它是在 conf/autoload_configs/event_socket.conf.xml 中定义的,找到它并把其中的 8021 改成另一个端口,比如 9021。接着修改 conf/vars.xml,把其中的 5060、5080 也改成其他的值,如 7060 和 7080。如果仅做简单的测试的话,改这两个地方就够了。但如果用于生产环境,还需要修改两个实例的配置文件,将其中的 RTP端口号的范围改成不冲突的值。FreeSWITCH中自己维护一个 RTP端口池,可以在 conf/autoload_config/switch.conf.xml 中修改,默认值如下(修改成不相互冲突的范围就行):
    <param name="rtp-start-port" value="16384"/>
    <param name="rtp-end-port" value="32768"/>
  • 当然如果还加载了其他的模块,注意要把可能引起冲突的资源都改一下。比如因为笔者要用到mod_erlang_event 模块,就需要改 autoload_configs/erlang_event.conf.xml中的 listen-port 和nodename。

下面我们就可以启动测试了:

cd /usr/local/freeswitch2/

/usr/local/freeswitch2/bin/freeswitch -conf conf -log log -db db

以上命令分别用 -conf、-log、-db 指定新的环境目录。启动完成后将进入控制台。如果想使用fs_cli 连接该实例的话,则可以打开另外一个终端窗口,使用如下命令连接(上面是把端口改成9021):/usr/local/freeswitch2/bin/fs_cli -P 9021

找个软电话注册到 7060 端口试试,比如使用 X-Lite 注册时,服务器地址填的是192.168.1.100:7060。当然,为了以后启动方便,也可以将上述启动第二个FreeSWITCH的命令写到一个简单的脚本里面。例如下面我们使用了 bash 脚本:

#!/bin/bash

FSDIR=/usr/local/freeswitch2

cd $FSDIR

$FSDIR/bin/freeswitch -conf conf -log log -db db

当然,如果需要启动多个实例,只需要重复本节的步骤,将不同实例的配置文件放到不同的目录中去,将相关的参数修改成不冲突的值就可以了。

不同版本 FreeSWITCH

两个FreeSWITCH 实例运行的是同一份代码。有时候,你还可能运行两个不同版本的FreeSWITCH。在这种情况下,在编译的时候就需要指定一个不同的安装目录,比如下面我们指定将新版本的FreeSWITCH安装到/opt/freeswitch目录中:

./configure --prefix=/opt/freeswitch

make && make install

安装完毕后,如果执行 /opt/freeswitch/bin/freeswitch 命令,它就默认使用 /opt/freeswitch/conf 目录下面的配置文件,我们也不需要再复制一份配置文件了。

当然,如果需要两个实例同时运行,还是要修改其中一个的相关端口号,以避免冲突。改完以后就可以使用下列命令启动新的FreeSWITCH实例了:/opt/freeswitch/bin/freeswitch

FreeSWITCH 对接 FreeSWITCH

多机对接的目的就是让不同交换机上的用户能互相打电话。从呼叫上来讲,就是把一个呼叫路由到正确的目的地。在 FreeSWITCH 中,就是设置正确的 Dialplan。

先来看一下不同的FreeSWITCH 交换机(不同的主机或不同的实例)之间的对接。

双机 对接

有两台 FreeSWITCH 主机,

IP 地址分别为

  • 192.168.1.A
  • 192.168.1.B

每台机器均使用默认配置,也就是说在每台机器上1000~1019这20个号码之间可以互打电话。位于同一机器上的用户称为本地用户,如果需要与其他机器上的用户通信,则其他机器上的用户就称为外地用户。如果 A上的 1000 想拨打 B 上的 1000,则B上的1000相对于A上的1000来说就是外地用户。就一般的企业PBX而言,一般拨打外地用户就需要加一个特殊的号码,比方说 0。这时0就称为出局字冠。由上面例子可知,为了完成该实验,我们规定:不管是A上的用户还是B上的用户,拨打外网用户均需要在实际的电话号码前加拨0。

在A机上,把以下 Dialplan片断加到 conf/dialplan/default.xml 中

<extension name="B">
	<condition field="destination_number" expression="^0(.*)$">
		<action application="bridge" data="sofia/external/sip:$1@192.168.1.B:5080"/>
	</condition>
</extension>
  • 正则表示式 ^0(.*)$ 表示匹配所有以0开头的被叫号码,匹配完成后,括号中的匹配结果会被绑定到变量$1中。因此,如果A上的用户呼叫01000,则$1的值就是1000,bridge是一个 App,它的参数就变成 sofia/external/sip:1000@192.168.1.B:5080,它是一个呼叫字符串。在一个呼叫中,当FreeSWITCH执行到这里的 bridge 时,就会从本机的 external Profile(本机的5080端口)向B的5080端口发送INVITE呼叫请求。

注意,在上述过程中,被叫号码中的第一个0在到达B时丢失了。这是系统对接中常用的一个策略,俗称把0“吃掉”了。

B在5080端口上收到INVITE请求后,由于5080端口默认走public Dialplan,所以查找 public.xml, 可以找到以下的Dialplan配置项:

<extension name="public_extensions">
	<condition field="destination_number" expression="^(10[01][0-9])$">
		<action application="transfer" data="$1 XML default"/>
	</condition>
</extension>

上述Dialplan中的正则表达式 ^(10[01][0-9])$会匹配被叫号码1000,然后执行transfer,并把来话转到default Dialplan。呼叫转到default Dialplan后,路由规则就跟本地用户的来话一样了,因而最终B上的1000就会振铃。如果B摘机接听,电话就可以接通了。

如果B上的用户也要拨打A上的用户,那么只需要在B上也做类似A上的配置就可以了。因为A和B是完全对称的。

汇  接

以前的交换机又叫交换局,因此呼出和呼入就常称为出局和入局。

理论上来讲,所有的对接模式都可以采用上面的双机对接模式,即上述的对接模式是一切对接的基础。下面来考虑一些更复杂的情况。比方说,假设全世界的交换机都变成FreeSWITCH的情况。我们将各FreeSWITCH编号为A、B、C、D、E、F、G……如果A上的用户要跟世界上所有的用户都能通话,那么A上就需要配置到所有其他主机的路由,这显然是不现实的。

为了解决这一问题,A、B、C、D开会讨论。D主动说:“我精力比较旺盛,给你们3个提供转接服务吧,你们就不用费心了”。这时候,D就成了一个汇接局,为A、B、C之间的通话做转接服务。A、B、C就称为端局,因为他们只有终端用户(本地用户)。拓扑结构如图13-2所示。

同时,大家商量了新的拨号规则:本地用户之间的通话拨号规则不变,但是如果拨叫其他外地用户的话,则需要拨打相应的局号(即,如果A上的用户呼叫B上的1000,则拨打B1000),并统一送到D进行汇接。以A为例,它上面的Dialplan如下:

<extension name="D">
	<condition field="destination_number" expression="^([B-Z].*)$">
		<action application="bridge" data="sofia/external/sip:$1@192.168.1.D:5080"/>
	</condition>
</extension>

其中,正则表达式^([B-Z].*)$表示任何以B~Z开头的号码都送到D(即192.168.1.D)的5080端口上去。注意,这里并没有“吃掉”第一位号码,因为如果吃掉的话,D就不知道如何进行下一步路由了。所以,如果A上的用户拨打B1000,在D上将收到B1000。

在D上,收到5080端口的呼叫请求后,查找public Dialplan对来话进行路由。它的Dialplan设置如下:

<extension name="D">
	<condition field="destination_number" expression="^D(.*)$">
		<action application="transfer" data="$1 XML default"/>
	</condition>
</extension>

上述Dialplan说明,如果被叫号码的首位是 D,则说明是一个本地用户,所以“吃掉”首位的“D”,然后把路由转(transfer)到default Dialplan进行处理。

对于被叫号码不在本地的用户,则使用下列Dialplan:

<extension name="D">
	<condition field="destination_number" expression="^([A-CE-Z])(.*)$">
		<action application="bridge" data="sofia/external/sip:$2@192.168.1.$1:5080"/>
	</condition>
</extension>

其中,正则表示式^([A-CE-Z])(.*)$匹配所有除D以外的A到Z开头的被号码。我们还是以有人拨打B1000为例,匹配成功后,$1的值为B,而$2的值为1000,所以,bridge的参数中呼叫字符串就会变成sofia/external/sip:1000@192.168.1.B:5080,因而相当于在D上“吃掉”了被叫号码中最首位的“B”,并把电话送到B的5080端口上。

电话到达B后,B上默认的Dialplan就可以将电话路由到本地用户上,因而电话接通。这种方式就称为汇接模式。由于D即带本地用户,又作为汇接局,因而它是一种混合模式的汇接局。如果D上不带用户,而专门做汇接这项工作,就称为一个纯粹的汇接局。

所有局间的通话都是通过D的,因而它的压力比较大。在实际应用中,如果 A、B、C之间的网络直接可达,则可以让D仅转发SIP信令,而让RTP流直接在端局之间传递。在上述配置的bridge Action之前增加如下参数可以让D工作在Bypass Media(媒体绕过)模式,仅转发SIP信令,而让RTP媒体流在端局之间传送:

<action application="set" data="bypass_media=true"/>

上述参数是在Dialplan中设置的,因而仅针对当前通话有效。如果想让D对所有通话,不管什么情况都使用Bypass Media,则可以直接在Profile(这里我们使用的是external)中添加如下设置:

<param name="inbound-bypass-media" value="true"/>

当然,理论上讲,除了D之外,其他的端局也可以采用 Bypass Media 技术,让媒体流只在终端用户之间发送,而不经过 FreeSWITCH。但是,一般来说,终端用户可以会位于 NAT 设备的后面,它们之前的媒体流互通并不总是可行的,因而在端局很少这么做。

双归属  ( 实现负载均衡 )

即使采用了 Bypass Media,D的压力还是很大。当然,不一定是电话的压力,更可能是精神上的压力。因为,如果一旦D出现了故障,则 A、B、C 之间的用户就都打不通电话了。

为了解决这一问题,又找到来了E,让它做一个备份的汇接局。并且,把D和E的本地用户都分了一下,放到A、B、C上,让D和E专门做汇接。拓扑结构如图13-3所示。

这样就可以把E上的路由规则配置得跟D上一模一样。但每个端局都同时连接到两个汇接局上。一旦其中一个出现故障,则另一个可以接替工作。这种拓扑方式就称为双归属(每个端局都归属于两个汇接局)。端局的配置如下(仍以A为例,注意,由于“192.168.1”太长了,为了排版方便,我们以“IP.”代替它,读者知道它是一个IP地址就行了):

<extension name="DE">
	<condition field="destination_number" expression="^([B-Z].*)$">
		<action application="bridge" data="sofia/external/sip:$1@IP.D:5080|sofia/external/sip:$1@IP.E:5080"/>
	</condition>
</extension>

这里我们修改了呼叫字符串,增加了使用“|”连接起来的两个呼叫字符串。它的意思是,如果第一个呼叫不成功(到D的),则使用下一个呼叫字符串(送到E上)。这种方式就称为主备用模式。

很容易看出,上述配置虽然解决了D的精神压力,但是,它实际的话务压力还是很大的。因为,只要它不出故障,E就没事干,显然很不公平。所以,实际的双归属汇接局大部分是以话务负荷分担的方式进行的。所谓负荷分担,就是平时在端局将50%的通话送到D上,50%通话送到E上,让两端的负载差不多。一旦其中一台发生故障,则所有的通话都送到另外一台上。负荷分担的算法有很多,这里我们可以使用如下Dialplan实现(以A上的出局路由为例):

<extension name="DE">
	<condition field="destination_number" expression="^([B-Z].*[13579])$">
		<action application="bridge" data="sofia/external/sip:$1@IP.D:5080|sofia/external/sip:$1@IP.E:5080"/>
	</condition>
</extension>
<extension name="ED">
	<condition field="destination_number" expression="^([B-Z].*[24680])$">
		<action application="bridge" data="sofia/external/sip:$1@IP.E:5080|sofia/external/sip:$1@IP.D:5080"/>
	</condition>
</extension>

通过上述配置,

  • 根据正则表达式 ^([B-Z].*[13579])$,所有被叫号码以奇数结尾的都优先送到汇接局D上(如果D失败仍然会送到E)。
  • 同理,所有以偶数结尾的被叫号码都会优先送到E上,如果失败后则尝试D。

这就实现了一个简单的 50% /50%负荷 分担策略。其他类似的策略可以使用随机数实现,也可以使用 mod_distributor(FreeSWITCH中的一个模块)中提供的方法实现。当然,实际应用中需要考虑的事情还有很多。

长途局

一般来说,一个地级市的电话网络有两个汇接局就够用了。如果需要在不同的地级市间打长途电话,上面需再建设长途局,以便与其他地级市的长途局互通。

长途局也是采用成对配置的方式,汇接局跟长途局之间也采用双归属。另外,如果某地级市到另外一个地级市的话务量比较高,也可以从汇接局直接向另外一个地市的长途局开中继并设置高速直达路由(如E和t1之间),它可以避免占用本地长途局的资源。当然,极少数情况下也可能从本地端局直接向另外一个地级市的其他局(如汇接局,C和d之间)开通高速直达中继。

有了长途局,就有了长途区号。假设长途局 T1 的长途区号就是 T1,按习惯使用0作为国内长途字冠,那么,如果端局a上的用户1000呼叫端局A上的用户1000时,它要拨的号码就是0T1A1000。这样一通电话经过的各交换机的号码分析和路由,最后走的路径是:1000 ---> a ---> d(或e) ---> t1(或t2) ---> T1(或T2) ---> D(或E) ---> A ---> 1000。

具体的配置方式就不多讲了。总之,电话网就是这样以树状和网状的结构向外延伸,最终到达世界的每一个角落。电话网内所有的用户也都可以通过一定的拨号规则打通其他的用户的电话。

ACL

在生产环境中,只考虑把电话接通还是不够的,还得考虑安全性。上面的方法只使用 5080 端口从public Dialplan 做互通,而发送到 5080 端口的 INVITE 是不需要鉴权的,这意味着任何人均可以向它发送 INVITE 从而按你设定的路由规则打电话。这种方式用在端局上问题可能不大,因为你的public Dialplan 仅将外面的来话路由到本地用户。但在汇接局模式下,你可能将一个来话再转接到其他局去,那你就需要好好考虑一下安全问题了,因为你肯定不希望全世界的人都通过你的汇接局免费的往各端局打电话。

为了防止这个问题,我们在汇接局上关闭 5080 端口,而让所有来话都送到5060端口上(internal Profile)。5060端口上的来话是需要先鉴权才能路由的。在这种汇接局模式中,一般会使用 IP 地址鉴权的方式。而 IP 地址鉴权就会用到 ACL。

ACL(Access Contorl List)即访问控制列表,它通过一个列表矩阵来控制哪些用户可以访问哪些资源。在 FreeSWITCH 中,实现了基于ACL的鉴权(当然,ACL不仅用于SIP鉴权,还可以用在其他地方)。

其中,internal Profile 默认使用 "domains" 这个ACL进行鉴权。读者可以在 internal.xml 配置中找到如下的配置:<param name="apply-inbound-acl" value="domains"/>

上述配置说明,当收到呼叫(INVITE)请求时,要查看“domains”这个ACL,看是否允许来源IP地址进行呼叫。ACL 是在conf/autoload_configs/acl.conf.xml中配置的,其中 domains 的默认配置如下:

其中,第1行说明该列表项的名称是“domains”,可以在其他地方引用,默认(default)的规则是拒绝请求(deny)。其他几行的的含义我们就不在这里介绍了。

如果我们想在D上允许来自A、B、C的呼叫,就可以把A、B、C的地址加到上述配置里面,如可以添加以下配置,以允许(allow)来自这些IP的呼叫

<node type="allow" cidr="192.168.1.A/32"/>
<node type="allow" cidr="192.168.1.B/32"/>
<node type="allow" cidr="192.168.1.C/32"/>

通过设置上述ACL,我们就保证了只有授权的用户才能从D上进行路由(当然要记得我们关闭了5080端口,因此所有呼叫要送到5060端口上)。

FreeSWITCH 作为 PBX

图网络结构中,,假设A、B、C一层的都是运营商提供的端局交换机,最底层的1000、1001等是端局上的用户。如果再向下延伸,我们可以在端局用户这一层再搭建自己的PBX,下面再挂分机用户。

普通的PBX设置

FreeSWITCH 的配置就是一个全功能的 PBX。假设我们有了A上1000这个用户账号,我们以前是接了一个SIP软电话往外打电话的。现在,我们把它换成FreeSWITCH(我们称为F),并且在FreeSWITCH上创建 600~619 这20个用户,这时候的拓扑结构如图所示。

因为F是作为A上的一个用户(1000)存在的,所以它只能作为一个普通用户向A去注册。对于A而言,它就认为F是一个普通的SIP电话客户端(或软电话)。

为了能使F能向A注册,我们在 F上添加一个网关(gateway)指向A,配置( FreeSWITCH/conf/sip_profiles/external/example.xml )如下:

<include>
	<gateway name="gw_A">
		<param name="realm" value="192.168.1.A"/>
		<param name="username" value="1000"/>
		<param name="password" value="1234"/>
	</gateway>
</include>

添加完配置后,就有了一个名称为 gw_A 的网关。本地的用户 600~619 就可以通过如下的 Dialplan 拨打外部的电话了( 假设PBX的出局码为0):

<extension name="ga_A">
	<condition field="destination_number" expression="^0(.*)$">
		<action application="bridge" data="sofia/gateway/gw_A/$1"/>
	</condition>
</extension>

该 Dialplan 的含义就是遇到以0开头的被叫号码,“吃掉”0,然后将电话送到 gw_A 定义的网关上。

  • 600  --->  F(1000)  --->  A(1000~1019) :假设分机 600 想拨打 A 上的 1001,就可以直接拨打 01001,600 的SIP客户端将向 F 发送 INVITE(呼叫01001),F 再向 A 发送 INVITE(1001),对于 A 而言,F 到底是用的 FreeSWITCH 还是用的 SIP 软电话都是一样的。在1001 上收到呼叫以后,看起来也是从 1000 打过来的(来电显示的号码是 1000 )。
  • 600  <---  F(1000)  <---  A(1000~1019) :如果A上的1001想呼叫600,则它必须先呼叫1000,因为600这个号码是无法直达的(如600这个人的名片上可能印着“电话:总机(A)1000转600”),然后F上会启动一个IVR,让1001输入一个分机号,并为其转接到相应的分机号。

号码 透传(穿透)

一般来说,端局A不允许主叫号码透传,即不管F上的哪个分机往外打电话,都会在对方的话机上显示1000这个主叫号码。当然,也可以设置允许1000往外打电话时进行主叫号码透传。只需要在A上找到 1000 这个用户的配置文件(1000.xml),将下面的行注释掉就可以了。

<variable name="effective_caller_id_number" value="1000"/>

其中,effective_caller_id_number 就表示1000这个用户如果发起呼叫时对外显示的号码是什么,默认的设置就是1000 (就是从F来的所有呼都变为是1000这个用户发起的)。注释掉该项后,就会根据实际的来电号码对外进行发送。

那么,如果 600 发起呼叫时,在A上看到的实际的来电号码是什么呢?首先,600发起呼叫时,呼叫到达F,F查找600的用户配置文件,应该能找到类似的如下的配置,

<variable name="effective_caller_id_number" value="600"/>

因此F将对外发送主叫号码600,呼叫到达A后,它看到F送过来的主叫号码是600,而 1000 这个用户又没有设置 effective_caller_id_number 这个参数(从F来的所有通话均认为是从1000来的,刚才该参数已经被我们注释掉了)便可以对外显示这个600,因而 1001 话机上就显示了600,实现了电话号码的透传。

可以看出,实现电话号码透传本身是很简单的事情。所以它本身不是什么技术问题,而在实际应用中主要是面临一个现实问题,那就是如果所有人都可以做任意透传电话号码,那么所有人都可以任意冒充任何人打电话(试想一下有人冒充110或国务院的电话的情况)。

号码 透传(穿透) 之 回呼

电话号码透传带来的另一个问题就是回呼。在上述情况下,如果 1001 收到 600 来的呼叫,显示的主叫号码是 600,显然这个号码是无法回呼的,即 1001 无法通过这个号码直接呼通 600,因为600 这个号码理论上讲对于 1001 所在的交换机A来说是不存在的。它最多只能呼通到1000。

为了解决这个问题,先把600呼出时对外显示的主叫号码变换一下,下面的代码即 Dialplan 将 600的主叫号码变换为 1000600:

<extension name="ga_A">
	<condition field="destination_number" expression="^0(.*)$">
		<action application="set" data="effective_caller_id_number=1000${caller_id_number}"/>
		<action application="bridge" data="sofia/gateway/gw_A/$1"/>
	</condition>
</extension>

至于为什么这么变换,后面给出答案,并给出回呼解决方案。

DID ( 动态改变呼叫号码 )

对于F而言,号码 1000 就是一个 DID。因为在A上的其他用户都可以通过拨打 1000 这个号码打到F上,其他交换机如 B、C 上的用户也可以通过拨打 A1000 呼叫到 F上。当然,其他用户如果想呼叫F内部的分机,就只能先拨叫1000这个DID号码,然后,再通过 IVR,以人工总机或自动总机转接的方式转到对应的分机上。那么能不能直接呼叫到内部的分机呢?

要实现这个在技术上当然是可以的。来考虑这样一种拨号方案:对所有A上的用户而言(如1001),如果想直接呼叫F上的内部分机600~619,就需要呼叫1000加上这些分机号。如1001呼叫F上的600,则需要呼叫1000600。

上面将600对外呼出的主叫号码设置成了可以对外显示1000600,因此只要能在A上设置正确的回呼路由,电话就应该能呼通了。

确定了拨号方案以后,再来看在A上的实现。为了要让A上其他用户打1000开头的电话号码都送到F上,我们需要在A上增加一个Dialplan项,于是我们很快想到了如下的Dialplan:

<extension name="F-DID">
	<condition field="destination_number" expression="^(1000.*)$">
		<action application="bridge" data="..."/>
	</condition>
</extension>

其中,正则表达式 ^(1000.*)$ 表示匹配任何以1000开头的电话号码。如果匹配到这样的电话号码,则将电话桥接到我们希望的地方。可是到这里,bridge后面的参数怎么写呢?我们知道这里要给一个呼叫字符串,但是这里的情况不同于上面讲到的任何一种情况,呼叫字符串该怎么写呢?

首先,看一下这里的情况与上述讲的有什么不同。在上面的各种情况中,各FreeSWITCH之间的对接都是通过IP方式的,即一般来说各FreeSWITCH之间都互相知道对方的IP地址,也就是说各FreeSWITCH的IP是相对固定的。而在这里,F的IP地址对A来说是不固定的。因为F是以动态注册的方式注册到A上的,因而只有F向A注册的时候A才能知道F的IP地址,并且F的IP是可以不断变化的,每次变化后F都会重新向A注册它的IP地址,以让A能找到它。在这种情况下,如果让A能找到F,应该就能在A上动态地获取F的IP地址了。实际上A本来就能动态获取F的IP地址,下面的配置方式我们已经很熟悉了:

<action application="bridge" data="user/1000"/>

它的作用就是在A上找到1000这个注册用户注册时的 Contact 地址(即F的地址),然后向该地址上发送INVITE请求。但在这里,如果还是这样配置的话,很显然不能达到要求。因为使用这种方法获取到的呼叫字符串,在呼叫到达F后,被叫号码(即 DID)永远是1000,而我们想要在有人呼叫 1000606 时把被号码变成 1000606。所以,这里就需要解决两个问题:

  • 1. 在 A 上动态能找到 F 的 IP 地址。
  • 2. 呼叫到达 F 后,被叫号码变成我们指定的被叫号码。

先一步一步来看。当 F 向 A 注册时,它提供了自己的 Contact 地址,可以用以下命令在 A 上查看相关信息(以1000为过滤条件显示注册用户):freeswitch (A) > sofia status profile internal reg 1000  可以发现它并没有显示出1000的注册信息。

这是为什么呢?在回答为什么之前,先来试一下以下的命令,去掉了1000 这个过滤条件,从中可以看到类似如下的输出信息(省略其他不相关的输出):

freeswitch (A) > sofia status profile internal reg

可以看到 User 哪一行的确有一条来自 192.168.1.F 上的用户 1000 的注册信息,不过该用户的注册信息中的 Contact 地址是比较奇怪的。由于它里面没有包含1000这样的用户名,所以在上面想用1000为过滤条件限制输出结果时失败了。

F为什么这么做?F 向 A 注册是以在F中添加一个到A的网关实现,并以 gw_A 这个名字来标志这个网关。F向A注册时使用了这样的 Contact 字符串。注册完成后,如果A上有电话需要呼叫F时,A向F发的INVITE请求如下:

这样当F上收到上面的INVITE请求后,就可以知道该呼叫是从gw_A这个网关来的,这样便于知道呼叫的来源。否则的话,F 便会使用像“1000@192.168.1.F:5080”这样的Contact字符串,可是那样的话它会收到类似“INVITE 1000@192.168.1.F:5080”这样的呼入请求,不便于将呼叫来源与F中本身已定义的gw_A网关关联。

继续看上面的INVITE请求。其中有两个 gw_A。第一个 gw+gw_A 实际上出现在被叫号码的位置,这时候如果F检测到被号号码中有 gw+ 以后,就能找到 gw_A 这个网关了。有时候,在A是其他SIP 服务器的情况下,可能会将该字段放入真正的被叫号码,如“ INVITE 1000@192.168.1.F...”,这样,F就会尝试从后面的 gw=gw_A 这个位置(分号后面都相当于参数)找到 gw_A 这个网关名称。当然,如果A不发送这些参数的话,F就无法与系统中配置的网关进行关联了(不过电话仍然可以通)。F 收到上述的 INVITE 请求后就会在本地的网关 gw_A 中找到真正的被叫号码 1000(从extension参数或username参数中找)。实际上,上述的Contact地址已经包含了F的IP,可以在A的命令行上使用下列命令找到它:

其中,sofia_contact 是一个API命令,它的参数格式为 “Profile/User@Domain”。这里的Profile是internal,User是1000,Domain就是A的IP地址。

下面会用到 echo 和 expand 这两个 API 命令。其中,echo命令会将字符串原样输出,如:

虽然已经知道A上的Domain(即A的IP地址)是192.168.1.A,而且它也是相对固定的。但是,在实际应用中还是用变量引用比较方便一些。默认情况下,在XML中的配置都是会进行变量替换的,但在命令行上不会。如果要在命令行上进行变量替换,就需要用到一个expand API命令:

可以看到由于 expand 的作用,$${domain} 被替换成了实际的值。

可以使用 "${API命令(参数)}" 的形式引用一个API命令的结果。所以上述的 originate 命令后面的参数需要的呼叫字符串就是用 "${sofia_contact()}" 动态获取的。当然,上述的命令是 echo 输出后的结果,如果去掉 echo,就可以直接执行 originate 呼叫1000了:

然后 F 在收到上述请求后,就能找到 gw_A 这个在本地配置的网关,进而找到对应的被叫号码1000,并进行路由。

如果要将被叫号码改为指定的被叫号码(如1000606),只需要想办法改变INVITE请求中的被叫号码部分,即将 sip:gw+gw_A@192.168.1.A:5080变成 sip:1000606@192.168.1.A:5080即可。为了达到这个目的,可以使用正则表达式替换。其中,FreeSWITCH提供了一个regex API命令可以进行正则表达式替换,它的语法是“regex原字符串|正则表达式|替换后的内容”,如:

其中,使用 “^sip:gw\+(.*)” 作为正则表达式匹配就可以将原字符串 “sip:gw+gw_A” 替换为“sip:1000606”,这也是主要需要替换的部分。看一个完整的替换,替换前的字符串为:

可以看到,我们得到了需要的呼叫字符串,接下来就可以去掉上面的“echo”进行呼叫测试了。如果在F的日志中,看到类似“Processing<0000000000>->1000606 in context public”这样的行就说明我们修改被叫号码成功了。

接下来,可以就可以完成 F-DID Dialplan了:

<extension name="F-DID">
	<condition field="destination_number" expression="^(1000.*)$">
		<action application="bridge" data="${regex(${sofia_contact(internal/1005@$${domain})}|^(.*)sip:gw\+xyt@(.*)|%1sip:$1@%2)}"/>
	</condition>
</extension>

注意,与在命令行上不同的是,由于 Condition 条件中的正则表达式匹配中的 Capture 结果已经占用了 $1、$2 之类的变量,因而为避免冲突,在 Dialplan 中使用 ${regex()} 之类的表达式时,其Capture 结果中的变量用 %1、%2 之类表示。

这里演示了一个端局交换机对下面注册上来的PBX中的分机用户的一种直接呼叫方案。虽然该方案不是唯一的解决办法,但它至少是一种有效的解决方案。其他的解决方案原理与此差不多,都是要想办法找到用户注册上来的联系地址,并进行相应的替换。当然,在实际应用中,这种方案是否可行还需要局端交换机A与PBX交换机F相互配合来决定。

使用 PBX 上的网关呼出

另外一种场景。如图所示,假设A是运行在公网上的 FreeSWITCH,而 F 是运行在私网上的 PBX(也是FreeSWITCH),F 仍然使用 1000 这个号码向A注册,并且 F 上自己带了 600~619 之间的分机用户。另外,F上还可以通过另外的一个网关G与外界沟通。在现实场景中,G就可能是一个连接模拟线的模拟网关,该网关一端跟 FS 通过 SIP 相连,另一端则通过模拟电话线连接 PSTN交换机。由于运行在公网上的A可能没有对外的中继,因而它上面的用户1000~1019可能也希望通过 F上的网关 G 对外呼出。

实现这个也很简单。首先就是将A上的外呼请求先转发到F上。可以使用Dialplan配置来做到:

用正则表达式 "^(0.*)$" 来匹配以0开头的被叫号码,然后通过 bridge 将这种呼叫送到 F 上。在 F上 收到这种呼叫请求后,就可以使用下列的 Dialplan 进行呼出了:

<extension name="F-GW">
	<condition field="destination_number" expression="^(0.*)$">
		<action application="bridge" data="sofia/gateway/G/$1"/>
	</condition>
</extension>

7、FreeSWITCH 对接其他设备

FreeSWITCH 支持多种协议,可以对接大部分电信设备或系统。

使用 Doubango 客户端连接

Doubango 是一个不错的开源框架,它跟电信业务走得比较近,主要集中在以3GPP、TISPAN、Packet Cabel、WiMax、GSMA、RCS-e、IETF等为标准的NGN技术、音视频处理技术、云计算以及WebRTC技术等中。该框架使用ANSI C编写,具有很好的可移植性。在很多平台上都有基于Doubango的客户端实现,如Windows上的Boghe、Mac上的iDoubs以及Android系统上的IMSDroid等。

对接 IMS

IMS 是新一代通信网络事实上的标准,它旨在在过渡阶段兼容原有的PSTN TDM网络(基于电路交换的网络,在IMS中称为CS域),并最终将所有的业务都转移到基于包交换的网络(PS域)中。

目前,国内的运营商都部署了IMS。IMS网元众多,系统各网元之间通过高速网络相连。就与交换最相关的核心功能来讲,在IMS内部,包含P-CSCF、I-CSCF、S-CSCF及AS等功能与实体。其中P-CSCF位于网络的边缘,接收SIP消息,并将SIP消息转发到内部的S-CSCF及AS上。

一般来说,IMS运行在运营商内部专门的网络上(称为承载网),而在外部是无法接触到运营商的内部网络的。如果要通过互联网与运营商的IMS对接,就需要通过SBC设备及层层防火墙。SBC设备横跨运营商的承载网及互联网。所以,一般来说,我们可以通过SBC连接到IMS,进而能够与PSTN网络上所有的电话进行通信。所以我们可以把SBC设备和IMS看成一个SIP转PSTN的“大”网关。实际上,在实际使用中,大家也不关心是否有SBC设备,而只认为运营商有一个“大大”的SIP服务器,它可以给我们开放账号。

目前,有些运营商也开始在IMS系统上针对一些话务批发商或企业应用放号,因此如何使用FreeSWITCH与其对接就是我们需要研究的了。

与IMS对接最好的方式是使用SIP中继方式对接,配置简单使用起来也灵活。但很少有运营商允许你这么做。比较典型的方式还是使用类似电话号码的方式,一个账号一个账号地对接。

下面是一个与云南移动的IMS使用账号对接的实例。笔者拿到的是一个试验号码,实际上,对我们来讲,它就是一个SIP服务器上的账号。为了与其对接,我们要将FreeSWITCH当作一个SIP客户端注册到IMS上去。在FreeSWITCH中,这可以通过添加一个网关实现。下面是一个网关的配置文件:

<gateway name="ims">
	<param name="realm" value="ims.yn.chinamobile.com"/>
	<param name="register-proxy" value="211.139.x.x"/>
	<param name="username" value="+86871xxxxxxxx@ims.yn.chinamobile.com"/>
	<param name="password" value="1234"/>
	<param name="from-user" value="+86871xxxxxxxx"/>
	<param name="from-domain" value="ims.yn.chinamobile.com"/>
	<param name="register" value="true"/>
	<param name="outbound-proxy" value="211.139.x.x"/>
</gateway>

在网关的配置参数中:

  • ims.yn.chinamobile.com为云南移动内部的域。在公网上,它并不是一个合法的域名。那么我们怎么能注册到该服务器上去呢?实际注册的服务器地址是用 register-proxy 定义的,这里211.139.x.x 是一个 SBC 的地址。以前,我们在配置网关时都不写这个register-proxy参数,它默认就等于realm。
  • username是IMS中的用户账号,它和password配合用于鉴权(前者将出现在SIP消息中的Authorization头域中)。该用户账号中包含了用户所属的域,其用户名部分是一个PSTN网络中的E164格式的电话号码,前面几位中的“+86”表示中国的国家代码,“871”是云南昆明的区号,后面的“xxxxxxxx”则是一个本地的电话号码。
  • from-user指定在SIP消息中的源用户信息,from-domain则是指定域,它们会影响SIP中的From头域。
  • register的值为true表示FreeSWITCH会向该网关发起注册。
  • 前面的信息都是与注册相关的,最后的outbound-proxy表示呼叫(即INVITE消息)应该发到什么地址,它可以是与注册服务器不同的地址,不过在本例中它与register-proxy是相同的。

当然,上述这些参数的值都是在IMS系统上配置的。从IMS上取得正确值并进行配置后,我们就完成了从FreeSWITCH注册到IMS,这时就可以对外打电话了。

通过 IMS 呼出

完成上一节的配置后,我们可以很快地在FreeSWITCH中输入以下命令试一试它是否真的能打到我们的手机上:originate sofia/gateway/ims/0186xxxxxxxx &echo

电话接通后,应该能听到自己的声音(回音)。注意,上述账号就相当于一个云南昆明本地的电话号码,因而拨云南以外的手机号需要加“0”。

在Dialplan中,进行如下设置就可以通过上面的网关往外打电话了:

<extension name="IMS gateway outbound">
	<condition field="destination_number" expression="^(0.*)$">
		<action application="bridge" data="sofia/gateway/ims/$1"/>
	</condition>
</extension>

当然,如果运营商给我们开的账号支持号码透传的话,我们就可以透传任何号码了。比如下面的代码,透传了一个美国的911出去(注意,最好不要透传110):

<action application="set" data="effective_caller_id_number=911"/>
<action application="bridge" data="sofia/gateway/ims/$1"/>

其实,有意思的是,有的运营商开的账号默认就是支持透传的,这样就会将FreeSWITCH内部的分机号(如1000)透传出去。为了能对所有呼出的电话都显示运营商给我们分配的号码(这里的“xxxxxxxx”),我们可以使用如下Dialplan设置:

<action application="set" data="effective_caller_id_number=xxxxxxxx"/>
<action application="bridge" data="sofia/gateway/ims/$1"/>

也可以使用如下设置:

<action application="bridge" data=" {origination_caller_id_number=xxxxxxxx}sofia/gateway/ims/$1"/>

通过 IMS 呼入

在我们正确向IMS注册后,这种号码也支持呼入。如果有人呼叫该号码(即这里的“xxxxxxxx”),IMS就会给我们的FreeSWITCH发SIP INVITE消息。我们的FreeSWITCH收到该消息后,就可以在FreeSWITCH中进行路由了。一般来说,呼入的DID是不带区号的本地号码,就是上面网关配置中的“xxxxxxxx”部分,然后,我们就可以设置如下Dialplan将来话路由到一个IVR:

<extension name="IMS gateway outbound">
	<condition field="destination_number" expression="^(xxxxxxxx)$">
		<action application="answer" data=""/>
		<action application="ivr" data="demo_ivr"/>
	</condition>
</extension>

当然,如果我们很神奇地从运营商那里获得了一个号段(如一个千群或一个万群),也可以把这个号码与我们内部的分机号做成一对一的关系。如,下面Dialplan设置可以将来话DID的后4位与我们内部的分机号一一对应:

<extension name="IMS gateway outbound">
	<condition field="destination_number" expression="^xxxx([0-9]{4})$">
		<action application="answer" data=""/>
		<action application="bridge" data="user/$1"/>
	</condition>
</extension>

与这种IMS对接的难点就是调试比较困难。一般来说,也很难找到对端真正懂技术的技术人员配合。所以,在对接遇到问题时,解决的方法基本上就是抓包,看SIP消息,然后瞎猜,把所有可能的参数都试一遍。

在一次与IMS的对接测试中,发现主叫听不到被叫方的回铃音。经过无数次的探索,终于发现,对方需要一个特殊的SIP消息头“P-Early-Media:supported”。该消息头是在RFC5009 [1]中定义的。在FreeSWITCH的Dialplan中,可以使用“sip_h_”开头的通道变量添加扩展的SIP消息头,实现添加上述SIP消息头的Dialplan设置如下:

<action application="bridge" data="{sip_h_P-Early-Media=supported}sofia/gateway/ims/$1"/>

一般来讲,SIP中的“1xx”响应消息是不需要证实的,但在IMS系统中,大部分情况下183消息也是需要证实的,否则可能无法听到正常的回铃音甚至无法正常接续。与普通的证实消息ACK不同的是,对于“1xx”的消息需要使用PRACK [2](即Pre-ACK)消息证实。通过在Profile中开启如下参数可以让FreeSWITCH在收到183时发送PRACK证实消息:

<param name="enable-100rel" value="true"/>

通过这些配置,FreeSWITCH就可以完美地与IMS系统对接并进行呼入呼出了。

模拟话机、模拟中继线

在实际应用中,不可避免地要连接模拟话机。这里说的模拟电话机就是我们在家里或公司中常见的普通电话机。当然,在没有FreeSWITCH之前,我们的电话机就是通过一根普通模拟电话线连接出去的,并通过这条电话线往外打电话。

现在,我们有了FreeSWITCH,那我们就会想到两个问题:

  • 能不能把模拟话机也连接到FreeSWITCH上?这样模拟话机就可以与我们的SIP软电话(或硬电话)通话了。
  • FreeSWITCH能不能通过模拟电话线也往外打电话,甚至别人也可以通过与该电话线对应的号码呼叫我们?

答案是肯定的。

FXS和FXO

普通电话机是模拟的,通过一根模拟线连接到距离最近的电话交换机上。根据所处位置的不同,这个交换机可能是处在运营商机房的交换机,也可能是运营商设在用户小区的一个模块局中的交换机,还可能是本企业自己建设的用户小交换机。近几年,为响应国家光进铜退的号召,有些运营商就直接将光纤拉到用户家里,通过一个“小盒子”(因特网接入设备,称为IAD)将光纤接口转成普通的以太网口和模拟电话接口,然后再在模拟电话接口上接电话机。总之,普通电话都要通过一根电话线连接到某个设备上。该设备提供一个模拟电话线的接入端口,在技术上,称为FXS口。同时,电话机上也有一个模拟线的接口,称为FXO(在物理上,就是一个RJ11接口 )。FXS和FXO的连接方式如图14-2所示。

从常识来讲,我们知道,电话线是带电的,即使家里停电了也可以打电话,如果不小心摸到裸露的电话线也会有“触电的感觉”;而话机本身是不带电的,直接在话机上接上电话线并也不会感觉到有电 。由此我们可以联想到, FXS口是带电的,而FXO口是不带电的。这也是两者的根本区别。

实际上,FXS 提供的是 –48V 的直流电,主要是为了能感知话机摘挂机的变化、给话机提供拨号音、振铃及其他信号音等。在摘机状态下,电压将降至 [3]–7V,而在振铃状态下电压可能增大至–90V。也可以这样认为,FXS与FXO的区别是: 前者能提供拨号音。

在实际使用中不要将两个FXS口用电话线连接在一起,否则两者互相供电,后果是不堪设想的。

拓扑结构

有了上述基本概念以后,构建拓扑结构如图所示:FreeSWITCH提供一个FXO接口用于连接原来的交换机;另外提供一个FXS接口用于连接原来的模拟电话。其他的SIP话机也可以通过以太网(Ethernet)口接入FreeSWITCH 以实现互相通信。

但是 FreeSWITCH 是一个软件,不能提供FXO、FXS硬件接口。有以下两种方案:

  • 在FreeSWITCH所在的服务器上安装相关的硬件板卡,该硬件板卡负责提供连接FXO及FXS的硬件接口。FreeSWITCH就可以通过相应的驱动程序去控制这些板卡。因而相当于给FreeSWITCH增加了FXO及FXS的接入能力。这类板卡中比较有代表性的有Sangoma及Diguim生产的模拟和数字板卡等。国内也有许多厂商生产所谓的“Asterisk兼容卡”,它们有的(如OpenVox)也能与FreeSWITCH搭配使用。
  • 通过外部网关来实现。有些厂商针对这一问题专门生产了支持SIP到模拟线的转换网关,称为模拟网关。对于FreeSWITCH来讲,这种网关就是一个普通的SIP UA;而对于模拟话机来讲它就是一个电话交换机;对于电话交换机来讲,它就相当于一个普通模拟话机。

还有其他解决方法,没有标准的答案。如果有的话,也是具体问题具体分析。不过,一般来说,建议:对于模拟线路,选用第二种方案,因为这些网关设备很容易买到,而且坏了可以随时更换,兼容性也比较强;而第一种方案中支持这些板卡的驱动往往需要编译内核等,操作起来比较复杂。后面讲到用于数字线路的数字板卡时则可以考虑使用第一种方案,性价比和可控性会高一些。

增加了网关后的拓扑结构如图所示:

在实际应用中,有单纯的FXS网关及FXO网关,端口数从1口、2口到几十口不等。也有的网关是FXS和FXO混合装在同一设备上的,根据实际需求可以选用不同的网关。

使用潮流网关连接模拟话机

潮流网络公司有一款型号为HT701的单口模拟网关,它有一个FXS口和一个以太网接口,FXS口用于连接话机,以太接口用于通过以太网连接FreeSWITCH。如果把它和与之相连的模拟话机看成一体的话,实际上就相当于一个SIP话机。也可以说,这款模拟网关能把普通的模拟话机“变”成SIP话机。该网关小巧方便,比较适合在桌面上使用。该网关有一个简单的Web配置界面

使用迅时网关连接模拟话机和模拟中继线

通过 E1 线路与其他系统对接

上面是通过 IMS 或模拟中继线连接PSTN网络的例子。但很多时候,我们还是需要使用E1与其他系统对接。很典型地,某些运营商可能只提供E1线路,或者某些设备只提供E1接口。

对接 Asterisk

Asterisk是老牌的开源VoIP软件。事实上,FreeSWITCH的作者Anthony Minessale在早些年也是Asterisk的开发者,后来由于对Asterisk的架构设计和性能问题有意见,提议开发了一个新的Asterisk分支,但是开发新分支的想法却未得到Asterisk社区主要负责人的支持,因此便从头开发了FreeSWITCH。

由于Asterisk比FreeSWITCH资格老,因此在国内有大量的用户群,在很多情况下都需要与FreeSWITCH进行对接。

使用中继方式(在Asterisk中称为peer,即对等的连接方式)与Asterisk进行对接

8、技巧、实例

FreeSWITCH 功能非常多,还有一些常用的功能,如企业通信中常用的电话转接、代接、以及生产环境中关心的压力测试、安全等话题

转接、代接

来电转接和代接是企业PBX中的常用功能。来电转接分为盲转(Blind Transfer)和协商转(Attended Transfer)两种

顾名思义,盲转就是将来电直接转到某一分机。该业务一般用于电话已经接听的情况,举例来说,转接的步骤一般是这样:A呼叫B,B接听,A与B通话,A要求转C,B通过一个操作将来电转接到C,C开始振铃,B挂断,C接听,A与C通话。

FreeSWITCH 默认的 Dialplan 实现了这种转接,方法是B在通话中按“*1”两个键,听到拨号音后,拨叫C的号码,B挂机,A与C通话。

在默认的 Dialplan 中的 Local_Extensions中可以找到:

<action application="bind_meta_app" data="1 b s execute_extension::dx XML features"/>

首先,有电话打入后,如果是最终路由到一个内部分机,就会执行到这一行。至此,来话是a-leg,现在还没有b-leg。

bind_meta_app是一个App,它会在本次通话上绑定参数中指定的“1”这个按键,这个可以后续可由“*1”两个键激活。注意,上述代码中,“1”后面的“b”表示这个功能要绑定到b-leg上,虽然b-leg现在还没有。但是到有了以后,b-leg可以按“*1”激活该项功能,而不是a-leg。想想为什么?

接着说,“b”后面的“s”表示什么呢?它表示,如果b按键激活该功能后,要在哪条腿上执行这个动作。这里的s表示same,就是说,在哪条腿上接收到按键就在哪条腿上执行。

后面的“execute_extension”表示去Dialplan中找一个extension去执行一下,这里指到XML features(它是一个Context)中去找dx这个extension。在默认的配置中可以在features.xml文件中找到。

总之,这一行执行完毕后只是相当于设了这么一个“套”,指出后面该怎么执行,直到b-leg按“*”才能激活该功能。接下来,Dialplan最终会执行到后面的bridge(user/B),A与B桥接并开始通话。

通话后,b-leg 就有了,它就是用户B所在的那条腿。在通话中,B按下“*1”,就会执行到“dx”,我们到features.xml中去看具体的实现:

<extension name="dx">
	<condition field="destination_number" expression="^dx$">
		<action application="answer"/>
		<action application="read" data="11 11 'tone_stream://%(10000,0,350,440)' digits 5000 #"/>
		<action application="execute_extension" data="is_transfer XML features"/>
	</condition>
</extension>

这里的answer只是保证电话是应答的状态。后面的read表示等待用户按键,也就是等待用户输入一个分机号(如C的号码)。参数“11 11”是自动收号位数,表示最少接收11位,最大接收11位。如果在收号近程中用户按了“#”(最后一个参数)号,则会立即停止接收(而不管最小和最大收号位数)。tone_stream会产生一个拨号音,以提示B可以拨号了。B拨完号之后,拨号的数据就存到了digits这个通道变量中。电话流程继续往下执行,接着又是execute_extension,它的参数is_transfer是另一个extension,我们可以在同一个文件中继续往下找到,其内容如下:

<extension name="is_transfer">
	<condition field="destination_number" expression="^is_transfer$"/>
	<condition field="${digits}" expression="^(\d+)$">
		<action application="transfer" data="-bleg ${digits} XML default"/>
		<anti-action application="eval" data="cancel transfer"/>
	</condition>
</extension>

可以看到,到了“is_transfer”以后,通过正则表达式“^(\d+)$”判断收到的数字是不是合法的号码,如果是,则执行transfer App进行转移。

先说上述正则表达式不匹配的情况,即用户B输入的不是一个合法的分机号,那么Dialplan就会执行

到“anti-action”一行,取消本次操作。最终结果就是——B在听了一会拨号音并瞎按了半天后继续与A通话。

再说B按对了C号码的情况。执行到transfer App后,该App的参数中有一个“-bleg”,它什么意思呢?实际上,它的意思并不是绝对的b-leg,而是一个相对的概念,即与当前那条腿桥接的另外一条腿。如果这么说还不明白,可以这样考虑:现在是B做了这么多操作,因而这些操作都是在原来的b-leg上执行的。而transfer这里是要把A转走,由于当前的是b-leg,再加上“-bleg”后就相当于“负负得正”,因而意思是把a-leg转走。转到哪儿呢?转到“digits”变量所代表的值,即刚才B输入的C的号码。

这时C开始振铃,如果C接听,A就可以与C通话了。同时,A被转走后,B就没事干了,电话自己就挂掉了。

当然,上面绕了这么一大圈才完成这个转接功能,看起来挺复杂的。实际上,聪明的读者也许已经注意到了,在“dx”那个extension中有这么一句注释:

<!-- In call Transfer for phones without a transfer button -->

它的意思是,该功能是给那些没有Transfer键的话机用的(也就是一般的模拟话机,由于没有Transfer按键,因此一般需要靠DTMF按键执行这些功能)。对于SIP话机而言,通常都会有一个Transfer键,一按就转了,很方便。那么这是怎么实现的呢?答案是SIP REFER。

REFER是在RFC3515 [3]中定义的,它规定了SIP中电话转接的几种实现方式,具体协议流程的实现跟具体话机

终端有关,图15-1描述了一种典型的实现方式。

首先A与B已建立通话,这时候B想把A转接给C。这里B称为Transferor,它是转接的发起者;而A称为Transferee,它是被转接的一方;C称为Target,是转接的目的地。转接成功后A与C通话。

B首先发re-INVITE请求给FS(FreeSWITCH),请求将B的电话置为Hold(保持)状态,FS收到请求后就给A播放保持音乐。同时,B的话机放拨号音,以提示用户输入被叫号码。B输入C的号码后,B给FS发REFER请求。FS收到后会释放B,并同时呼叫C。如果C正常接听,则A与C通话,转接完成。

协商转

这里可能会发现一个问题:在上述盲转的情况下,如果C长时间不接听(久叫不应)或C占线,则转接会失败,A的电话会被挂断。A可能会重新呼叫B要求转接,这对A的体验是很不好的。

FreeSWITCH通过att_xfer这App支持协商转。在默认的拨号计划中也是可以实现的,与盲转不同的是,在进行转移时,B按“*4”激活“att_xfer”功能,配置如下:

<action application="bind_meta_app" data="4 b s execute_extension::att_xfer XML features"/>

同样,我们在features.xml中找到“att_xfer”条件:

<condition field="destination_number" expression="^att_xfer$">
	<action application="read" data="3 4 'tone_stream://%(10000,0,350,440)' digits 30000 #"/>
	<action application="set" data="origination_cancel_key=#"/>
	<action application="att_xfer" data="user/${digits}@$${domain}"/>
</condition>

它照样使用read播放拨号音并等待3~4位按键,如果输入正确,则使用att_xfer这个App处理呼叫转移。att_xfer会呼叫C的号码,同时让A听等待音乐。如果呼叫C失败,则B仍然可以与A通话;如果C长时间不应答,则B可以按“#”(由origination_cancel_key设置)号键取消呼叫,继续与B通话;如果C接听后,B与C通话,此时B可以询问C是否愿意接听电话 [4],如果C不愿意,则C挂机,B仍然可以跟A通话;如果C接受通话,则B挂机,A与C通话;如果B不挂机,并按3,则可以形成三方通话,大家一起说。还有更有意思的,B还可以随时按1与A通,按2与C通,而让A与C永远不通……上述功能是在FreeSWITCH中通过DTMF按键实现的。某些话机支持多路通话,因而可以在话机端(通过Refer)实现协商转。典型的,话机终端B可以把第一路电话置于Hold状态,然后再发起另外一路通话到C,C接听后B可以任意切换与A和C之间的通话,并可以通过本地会议桥进行混音以支持三方通话(也叫会议)。

此时B如果想退出A与C的通话,则可以发送REFER消息,让服务器把通话中的B替换为C。该消息与盲转不同,它带了Replaces参数:Refer-To: sip:1002@192.168.1.118?Replaces=1388923627@192.168.1.110;to-tag= NDj261X80jpKF;fromtag=1013380895>

为了阅读方便,上面的消息是经过“urldecode”后的,实际的消息内容是用“urlencode”编码的字符串。它是这样产生的:A(1000)呼叫B(1004),此处B是亿联话机,B接听后,按下话机上的Conf软键(代表Conference,会议)然后呼叫C(1002)。C接听后,B与C通话。B可以通过话机上的Swap按钮进行切换并与A或与C通话,也可以再次按Conf键启用本地混音实现A、B、C三方的电话会议。与我们上面讲的例子不同,上面的例子三方通话是由att_xfer实现的,参与方只有三方,即三个Channel;而本例中是4个Channel,因为B话机同时有两个Channel连接到FreeSWITCH。

如果B想退出会议,但保持A与C的通话,则B发送REFER消息让FreeSWITCH把它替换掉,即让FreeSWITCH把A与C桥接起来,释放掉与B的两路通话。

完整的REFER消息如下:

代接

代接是指(别人给A打电话时)A电话振铃后,在B话机上进行接听(代替A来接听)。一般用于办公室中某工位上没人其他工位上的人代为接听的场景。

FreeSWITCH默认的Dialplan中就有与代接相关的例子。其中,886为全局代接。即当有分机振铃时,在另外的话机上直接按886就能接听,同时原先振铃的话机结束振铃。“*8”为组内代接,也就是同组代答,即在上述情况下按“*8”只能代接本组内的正在振铃的分机。以上两种方式在有多个分机同时振铃时只能接听最后振铃的那一个。此外,还有一个“**”前缀码,拨打“**”加上指定的分机号就能直接代接指定分机,如拨“**1001”就可以接听正在振铃的1001分机上的电话。

代接是使用intercept App实现的,具体的Dialplan我们在此就不详细解释了,有兴趣的读者可以参考默认的Dialplan(dialplan/default.xml)中的相关内容(搜索intercept),结合我们本书中讲过的知识是很容易理解的。

共享线路呈现

共享线路呈现(Shared Lines Appearence,SLA)也是企业应用中一个非常有用的功能。该功能在模拟话机中是没有的,只有SIP话机才能实现该功能。使用该功能可以在自己的话机上监视其他话机的状态,从而知道另一个电话是否处于忙或闲的状态。比如说在老板-秘书的场景中,如果有人打秘书的电话想找老板,而秘书转电话时碰巧老板的电话正在占线,就会导致转接不成功,耽误时间。而如果秘书事先知道老板的电话是否在忙,就可以直接判断是否要将电话转给老板,或者告诉主叫用户先等一会。FreeSWITCH就支持这种功能。在使用时应确认Profile中的设置开启了以下两项:

<param name="manage-presence" value="true"/>
<param name="manage-shared-appearance" value="true"/>

下面以Seven Du(607)和Sonic Gao(608)为例。607使用XLite注册。注册完毕后通过菜单项“Contacts”→“Add Contact”添加一个联系人608,并确保开启了Presence功能,如图15-2所示。

使用组播功能做网络广播

在某些特殊的场景下,拿起电话拨打一个号码,就可以对一大群人喊话,广播功能可以大大提高工作效率。

广播功能可以使用会议实现,简单地发起N路通话加入一个会议也可以做到广播的效果。不过,那样实现要建立N路通话,需要消耗很多的网络资源;另外,也无法保证对方能及时接听,影响信息的送达。实现该业务模式最经济的方式就是使用组播(Multicast,或称多播)。组播只向组播地址发送一个RTP流,而监听该组播地址的所有主机就都能收到。

FreeSWITCH有一个mod_esf(Extra SIP Functionality)模块,它提供一个esf_page_group的App可以支持组播。

esf_page_group有三个参数,分别是:

  • ·组播地址,默认为224.168.168.168。
  • ·端口号,默认为35467。
  • ·控制端口号,默认为6001。

要能收到组播包并播放声音,也需要配置亿联话机以启用这项功能

该配置项比较难找,依次找到菜单“电话簿”→“组播”,在第一个IP地址框里填入224.168.168.168:34567,标签栏可以任意填,起一个好记的名字就行。

在FreeSWITCH默认的配置中,拨打号码7243就直接向该地址发送组播,所以现在你可以拿电话拨打7243试一下了。

默认的Dialplan配置如下:

<extension name="rtp_multicast_page">
	<condition field="destination_number" expression="^pagegroup$|^7243$">
		<action application="answer"/>
		<action application="esf_page_group"/>
	</condition>
</extension>

如果你想发到其他的地址,可以配置相关参数,如下列配置可以将RTP包发到组播地址224.0.0.100:

<action application="esf_page_group" data="esf_page_group 224.0.0.100 34567 6001"/>

与普通的IP地址不同,组播需要配置组播地址。众所周知,在IPv4中,组播地址的范围是从224.0.0.0到239.255.255.255,由于实际用到组播的业务却很少,因而好多人可能不是很熟悉。在有的系统上需要配置组播路由,如以下命令可以在Linux系统的eth0上配置组播路由:

ip route add 224.0.0.0/4 dev eth0 src 192.168.5.2

使用上述命令还需要iproute2软件包支持。另外,在实际应用中为避免组播风暴及潜在的冲突,可能需要把具有组播功能的话机都划分到一个VLAN上。再者,组播包一般不能穿越路由器,如果要跨路由器组播的话,需要支持组播的路由器,并进行适当的配置,这些都超出了本书的范围,有兴趣的读者可以自行参考相关资料。

DTMF

DTMF(Double Tone Multiple Frequecy,双音多频)是一种通话过程中的号码传输方式,特别是在IVR类的应用中,一般的电话菜单都是通过按键控制的。在传统的TDM电话网络中,如果通话中的用户按下了话机上的一个键,话机就会产生一个DTMF的音频信号送到远端的交换机上,交换机会将该信号送到对端的用户那里,如果对端也是一个用户,则他就能听到一些“嘀嘀”的按键音;如果对端是一个IVR,它就能检测这种信号,并还原成用户按键的数字,进而知道用户选择了哪个菜单。

在SIP通话中,传输按键信息的方式相对较多。虽然有些信息可以完全不用双音多频的信号来表示,但沿用传统的叫法,我们仍然把按键信息称为DTMF。

带内 DTMF

在基于SIP/RTP的通话中,如果跟传统的TDM电话对接,直接把TDM电话设备产生的双音频信号按与音频编码同样的方式进行编码并放到RTP数据中传输,则这种DTMF称为带内DTMF(Inband)。FreeSWITCH默认不使用带内DTMF。因为,检测带内DTMF必须要实时检测RTP包中的内容,会多耗费很多CPU。不过,传统的TDM设备只支持这种方式,因而如果要跟传统的PSTN网络的交换机进行SIP对接,则往往默认会使用这种方式。由于PSTN网络上的交换机一般不会使用到DTMF,它们只需要对DTMF信号进行简单的透传,而不需要进行深入检测,因而对它们来说无所谓是否耗费资源。

如果在FreeSWITCH中使用带内DTMF,则需要在对应的Profile中(如“internal”或“external”)中设置如下参数:

<param name="dtmf-type" value="inband"/>

或者在通话时,在Dialplan中通过设置如下的通道变量实现:

<action application="set" data="dtmf_type=inband"/>

当然,设置了上述变量并不能使FreeSWITCH检测到带内的DTMF,还必须在Dialplan中明确使用start_dtmf这个App打开DTMF检测。比如,对于来话而言,就需可以使用如下的Dialplan:

<action application="set" data="dtmf_type=inband"/>

<action application="start_dtmf"/>

<action application="ivr" data="some_ivr"/>

对于去话,则可以通过设置一个钩子通道变量 [1]“execute_on_answer”实现,如:

<action application="export" data="nolocal:execute_on_answer=start_dtmf"/>

<action application="bridge"data="sofia/gateway/pstn/${destination_number}"/>

在上述Dialplan中,我们使用“export”设置了一个变量“execute_on_answer”,该变量是

以“nolocal:”开头的,因此,它将只在b-leg上生效。在执行到“bridge”时,将产生b-leg。本例中的b-leg通过一个pstn网关呼叫被叫号码,当对端接听后,FreeSWITCH就会执行execute_on_answer钩子变量所指定的App,即start_dtmf,它用于开启带内DTMF检测。

当然,如果对export”这个App以及其原理比较熟悉的话(参见6.1.9节),便知道上述Dialplan跟如下的写法是等价的:

<action application="bridge" data="{execute_on_answer=start_dtmf}sofia/gateway/pstn/${destination_number}"/>

号码连选

虽然有些运营商也开始尝试开放SIP号码,但大部分还是谨慎的、试探性的。因此一般不提供SIP对开中继的方式,而是开放单个的接入号码。虽然这不是理想的方式,但有总比没有好。假设从运营商获得10个SIP账号,号码范围是xxxxxx30~xxxxxx39。来看一下如何有效地使用这些号码

注册到运营商服务器

我们可以在FreeSWITCH中添加一些网关,以便注册到运营商的SIP服务器上去(应该是一个SBC)。

网关的配置文件如下,为了使用方便,我们让网关名称(name)的后两位与号码的最后两位相同:

<gateway name="gw30">
	<param name="realm" value="218.56.x.x"/>
	<param name="username" value="xxxxxx30"/>
	<param name="password" value="xxxx"/>
	<param name="register" value="true"/>
</gateway>
<gateway name="yt31">
	<param name="realm" value="218.56.x.x"/>
	<param name="username" value="xxxxxx31"/>
	<param name="password" value="xxxx"/>
	<param name="register" value="true"/>
</gateway>

上面仅列出了两个网关账号的配置,其他账号依此类推。

通过单个号码呼出

配置的网关注册成功后,就可以通过这个号码(又称为线路)打入打出电话了。可以使用如下命令快速试一下是否能通过某一条线路(如gw30)成功呼出:

freeswitch> originate sofia/gateway/gw30/1860535xxxx &echo

测试成功后,就可以设置如下的 Dialplan 让所有分机都可以通过该网关呼出了:

<extension name="Outbound Call">
	<condition field="destination_number" expression="^(1[358].*)$">
		<action application="bridge" data="sofia/gateway/gw30/$1"/>
	</condition>
</extension>

使用随机数做号码连选

为了能自动选择一个网关呼出,我们想办法从这10个网关中自动选择一个进行呼出。这种选择的过程就称为选线,也称为号码连选。当然,号码连选最简单的实现方法是使用一个随机数。见下面的Dialplan:

<action application="set" data="gw=gw${expr(randomize(&x);ceil(random(30,39,&x)))"/>
<action application="bridge" data="sofia/gateway/${gw}/$1"/>

其中,“expr”是一个API,randomize方法产生一个从30到39之间的随机数(如33),在该随机数前面加上“gw”字符(变为gw33),并把它赋值给一个“gw”通道变量(使用“set”)实现。有了该通道变量后,在“bridge”的参数中就可以使用“${gw}”引用该变量(在本例中它的值就是gw33),实现动态选择一个随机的网关。

当然,这种选线算法有一个缺点,就是它不记录实际号码的忙闲状态,如果选到正在通话的号码时,通话还是会失败。通过下面的方式,我们可以做一个改进的算法:

<action application="set"
data="gw1=gw${expr(randomize(&x);ceil(random(30,39,&x)))"/>
<action application="set"
data="gw2=gw${expr(randomize(&x);ceil(random(30,39,&x)))"/>
<action application="bridge"
data="sofia/gateway/${gw1}/$1|sofia/gateway/${gw2}/$1"/>

该方法的思路是,同时选择两个网关(gw1和gw2),如果一个失败,则走另一个(该方法不能从根本上解决选到正在通话的线路问题,但可以大大减少失败的概率)。

使用 mod_distributor 进行连选

除随机数外,FreeSWITCH也提供了一个mod_distributor模块模块,专门实现这种连选功能。首先,要安装该模块,进入FreeSWITCH的源代码目录,执行如下命令:

make mod_distributor-install

然后,在conf/autoload_configs/distributor.conf.xml中进行如下设置:

<list name="dist1" total-weight="10">
	<node name="30" weight="1"/>
	<node name="31" weight="1"/>
	<node name="32" weight="1"/>
	<node name="33" weight="1"/>
	<node name="34" weight="1"/>
	<node name="35" weight="1"/>
	<node name="36" weight="1"/>
	<node name="37" weight="1"/>
	<node name="38" weight="1"/>
	<node name="39" weight="1"/>
</list>

其中,我们配置了一个列表(list),它的名字是“dist1”,总体的权重(total-weight)是“10”。该列表有好多节点(node)组成,其中每个节点的权重(weight)为“1” [1]。可以看出,这些节点的名字跟我们线路号码的最后两位相同。

FreeSWITCH加载该模块后,我们就可以先用如下的命令进行一下测试:

freeswitch> distributor dist1 32

freeswitch> distributor dist1 35

distributor是该模块提供的一个API命令,它可以用于从预定义的列表中根据权重选择一个节点,并返回该节点的名称。然后,我们就可以在Dialplan中使用它来帮助我们选线了:

<extension name="gw">
	<condition field="destination_number" expression="^(01[358].*)$">
		<action application="bridge"data="sofia/gateway/gw${distributor(dist1)}/$1" loop="2"/>
	</condition>
</extension>

其中,我们使用${distributor(dist1)}让mod_distributor帮我们选择一个节点,并使用它作为网关的名字向外呼出。当然,与上一节使用的随机数的方案相比,它也聪明不了许多,因为,该模块也没有记录线路是空闲还是忙的。因此,我们使用了Dialplan Action中的loop属性,如果第一次呼叫失败,它将再试一次(重新执行该Action)。

当然,与上一节的随机数方案比起来,它还是聪明一点的。例如,虽然我们配置了10个网关,但并不一定所有时间所有的网关都能正常注册上。通过如下方法,就可以让distributor在生成选择节点时,排除掉处于down状态(即不可用状态)的网关:

<action application="bridge" data="sofia/gateway/gw${distributor(dist1 ${sofia(profile internal gwlist down)})}/$1"/>

收发传真

无论社会如何发展,老技术永远有它特殊的生命力,生生不息。传真技术也是如此。在互联网技术飞速发展的今天,电子邮件、即时通信以及基于移动互联网的各种应用,如微博、微信等,铺天盖地地发展,但我们在企业应用中,还是离不开传真机。当然,现代的传真机大多数都集打印、扫描等功能于一体了,但还是使用TDM通信的方式收发传真。

连接TDM传真机最简单的方式是使用一个模拟转SIP的网关,将它变成SIP后在FreeSWITCH中收发传真就变得容易了。如我们可以使用如下命令,呼叫一个号码,并开始收传真。

freeswitch> originate sofia/gateway/gw1/xxxx &rxfax(/tmp/test.tiff)

传真功能是在mod_spandsp [1]中实现的。它实现了一个rxfax的App用于收传真。普通的传真机是这样工作的,A呼叫B,首先建立正常的通话,电话打通以后A告诉B它想发一个传真,因此B会按下一个按钮将传真机切换到传真模式,这时A这一端将听到“吱吱”的传真音,然后A端按下传真机上的“发送键”(或传真开始键)发送传真。

有SIP参与的传真过程也是类似的。它们首先先建立正常的SIP连接,然后如果一方想发传真或收传真,则它的终端就会给另一方发送SIP“re-INVITE”消息,与对方协商将RTP媒体流切换到T.38(或T.30)传真图像模式,协商完成后开始发送传真。

上面我们讲到的“rxfax”是收传真的App,它会将接收到的传真存到本地的一个TIFF [2]格式的文件中。

发送传真与与此类似,只是需要预先将欲发送的内容转成TIFF格式的文件。如下命令可以呼叫一个号码并发送传真:

freeswitch> originate sofia/gateway/gw1/xxxx &txfax(/tmp/test.tiff)

可以使用Gostscript或Imagemagick图像处理工具将欲发送的传真内容从原来的格式(如PDF)转换成TIFF格式。如,下列gs(即Gostscript)命令可以将fax.pdf转换成fax.tiff:

gs -q -r204x98 -g1728x1078 -dNOPAUSE -dBATCH -dSAFER -sDEVICE=tiffg3 -sOutputFile=fax.tiff -- fax.pdf

注意,其中“204x98”表示分辨率。传真文件的分辨率就是比较奇怪——横向和纵向的分辨率是不同的。另外,传真页面也需要有特定的大小,大部分传真机都能接收页面大小为“1728x1078”的传真,其他尺寸的因传真机而异。

上面我们只讨论了收发两端均是SIP的情况。如果在FreeSWITCH内部也使用如ftdm这样的Endpoint(配合模拟或数字板卡),那么FreeSWITCH也可以通过t38gateway这个App进行传真媒体的转换 [3]。

最后,我们再来看一下默认的Dialplan的配置。配置的Dialplan配置了两个Extension,“9178”用于收传真,“9179”用于发传真。具体内容如下:

<extension name="fax_receive">
	<condition field="destination_number" expression="^9178$">
		<action application="answer" />
		<action application="playback" data="silence_stream://2000"/>
		<action application="rxfax" data="/tmp/rxfax.tif"/>
		<action application="hangup"/>
	</condition>
</extension>
<extension name="fax_transmit">
	<condition field="destination_number" expression="^9179$">
		<action application="txfax" data="/tmp/txfax.tif"/>
		<action application="hangup"/>
	</condition>
</extension>

多租户

很多人将 FreeSWITCH 用于云计算平台,而VoIP云计算除了要支持大规模的并发呼叫外,更重要的是要支持多租户技术。简单来讲,多租户就是在一个系统中(或更简单点,一台FreeSWITCH服务器上),支持多个彼此相互独立的PBX(如属于不同公司的)应用,这些不同的PBX中可能有相同的分机号,而不会产生冲突。

FreeSWITCH在设计之初就考虑到了这一点,它是用 Domain 实现的。

FreeSWITCH中的Domain是指一个域,或者在这里我们说到多租户的时候,可以说它指一个租户。实际上,它就是唯一标志一个域的字符串。当然,由于它可以是任意的字符串,那么用实际的域名和IP地址也是没有问题的。虽然可以使用域名或实际的IP地址作为Domain,但并不表示Domain跟域名或IP地址以及DNS有任何关系,FreeSWITCH也永远不会把Domain通过DNS解析成域名,反之亦然。那么为什么这么设计?答案很简单,为了支持多租户。

我们首先来看一下conf/vars.xml中如下的配置:

<X-PRE-PROCESS cmd="set" data="domain=$${local_ip_v4}"/>
<X-PRE-PROCESS cmd="set" data="domain_name=$${domain}"/>

其中,上述配置配置了两个全局变量domain和domain_name,前者用作一个核心变量——当系统需要一个Domain值,而通过当前的环境又找不到这么一个值时,就用该变量的值顶替;而后者用作一个Dialplan中的变量,它只是跟每一通电话相关的。当然默认两者的值都等于local_ip_v4的值。而local_ip_v4是一个动态计算出的变量值,它一般是当前能上网的网卡的IP地址(在取不到的情况下可能为127.0.0.1)。通过将local_ip_v4的值设成两个变量的默认值是因为这样的话在默认安装后不需要任何配置就可以在所有环境中都正常使用。

为了说明Domain跟实际的FQDN域名没有任何关系,我们来做如下实验。首先,如图15-7所示,在Domain中填入“dujinfang.com”。注册时选择“Send outbound via Proxy”,并在Proxy中输入FreeSWITCH的IP地址。在本例中,笔者试验的客户端和服务器的IP地址都是192.168.7.5。

上面讲到的注册是可以成功的,而且我们在SIP消息中看到到处都是我们设置的Domain(dujinfang.com)。但是是否注册成功了就说明这支持多租户了呢?答案是否定的。我们还需要做一些工作。首先,我们用以下Dialplan测试一下,看domain及domain_name两个变量能告诉我们什么。

<extension name="Domain">
    <condition field="destination_number" expression="^1234$">
        <action application="log" data="INFO domain=${domain}"/>
        <action application="log" data="INFO domain_name=${domain_name}"/>
    </condition> 
</extension>

通过上述Dialplan的设置,在拨打1234后,我们在日志中可以看到如下输出:

[INFO] mod_dialplan_xml.c:558 Processing 1007 <1007>->1234 in context default [INFO] mod_dptools.c:1595 domain=192.168.7.5

[INFO] mod_dptools.c:1595 domain_name=192.168.7.5

可以看出,由于domain是一个核心的变量,它继续保持192.168.7.5这个IP地址值倒也罢了,但domain_name也不变,我们根据什么来做多租户的路由呢?即我们根据凭什么说这是某某域里的1007发起的呼叫而不是另一个域中的1007发起的呢?

其实,这是由于FreeSWITCH默认安装要在所有环境下都有运行的根本宗旨决定的,要达到我们的要求,需要检查Profile(internal)中的如下参数:

<param name="force-register-domain" value="$${domain}"/>
<param name="force-subscription-domain" value="$${domain}"/>
<param name="force-register-db-domain" value="$${domain}"/>

可以看出,在默认的配置中,上述参数都是生效的,也就是说,不管你的Domain怎么配置,都让它强制(force)成了默认的Domain。因此,在此我们需要删除这些参数(或者在配置文件中注释掉这些行)。然后,重启该Profile,再次注册时就会看到如下的警告,而且,我们的SIP客户端再也注册不上了。

[WARNING] sofia_reg.c:2679 Can't find user [1007@dujinfang.com] from 192.168.7.5

You must define a domain called 'dujinfang.com' in your directory and add a user with the id="1007" attribute

and you must configure your device to use the proper domain

in it's authentication credentials.

虽然是个警告,但是个好的警告,它至少告诉我们它在配置文件中找不到这个Domain和这个用户了。我们看

一看默认的用户目录配置文件conf/directory/default.xml,它里面定义了一个Domain,是默认的“$${domain}”值,具体如下:

<include>

<!--the domain or ip (the right hand side of the @ in the addr--> <domain name="$${domain}">

<params> ......

找到它后,我们只需要照着它复制一份新的就好了(这里当然也可以把“$${domain}”改成我们想要的“dujinfang.com”,但我们除此之外还要加别的Domain,因而到后面总是要复制的)。

将上述文件复制到conf/directory/dujinfang.com.xml,然后修改其中的domain_name为如下的值:

<domain name="dujinfang.com">

在FreeSWITCH控制台中执行reloadxml使之生效,然后,重新注册客户端,就发现能注册成功了。通过如下的命令也能列出注册信息:

freeswitch> sofia_contact 1007@dujinfang.com

sofia/internal/sip:1007@192.168.7.5:34088;rinstance=d3ba18cdf1903f17

freeswitch> sofia status profile internal reg 1007

Registrations: ================================================================

Call-ID: ZWRhZjA3NjI0NjYxODA1MDdkOGI0YzYxZWVkNGRmZjA

User: 1007@dujinfang.com

Contact: "1007" <sip:1007@192.168.7.5:34088;rinstance=d3ba18cdf1903f17>

Agent: Bria 3 release 3.5.0b stamp 69410

Status: Registered(UDP)(unknown) EXP(2013-10-20 00:56:40) EXPSECS(94)

Host: seven.local

IP: 192.168.7.5

Port: 34088

Auth-User: 1007

Auth-Realm: MWI-Account:    dujinfang.com 1007@dujinfang.com

接着,拨打1234进行测试,就可以看到如下的输出了:

[INFO] mod_dialplan_xml.c:558 Processing 1007 <1007>->1234 in context default [INFO] mod_dptools.c:1595 domain=192.168.7.5

[INFO] mod_dptools.c:1595 domain_name=dujinfang.com

从中可以看出,这里的domain_name变量也变成我们期望的值了。

当然,我们新建的dujinfang.com.xml与原来的default.xml都装入了conf/directory/default目录下的用户配置文件。既然是不同的租户,我们就需要把它们分开,因此我们也把整个default目录复制一份,放到到新的dujinfang.com目录中,并且将dujinfang.com.xml中的“include”一行改为:

<users>

<X-PRE-PROCESS cmd="include" data="dujinfang.com/*.xml"/> </users>

还没有完,我们要让自己域中的用户使用单独的Dialplan进行路由,因此进入dujinfang.com目录,将里面所有的用户配置文件(如1007.xml)中的user_context参数全部改成如下的值:

<variable name="user_context" value="dujinfang.com"/>

执行reloadxml命令后再拨打1234发现打不通了,原因很显然,我们还没有在Dialplan中设置dujinfang.com这个Context。当然,设置它也很简单。进入conf/dialplan/目录,将default.xml复制为dujinfang.com.xml,并将里面的Context的名称修改为我们需要的值,如:

<include>

<context name="dujinfang.com">

再一次执行reloadxml命令并拨打测试,就可以看到,我们这里的路由也跟默认的配置完全分离了。这就基本上达到了我们想要的效果。读者接下来可以参照这里的步骤再配置几个新的域以对比测试。

使用多Domain(及多Directory、多Dialplan)支持多租户以后,有以下几个事情是需要注意的:

  • ·Dialplan中的呼叫字符串不能再使用类似“user/1000”这样的缩写形式了,而要写成如“user/1000@${domain_name}”的形式。
  • ·在使用会议、fifo等类的应用时,注意不要像本书中的例子一样图省事 [3]把会议的名称写成“3000”或把fifo的名称写成“book”,一定要写成如“3000-${domain_name}”或“book@${domain_name}”之类带Domain的长名字。
  • ·在实际应用中Domain可以与实际的FQDN相同,最好能使用DNS的SRV记录进行解析。不同的机构可以使用不同的域,如dujinfang.com、microsoft.com等;同一个机构不同的分支机构间也可以使用不同的域,如beijing.dujinfang.com、shanghai.dujinfang.com等。
  • ·如果需要计费,话单等也是需要特别注意的。
  • ·除此之外,还可以进一步细分,将不同的租户分到不同的Sofia Profile中去。即每个租户都有自己的的

Profile(分别占用不同的端口号),它们的Profile中的信息及各种参数都可以不同。当然,若所有的租户都想使用5060这样的端口号,则可以使用DNS的SRV记录来解决。关于SRV记录的用法超出了本书的范围,读者可以自行研究。

使用 loopback Endpoint 外呼

如果要在 FreeSWITCH 中向外发起呼叫,需要一个呼叫字符串。前面学习过几种呼叫字符串,如 user/1000、sofia/gateway/gw1/xx、ftdm/1/a/1000 等。

但有些情况下,找到呼叫字符串不是那么容易。比方说,我们做了一套企业应用,该企业比较大,有很多分支机构,而且由于历史原因号码分配比较乱,因此我们需要在Dialplan中设置了几百个Extension,以匹配不同号码段正确路由,比如下面的代码:

<condition field="destination_number" expression="^0(.*)$">
<condition field="destination_number" expression="^11(2.*)$">
<condition field="destination_number" expression="^1234(852.*)$">
<condition field="destination_number" expression="^1234(52.*)$">
<condition field="destination_number" expression="^1234(.*)$">

如果在我们的应用中,在某些场合下(如自动传真、催缴费等)需要在FreeSWITCH中主动呼叫这些号码,则我们可以使用originate进行外呼,但在外呼时需根据不同的号段我们生成不同的呼叫字符串。这样我们的程序中就需要另外一套路由数据,这不仅与Dialplan中的数据重复,而且不利于维护。因此就考虑能否在用originate外呼的时候也使用Dialplan呢?答案是肯定的,这里我们使用一个新的Endpoint,称为 loopback。

在理解它的实用原理之前,我们先来看一个简单的例子。我们知道,路由到9664后会播放保持音乐,而路由到9196后会执行echo。以前只有有了一个Channel以后才能进入Dialplan,并路由到9664,而现在我们可以使用loopback提前进入Dialplan进行路由。如下面的命令:

freeswitch> originate loopback/9664 &bridge(loopback/9196)

这些Channel都是在FreeSWITCH内部发生的,运行完毕后可以使用下列命令看到,上述命令建立了4个Channel(由于输出行太长,我们简单了输出,仅列出有代表性的字段):

freeswitch> show channels 22937c99-7710-4f64-869f-4f441cc15b54,inbound,loopback/9664-b ... 66f83997-5115-43e9-b1b1-b786ac64a9d2,outbound,loopback/9664-a ... d91f7824-b314-4996-acc4-e27fb846e66a,outbound,loopback/9196-a ... 57416b98-702b-4d38-bc1b-084966fd04e3,inbound,loopback/9196-b ...

那么为什么它有4个Channel呢?我们知道在普通情况下,如果执行如下呼叫,将只产生两个Channel:

freeswitch> originate user/1000 &bridge(user/1001)

loopback 这个 Endpoint 的关键就在这里,当然它的实现原理很简单但讲起来却很复杂。为了能让让呼叫先到达Dialplan查找路由,它首先创建一个假的腿(即Channel,称为loopback-a,我们称为第一条腿),然后让该腿进入Dialplan进行路由,执行playback播放保持音乐,然后再创建另一条腿(称为loopback-b,我们称为第二条腿),以继续下一步的动作。在此,它执行bridge。我们在bridge中也使用了loopback,因而会发生同样的事情,先创建一条假腿(另一个loopback-a,第三条腿)到Dialplan中找到适当的路由,执行echo,然后又创建一条腿(第四条腿)再与原先的那个腿bridge起来。如果还是不理解的话,可以查看一下刚才呼叫的日志,就能比较清楚了:

[NOTICE] switch_channel.c:1044 New Channel loopback/9664-a [66f83997-... [NOTICE] switch_channel.c:1042 Rename Channel loopback/9664-a->loopback/9664-a [NOTICE] switch_channel.c:1044 New Channel loopback/9664-b [22937c99-... [NOTICE] mod_loopback.c:946 Channel [loopback/9664-a] has been answered [NOTICE] mod_dptools.c:1225 Channel [loopback/9664-b] has been answered [NOTICE] switch_channel.c:1044 New Channel loopback/9196-a [d91f7824-... [NOTICE] switch_channel.c:1042 Rename Channel loopback/9196-a->loopback/9196-a [NOTICE] switch_channel.c:1044 New Channel loopback/9196-b [57416b98-... [NOTICE] mod_loopback.c:946 Channel [loopback/9196-a] has been answered [NOTICE] mod_dptools.c:1225 Channel [loopback/9196-b] has been answered

从日志中可以看到,一共有4条腿(4个Channel)参与到该通话中,它们的名字分别是loopback/9664-a、loopback/9664-b、loopback/9196-a、loopback/9196-b。

当然,你也可以执行以下的例子,它将先呼叫1000,再呼叫1002,仍然会产生4条腿:

freeswitch> originate loopback/1000 &bridge(loopback/1001)

下面的命令的效果是一样的,但只会产生3条腿。

freeswitch> originate loopback/1000 1001

这是因为,1001那一端没有使用loopback,在1001那一端产生了两条腿以后,呼叫进入Dialplan进行路由,然后又“bridge”到1001,产生了第三条腿。

理解了这些之后,就可以对本节最开始的时候提出的那些号码进行呼叫了,而不用管它们是具体怎么路由的,如呼叫某个号码并自动发送传真:

freeswitch> originate loopback/123485234xxx &txfax(/tmp/test.tiff)

需要注意的是,由于使用loopback将比普通的呼叫多产生一条腿,因而话单可能受影响。为此,所有的loopback的Channel中都设置了一个is_loopback变量,以方便其他程序(如计费等)进行后续处理。

为了最大限度地减少因额外的腿产生的影响,也可以通过一个通道变量让这条额外的腿在完成它的使命之后尽快释放(或者说两条腿合并为一条),如下:

freeswitch originate {loopback_bowout=true}loopback/1000 &echo

上述命令将先建立两条腿,呼叫1000,在成功执行echo命令时,两条腿将条合并为一条。读者可以使用show Channels命令进行查看。

讲解 loopback 是为了给大家讲清楚默认 Dialplan 中的 Local_Extension 部分的路由。在6.1.9节中,我们为了方便讲解,把默认Dialplan中呼叫失败后自动入进入Voicemail的Dialplan Action改写成了如下的形式:

<action application="voicemail" data="default ${domain_name} ${dialed_extension}"/>

但实际上,它是如下的形式:

<action application="bridge" data="loopback/app=voicemail:default ${domain_name} ${dialed_extension}"/>

后面这种方式使用了loopback。在大多数情况下,两者的效果是一样的。只不过,后者因为能在进入Voicemail后能产生一个Channel,因而还可以配合att-xfer(见第15.1.2节)一起使用。

在这里,与前面讲的loopback会先进入Dialplan进行路由不同,它直接使用“app=”这样的语法执行一个App,相当于loopback Dialplan的inline形式(关于inline Dialplan参见6.2节)。

在 Web 浏览器中打电话

关键字:web voip sip

WebRTC 的全称是 Web RealTime Communication,即基于Web的实时通信。实际上WebRTC提供了在浏览器中使用JavaScript API访问本地的声音和视频设备的手段。由于保护隐私和安全性的原因,浏览器在每次使用用户的声音或视频设备时都会询问是否允许。

WebRTC仅提供了访问音视频设备这些媒体资源的方法,并没有规定信令是怎么走的。现有的例子大部分都使用了Google App Engine Channel API传输信息控制协议。另外一个可以传输信令的就是WebSocket

WebSocket是HTML5最初提供的一种浏览器与服务器间进行双向全双工通讯的网络技术。大家熟知的HTTP协议仅能用于通过浏览器单向请求服务器资源,而为了能使服务器主动向浏览器客户端推送资源,人们想尽了办法,如使用Ajax轮循、HTTP长连接、Flash提供的Socket功能及IE中的ActiveX控件等。WebSocket的出现解决了这些连接的混乱,它可以直接使用浏览器提供的功能和API与后端建立双向的Socket连接,包括IE9在内的现代浏览器都支持WebSocket。

有了WebSocket以后,SIP的承载方式就又多了一个选择。大家都已经知道,SIP一般通过UDP承载,也可以通过TCP或TLS承载,而出现了WebSocket以后,它也可以承载SIP,这种技术就称为SIP over WebSocket

JsSIP

JsSIP:https://github.com/versatica/JsSIP

从 3.0.0 开始,JsSIP 不再包含 rtcninja 模块。但是,jssip-rtcninja 包基于分支 2.0.x ,其中确实包含 rtcninja

sipML5

sipML5是由Doubango团队做出的SIP Over Websocket的开源实现。该项目可以在线试用,也可以在自己本地试用。

HA(High Availability)高可用

HA(High Availability)即高可用(可靠)性。在对稳定性要求非常高的场合,需要一定的机制来保证服务的稳定。一般的HA都是用双机(或多机)实现的,它的理论是,根据概率来计算,如果一台机器的稳定性(不出故障的概率)为99%,则两台机器的稳定性就是99.99%。

FreeSWITCH 对 HA 有内置的支持,如果相互配对的两个FreeSWITCH中有一个崩溃或因硬件故障宕机,则另一台能在几秒内迅速接替它的工作,保证正在进行的通话不断话(正在接续的通话可能受影响)。下面我们先来做一个实验。

崩溃恢复实验

简单实验:先用一个FreeSWITCH实例来做实验。首先配置好一个FreeSWITCH实例,用一个SIP客户端发起一路通话呼叫9664听保持音乐;然后让FreeSWITCH崩溃,这时候客户端会短暂地听不到声音;接着,我们重启FreeSWITCH并恢复崩溃前的通话,客户端上就又重新听到的声音。

为了让FreeSWITCH能在崩溃后恢复,FreeSWITCH需要将当前通话的信息写到数据库中。默认的SQLite数据库就可以使用,只需要配置Sofia Profile(internal)中如下一个参数:

<param name="track-calls" value="true"/>

配置好该参数后,重启使该Profile使该参数生效:

freeswitch> sofia profile internal restart

FreeSWITCH就会将通话的当前状态实时写到数据库里。然后,用一个SIP客户端呼叫9664,听到保持音乐后,用下面的命令让 FreeSWITCH 崩溃:

freeswitch> fsctl crash

当然,上述命令是一个测试的命令,与“kill-9<pid>”是类似的。总之,至此 FreeSWITCH 就崩溃了。然后,我们重启 FreeSWITCH,重启完成后,再执行:

freeswitch> sofia recover Recovered 1 call(s)

该命令是最关键的。通过它 FreeSWITCH就会从数据库中读出崩溃前的通话信息并恢复。当然,该实验能成功的前提是你做得足够快(通过几秒或几十秒),因为有些SIP客户端在长时间收不到 RTP 流后,就会自己挂机,当客户端挂机了FreeSWITCH端再恢复也没什么用了。

其实这里能恢复的原理很简单。由于在SIP会话建立后,电话进入正常通话中,一般在挂机之前再没有SIP消息交互了,只有RTP用于传送媒体数据。如果FreeSWITCH崩溃,则RTP数据只是短暂不能收发,而由于RTP是UDP的,因此客户端感觉不到FreeSWITCH崩溃而仍然发送RTP流(也许有一点点感觉就是暂时收不到RTP流了,而短时间内收不到RTP流的话客户端也不会抱怨)。因而在FreeSWITCH恢复后,电话马上就恢复了。当然,FreeSWITCH在恢复通话后,会重新发一个re-INVITE消息,以确定对方是否还在。

注意:这里讨论的崩溃恢复是假设SIP采用UDP的情况。如果SIP采用TCP方式连接,由于TCP是面向连接的协议,就不能保证总是能够恢复成功(跟对端和网络环境有关)。

为了能使一台主机能完全接替另一台主机提供同样的服务,两台主机必须完全相同。下面我们以两台FreeSWITCH主机为例来说明一下

有两个FreeSWITCH节点

  • 节点1,IP地址10.0.0.11,节点1为主用节点,因此它绑定了一个浮动IP 10.0.0.10。
  • 节点2,IP地址10.0.0.12

对于SIP终端来讲,它们不知道节点1和节点2的存在,只知道它在跟一个IP地址是10.0.0.10 的SIP服务器进行通信。现在,IP地址10.0.0.10绑定在节点1上,如果任何时候节点2发现节点1故障,它就会将 IP 地址10.0.0.10绑定到自己的网卡上,并接替节点1继续进行工作

为了能使节点2能实时监测到节点1的健康状态,两者之间使用心跳线连接,并定期交换心跳(Heartbeat)消息。如果节点2在规定的时间内(通常几秒内)收不到对方的心跳,则认为节点1故障,并触发倒换过程,自己变为主节点。在物理上,心跳线可以用串口线,也可以用以太网线。

如果节点1在一段时间内恢复,这时候一般有两种选择:①它自动变为备用节点,下次节点2再出故障时它再变为主节点;②它依然作为主节点,节点2则变回从节点。具体的倒换策略视具体应用情况而定。为了能使节点2能恢复节点1上正在进行的通话,两个节点需要连接到一个共享的数据库。

可以想象,节点2对节点1健康状况的判断仅依靠心跳线,如果心跳线出现故障,则可能引起错误的判断,造成不必要的主备切换。更严重的是,网络上会出现两个主机具有相同的IP地址(即浮动IP为10.0.0.10),两台主机也可能同时访问数据库中的共享资源引起数据混乱。这种可能的错误称为脑裂。为了预防脑裂的发生,一般有以下两种解决办法:

  • 再增加一条心跳线,以降低由于心跳故障引起的错判;
  • 增加 STONITH 设备,强制关闭另一台设备的电源。

当然,到这里读者也可以看到,如果节点1真的发生故障了,那么节点2检测到以后便会启动倒换过程。如使用STONITH工具关掉对方节点的电源、将浮动IP绑定到本机上、启动FreeSWITCH、恢复通话等。这些工作可以使用一些脚本来解决,也可以使用专用的CRM [4]软件解决。FreeSWITCH可以很好地配合各种CRM软件工作,不过,在本书中,限于篇幅,我们就不涉及了。

需要指出,这里讲的是通用的HA的基本知识,以FreeSWITCH作为例子进行讲解只是为了能方便直观一些,其他的系统HA也与之类似,如在数据库HA系统中,可以把这里的FreeSWITCH节点看做是数据库节点,而这里的共享数据库资源就可以看作是数据库HA系统中的共享存储(磁盘阵列、SAN存储等)。

需要两台一模一样的主机,安装操作系统和必要的软件,并安装FreeSWITCH。然后安装心跳软件。一般来说,可选的心跳软件有 Keepalived 和 Heartbeat,前者比较古老,后者比较新,它们都能在检测到心跳失败时调用相关的脚本切换IP和FreeSWITCH。

当发生主备机切换时,需要在备机上启动FreeSWITCH,然后执行sofia recover命令,以恢复话务。这些过程都可以写一个简单的Shell脚本来实现,也可以直接配置FreeSWITCH(在vars.xml中)在系统启动时自动执行sofia recover,如:

<X-PRE-PROCESS cmd="set" data="api_on_startup=sofia recover"/>

当然,FreeSWITCH的过程可能会比较慢,因而会延长总体切换的时间。为了节省启动时间,我们可以使用FreeSWITCH的StandBy模式,即先在备机上启动FreeSWITCH,让其什么也不干,等到IP切换过来后就可以立即执行sofia recover以恢复业务。为了能支持这种模式,需要修改操作系统内核参数,允许FreeSWITCH监听在“不存在”的IP地址上(因为以后要使用的浮动IP现在还绑定在主用节点上,备用节点上还没绑定)。在Linux上可以使用如下命令实现:

echo 1 > /proc/sys/net/ipv4/ip_nonlocal_bind

上述命令会动态更改内核参数,但如果想使该参数在系统重启后也生效,需要将配置选项写到系统配置文件中,如:

echo “net.ipv4.ip_nonlocal_bind=1 ” >> /etc/sysctl.conf

当然,为了能正常恢复话务,还需要在FreeSWITCH的Sofia Profile中配置ODBC(或PGSQL)数据库支持以及开启track_calls参数。除此之外,还要注意配置文件中domain的值,注意这需配一个公共的值(如使用浮动IP或一个域名)而不是使用默认的、本机的固定IP地址。

其实FreeSWITCH的HA没有什么神秘的,相信如果读者有一些Linux经验并参照本节中所讲的知识应该都能配置出来。只是在实际应用中更需要注意以下几个问题:

  • 性能降低。 为了能实现HA,需要把通话数据实时写到数据库中,这无形地就增加了资源占用,降低了系统的吞吐量。
  • 成本增加。 增加的另一台配对主机以及需要增加的网络设备至少需要2倍的硬件成本,也需要增加维护成本,并且收效不是那么明显——如果一台永远不出故障,钱就白花了;而且,如我们在15.10节讲的,它提高的可靠性却有限(从99%增加到99.99%只是增加了0.99%的可靠性 [5],显然与投入的成本不成正比)。
  • 人为原因。 并不是配置好HA就能放心睡大觉了,一个成熟的系统需要大量的测试以及长时间的优化;另外,HA也并不是解决一切问题的良药,更多的系统故障都是人为原因造成的。
  • 带来的好处。 当然,它带来的好处就是稳定性确实提高了。其实,更常用的功能就是系统升级时可以先升级备用的主机,然后进行主备倒换,再升级另一台主机而不会影响业务。
  • 其他。 FreeSWITCH在底层实现了从崩溃中快速恢复通话的机制,剩下的就要靠在使用过程中应用层的发挥了。最典型的,在大规模的集群中使用N+M 的策略应该是对投入和产出最好的平衡。

Opensips+RTPEngine+FreeSwitch实现FS高可用

https://wandouduoduo.github.io/articles/ec271ad5.html

  • opensips 仅作为消息转发,不负责语音通讯
  • 使用rtpengine来进行rtp转发以及sdp的协商
  • 一台opensips后对应多台freeswitch
  • opensips需要数据库存储相关负载以及保活信息
  • 多台FS共用数据库
  • 多台opensips间使用负载均衡中间件(阿里SLB,提供端口检测心跳)

集群、分布式部署

与 HA 相对的另一个概念是 HP(High Performance),即高性能。它原先主要指大规模的并行计算,现在延伸到互联网域,就是大规模的集群。大规模的集群也是当下一个更时毛的词 "云"。由于这种方式一般采用多节点负载分担的方式工作,因而又称为负载均衡(Load Balance,LB)。

大规模集群的总体结构

如图显示了一个基于互联网的大规模的集群的拓扑结构。

一般来说,用户通过客户端(Client)访问网络上的服务。客户端会使用DNS查询找到它要访问的主机。DNS服务一般使用轮循的方法,通过返回多个IP地址,将客户端的访问流量分散到不同的IP地址上。另外,有的DNS服务也可以根据用户的来源(地理位置)返回不同的IP,以将用户分配到就近的数据中心的服务器上。

当客户的请求到达某一数据中心后,前端的路由器设备(或以太网交换机)会根据访问的服务进行合理的分发,将请求发到后端的代理服务器(Proxy)上。这些路由器比较有代表性的有硬件的F5或软件的LVS [1]等,它们都是在IP层(或应用层)进行转发分配的。

当请求到达某一个代理服务器后,它会检查本地的设置,将请求数据再转发到某一个计算节点(Node)上。而后端的计算节点间也可以通过内部的网络进行通信,或访问共享的数据等。最后,计算节点将执行相关的服务程序对收到的请求进行服务,有必要的话,它也可能查询数据库或与其他系统交互。

总之,互联网上海量用户的请求就是这样通过前端的DNS、路由器以及代理服务器分配到不同的计算节点上,并由其对用户进行服务的。

具体来讲,对于支撑整个互联网基础的Web应用,比较典型的代理服务器有 HA-Proxy、Nginx、Apache 等;后端的计算节点可以认为是 Apache/PHP、Tomcat(Java)、Thin/Mongrel/Unicorn(Ruby/Rails)、Python 等(当然,实际的情况还涉及后面的数据库、消息队列、大型存储等,比这里讲的要复杂得多);而对于我们关心的 VoIP应用来说,常用的代理服务器有 OpenSIPS 和Kamailio 等,后端的节点可能是 FreeSWITCH 或 Asterisk。当然,VoIP 通信与 Web 服务不同的是,VoIP 对于实时性的要求也比较高,而且信令跟媒体也是可以分离的(因而也有专门代理媒体的代理服务器,如 RTPProxy)。

在实际的应用中,这些代理服务器应该足够“聪明”,即首先它能够比较智能地分配客户端的请求,力求使后端的计算节点负载比较均衡;其次,如果后端的计算节点出现故障,它也能很快探测到,并停止向其分发客户端的请求 [2]。当然,为了最大程度地保持稳定,代理服务器甚至前端的路由器及网交换机等都可以进行成对配置。

负载均衡配置实例

具体到FreeSWITCH,它最典型的应用就是作为集群中的一个节点提供具体的服务,而前面的DNS、路由器以及各种代理服务等则主要是负责将相应的注册和呼叫等请求分配到相应的FreeSWITCH服务器上进行处理。

下面来做一个负载均衡的实验。如图所示

以一台 OpenSIPS 和两个 FreeSWITCH(A、B)节点为例。OpenSIPS 位于前端,做Proxy;FreeSWITCH 在后端,进行话务处理;它们都共享同一个数据库。OpenSIPS 用于分发SIP请求,而后面的 FreeSWITCH 节点负责处理实际的通话,RTP 媒体流仍然在用户UA和FreeSWITCH之间直接传送。

OpenSIPS 是著名的 SIP Proxy,它的前身是 OpenSER,而OpenSER的前身是SER(SIP Express Router)。后来,由于版权的原因OpenSER更名为OpenSIPS,而同时开发团队中的一部分人则开发出了另外一个分支,这一分支称为Kamailio。我们无意讨论它们之间的差别与优劣,总之它们的根是一样的,配置使用起来也差不多。

配置 FreeSWITCH

在配置 OpenSIPS 之前,应该先配置两台一样的 FreeSWITCH(A和B),分别测试注册和打电话并保证都没问题。然后,两个 FreeSWITCH 要连接一个共享的数据库,可以使用原生的PostgreSQL 驱动。这里可以把数据库放到跟OpenSIPS相同的机器上,当然数据库也可以放到独立的数据库服务器上。重要的一点是,两个 FreeSWITCH 要连接同一个数据库,并且设置vars.xml 中的 domain 参数为一个同一个名称,如 sip.example.com,将该 DNS 指向 OpenSIPS 服务器的IP地址。修改domain,将vars.xml中的

<X-PRE-PROCESS cmd="set" data="domain=$${local_ip_v4}"/>

改为:

<X-PRE-PROCESS cmd="set" data="domain=sip.example.com"/>

所以,这里需要用到DNS服务器支持。如果没有DNS服务器,也可以尝试将这里的domain设为OpenSIPS服务器的IP地址,或者使用15.7.1节介绍的Domain知识进行相关配置。

安装配置 OpenSIPS

OpenSIPS 版本号不同,配置文件也略有出入,其中“#”是注释起始符。

OpenSIPS的安装应该很简单,笔者在这里使用的是PostgreSQL数据库,如果你使用MySQL,也可以进行相应的替换。使用如下命令进行OpenSIPS的安装:

cd opensips-1.8.2-tls

make all include_modules="db_postgres"

make include_modules="db_postgres" prefix="/usr/local" install

(1)创建数据库

可以使用opensipsctl命令创建数据库。在使用之前需要修改如下配置文

件/usr/local/etc/opensips/opensipsctlrc,以提供相应的参数。参数名称都很直观,在此就不多讲了,如:

# SIP_DOMAIN=opensips.org DBENGINE=PGSQL DBHOST=localhost DBNAME=opensips DBRWUSER=opensips DBRWPW="opensips" DBROOTUSER="opensips"

然后运行以下命令创建数据库:

# opensipsdbctl create

在配置好所有参数之前,我们先将OpenSIPS启动到前台,这样便于调试。配置文件及解释如下,首先是基本的参数:

压力测试

如果将 FreeSWITCH 应用于生产系统,一般需要经过一定的压力测试。下面讨论几种针对FreeSWITCH 进行压力测试的方法以及相关参数和性能指标

拿到一台 FreeSWITCH 服务器时,一般只关心两个问题

  • 它最大支持多少用户?
    与传统的TDM交换机相比,FreeSWITCH支持的最大用户数量几乎是无限的。因为,传统的TDM交换机中,每个用户(即每个电话)都会占用一个物理的硬件端口,这是一个绕不开的限制;而FreeSWITCH是IP的,所以基本上它仅受服务器内存及网络带宽的限制,可以很轻松地支持很大数量的用户。一般来说,FreeSWITCH的本地用户即使不打电话也会周期性地发送SIP注册(REGISTER)请求,因而支持的最大用户的数量就可以认为是跟FreeSWITCH所能处理的注册消息正相关的。一般来说,SIP客户端的注册失效(EXPIRE)时间是1小时,而根据SIP协议,客户端应该在半小时内重新发送注册消息,所以FreeSWITCH在1小时内仅需要处理一个用户的两次注册请求。假设FreeSWITCH在1秒内能处理100个注册请求的话,即它1秒就能服务50个用户,那么1小时就能服务3600×50=18000个用户。
    当然,在NAT环境中,有的客户端为了保持NAT畅通,可能每十几秒就重发一次注册请求(实际上最好是重发OPTION)。同样,FreeSWITCH也可能1秒内能处理成千上万的用户注册。同时,具体的服务器以及网络环境也千差万别,因而,这些指标需要经过具体的测试才知道。
  • 最大支持多少用户同时通话?
    最大并发数就是说一个系统最大可以支持多少人同时打电话。FreeSWITCH是一个典型的B2BUA,因而一般两个终端之间的通话就需要两个Channel,而像会议或IVR之类的应用就是每个终端一个Channel。简单起见,我们以系统并发的Channel的数量作为我们的并发指标。与最大并发数相关的另一个指标是每秒能处理的呼叫数。一般来说,处理通话比较消耗资源的地方在于通话的建立和释放阶段——通话建立阶段需要处理Session的建立,媒体协商等;通话释放阶段需要处理Session的释放、写话单、清理现场等。而一旦通话建立后,双方互相收发媒体流,对系统资源的占用就可以认为是个常数了。如果说系统每秒能处理30个呼叫(Channel)的建立与释放,每个呼叫持续30秒,那么系统的最大并发数就是30×30=900,而系统每小时能处理的呼叫数就是30×3600=108000。系统每小时所能处理的呼叫称为小时呼,又称BHCA(Busy Hour Call Attempt,即最大忙时试呼次数),每秒能处理的呼叫称为CPS(Call Per Second)。增加呼叫时长和增加每秒发起的呼叫数可以增加并发数。

在 FreeSWITCH 中,有两个参数可以限制系统能达到的最大呼叫量,以防止资源耗尽。其中,max_sessions 控制最大并发数,它的默认值是1000;sps 控制最大的每秒呼叫量,默认值是30。在系统中可以使用 status 命令查看这两个值。也可以使用如下命令修改这两个值:

freeswitch> fsctl max_sessions 5000

freeswitch> fsctl sps 100

上述命令仅在当前环境中生效,如果要使修改永远生效,则可以修改配置文件conf/autoload_configs/switch.conf.xml 中如下的参数:

<param name="max-sessions" value="1000"/>

<param name="sessions-per-second" value="30"/>

呼叫测试

最简单的测试方法就是用两台 FreeSWITCH 对着呼。设置两台 FreeSWITCH 服务器,如A和B,它们的IP地址分别为 192.168.1.A 和 192.168.1.B,则可以在B上使用如下命令呼叫A,并在本端执行 echo App:

freeswitch> originate sofia/external/service@192.168.1.A:5080 &echo

注意,我们这里直接呼叫A的5080端口,以避免认证。当然,在A上的Public Dialplan中我们需要设置一个路由播放音乐:

<extension name="Load Test">
	<condition field="destination_number" expression="^service$">
		<action application="answer"/>
		<action application="playback" data="local_stream://moh"/>
	</condition>
</extension>

为了能方便的发起大量呼叫,我们在B上简单地写一个Shell脚本,具体如下:

#!/bin/bash
IP=192.168.1.A
CMD="bgapi originate sofia/external/service@$IP:5080 &eccho"
for f in `seq 1 10`; do
    for f in `seq 1 30`; do fs_cli -x $CMD
    done
    sleep 1
done

上述脚本使用两个for循环,内循环执行30次发起30个呼叫,然后停顿1秒,再次发起30个呼叫;外循环则控制总的循环次数。

这仅仅是一个简单的脚本,通话一旦建立便会一直处理通话状态,如果要控制通话的时长,可以在A的Dialplan中播放一个特定长度的文件,也可以在playback之前使用sched_hangup App设置多长时间后挂机, 如:

<action application="sched_hangup" data="+60"/>

上述配置表示从现在开始,60秒后挂断通话。当然,如果仅仅测最大呼叫量的话,也可以不要让它自动挂机,而是在测试完成后手工使用 hupall 命令挂机。

使用 SIPP 进行测试

使用 SIPp 进行压力测试

https://www.cnblogs.com/garvenc/p/stress_testing_with_sipp.html

SIPP 是一个很好的SIP测试工具,它可以对SIP协议及服务器性能进行比较全面测试。这里我们仅讨论最简单的情况。首先,我们还是对上一节的FreeSWITCH服务器A进行测试,SIPP可以运行在A上,也可以运行在B上。它的使用方法很简单,只需要运行以下命令就可以了:

$ sipp -sn uac -r 1 -d 10000 -rtp_echo 192.168.1.A:5080

  • -sn uac 表示使用SIPP默认的场景文件(它将作为一个UAC,即SIP客户端);
  • -d 10000 表示每个请求持续10000毫秒(即10秒);
  • -r 1 表示每秒发一个呼叫请求;
  • -rtp_echo 表示将收到的RTP流原样发回去(相当于FreeSWITCH中的echo App);
  • 最后的参数表示发到FreeSWITCH服务器A的5080端口

上述命令运行后,可以通过键盘上的 1、2、3、4 等按键控制界面上显示的统计信息,包含呼叫次数,成功、失败次数及各种时长信息等。

图中显示了在 20cps 的情况下,当前有 2001 个并发呼叫,目前已成功进行了 2137176 次呼叫。

SIPP 使用 XML 描述的场景文件来描述何时该收、发什么样的SIP消息。不过其默认的配置文件少了点配置,因此 FreeSWITCH 社区推荐使用以下配置文件进行测试:http://www.freeswitch.org/eg/load_test/dft_cap.xml  下载完上述文件后,可以通过 -sf 参数指定使用该场景文件,如:

sipp -sf dft_cap.xml -r 1 -d 10000 -rtp_echo 192.168.1.A:5080

  • -r 表示每秒发一个请求
  • -d 10000 表示每个呼叫持续10000毫秒(即10秒)
  • 192.168.1.21:5080 即FreeSWITCH的IP和端口
  • -rtp_echo 表示我们把收到的RTP信息原样送回去,跟FreeSWITCH中的echo App类似

SIPP 默认使用的被叫号码为 service,当然,也可以使用 -s 参数指定一个其他的被叫号码,如下列代码指定被叫号码为load_test:

sipp -sf dft_cap.xml -s load_test -r 1 -d 10000 -rtp_echo 192.168.1.A:5080

当然,SIPP 还有好多选项,可以研究一下自己来写场景文件。更多的测试方法请参阅SIPP

注册 测试

SIPP有一个场景文件可以支持注册测试,该文件可以在http://sipp.sourceforge.net/doc/branchc.xml 处下载。有了该场景文件后,还需要配置一个数据文件,该文件提供注册用户的用户名密码等。如笔者使用的数据配置文件内容如下:

其中,“SEQUENTIAL”表示顺序执行。将上述文件存为users.csv,然后就可以使用如下的SIPP进行测试了:

sipp -aa -sf branchc.xml -inf users.csv -r 10 -p 6060 192.168.7.102

  • -inf users.csv 指定我们使用的数据文件;
  • -r 10 表示每秒注册10个;“
  • -p 6060 指定SIPP监听的本地的端口号,它的默认值是5060,明确指定端口号是为了防止当SIPP与FreeSWITCH在同一台主机上运行时产生冲突;
  • 最后的IP地址为FreeSWITCH的地址。

运行上述命令后,可以在SIPP中看到相关的统计信息,在FreeSWITCH中也可以使用“sofia status profile internal reg”命令查看客户端是否已正确注册。

编解码 测试

FreeSWITCH支持很多语音编码,不同编码的性能也不同。比如 G711(PCMU/PCMA)编解码基本上只要靠查表就行,所以是最高效的,而G729或iLBC之类的编码就需要比较高的CPU。测试编解码的性能也很简单,如笔者在测试时在FreeSWITCH的external Profile中使用如下参数设置语音编码为G729:

<param name="inbound-codec-prefs" value="G729"/>

然后在另一个(或几个)FreeSWITCH服务器上向该被测试的FreeSWITCH上发起呼叫,或者使用SIPP对它进行呼叫。当有测试到来时,进入相关的Dialplan播放一个声音文件即可。

笔者曾在一台虚拟机上简单进行过测试,编解码的性能从高到低依次是:PCMU/A、G729(FreeSWITCH官方商业版)、SILK、iLBC、iSAK,后面的几种编码需要比PCMU多使用几倍的CPU。限于当时的环境没有测试其他编解码。当然,笔者的测试结果仅供参考,读者如果感兴趣可以使用这里的方法自己测试一下。

稳定性、安全性

如果将FreeSWITCH用于生产环境,或者用于公网上,那么稳定性和安全性是必须考虑的。一般来说,FreeSWITCH是比较稳定的,其默认的配置也是比较安全的。但真正的应用中肯定需要考虑稳定性的问题,而且仅使用其默认的配置也肯定不能满足应用要求。在此我们来简单介绍一下这两个问题

FreeSWITCH本身是比较稳定的。它是很优秀的开源软件,全世界有很多人将它用于各种应用场景,因此,如果它有导致不稳定的地方,总会有人将问题反馈到社区,而且一般也会很快有人修复该问题

另一方面来讲,FreeSWITCH又是比较激进的,它的开发分支里很快会有大量的新特性加入,因此在测试不全面的情况下出现不稳定的情况也是难免的。

所以,如果用于生产环境,使用一个稳定的分支是有必要的。另外,即使稳定版也可能会崩溃。一旦出现这种情况,最好的解决方案就是立即恢复系统运行。在UNIX类系统上,可以使用监控工具监控FreeSWITCH进程,并在崩溃后快速恢复FreeSWITCH运行。在对稳定性要求更高的场合,可以考虑使用双机HA方案或多机集群。

对于安全性而言,我们来考虑以下几个方面:

  • FreeSWITCH默认所有用户的密码都是1234,这个一定是需要改的。可以修改vars.xml中的default_password全局变量,也可以针对每个用户在conf/directory/default/*.xml中修改。
  • Event Socket(就是使用fs_cli)默认监听在本地Loopback IP 127.0.0.1上,因此应该是安全的。但如果开了远程访问(如在其他主机上使用Event Socket与它连接)就记得修改一下这个密码。ESL连接的密码传输都是明文的,因此不建议在公网上使用,除非在专用的VPN或加密隧道中。
  • 默认情况下,mod_xml_rpc 是不加载的。如果你需要加载它,也应记住修改该模块的密码。它使用HTTP Basic来认证,因为密码都是明文传输的,不建议在公网上使用。如果需要使用的话建议在前面加HTTPS代理服务器。
  • 默认的5080端口是允许任何人呼入的,因此需要确认它对应的public Dialplan里是否有最少的路由规则。
  • 如果有黑客攻击,一般会发送大量的注册或呼叫请求暴力尝试所有可能的密码,可以考虑使用Fail2ban 功能来抵御这种攻击。
  • 在操作系统层,可以使用考虑使用iptables来设置合适的防火墙。另外,配合使用Snort 或AIDE 等防止入侵检测系统也非常有用。
  • 最后,最可怕的是DDOS [6]攻击。这是一个世界性的难题,不过,如果你不幸遇到这种攻击了,那说明别人可能是找错了门,或者你的业务已经是世界级的大了。那时候你将可以有更多的资源来解决这一问题。不过为了防患于未然,一般来说选择一个好的数据中心能在一定程度上帮你解决这个问题。

总之,达到一定稳定性和安全性的同时肯定会带来一定的系统复杂性,另外也会增加一定的实施和维护成本,这是不可避免的。在本小节中,我们仅点出了一些应注意的问题,并提到了一些常用的软件和工具,希望对读者有所帮助。如果将FreeSWITCH真正应用于生产环境,肯定还有更多的问题需要考虑。更深入的讨论超出了本书的范围,在此我们就不再展开了。

  • 23
    点赞
  • 46
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值