FreeSWITCH 常见问题(个人整理汇总)
忠告
千万别使用 CentOS 部署 FreeSWITCH !!!
以下大部分都是笔者用 CentOS7 自编译踩得坑。
建议直接 Debian 安装官方编译好的包!!!可以少踩很多坑(严重怀疑官方歧视 CentOS)。
前言
FreeSWITCH 是一个自由开源的软件型电话交换机。它采用 Mozilla Public License(MPL)授权协议,MPL 是一个开源的软件协议。它的核心库 libfreeswitch 可以嵌入其它系统或产品中,也可以做一个单独的应用存在。
本文汇总 FreeSWITCH 配置时的一些常见问题。
端口介绍
防火墙端口 | 网络协议 | 应用协议 | 描述 |
---|---|---|---|
1719 | UDP | H.323 Gatekeeper RAS port | |
1720 | TCP | H.323 Call Signaling | |
2855-2856 | TCP | MSRP | 用于短信通话 |
3478 | UDP | STUN service | 用于 NAT 穿透 |
3479 | UDP | STUN service | 用于 NAT 穿透 |
5002 | TCP | MLP protocol server | |
5003 | UDP | Neighborhood service | |
5060 | UDP & TCP | SIP UAS | 用于 SIP 信令(标准 SIP 端口,用于默认的内部配置文件) SIP 端口,用于处理 SIP 消息和呼叫控制协议。 |
5061 | UDP & TCP | SIP UAS | SIP/TLS 端口,用于加密 SIP 信令。(笔者补充的,非官方文档描述!!!) |
5070 | UDP & TCP | SIP UAS | 用于 SIP 信令(用于默认的 "NAT "配置文件) |
5080 | UDP & TCP | SIP UAS | 用于 SIP 信令(用于默认的 "外部 "配置文件) |
8021 | TCP | ESL | 用于 mod_event_socket Event Socket 端口,用于与 FreeSWITCH API 进行交互。 |
16384-32768 | UDP | RTP/ RTCP multimedia streaming | 用于 SIP、Verto 和其他协议中的音频/视频数据 RTP 端口范围,用于传输音频和视频数据。 |
5066 | TCP | Websocket | 用于 WebRTC(ws) |
7443 | TCP | Websocket | 用于 WebRTC(wss) |
8081-8082 | TCP | Websocket | 用于 Verto |
除了上述常用端口,还有其他一些端口也可能用于特定的 FreeSWITCH 插件或模块。例如:
- 8080:FreeSWITCH ESL(Event Socket Library)HTTP 端口,用于 Web 控制台和与 FreeSWITCH API 进行交互。
- 5555:mod_xml_curl 模块使用的端口,用于从远程服务器获取 XML 配置文件。
- 5070:mod_gsmopen 模块使用的端口,用于与 GSM 网络通信。
请注意,这些端口的用途可能随着 FreeSWITCH 版本和配置的不同而有所变化。为了确保正确的端口使用,建议查阅 FreeSWITCH 官方文档或配置文件。
号码信息
号码段 | 描述 | 会议音频采样率 |
---|---|---|
1000 - 1019 | 默认分机号(默认密码为 1234) | |
2000 - 2002 | 呼叫组 | |
3000 - 3099 | 电话会议 | 8 kHz |
3100 - 3199 | 电话会议 | 16 kHz |
3200 - 3299 | 电话会议 | 32 kHz |
3300 - 3399 | 电话会议 | 48 kHz |
4000 | 听取语音信箱 | |
5000 | 示例 IVR | |
5900 | 呼叫挂起 | |
5901 | 接听挂起的呼叫 | |
9178 | 收传真 | |
9179 | 发传真 | |
9180 | 铃音测试,使用远端生成的回铃音 | |
9181 | 铃音测试,产生英式铃音 | |
9182 | 铃音测试,使用音乐当铃音,彩铃 | |
9183 | 先应答,然后发送英式铃音 | |
9184 | 先应答,然后发送音乐铃音 | |
9192 | 调用 info 在 log 中显示 Channel 信息 | |
9195 | echo,回声测试,延迟 5 秒 | |
9196 | echo,回声测试 | |
9197 | 使用 tone_stream 来播放一个连续的 1004hz 的音 | |
9198 | 使用 tone_stream 播放俄罗斯方块音乐 | |
9664 | 保持音乐 | |
9888 | FreeSWITCH 公共会议 | |
91616 | FreeSWITCH 公共会议 | 16 kHz |
93232 | FreeSWITCH 公共会议 | 32 kHz |
更多号码请参考官方文档。
配置文件
FreeSwitch 的配置文件一般是放在 FreeSwitch/conf 下,但笔者这里配置文件是存放在 /etc/freeswitch 下,最顶层是的 freeswitch.xml,也就是说最先加载的就是这个 XML, FreeSwitch 根据这个 XML 依次加载 conf 目录下的其它配置文件。
先说 conf 根目录下的文件:
- freeswitch.xml:主配置文件,它会使用 include 语句装入其它文件。
- vars.xml:一些常用变量,都在这个配置文件中定义。
再说 conf 下的文件夹:
autoload_configs:存放自动加载的配置文件。
- modules.conf.xml:配置当 freeswitch 启动时自动装载哪些模块。
- 其它 xml:一般来说都是对应每个模块的配置文件。
- chatplan:存放的是聊天计划配置文件。
- dialplan:存放的是拨号计划配置文件。
directory:用户目录,存储跟用户相关的信息。
- defalut:该目录是默认的用户目录配置
- default.xml:该文件是对应每个 sip 用户的,每个 sip 用户都有一个配置文件。
- ivr_menus:IVR 菜单配置文件。
lang:多语言支持配置文件。
- en:英语
- fr:法语
sip_profiles 文件夹:sip 配置文件
- internal.xml:一个 SIP profile,或称作一个 SIP-UA,监听在本地 IP 及端口 5060。
- externa.xml:另一个 SIP-UA,用作外部连接,端口 5080。
- skinny_profiles:思科 SCCP 协议话机的配置文件。
- jingle_profiles:连接 Google Talk 的相关配置文件。
- mrcp_profiles:MRCP 的相关配置,用于跟第三方语音合成和语音识别系统对接。
模块介绍
SIP 模块
基本概念
- Sofia-SIP FreeSwitch 的 SIP 功能是在 mod_sofia 模块中实现的。FreeSwitch 并没有自己开发新的 SIP 协议栈,而是使用了比较成熟的开源 SIP 协议栈 Sofia-SIP。
- Endpoint 在 FreeSwitch 中,实现一些互联网协议接口的模块称为 Endpoint。FreeSwitch 支持很多类型的 Endpoint,如 SIP,H232 等。这些不同的 Endpoint 主要是使用不同的控制协议跟其他的 Endpoint 通话。
- mod_sofia mod_sofia 实现了 SIP 中的注册服务器、重定向服务器、媒体服务器,呈现服务器、SBC 等各种功能。它的定位是一个 B2BUA。
- SIP Profile 在 mod_sofia 中,SIP Profile 相当于一个 SIPUA,通过各种不同的参数可以配置一个 UA 的行为。一个系统可以有多个 SIP Profile,每个 SIP Profile 都可以监听不同的 IP 地址和端口。
- Gateway 一个 SIP Profile 中有多个 Gateway(网关),它主要用于定义一个远程的 SIP 服务器,使 Freeswitch 可以与其他服务器通信。
- 本地 SIP 用户 FreeSwitch 可以作为注册服务器,这时候其他 SIP 客户端就可以向它注册。FreesWitch 将通过用户目录中(conf/Directory)中的配置信息对注册用户进行鉴权。这些 SIP 客户端锁代表的用户就称为本地 SIP 用户,简称本地用户
- 来电去话,中继来电,中继去话
配置文件
Sofia 的配置文件在 /freeswitch/autoload_configs/sofia.conf.xml
中。Sofia 支持多个 Profile,而每一个 Profile 相当于一个 SIP UA,在启动后悔监听一个 IP:PORT
对。FreeSwitch 默认的配置带了三个 Profile(也就是三个 UA)。我们不讨论 IPv6,仅讨论 internal 和 external(分别在 internal.xml 和 external.xml 中定义的,分别运行在 5060 和 5080 端口上)
Profile 的几个重要参数,节选 internal.xml 部分配置:
<profile name="internal">
<aliases>
<!-- 别名 呼叫字符串中可以使用别名 例如 sofia/default/10086@127.0.0.1-->
<alias name="default"/>
</aliases>
<!--网关配置,一般在external上定义网关配置,网关配置就是对远程SIP服务器的一些参数配置,具体的参数配置由SIP服务器来决定-->
<gateways></gateways>
<domains>
<domain name="all" alias="true" parse="false"/>
</domains>
<settings>
<!--设置来话将进入Dialplan中的哪个Context进行路由-->
<param name="context" value="public"/>
<!-- 设置默认的dialplan类型,即在该Profile上有电话呼入后到哪个Dialplan中进行路由-->
<param name="dialplan" value="XML"/>
<!-- 设置支持的来电媒体编码,用于编码协商-->
<param name="inbound-codec-prefs" value="$${global_codec_prefs}"/>
<!-- 设置支持的去话媒体编码,用于编码协商-->
<param name="outbound-codec-prefs" value="$${global_codec_prefs}"/>
<!-- ip address to use for rtp, DO NOT USE HOSTNAMES ONLY IP ADDRESSES -->
<param name="rtp-ip" value="$${local_ip_v4}"/>
<!-- ip address to bind to, DO NOT USE HOSTNAMES ONLY IP ADDRESSES -->
<param name="hold-music" value="$${hold_music}"/>
<!--是否开启媒体绕过功能-->
<!--<param name="inbound-bypass-media" value="true"/>-->
<!-- 是否对来电进行鉴权-->
<param name="auth-calls" value="$${internal_auth_calls}"/>
<!-- external_sip_ip
Used as the public IP address for SDP.
Can be an one of:
ip address - "12.34.56.78"
a stun server lookup - "stun:stun.server.com"
a DNS name - "host:host.server.com"
auto - Use guessed ip.
auto-nat - Use ip learned from NAT-PMP or UPNP
-->
<param name="ext-rtp-ip" value="$${external_rtp_ip}"/>
<param name="ext-sip-ip" value="$${external_sip_ip}"/>
</settings>
</profile>
external.xml 的配置与 internal.xml 的配置大部分相同,最大的不同是 auth-calls
参数,internal.xml 默认为 true,而 external.xml 默认为 false。也就是说,客户端发往 FreeSwitch 的 5060 端口的 SIP 消息需要鉴权(一般只对 REGISTER 和 INVITE 消息进行鉴权),而发往 5080 端口的消息不需要鉴权。我们一般把本地用户都注册到 5060 上,所以它们打电话时要经过鉴权,保证只有授权用户(本地用户目录中配置的)才能注册和拨打电话。而 5080 则不同,任何人均可以向该端口发送 SIP INVITE 请求。
Gateway(网关)
在 external.xml 中我们可以看到它使用预处理指令将 external 目录下的所有 XML 配置文件都装入到 external 的 Profile 文件的 gateways 标签中:
<profile name="external">
<gateways>
<X-PRE-PROCESS cmd="include" data="external/*.xml"/>
</gateways>
</profile>
节选部分 Gateway 配置:
<gateway name="唯一网关名称">
<param name="realm" value="SIP服务器地址:默认端口5060"/>
<param name="username" value="用户名"/>
<param name="password" value="密码"/>
<!-- SIP消息中From字段的值,默认与username相同 -->
<param name="from-user" value="cluecon"/>
<!-- From字段的domain值,默认与realm相同 -->
<param name="from-domain" value="asterlink.com"/>
<!-- 来话中的分机号,即被叫号码,默认与username相 -->
<param name="extension" value="cluecon"/>
<!-- 代理服务器地址,默认与realm相同 -->
<param name="proxy" value="asterlink.com"/>
<!-- 代理注册服务器地址,默认与realm相同 -->
<param name="register-proxy" value="mysbc.com"/>
<!-- 注册SIP消息的Expires字段的值,单位为minute -->
<param name="expire-seconds" value="60"/>
<!-- 是否需要注册,有些网关必须注册才能打电话,有些不需要 -->
<param name="register" value="false"/>
<!-- SIP消息是否udp还是tcp -->
<param name="register-transport" value="udp"/>
<!-- 注册失败或超时后,等待多少秒后重新注册 -->
<param name="retry-seconds" value="30"/>
<!-- 将主叫号码放到SIP的From字段中。 -->
<param name="caller-id-in-from" value="false"/>
<!-- 设置SIP协议中的Contact字段中额外的参数 -->
<param name="contact-params" value=""/>
<!-- 每隔一段时间发送一个SIP OPTIONS消息,如果失败,则会从该网关注销,并将其设置为down状态。心跳检测服务是否畅通 -->
<param name="ping" value="25"/>
</gateway>
呼叫是如何工作的
我们假设用的是默认配置,并从 1000 呼叫 1001。
- 1000 的 SIP 话机作为 UAC 会发送 INVITE 请求到 FreeSwitch 的 5060 端口,也就是达到 mod_sofia 的 internal 这个 Profile 所配置的 UAS,该 UAS 收到正确的 INVITE 请求后会返回 100 响应码,表示我收到你的请求了。该 UAS 对所有收到的 INVITE 都要进行鉴权(因为 auth-calls=true)。它会检测 ACL(访问控制列表,一般用于 IP 鉴权)。默认的 ACL 检查是不通过的因此就会走到密码鉴权(HTTP 协议中的 Digest Auth)阶段。一般是 UAS 回复 401。
UAC 重新发送带鉴权信息的 INVITE,UAS 收到后,便将鉴权信息提交到上层的 FreeSwitch 代码,FreeSwitch 就到会 Directory(用户目录)查找相应的用户。此处,它会找到
/freeswitch/directory/default/1000.xml
文件中配置的用户信息,并根据其中配置的密码进行鉴权。如果鉴权不通过,返回 403 Forbidden 等错误信息,通话结束。如果鉴权通过,FreeSwitch 就取到了用户的信息,比较重要的是 user_context,在我们的例子中它的值为 default。接下来电话进入路由(routing)阶段,开始查找 Dialplan。用于该用户的 Context 是 default,因此路由就从 default 这个 Dialplan 查起(即/freeswitch/dialplan/default.xml
)1001.xml 配置示例:
<include> <user id="1001"> <params> <param name="password" value="$${default_password}"/> <param name="vm-password" value="1001"/> </params> <variables> <variable name="toll_allow" value="domestic,international,local"/> <variable name="accountcode" value="1001"/> <variable name="user_context" value="default"/> <variable name="effective_caller_id_name" value="Extension 1001"/> <variable name="effective_caller_id_number" value="1001"/> <variable name="outbound_caller_id_name" value="$${outbound_caller_name}"/> <variable name="outbound_caller_id_number" value="$${outbound_caller_id}"/> <variable name="callgroup" value="techsupport"/> </variables> </user> </include>
查找 Dialplan,找到 1001 这个用户,并执行 bridge user/1001,在这里 user/1001 称为呼叫字符串,它会再次查找 Directory,找到
/freeswitch/directory/default/1000.xml
里配置的参数,由于 1001 是被叫,因此他会进一步查找直到查到 1001 实际注册的位置,由于所有用户的规则都是一样,因此该参数被放到/freeswitch/direcotry/default.xml
中,在该文件中可以看到如下配置<domain name="$${domain}"> <params> <param name="dial-string" value="{^^:sip_invite_domain=${dialed_domain}:presence_id=${dialed_user}@${dialed_domain}}${sofia_contact(*/${dialed_user}@${dialed_domain})},${verto_contact(${dialed_user}@${dialed_domain})}"/> <!-- These are required for Verto to function properly --> <param name="jsonrpc-allowed-methods" value="verto"/> <!-- <param name="jsonrpc-allowed-event-channels" value="demo,conference,presence"/> --> </params>
其中,最关键的是 sofia_contact 这个 API 调用,它会查找数据库,找到 1001 实际注册的 Contact 地址,并返回真正的呼叫字符串。
freeswitch@CentOS7> sofia_contact 1001 sofia/internal/sip:46598071@10.211.55.2:51032
- 找到呼叫字符串后,FreeSwitch 又启动另外一个会话作为一个 UAC 给 1001 发送 INVITE 请求,如果 1001 摘机,则 1001 向 FreeSwitch 回送 200 OK 消息,FreeSwitch 再向 100 返回 200OK,通话开始。
FreeSwitch 是一个 B2BUA,上面的过程建立了一通会话,其中有两个 Channel。我们可以跟踪 SIP 消息试一下
sofia profile internal siptrace on/off
在 FreeSwitch 的默认配置中,external 对应的 Profile 是不鉴权的,凡是送到 5080 端口的 INVITE 都不需要鉴权。
- SIPUA 直接把 INVITE 送到任意端口,一般用于中继方式对接
- FreeSwitch 作为一个客户端,若要添加一个网关,则该网关会被放到
/freeswitch/sip_profiles/external/
的文件中,它就会被包含到/freeswitch/sip_profiles/external.xml
中。它向其他服务器注册时,其中的 Contact 地址就是 IP:5080,如果有来话,对方的服务器就会把 INVITE 送到它的 5080 端口
SIP 相关知识
这里就介绍点基础知识。
错误码
参考改文章 SIP / FS 状态码&错误码详解
SIP 协议简介
SIP 是一个对等的协议,类似 P2P。它可以在不需要服务器的情况下进行通信,只要通信双方都彼此指导对方的地址(或者只有一方知道另一方的地址)即可,这种情况称为点对点通信。详细标准可查看 RFC3261
SIP 协议采用 Client/Server 模型,每一个请求(request),Server 从接受到请求到处理完毕,要回复多个临时响应,和有且仅有一个终结响应(response)。
概念
- Transaction 请求和所有的响应构成一个事务,一个完整的呼叫过程包括多个事件。
- UA 用户代理,是发起或接受呼叫的逻辑实体
- UAC 用户代理客户端,用于发起请求
- UAS 用户代理服务器,用于接受请求
- UAC 和 UAS 的划分是针对一个事务的,在一个呼叫的多个事务中,UAC 和 UAS 的角色是可以互相转换的
- B2BUA 是一个 SIP 中逻辑上的网络组件,用于操作不同会话的端点,它将 channel 划分为两路通话,在不同会话的端点直接通信。例如,当建立一通呼叫时,B2BUA 作为一个 UAS 接受所有用户请求,处理后以 UAC 角色转发至目标端。
SIP URI
设备环境
名称 | IP 地址 |
---|---|
Bob 的终端 | 192.168.1.100 |
Alice 的终端 | 192.168.1.200 |
FreeSwitch 服务器 | 192.168.1.9 |
假设 Bob 在服务器 192.168.1.100,Alice 在 192.168.1.200 上,FreeSwitch 在 192.168.1.9 上。
简单呼叫流程说明
Alice 注册到 FreeSwitch 上,Bob 呼叫 Alice 时,使用 Alice 的服务器地(又称逻辑地址)而 Bob 只知道服务器地址,即 sip:Alice@192.168.1.9
。
FreeSwitch 接受到请求后,查找本地数据库,发现 Alice 的实际地址(Contact 地址,又叫联系地址,亦称物理地址)是 sip:Alice@192.168.1.100
,便可以建立呼叫。
Bob 作为主叫方,它已经知道服务器地址,可以直接发送 INVITE 请求,是不需要注册的,而 Alice 作为被叫的一方,为了让服务器能找到它,它必须事先通过 REGISTER 消息注册到服务器上。
报文头域
所有 SIP 消息都必须包含以下前 6 个头域:
- Call-ID 用于区分不同会话的唯一标志
- CSeq 序列号,用于在同一会话中区分事务
- From 说明请求来源
- To 说明请求接受方
- Max-Forwars 限制跳跃点数和最大转发次数
- Via 描述请求消息经过的路径
request 和 response 报文的 From 和 To 是完全一致的,尽管他们的方向是相反的。From 和 To 的值是根据 request 来定义的。 Via 中的 branch 标记腿的 id 当有使用 proxy 代理时,请求转发时用的还是同一个 branch,From 和 To 中的 tag 可以作为 id 标记是否为原始请求。
Via 的一个示例:
更多的请参考 SIP 参数
SDP 协议简介
SIP 负责建立和释放会话,一般来说,会话会包含相关的媒体,如视频和音频。媒体数据是由 SDP(Session Description Protocol,会话描述协议)描述的,SDP 一般不单独使用,它与 SIP 配合使用时会放到 SIP 协议的 Boby(正文)中。会话建立时,需要媒体协商,双方才能确定对方的媒体能力以交互媒体数据,比如确认支持的数据格式。 SDP 的特点:
- 是一个结构化的文本协议
- 表述 session 的媒体,协议,编码译码格式等
- 通常的 SDP 消息格式为
type=parameter1 parameter2 ... parameterN
v = 0
o = mhandley2890844526 2890842807 IN IP4 126.16.64.4
s = SDP Seminar
i = A Seminar on the session description protocol
u = http://www.cs.ucl.ac.uk/staff/M.Handley/sdp.03.ps
e = mjh@isi.edu(Mark Handley)
c = IN IP4 224.2.17.12/127
t = 2873397496 2873404696
a = recvonly
m = audio 49170 RTP/AVP 0
m = video 51372 RTP/AVP 31
m = application 32416udp wb
a = orient:portrait
SDP 协议就类似以上这种键值对的形式,它通常不是单独出现的,而是作为 SIP 协议的报文进行传输。
- v=:Version,表示协议的版本号。
o=:Origin,表示源。
#username(用户名) sess-id(会话ID) sess-version(会话版本号) nettype(网络类型) addrtype(地址类型) unicast-address(单播地址) o= (owner/creator and session identifier)
- s=:Session Name,表示本 SDP 所描述的 Session 的名称。
c=:Connection Data,连接数据。其中值域中已空格分配的两个字段分别是网络类型和网络地址,以后的 RTP 流就会发到该地址上。
# 网络类型 网络地址 (RTP数据流发送到该地址) c=* (connection information - not required if included in all media)
- t=:Timing,起止时间。0 表示无限。
m=:Media Type,媒体类型。audio 表示音频。空格后面的数字表示端口号。RTP/SAVPF 是传输协议;后面是支持的编码类型。
# 媒体类型 音频端口号 传输协议 支持的codec类型 m= media port transport format-list
a=:Attribute,属性。它用于描述上面音频的属性,如本例中的 9 代表 G722 编码,0 代表 PCMU 编码。
# 描述上面音频的属性,一般m后面跟多个a a=* (zero or more session attribute lines)
具体介绍请查看该文章:SDP 协议基础
常用命令
启动 FreeSWITCH
启动
freeswitch
后台启动
freeswitch -nc
关闭 FreeSWITCH
关闭
shutdown
后台关闭
freeswitch -stop
查看 FreeSWITCH 是否正常启动
netstat -anp | grep freeswitch
正常启动将会有如下端口:
[root@localhost etc]# netstat -anp | grep freeswitch
tcp 0 0 192.168.0.112:8081 0.0.0.0:* LISTEN 14622/freeswitch
tcp 0 0 192.168.0.112:8082 0.0.0.0:* LISTEN 14622/freeswitch
tcp 0 0 192.168.0.112:7443 0.0.0.0:* LISTEN 14622/freeswitch
tcp 0 0 192.168.0.112:5080 0.0.0.0:* LISTEN 14622/freeswitch
tcp 0 0 192.168.0.112:5081 0.0.0.0:* LISTEN 14622/freeswitch
tcp 0 0 192.168.0.112:5060 0.0.0.0:* LISTEN 14622/freeswitch
tcp 0 0 192.168.0.112:5061 0.0.0.0:* LISTEN 14622/freeswitch
tcp 0 0 192.168.0.112:5066 0.0.0.0:* LISTEN 14622/freeswitch
tcp 0 0 192.168.0.112:7443 172.16.30.53:10825 ESTABLISHED 14622/freeswitch
tcp6 0 0 ::1:8081 :::* LISTEN 14622/freeswitch
tcp6 0 0 ::1:8082 :::* LISTEN 14622/freeswitch
tcp6 0 0 :::8021 :::* LISTEN 14622/freeswitch
tcp6 0 0 ::1:5080 :::* LISTEN 14622/freeswitch
tcp6 0 0 ::1:5081 :::* LISTEN 14622/freeswitch
tcp6 0 0 ::1:5060 :::* LISTEN 14622/freeswitch
tcp6 0 0 ::1:5061 :::* LISTEN 14622/freeswitch
udp 0 0 0.0.0.0:1337 0.0.0.0:* 14622/freeswitch
udp 0 0 192.168.0.112:34132 192.168.0.254:5351 ESTABLISHED 14622/freeswitch
udp 0 0 192.168.0.112:5060 0.0.0.0:* 14622/freeswitch
udp 0 0 192.168.0.112:5080 0.0.0.0:* 14622/freeswitch
udp6 0 0 ::1:5060 :::* 14622/freeswitch
udp6 0 0 ::1:5080 :::* 14622/freeswitch
unix 3 [ ] STREAM CONNECTED 273797 14622/freeswitch
unix 3 [ ] STREAM CONNECTED 302682 14622/freeswitch
unix 3 [ ] STREAM CONNECTED 305432 14622/freeswitch
unix 3 [ ] STREAM CONNECTED 307950 14622/freeswitch
unix 3 [ ] STREAM CONNECTED 305433 14622/freeswitch
unix 3 [ ] STREAM CONNECTED 302681 14622/freeswitch
unix 3 [ ] STREAM CONNECTED 228893 14622/freeswitch
unix 3 [ ] STREAM CONNECTED 307949 14622/freeswitch
unix 3 [ ] STREAM CONNECTED 307951 14622/freeswitch
unix 3 [ ] STREAM CONNECTED 228894 14622/freeswitch
unix 3 [ ] STREAM CONNECTED 302679 14622/freeswitch
unix 3 [ ] STREAM CONNECTED 307952 14622/freeswitch
unix 3 [ ] STREAM CONNECTED 306937 14622/freeswitch
unix 3 [ ] STREAM CONNECTED 306938 14622/freeswitch
unix 3 [ ] STREAM CONNECTED 302680 14622/freeswitch
unix 3 [ ] STREAM CONNECTED 273796 14622/freeswitch
[root@localhost etc]#
连接控制台
fs_cli
返回如下:
[root@localhost etc]# fs_cli
.=======================================================.
| _____ ____ ____ _ ___ |
| | ___/ ___| / ___| | |_ _| |
| | |_ \___ \ | | | | | | |
| | _| ___) | | |___| |___ | | |
| |_| |____/ \____|_____|___| |
| |
.=======================================================.
| Anthony Minessale II, Ken Rice, |
| Michael Jerris, Travis Cross |
| FreeSWITCH (http://www.freeswitch.org) |
| Paypal Donations Appreciated: paypal@freeswitch.org |
| Brought to you by ClueCon http://www.cluecon.com/ |
.=======================================================.
.=======================================================================================================.
| _ _ ____ _ ____ |
| / \ _ __ _ __ _ _ __ _| | / ___| |_ _ ___ / ___|___ _ __ |
| / _ \ | '_ \| '_ \| | | |/ _` | | | | | | | | |/ _ \ | / _ \| '_ \ |
| / ___ \| | | | | | | |_| | (_| | | | |___| | |_| | __/ |__| (_) | | | | |
| /_/ \_\_| |_|_| |_|\__,_|\__,_|_| \____|_|\__,_|\___|\____\___/|_| |_| |
| |
| ____ _____ ____ ____ __ |
| | _ \_ _/ ___| / ___|___ _ __ / _| ___ _ __ ___ _ __ ___ ___ |
| | |_) || || | | | / _ \| '_ \| |_ / _ \ '__/ _ \ '_ \ / __/ _ \ |
| | _ < | || |___ | |__| (_) | | | | _| __/ | | __/ | | | (_| __/ |
| |_| \_\|_| \____| \____\___/|_| |_|_| \___|_| \___|_| |_|\___\___| |
| |
| ____ _ ____ |
| / ___| |_ _ ___ / ___|___ _ __ ___ ___ _ __ ___ |
| | | | | | | |/ _ \ | / _ \| '_ \ / __/ _ \| '_ ` _ \ |
| | |___| | |_| | __/ |__| (_) | | | | _ | (_| (_) | | | | | | |
| \____|_|\__,_|\___|\____\___/|_| |_| (_) \___\___/|_| |_| |_| |
| |
.=======================================================================================================.
Type /help <enter> to see a list of commands
+OK log level [7]
freeswitch@localhost.localdomain>
常用 fs_cli 命令
快捷键
快捷键 | 命令 | 备注 |
---|---|---|
F1 | help | |
F2 | status | |
F3 | show channels | |
F4 | show calls | |
F5 | sofia status | 查看 sofia 状态 |
F6 | reloadxml | 重新加载配置 |
F7 | console loglevel 0 | |
F8 | console loglevel 7 | |
F9 | sofia status profile internal | 查看 profile 信息 |
F10 | sofia profile internal siptrace on | |
F11 | sofia profile internal siptrace off | |
F12 | version |
查看 FreeSWITCH 状态
status
返回如下:
freeswitch@localhost.localdomain> status
UP 0 years, 1 day, 21 hours, 8 minutes, 21 seconds, 823 milliseconds, 189 microseconds
FreeSWITCH (Version 1.10.9-release 64bit) is ready
148 session(s) since startup
0 session(s) - peak 5, last 5min 0
0 session(s) per Sec out of max 30, peak 2, last 5min 0
1000 session(s) max
min idle cpu 0.00/99.97
Current Stack Size/Max 240K/8192K
freeswitch@localhost.localdomain>
查看 SIP 网关状态
sofia status
返回如下:
freeswitch@localhost.localdomain> sofia status
Name Type Data State
=================================================================================================
external-ipv6 profile sip:mod_sofia@[::1]:5080 RUNNING (0)
external-ipv6 profile sip:mod_sofia@[::1]:5081 RUNNING (0) (TLS)
192.168.0.112 alias internal ALIASED
external profile sip:mod_sofia@117.29.37.164:5080 RUNNING (0)
external profile sip:mod_sofia@117.29.37.164:5081 RUNNING (0) (TLS)
external::example.com gateway sip:joeuser@example.com NOREG
internal-ipv6 profile sip:mod_sofia@[::1]:5060 RUNNING (0)
internal-ipv6 profile sip:mod_sofia@[::1]:5061 RUNNING (0) (TLS)
internal profile sip:mod_sofia@192.168.0.112:5060 RUNNING (0)
internal profile sip:mod_sofia@192.168.0.112:5061 RUNNING (0) (TLS)
=================================================================================================
4 profiles 1 alias
freeswitch@localhost.localdomain>
查看 profile 信息
sofia status profile internal
返回如下:
freeswitch@localhost.localdomain> sofia status profile internal
=================================================================================================
Name internal
Domain Name N/A
Auto-NAT false
DBName sofia_reg_internal
Pres Hosts 192.168.0.112,192.168.0.112
Dialplan XML
Context public
Challenge Realm auto_from
RTP-IP 192.168.0.112
SIP-IP 192.168.0.112
URL sip:mod_sofia@192.168.0.112:5060
BIND-URL sip:mod_sofia@192.168.0.112:5060;transport=udp,tcp
TLS-URL sip:mod_sofia@192.168.0.112:5061
TLS-BIND-URL sips:mod_sofia@192.168.0.112:5061;transport=tls
WS-BIND-URL sip:mod_sofia@192.168.0.112:5066;transport=ws
WSS-BIND-URL sips:mod_sofia@192.168.0.112:7443;transport=wss
HOLD-MUSIC local_stream://moh
OUTBOUND-PROXY N/A
CODECS IN OPUS,G722,PCMU,PCMA,H264,VP8
CODECS OUT OPUS,G722,PCMU,PCMA,H264,VP8
TEL-EVENT 101
DTMF-MODE rfc2833
CNG 13
SESSION-TO 0
MAX-DIALOG 0
MAX-RECV-RPS 1000
NOMEDIA false
LATE-NEG true
PROXY-MEDIA false
AGGRESSIVENAT false
CALLS-IN 138
FAILED-CALLS-IN 21
CALLS-OUT 59
FAILED-CALLS-OUT 10
REGISTRATIONS 2
可用的编解码器
show codecs
返回如下:
freeswitch@localhost.localdomain> show codecs
type,name,ikey
codec,ADPCM (IMA),mod_spandsp
codec,AMR / Bandwidth Efficient,mod_amr
codec,AMR / Octet Aligned,mod_amr
codec,B64 (STANDARD),mod_b64
codec,G.711 alaw,CORE_PCM_MODULE
codec,G.711 ulaw,CORE_PCM_MODULE
codec,G.722,mod_spandsp
codec,G.723.1 6.3k,mod_g723_1
codec,G.726 16k,mod_spandsp
codec,G.726 16k (AAL2),mod_spandsp
codec,G.726 24k,mod_spandsp
codec,G.726 24k (AAL2),mod_spandsp
codec,G.726 32k,mod_spandsp
codec,G.726 32k (AAL2),mod_spandsp
codec,G.726 40k,mod_spandsp
codec,G.726 40k (AAL2),mod_spandsp
codec,G.729,mod_g729
codec,GSM,mod_spandsp
codec,LPC-10,mod_spandsp
codec,OPUS (STANDARD),mod_opus
codec,PROXY PASS-THROUGH,CORE_PCM_MODULE
codec,PROXY VIDEO PASS-THROUGH,CORE_PCM_MODULE
codec,RAW Signed Linear (16 bit),CORE_PCM_MODULE
codec,Speex,CORE_SPEEX_MODULE
codec,VP8 Video,CORE_VPX_MODULE
codec,VP9 Video,CORE_VPX_MODULE
26 total.
freeswitch@localhost.localdomain>
当前通道
精简信息
show channels
返回如下:
freeswitch@localhost.localdomain> show channels
uuid,direction,created,created_epoch,name,state,cid_name,cid_num,ip_addr,dest,application,application_data,dialplan,context,read_codec,read_rate,read_bit_rate,write_codec,write_rate,write_bit_rate,secure,hostname,presence_id,presence_data,accountcode,callstate,callee_name,callee_num,callee_direction,call_uuid,sent_callee_name,sent_callee_num,initial_cid_name,initial_cid_num,initial_ip_addr,initial_dest,initial_dialplan,initial_context
498dab6a-3761-4b70-b74a-a7150173e117,inbound,2023-02-17 15:11:51,1676617911,sofia/internal/1002@192.168.0.112,CS_EXECUTE,1002,1002,172.16.30.53,1000,bridge,user/1000@192.168.0.112,XML,default,PCMA,8000,64000,PCMA,8000,64000,,localhost.localdomain,1002@192.168.0.112,,1002,ACTIVE,Outbound Call,quao5624,SEND,498dab6a-3761-4b70-b74a-a7150173e117,Outbound Call,quao5624,1002,1002,172.16.30.53,1000,XML,default
edde0842-98d9-492a-88a3-e65af658bc51,outbound,2023-02-17 15:11:51,1676617911,sofia/internal/quao5624@okjusn1de44j.invalid,CS_EXCHANGE_MEDIA,Extension 1002,1002,172.16.30.53,quao5624,,,XML,default,PCMA,8000,64000,PCMA,8000,64000,srtp:dtls:AES_CM_128_HMAC_SHA1_80,localhost.localdomain,1000@192.168.0.112,,,ACTIVE,Outbound Call,quao5624,SEND,498dab6a-3761-4b70-b74a-a7150173e117,Extension 1002,1002,Extension 1002,1002,172.16.30.53,quao5624,XML,default
2 total.
freeswitch@localhost.localdomain>
详细信息
show channels verbose
返回如下:
freeswitch@localhost.localdomain> show channels verbose
uuid,direction,created,created_epoch,name,state,cid_name,cid_num,ip_addr,dest,application,application_data,dialplan,context,read_codec,read_rate,read_bit_rate,write_codec,write_rate,write_bit_rate,secure,hostname,presence_id,presence_data,accountcode,callstate,callee_name,callee_num,callee_direction,call_uuid,sent_callee_name,sent_callee_num,initial_cid_name,initial_cid_num,initial_ip_addr,initial_dest,initial_dialplan,initial_context
498dab6a-3761-4b70-b74a-a7150173e117,inbound,2023-02-17 15:11:51,1676617911,sofia/internal/1002@192.168.0.112,CS_EXECUTE,1002,1002,172.16.30.53,1000,bridge,user/1000@192.168.0.112,XML,default,PCMA,8000,64000,PCMA,8000,64000,,localhost.localdomain,1002@192.168.0.112,,1002,ACTIVE,Outbound Call,quao5624,SEND,498dab6a-3761-4b70-b74a-a7150173e117,Outbound Call,quao5624,1002,1002,172.16.30.53,1000,XML,default
edde0842-98d9-492a-88a3-e65af658bc51,outbound,2023-02-17 15:11:51,1676617911,sofia/internal/quao5624@okjusn1de44j.invalid,CS_EXCHANGE_MEDIA,Extension 1002,1002,172.16.30.53,quao5624,,,XML,default,PCMA,8000,64000,PCMA,8000,64000,srtp:dtls:AES_CM_128_HMAC_SHA1_80,localhost.localdomain,1000@192.168.0.112,,,ACTIVE,Outbound Call,quao5624,SEND,498dab6a-3761-4b70-b74a-a7150173e117,Extension 1002,1002,Extension 1002,1002,172.16.30.53,quao5624,XML,default
2 total.
freeswitch@localhost.localdomain>
当前所有呼叫
show calls
返回如下:
freeswitch@localhost.localdomain> show calls
uuid,direction,created,created_epoch,name,state,cid_name,cid_num,ip_addr,dest,presence_id,presence_data,accountcode,callstate,callee_name,callee_num,callee_direction,call_uuid,hostname,sent_callee_name,sent_callee_num,b_uuid,b_direction,b_created,b_created_epoch,b_name,b_state,b_cid_name,b_cid_num,b_ip_addr,b_dest,b_presence_id,b_presence_data,b_accountcode,b_callstate,b_callee_name,b_callee_num,b_callee_direction,b_sent_callee_name,b_sent_callee_num,call_created_epoch
498dab6a-3761-4b70-b74a-a7150173e117,inbound,2023-02-17 15:11:51,1676617911,sofia/internal/1002@192.168.0.112,CS_EXECUTE,1002,1002,172.16.30.53,1000,1002@192.168.0.112,,1002,ACTIVE,Outbound Call,quao5624,SEND,498dab6a-3761-4b70-b74a-a7150173e117,localhost.localdomain,Outbound Call,quao5624,edde0842-98d9-492a-88a3-e65af658bc51,outbound,2023-02-17 15:11:51,1676617911,sofia/internal/quao5624@okjusn1de44j.invalid,CS_EXCHANGE_MEDIA,Extension 1002,1002,172.16.30.53,quao5624,1000@192.168.0.112,,,ACTIVE,Outbound Call,quao5624,SEND,Extension 1002,1002,1676617915
1 total.
freeswitch@localhost.localdomain>
当前注册用户
精简信息
show registrations
返回如下:
freeswitch@localhost.localdomain> show registrations
reg_user,realm,token,url,expires,network_ip,network_port,network_proto,hostname,metadata
1000,192.168.0.112,us21f4moae9nucko3tfs,sofia/internal/sip:quao5624@okjusn1de44j.invalid;transport=ws;fs_nat=yes;fs_path=sip%3Aquao5624%40172.16.30.53%3A10825%3Btransport%3Dwss,1676616752,172.16.30.53,10825,udp,localhost.localdomain,
1002,192.168.0.112,4yuO1NzJ0UJ9yTCgvLT0Nw..,sofia/internal/sip:1002@172.16.30.53:61328,1676616756,172.16.30.53,61328,udp,localhost.localdomain,
2 total.
freeswitch@localhost.localdomain>
详细信息
sofia status profile internal reg
若只想查看某个用户,可以使用如下命令:
sofia status profile internal reg 1006
返回如下:
freeswitch@localhost.localdomain> sofia status profile internal reg
Registrations:
=================================================================================================
Call-ID: us21f4moae9nucko3tfs
User: 1000@192.168.0.112
Contact: "" <sip:quao5624@okjusn1de44j.invalid;transport=ws;fs_nat=yes;fs_path=sip%3Aquao5624%40172.16.30.53%3A10825%3Btransport%3Dwss>
Agent: SIP.js/0.21.1
Status: Registered(WSS-NAT)(unknown) EXP(2023-02-17 14:52:32) EXPSECS(540)
Ping-Status: Reachable
Ping-Time: 0.00
Host: localhost.localdomain
IP: 172.16.30.53
Port: 10825
Auth-User: 1000
Auth-Realm: 192.168.0.112
MWI-Account: 1000@192.168.0.112
Call-ID: 4yuO1NzJ0UJ9yTCgvLT0Nw..
User: 1002@192.168.0.112
Contact: "" <sip:1002@172.16.30.53:61328>
Agent: PortSIP UC Client Android - v11.5.2
Status: Registered(UDP)(unknown) EXP(2023-02-17 14:45:51) EXPSECS(139)
Ping-Status: Reachable
Ping-Time: 0.00
Host: localhost.localdomain
IP: 172.16.30.53
Port: 61328
Auth-User: 1002
Auth-Realm: 192.168.0.112
MWI-Account: 1002@192.168.0.112
Total items returned: 2
=================================================================================================
freeswitch@localhost.localdomain>
会议
发起会议
conference test bgdial user/1004
上面的命令表示,发起 1 个名为 test 的会话,同时拨打 1004 用户,如果该用户接听了,就相当于加入会议。
注:如果 1004 是第 1 个加入会议的人,此时会议室还没有人,只能听到背景音乐等待其它人加入。
如果再拉 1 个人进来,即:
conference test bgdial user/1000
这时 1000 与 1004,就可以相互听见对方的声音了。
列出所有在会议中的用户
conference list
查看某会议房间中的用户
conference test list
上面的命令,表示查看会议 test 中的用户列表,结果如下图:
freeswitch@localhost.localdomain> conference test list
2;sofia/internal/5gtg9jad@dci7le09la78.invalid;d902a860-c0f5-4ea7-8faa-38a988f221e1;Outbound Call;5gtg9jad;hear|speak|talking|video|floor|vid-floor;0;0;100
1;sofia/internal/1002@172.16.60.33:9952;621f7c15-c6a9-4226-a85e-e493de2fe5e3;Outbound Call;1002;hear|speak|talking|video;0;0;100
freeswitch@localhost.localdomain>
注意:每行最开始的数字(如用户 1002 的 member-id
为 1),即为用户的 member-id
,这个很有用,后面会讲到。
将某用户从会议中踢出
conference test kick 9
对某用户静音 / 解除静音
# 禁音
conference test mute 10
# 解除禁音
conference test tmute 10
表示在会议 test 中,对 member-id 为 10 的用户静音。如果把 mute 换成 tmute 即为解除静音。
让某用户听不到会议内容
# 屏蔽某人
conference test deaf 10
# 解除屏蔽
conference test undeaf 10
表示让 member-id 为 10 的用户,听不到 test 会议的内容,如果 deaf 换成 undeaf 即为恢复。
列出视频会议支持的布局样式
conference 3500 vid-layout list
其中 3500 是会议房间号
返回如下:
freeswitch@localhost.localdomain> conference 3500 vid-layout list
2x1-zoom
1x2
8x8
1up_top_left+5
1x1+2x1
5-grid-zoom
2x1
2x1-presenter-zoom
2up_middle+8
2up_top+8
1x1
presenter-dual-horizontal
3x1-zoom
presenter-overlap-large-bot-right
3up+4
presenter-overlap-small-top-right
presenter-dual-vertical
3up+9
5x5
4x4
4x2-zoom
overlaps
2x2
3x2-zoom
presenter-overlap-large-top-right
6x6
3x3
1up_top_left+7
7-grid-zoom
presenter-overlap-small-bot-right
2up_bottom+8
1up_top_left+9
视频会议布局
conference 3500 vid-layout 2x2
表示在会议房间 3500 中,将画面布局改为 2x2 模式。效果如图:
更改用户在布局中的位置
# 命令格式:conference <会议号> vid-layer <会议中的用户号> <画布中的位置>
conference 3500 vid-layer 27 3
具体步骤解释如下:
- 先查出会议房间 3500 中的所有用户。(这里有两个用户 27 与 28)
- 设置用户 27 到 2x2 布局画布上的第 3 个位置(即 左下角)
freeswitch@localhost.localdomain> conference 3500 list
28;sofia/internal/1000@192.168.0.112:5060;6722a4c0-4b10-46f5-8663-9afc7c39a286;1000;1000;hear|speak|video|floor|vid-floor;0;0;200
27;sofia/internal/1002@192.168.0.112;90ea9f4c-a917-486a-8243-59b63104826e;1002;1002;hear|speak|video;0;0;200
freeswitch@localhost.localdomain> conference 3500 vid-layer 27 3
+OK layer 3
freeswitch@localhost.localdomain>
效果如图:
给会议室人员指定所在画布
画布配置方案请参考 "多画布配置"1
# 语法:conference <会议号> vid-canvas <用户在会议中的编号> <画布编号>
conference 3500 vid-canvas 1 1
详细命令如下:
freeswitch@localhost.localdomain> conference 3500 list
3;sofia/internal/1003@192.168.0.112;63f71656-768f-4832-a95d-12581ae81290;1003;1003;hear|speak|video;0;0;200
2;sofia/internal/1000@192.168.0.112:5060;a4ad257f-e575-446e-9cc2-762dc8745576;1000;1000;hear|speak|talking|video|floor;0;0;200
1;sofia/internal/1002@192.168.0.112;bd6761a4-130c-4fe5-8d8d-4aa2ca6e07f1;1002;1002;hear|speak|video;0;0;200
freeswitch@localhost.localdomain> conference 3500 vid-canvas 1 1
+OK canvas 1
freeswitch@localhost.localdomain> conference 3500 vid-canvas 2 2
+OK canvas 2
freeswitch@localhost.localdomain> conference 3500 vid-canvas 3 2
+OK canvas 2
freeswitch@localhost.localdomain>
给会议室人员分配观看画布
画布配置方案请参考 "多画布配置"1
# 语法:conference <会议号> vid-watching-canvas <用户在会议中的编号> <画布编号>
conference 3500 vid-watching-canvas 2 1
效果如图:
conference 3500 vid-watching-canvas 2 2
效果如图:
让指定人员在会议中对某人失聪或发言
官方解释:
Mute or Deaf a specific member to another member
ChatGPT 翻译:意思是将某个成员对另一个成员的声音或麦克风静音或关闭。
Control who sees who's video when not using video mux
ChatGPT 翻译:意思是当会议中没有使用视频混合器时,可以控制哪个成员可以看到谁的视频。
# 语法:conference <confname> relate <member_id>[,<member_id>] <other_member_id>[,<other_member_id>] [nospeak|nohear|clear]
- member_id:指的是会议成员,该成员是关系的主体;换句话说,关系将与 <member_id> 相关。
- other_member_id:是会议成员,将受到关联命令的影响。
nospeak 、nohear 、clear
- nohear:member_id 将不再听到 other_member_id 的声音,也就是说,member_id 的听觉将被静音,以防止听到 other_member_id 的声音。换句话说,这是将一个会议成员静音,以使其无法听到另一个指定的会议成员的声音。
- nospeak:这句话的意思是,指定的参会者(member_id)不能再向被关联的参会者(other_member_id)说话。也就是说,该参会者的麦克风将被静音,无法在会议中向该被关联的参会者发言。
- clear:该命令用于重置两个会议成员之间的关系,这意味着无论是什么关系,都将被删除或取消。
让 成员 A 无法对 成员 B 说话
conference 3500 relate 24 23 nospeak
上面指令的意思是让 成员24
不能再与 成员23
说话,即 成员23
现在听不到 成员24
的声音。(会议中的其他人可以听到)。
返回如下:
freeswitch@debian> conference 3500 list
24;sofia/internal/1002@192.168.0.60;fc91a065-d910-4a41-8fd3-f829e258bf1b;1002;1002;hear|speak|talking|video;0;0;200
23;sofia/internal/1000@192.168.0.60:5060;4c3284ea-2469-4d81-a44e-70179687769e;1000;1000;hear|speak|talking|video|floor;0;0;200
freeswitch@debian> conference 3500 relate 24 23 nospeak
+OK 24->23 nospeak set
让 成员 A 对 成员 B 失聪
conference 3500 relate 23 24 nohear
上面指令的意思是让 成员23
听不到 成员24
的声音。(会议中的其他人可以听到)。
返回如下:
freeswitch@debian> conference 3500 list
24;sofia/internal/1002@192.168.0.60;fc91a065-d910-4a41-8fd3-f829e258bf1b;1002;1002;hear|speak|talking|video;0;0;200
23;sofia/internal/1000@192.168.0.60:5060;4c3284ea-2469-4d81-a44e-70179687769e;1000;1000;hear|speak|talking|video|floor;0;0;200
freeswitch@debian> conference 3500 relate 23 24 nohear
+OK 23->24 nohear set
清除成员间的关系
conference 3500 relate 23 24 clear
返回如下:
freeswitch@debian> conference 3500 list
24;sofia/internal/1002@192.168.0.60;fc91a065-d910-4a41-8fd3-f829e258bf1b;1002;1002;hear|speak|talking|video;0;0;200
23;sofia/internal/1000@192.168.0.60:5060;4c3284ea-2469-4d81-a44e-70179687769e;1000;1000;hear|speak|talking|video|floor;0;0;200
freeswitch@debian> conference 3500 relate 23 24 clear
+OK relationship 23->24 cleared.
给会议室人员增加标识
注意:官方安装包在 CentOS 下执行无反应(只有蓝底,无法显示文字),Debian 下一切正常。
# 语法:conference <confname> vid-banner <member_id>|all <文字>
conference 3500 vid-banner 11 'legend'
效果如图:
拉流
# 语法:conference <confname> play [{vol=<volume>,full-screen=true,png_ms=100}]<file-path> [async|<member_id> [nomux]]
conference 3500 play av://rtmp://172.16.30.53/20230309/screen async
conference 3500 play av://rtmp://172.16.30.53/20230309/screen 12 nomux
conference 3500 play {full-screen=true,png_ms=5000}/path/to/my/image/file.png /path/to/my/audio/file.wav
注意:前缀要加 av://
录制/推流
conference 3500 record rtmp://172.16.30.53/live/3500
上述命令是将 3500 会议的视频(FreeSWITCH 使用 MCU 技术混流,能减少带宽。参考 "知识科普"2)推到第三方媒体服务。
停止录制/推流
# 语法:conference <会议号> recording stop <文件路径>|all
conference 3500 recording stop rtmp://172.16.30.53/live/3500
查看录制信息
# 语法:conference <会议号> recording check
conference 3500 recording check
返回如下:
freeswitch@debian> conference 3500 recording check
+OK Record file {video_time_audio=false,channels=2,samplerate=48000,vw=1920,vh=1080,fps=30.30}rtmp://172.16.30.53/live/3500
+OK Record file {video_time_audio=false,channels=2,samplerate=48000,vw=1920,vh=1080,fps=30.30}/home/recordings/3500_2023-03-10-08-37-26.mp4 (Auto)
暂停录制
注意:笔者在推流后,暂停录制再恢复录制会出现错误( [ERR] avformat.c:2185 Error while writing audio frame: Broken pipe)
# 语法:conference <confname> recording pause <file-path>
conference 3500 recording pause rtmp://172.16.30.53/live/3500
恢复录制
注意:笔者在推流后,暂停录制再恢复录制会出现错误( [ERR] avformat.c:2185 Error while writing audio frame: Broken pipe)
# 语法:conference <confname> recording resume <file-path>
conference 3500 recording resume rtmp://172.16.30.53/live/3500
读文字
参考 "向会议朗读文本"3
# 语法:conference <confname> say <text>
conference 3500 say 'ip is 192.168.0.0.1'
结束会议
conference test hup all
表示结束会议 test,hup 即为 hangup 的缩写。
发起呼叫
文章可参考 《FreeSWITCH 权威指南》 第 4 章第 5 小节(即 4.5)
语法
originate <call_url> <exten>|&<application_name>(<app_args>) [<dialplan>] [<context>] [<cid_name>] [<cid_num>] [<timeout_sec>]
在使用过程中,call url [呼叫字符串] 常用常忘,这里就简单记录下常用的几种呼叫字符串,以示提醒(示例中 800000 是一个租户的 profile 名称):
- user/1000@default:其中 @default 可以省略,多租户情况不能省略。如:user/1000@800000,此时呼叫分机 1000
- group/sales@default:其中 @default 可以省略,多租户情况不能省略。如:group/sales@800000 , 此时呼叫全组,同时振铃,接听一个之后停止
- sofia/profile/1000:其中 profile 名称可以在 sip\_profiles 中找到,1000 是 profile 中的一个分机。 如:sofia/800000/1000,此时呼叫 800000 租户的 1000 分机
- sofia/gateway/***/159****:通过网关外呼电话
其它参数解析如下:
- exten:dialplan 中的一个规则号码
- application_name:application 的名称,可以使用 show application 查看所有的应用
- app_args:应用所需的参数,可以参考 show application
- dialplan:默认 XML
- context:dialplan 中的上下文 ,多租户情况中可以理解为租户号
- cid_name:call url 接通时,看到的主叫名。默认为:空
- cid_num:call url 接通时,看到的主叫号码。默认为:0000000000
- timeout_sec:用户无响应的超时时间。比如 给对方发 INVITE,对方在 timeout_sec 时间内没有回复 100 Trying,则意味着用户超时。
简单示例
可以在 FreeSWITCH 中使用 originate
命令发起一次呼叫。
假设用户 1000 已经注册,那么可以运行如下命令:
originate user/1000 &echo
上述命令在呼叫 1000 这个用户后,便执行 echo 这个程序。echo 是一个回音程序,即它会把任何它“听到”的声音(或视频)再返回(说/播)给对方。因此,如果这时候用户 1000 接了电话,无论说什么都会听到自己的声音。
执行其他程序:
# 挂起 单腿通话,等待接通,不带声音
originate user/1003 &park
# 挂起 有等待声音
originate user/1003 &hold
# 播放指定的音乐
originate user/1003 &palyback(/root/welcome.wav)
# 录音保存到(地址)
originate user/1003 &record(/tmp/voice.wav)
# 桥接到1002
originate user/1003 &bridge(user/1002)
# 根据1002和1003的uuid桥接
uuid_bridge <uuid_1002> <uuid_1003>
两个用户互打
一开始是 1000 振铃,1000 接通后 1001 就会振铃:
originate user/1000 1001
也是用于实现用户互打,1009 呼叫 1017:
originate user/1009 &bridge(user/1017)
拨打多通电话
同时拨打 两通电话(同振):
originate user/1000,user/1001 &echo
一通 呼不通,就打第二通(顺振):
originate user/1000|user/1001 &echo
桥接远程终端
可以接路由,路由走 dialplan,比如这个例子就是走了 9196 的路由。
originate user/1000 9196
桥接远程 SIP 终端
originate sofia/example/300@foo.com &bridge(sofia/example/400@bar.com)
指定主叫号码
originate user/1000 &echo XML default 'Seven Du' 7777
这样会更改 From 字段和 Remote-Party-ID 字段,如图:
设置超时时间
这个超时不是指对方不接听的超时,而是对方不回复 100 Trying 的超时时间,一般 表示 ip 地址不可达。
originate sofia/internal/1000@192.168.100.100 &echo XML default 'Seven Du' 7777 10
在 FreeSWITCH 发出 INVITE 消息后,由于没有收到 100 Trying 回复,于是 在 1 秒后重发 INVITE 消息,如果还收不到则于 2 秒、4 秒后重发,由 于我们指定了 10 秒超时,因此该呼叫于 10 秒后失败,返回 NO_ANSWER。
默认的 originate 命令是阻塞的,如果执行上述命令,则无法输入其 他命令或取消该呼叫。一般用 bgapi 避免阻塞。举例:
bgapi originate sofia/internal/1000@192.168.100.100 &echo XML default 'Seven Du' 7777 10
参考文章
重新加载 XML 配置
一般用于配置修改(拨号计划、用户等)后的重新载入。
reloadxml
重新加载 ACL 配置
reloadacl
加载模块
load mod_xxx
注意:xxx 表示模块名称
是否加载模块
module_exists mod_xxx
注意:xxx 表示模块名称
退出
# 任意一个都行
/quit
/bye
/exit
注意:别担心,不会关闭 FreeSWITCH
30s 自动挂断
因为 NAT 的关系,我们将 /freeswitch/sip_profiles/internal.xml
中的配置修改为如下,使其可以穿透 NAT ,也可以使用 stun server 进行 NAT 穿透。
<!-- external_sip_ip
Used as the public IP address for SDP.
Can be an one of:
ip address - "12.34.56.78"
a stun server lookup - "stun:stun.server.com"
a DNS name - "host:host.server.com"
auto - Use guessed ip.
auto-nat - Use ip learned from NAT-PMP or UPNP
-->
<!-- <param name="ext-rtp-ip" value="$${external_rtp_ip}"/> -->
<!-- <param name="ext-sip-ip" value="$${external_sip_ip}"/> -->
<param name="ext-rtp-ip" value="auto-nat"/>
<param name="ext-sip-ip" value="auto-nat"/>
在内网部署中,笔者直接用这个方法解决了 "服务器部署语音不通"4 的问题。
服务器部署语音不通
SDP 示例:
v=0
o=FreeSWITCH 1598186068 1598186069 IN IP4 39.99.55.147
s=FreeSWITCH
c=IN IP4 39.99.55.147
t=0 0
m=audio 4964 RTP/SAVPF 102 9 0 8 103 101
a=rtpmap:102 opus/48000/2
a=rtpmap:9 G722/8000
a=rtpmap:0 PCMU/8000
a=rtpmap:8 PCMA/8000
a=rtpmap:103 telephone-event/48000
a=rtpmap:101 telephone-event/8000
由于 freeswitch 有自动 NAT 转换的功能,所以如果客户端发过来的 SDP 信息中 c 中的地址是内网地址,freeswitch 也会自动转换成路由器发过来的实际地址。但是客户端就没有这么聪明了,如果客户端解析来自 FreeSWITCH 的 SDP 消息中 c 的内容不是 FreeSWITCH 的公网地址,那么客户端就不能把语音流发送到正确的地址了。
说白了就是 FreeSWITCH 需要 NAT 穿越穿透。
路由器去掉 upnp 和 alg 功能。服务器的路由本身没进行 UPNP,没打开 NAT ALG
阿里云服务器都没有!除非你自己去安装
修改配置文件
/freeswitch/sip_profiles/internal.xml
在 FS 端开启 rport 功能,这个配置默认是被注释掉了
不开启的话,默认是从 Contact 头部字段中取对方地址的,这可能会导致 IP 和端口是对方在其内网中而不是其外网的,就送不到了。
<!-- 默认注释的,得开启 --> <param name="NDLB-force-rport" value="true"/>
设置 rtp 自动调整功能
这个配置默认被注释掉了,而且原来设置的是 true
<!-- 开启,并改为false --> <!-- <param name="disable-rtp-auto-adjust" value="true"/> --> <param name="disable-rtp-auto-adjust" value="false"/>
设置 sip 和 rtp 的外网地址
<param name="ext-rtp-ip" value="autonat:公网IP"/> <param name="ext-sip-ip" value="autonat:公网IP "/>
设置 acl 参数,以此来判断内外网呼叫
<param name="local-network-acl" value="lan"/>
acl 中配置 lan
路径:
/freeswitch/autoload_configs/acl.conf.xml
<list name="lan"default="deny"/> <node type="allow"cidr="172.16.19.0/24" /> </list>
在开放 FreeSWITCH 的 sip 端口和 rtp 端口(安全组和防火墙都需要设置开放相应的端口)
实际上只开放 Profile 监听端口就行了,如 5060、5080; rtp 端口会自动 nat
针对没有 rport 功能的终端
在 reg.xml(分机配置文件)中加入以下配置:
<variable name="sip-force-contact" value="NDLB-connectile-dysfunction"/>
Freeswitch 在默认情况下不一定会有 reg.xml 文件。这个文件是 Freeswitch 的默认分机注册文件,包含了所有注册到 Freeswitch 中的分机信息。
所谓的分机配置文件即
/freeswitch/conf/directory/default
目录下的1000.xml
。示例:
<user id="1000"> <params> <param name="password" value="mypassword"/> </params> <variables> <variable name="sip-force-contact" value="NDLB-connectile-dysfunction"/> </variables> </user>
sip-force-contact
是 Freeswitch 的一个 SIP 参数,它用于强制指定 SIP 消息中的 Contact 头字段,从而可以控制 SIP 会话的建立。FreeSWITCH 用以下命令启动
freeswitch -nc -nonat
配置 WebSocket
因为 WebRTC 需要 https ,对应的 WebSocket 也要 SSL 。freeSWITCH 支持 SSL 但默认没打开。
注意:先别停服务器!!!
wss 配置如下:
/freeswitch/vars.xml
将
internal_ssl_enable
与external_ssl_enable
改为 true 。<!-- Internal SIP Profile --> <X-PRE-PROCESS cmd="set" data="internal_auth_calls=true"/> <X-PRE-PROCESS cmd="set" data="internal_sip_port=5060"/> <X-PRE-PROCESS cmd="set" data="internal_tls_port=5061"/> <!-- 将 false 改为 true --> <X-PRE-PROCESS cmd="set" data="internal_ssl_enable=true"/> <!-- External SIP Profile --> <X-PRE-PROCESS cmd="set" data="external_auth_calls=false"/> <X-PRE-PROCESS cmd="set" data="external_sip_port=5080"/> <X-PRE-PROCESS cmd="set" data="external_tls_port=5081"/> <!-- 将 false 改为 true --> <X-PRE-PROCESS cmd="set" data="external_ssl_enable=true"/>
/freeswitch/sip_profiles/internal.xml
确保下面两个配置打开(一般不用改):
<!-- for sip over websocket support --> <param name="ws-binding" value=":5066"/> <!-- for sip over secure websocket support --> <!-- You need wss.pem in $${certs_dir} for wss or one will be created for you --> <param name="wss-binding" value=":7443"/>
SIP 服务的端口是 5060 ,WebSocket(ws)服务的端口是 5066 , wss 端口是 7443 。
注意:不要用 Nginx 直接反代 5066 端口,笔者只注册成功了,但是登录不成功,无法使用。按这篇文章(freeswitch ngnix wss 反向代理及 jssip 修改(wss 连不上问题解决))的说法貌似需要改代码。
开启下方注释,并配置证书文件位置:
<param name="tls-cert-dir" value="/usr/local/src/freeswitch/certs"/>
证书导入(生成的证书需要合并!!!):
cd /usr/local/freeswitch/certs cat server.crt server.key > wss.pem
局域网使用时得做的配置(不过笔者没做也可以用,蛮记录下)
编辑
/freeswitch/autoload_configs/acl.conf.xml
,加入下面配置:<list name="localnet.auto" default="allow"> </list>
编辑
/freeswitch/sip_profiles/internal.xml
,加入下列配置:<param name="apply-candidate-acl" value="rfc1918.auto"/> <param name="apply-candidate-acl" value="localnet.auto"/>
先重新加载 xml
# 重新加载配置 reloadxml
然后重启
# 停止 freeswitch freeswitch -stop # 启动 freeswitch freeswitch -nc
最后进入 fs_cli 检查 wss 是否开启
用户配置
创建新用户
在 FreeSwitch 系统中,用户配置文件在 /freeswitch/directory/default
目录下,想要创建一个新用户,那就复制一份分机配置文件进行修改。操作如下:
# 这是笔者的路径,具体看自己的环境!!!
cd /etc/freeswitch/directory/default/
拷贝一份配置文件
cp -a 1000.xml 1020.xml
修改配置文件
vim 1020.xml
修改内容如下:
<include>
<!-- id 设置为语音号 -->
<user id="1020">
<params>
<param name="password" value="$${default_password}"/>
<param name="vm-password" value="1020"/>
</params>
<variables>
<variable name="toll_allow" value="domestic,international,local"/>
<!-- accountcode 设置为语音号 -->
<variable name="accountcode" value="1020"/>
<variable name="user_context" value="default"/>
<!-- effective_caller_id_name 设置来电显示名称 -->
<variable name="effective_caller_id_name" value="Extension 1020"/>
<!-- effective_caller_id_number 设置为语音号 -->
<variable name="effective_caller_id_number" value="1020"/>
<variable name="outbound_caller_id_name" value="$${outbound_caller_name}"/>
<variable name="outbound_caller_id_number" value="$${outbound_caller_id}"/>
<variable name="callgroup" value="techsupport"/>
</variables>
</user>
</include>
修改拨号计划(Dialplan)使其他用户可以呼叫它。
打开 /freeswitch/conf/dialplan/default.xml
拨号计划配置文件,找到如下配置:
<!--
dial the extension (1000-1019) for 30 seconds and go to voicemail if the
call fails (continue_on_fail=true), otherwise hang up after a successful
bridge (hangup_after_bridge=true)
机翻:
拨打分机(1000-1019)30秒,如果呼叫失败,就转到语音信箱。
呼叫失败(continue_on_fail=true),否则在桥接成功后挂断
桥接后挂断(hangup_after_bridge=true)
-->
<extension name="Local_Extension">
<condition field="destination_number" expression="^(10[01][0-9])$">
<action application="export" data="dialed_extension=$1"/>
<!-- bind_meta_app can have these args <key> [a|b|ab] [a|b|o|s] <app> -->
<action application="bind_meta_app" data="1 b s execute_extension::dx XML features"/>
<action application="bind_meta_app" data="2 b s record_session::$${recordings_dir}/${caller_id_number}.${strftime(%Y-%m-%d-%H-%M-%S)}.wav"/>
<action application="bind_meta_app" data="3 b s execute_extension::cf XML features"/>
<action application="bind_meta_app" data="4 b s execute_extension::att_xfer XML features"/>
<action application="set" data="ringback=${us-ring}"/>
<action application="set" data="transfer_ringback=$${hold_music}"/>
<action application="set" data="call_timeout=30"/>
<!-- <action application="set" data="sip_exclude_contact=${network_addr}"/> -->
<action application="set" data="hangup_after_bridge=true"/>
<!--<action application="set" data="continue_on_fail=NORMAL_TEMPORARY_FAILURE,USER_BUSY,NO_ANSWER,TIMEOUT,NO_ROUTE_DESTINATION"/> -->
<action application="set" data="continue_on_fail=true"/>
<action application="hash" data="insert/${domain_name}-call_return/${dialed_extension}/${caller_id_number}"/>
<action application="hash" data="insert/${domain_name}-last_dial_ext/${dialed_extension}/${uuid}"/>
<action application="set" data="called_party_callgroup=${user_data(${dialed_extension}@${domain_name} var callgroup)}"/>
<action application="hash" data="insert/${domain_name}-last_dial_ext/${called_party_callgroup}/${uuid}"/>
<action application="hash" data="insert/${domain_name}-last_dial_ext/global/${uuid}"/>
<!--<action application="export" data="nolocal:rtp_secure_media=${user_data(${dialed_extension}@${domain_name} var rtp_secure_media)}"/>-->
<action application="hash" data="insert/${domain_name}-last_dial/${called_party_callgroup}/${uuid}"/>
<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}"/>
</condition>
</extension>
将 ^(11[01][0-9])$
修改为 (10[01][0-9]|1020)$
举例如下:
<condition field="destination_number" expression="^(10[01][0-9]|1020)$">
修改配置完成后,记得通过 fs_cli
运行 reloadxml
命令就可以了。
修改分机号密码
在 FreeSwitch 系统中,所有用户的密码都是默认为 1234,该设置在 /freeswitch/conf/vars.xml
中,如下所示:
<X-PRE-PROCESS cmd="set" data="default_password=1234"/>
如果大家需要修改默认密码的话,那就直接修改这个地方就可以了。
那用户的密码和默认密码怎么关联起来呢?请看下面的配置文件,以 用户 1000 为例,打开 /freeswitch/conf/directory/default/1000.xml
,可以看到如下内容:
<include>
<user id="1000">
<params>
<param name="password" value="$${default_password}"/>
<param name="vm-password" value="1000"/>
</params>
<variables>
<variable name="toll_allow" value="domestic,international,local"/>
<variable name="accountcode" value="1000"/>
<variable name="user_context" value="default"/>
<variable name="effective_caller_id_name" value="Extension 1000"/>
<variable name="effective_caller_id_number" value="1000"/>
<variable name="outbound_caller_id_name" value="$${outbound_caller_name}"/>
<variable name="outbound_caller_id_number" value="$${outbound_caller_id}"/>
<variable name="callgroup" value="techsupport"/>
</variables>
</user>
</include>
找到下面这个设置:
<param name="password" value="$${default_password}"/>
若需要修改某个用户的密码,直接修改 value 值就可以了。
修改配置完成后,记得通过 fs_cli
运行 reloadxml
命令就可以了。
关闭用户认证
一般来说,FreeSWITCH 中的 SIP 用户都需要通过用户名和密码进行认证后才能注册成功的,并进行通话。如果有特殊需要,也可以设置为无认证就可以使用。
打开 /freeswitch/sip_profiles/internal.xml
,将以下两条设置去掉注释就可以:
<!-- accept any authentication without actually checking (not a good feature for most people) -->
<!-- 机翻:接受任何认证而不进行实际检查(对大多数人来说不是一个好的功能)。 -->
<param name="accept-blind-auth" value="true"/>
<!-- suppress CNG on this profile or per call with the 'suppress_cng' variable -->
<!-- 机翻:在此配置文件中抑制CNG,或通过'suppress_cng'变量在每次调用时抑制CNG。 -->
<param name="suppress-cng" value="true"/>
修改配置完成后,记得通过 fs_cli
运行 reloadxml
命令就可以了。
加快呼叫
安装完 FreeSWITCH 发现进行 SIP 呼叫的时候出现差不多延时 10 秒左右才能接受到信息 主要原因是 FreeSWITCH 中默认配置了延时时间 只需要注释掉就能解决这个问题。
打开 /freeswitch/dialplan/default.xml
文件,找到如下配置并注释掉:
<condition field="${default_password}" expression="^1234$" break="never">
<action application="log" data="CRIT WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING "/>
<action application="log" data="CRIT Open $${conf_dir}/vars.xml and change the default_password."/>
<action application="log" data="CRIT Once changed type 'reloadxml' at the console."/>
<action application="log" data="CRIT WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING "/>
<!-- 注释这一行即可 -->
<!--<action application="sleep" data="10000"/>-->
</condition>
修改配置完成后,记得通过 fs_cli
运行 reloadxml
命令就可以了。
中文语音
fs 默认不加载中文语音。需要在 fs 的 src 中首先编译中文模块。
编译模块
在编译(configure)之前,编辑 modules.conf , 取消 mod_say_zh 这行的注释
#say/mod_say_de say/mod_say_en #say/mod_say_es #say/mod_say_es_ar #say/mod_say_fa #say/mod_say_fr #say/mod_say_he #say/mod_say_hr #say/mod_say_hu #say/mod_say_it #say/mod_say_ja #say/mod_say_nl #say/mod_say_pl #say/mod_say_pt #say/mod_say_ru #say/mod_say_sv #say/mod_say_th say/mod_say_zh
补救方式,编译中文 say 模块
# 模块存放路径 freeswitch/src/mod/say/mod_say_zh cd /freeswitch/src/mod/say/mod_say_zh make && make install
加载模块
在 FreeSWITCH 控制台上加载该模块
load mod_say_zh
若想 FreeSWITCH 在每次启动的时候都加载该模块,编辑 /conf/autoload_configs/modules.conf.xml
找到下方配置并去掉注释:
<!-- Say -->
<load module="mod_say_en"/>
<!-- <load module="mod_say_ru"/> -->
<!-- 默认注掉的,将注释放开 -->
<load module="mod_say_zh"/>
<!-- <load module="mod_say_sv"/> -->
加入中文语音
上传语音包
一般情况下存放在 /usr/share/freeswitch/sounds
。
注意:下方配置文件中设置的语音包路径为 /zh/cn/link
,若无法播放声音,请仔细核对目录是否配置正确!!!
创建 zh.xml
cd conf/lang/
cp -fr en zh
mv zh/en.xml zh/zh.xml
编辑 zh.xml 文件
<include>
<!-- 替换路径 en/us/callie 和 en 等 -->
<!-- <language name="en" say-module="en" sound-prefix="$${sound_prefix}" tts-engine="cepstral" tts-voice="callie"> -->
<language name="zh" say-module="zh" sound-prefix="$${sounds_dir}/zh/cn/link" tts-engine="tts_commandline" tts-voice="Mandarin">
<phrases>
<macros>
<X-PRE-PROCESS cmd="include" data="demo/*.xml"/> <!-- Note: this now grabs whole subdir, previously grabbed only demo.xml -->
<!-- voicemail_en_tts is purely implemented with tts, we have the files based one that is the default. -->
<X-PRE-PROCESS cmd="include" data="vm/sounds.xml"/> <!-- vm/tts.xml if you want to use tts and have cepstral -->
<X-PRE-PROCESS cmd="include" data="dir/sounds.xml"/> <!-- dir/tts.xml if you want to use tts and have cepstral -->
<X-PRE-PROCESS cmd="include" data="ivr/*.xml"/> <!-- IVR and custom phrases go here -->
</macros>
<X-PRE-PROCESS cmd="include" data="vm/voicemail_ivr.xml"/>
</phrases>
</language>
</include>
<!--
For Emacs:
Local Variables:
mode:xml
indent-tabs-mode:nil
tab-width:2
c-basic-offset:2
End:
For VIM:
vim:set softtabstop=2 shiftwidth=2 tabstop=2 expandtab:
-->
修改 vars.xml
路径:/freeswitch/vars.xml
<!-- 屏蔽英文音频资源的路径设置 -->
<!-- <X-NO-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"/>
<!-- 设置默认的语言 -->
<X-PRE-PROCESS cmd="set" data="default_language=zh"/>
<!-- 设置默认的方言 -->
<X-PRE-PROCESS cmd="set" data="default_dialect=cn"/>
<!-- 设置默认的声音。注意,与中文音频资源的路径中的文件夹一致 -->
<X-PRE-PROCESS cmd="set" data="default_voice=link"/>
修改 freeswitch.xml
路径:/freeswitch/freeswitch.xml
<section name="languages" description="Language Management">
<!-- 加入中文语言信息 -->
<X-PRE-PROCESS cmd="include" data="lang/zh/*.xml"/>
<X-PRE-PROCESS cmd="include" data="lang/de/*.xml"/>
<X-PRE-PROCESS cmd="include" data="lang/en/*.xml"/>
<X-PRE-PROCESS cmd="include" data="lang/fr/*.xml"/>
<X-PRE-PROCESS cmd="include" data="lang/ru/*.xml"/>
<X-PRE-PROCESS cmd="include" data="lang/he/*.xml"/>
<X-PRE-PROCESS cmd="include" data="lang/es/es_ES.xml"/>
<X-NO-PRE-PROCESS cmd="include" data="lang/es/es_MX.xml"/>
<X-PRE-PROCESS cmd="include" data="lang/pt/pt_BR.xml"/>
<X-NO-PRE-PROCESS cmd="include" data="lang/pt/pt_PT.xml"/>
<X-NO-PRE-PROCESS cmd="include" data="lang/sv/*.xml"/>
</section>
测试验证
修改 dialplan ,在 /freeswitch/conf/dialplan/default.xml
的 <context name="default">
这一节点内增加如下配置:
<!-- 测试中文语音播报 -->
<extension name="testing">
<condition field="destination_number" expression="^1234$">
<action application="answer"/>
<action application="say" data="zh NUMBER ITERATED 1234567"/>
</condition>
</extension>
不过,笔者一般加在会议那一块,即加在 <extension name="nb_conferences">
标签前。
最后重新加载配置文件,拨打号码 1234 , 试验是否能听到“一二三四五六七”。
TTS 引擎
FreeSWITCH 有 4 个 TTS 模块:
- mod_cepstral: 使用 Cepstral 公司的 TTS 引擎进行语音合成。Cepstral 提供以下语言的合成语音。美式英语、英式英语、意大利语、德语、加拿大法语和美洲西班牙语。
- mod_flite: Flite(又名 Festival Lite)是一个小型的可嵌入的运行速度快且资源占用较少的 TTS 引擎,完全免费,但语音合成效果可能不如其他商业引擎。
- mod_pocketsphinx: 使用 CMU Sphinx 开源语音识别系统的 Pocketsphinx 模块进行语音合成。Pocketsphinx 可以运行在嵌入式设备等资源有限的环境中,同时提供一定的语音识别功能。
- mod_tts_commandline: 使用命令行工具进行语音合成。这个模块本身没有 TTS 能力,而是通过调用 TTS 引擎的命令生成语音文件,tts 命令可以配置,最终实现自动 TTS 语音播放的功能。
在这四个 TTS 引擎中,mod_flite 可能是对中文支持最友好的。因为 mod_flite 支持多种语言,包括中文,并且拥有中文发音数据集。同时,它也是免费和开源的。但是,需要注意的是,中文的发音质量可能不如英语等其他语言的发音质量,因为中文有很多不同的发音方式和变音规则。因此,您可能需要进行一些调整和优化来获得更好的中文发音效果。
但是嘛,还不够。这里推荐使用 Ekho (Ekho 『余音』是一个免费、开源的中文语音合成软件。它目前支持粤语、普通话『国语』、广东台山话、诏安客语、藏语、雅言『中国古代通用语』和韩语,英语则通过 eSpeak 或 Festival 间接实现。Ekho 支持 Linux、Windows 和 Android 平台。)
mod_flite
笔者这里没编译成功,这里不做记录。
有兴趣的可以参考这篇文章(轻量级的 TTS–mod_flite)自行进行尝试。
mod_tts_commandline
编译安装
在编译(configure)之前,编辑 modules.conf , 取消 mod_tts_commandline 这行的注释
#asr_tts/mod_cepstral asr_tts/mod_flite #asr_tts/mod_pocketsphinx asr_tts/mod_tts_commandline
编译中文 mod_tts_commandline 模块
# 模块存放路径 /freeswitch/src/mod/asr_tts/mod_tts_commandline cd /freeswitch/src/mod/asr_tts/mod_tts_commandline make && make install
在 FreeSWITCH 控制台上加载该模块
load mod_tts_commandline
若想 FreeSWITCH 在每次启动的时候都加载该模块,编辑
/conf/autoload_configs/modules.conf.xml
找到下方配置并去掉注释:<!-- ASR /TTS --> <!-- <load module="mod_flite"/> --> <!-- <load module="mod_pocketsphinx"/> --> <!-- <load module="mod_cepstral"/> --> <!-- 默认注掉的,将注释放开 --> <load module="mod_tts_commandline"/> <!-- <load module="mod_rss"/> -->
Ekho(中文 TTS)
编译安装
使用 mod_tts_commandline 模块 及 Ekho 实现。
首先,去 Ekho 官网下载源码
注意:最新版,请去官网下载!!!(编辑时间 2023 年 2 月 24 日 11:19:45)
- Linux: ekho-8.6.tar.xz (44M)
Windows (32 位, SAPI5)
- Ekho 和 eSpeak 英语: ekho-espeak-8.6.zip (30M)
- Ekho 和 Festival 英语: ekho-festival-8.6.exe (36M)
- Ekho 普通话和 eSpeak 西班牙语: ekho-mandarin-spanish-espeak-8.6.zip (56M)
- Ekho 普通话和 Festival 西班牙语: ekho-mandarin-spanish-festival-8.6.exe (62M)
- Android 4.0+: ekho-5.8.apk (12M),包含粤语和英语
安装 Ekho
yum install -y ncurses-devel libsndfile-devel espeak-devel pulseaudio-libs-devel lame-devel tar xJvf ekho-8.6.tar.xz cd ekho-8.6 ldconfig ./configure make & make install
编译错误
checking for gcc... no 或 checking for cc... no 或 checking for cl.exe... no
sudo apt-get install build-essential
在编译 Ekho 时发生错误:error: espeak-ng test failed 。
解决方案如下:
wget https://ghproxy.com/https://github.com/espeak-ng/espeak-ng/archive/refs/tags/1.51.tar.gz mv 1.51.tar.gz espeak-ng-1.51.tar.gz tar -zxvf espeak-ng-1.51.tar.gz cd espeak-ng-1.51 ./autogen.sh ldconfig ./configure --enable-shared make & make install
在编译 espeak-ng 时发生错误:./autogen.sh: 14: ./autogen.sh: aclocal: not found
sudo apt-get install make autoconf automake libtool pkg-config
在编译 espeak-ng 时发生错误:Makefile:2642: * 遗漏分隔符 。 停止。。
解决方案如下:
打开 Makefile 文件,定位到 2642 行:
docs_MARKDOWN != ls docs/*.md docs/*/*.md docs/*/*/*.md
发现该行内容没啥用处,那就注释掉。修改如下:
# 将 671 行进行如下修改 # docs_HTML = ${docs_MARKDOWN:.md=.html} docs_HTML = ${.md=.html} # 将 2642 行进行注释 # docs_MARKDOWN != ls docs/*.md docs/*/*.md docs/*/*/*.md
命令说明
root@debian:~# ekho -v
Ekho text-to-speech engine.
Version: 8.6
Syntax: ekho [option] [text]
-v, --voice=VOICE
Specified language or voice. ('Cantonese', 'Mandarin', 'Toisanese', 'Hakka', 'Tibetan', 'Ngangien' and 'Hangul' are available now. Mandarin is the default language.)
机翻:指定的语言或语音。 (“粤语”、“普通话”、“台山语”、“客家语”、“藏语”、“古代雅言”和“韩语”现已可用。普通话是默认语言。)
-l, --symbol
List phonetic symbol of text. Characters' symbols are splited by space.
机翻:列出文字的音标。字符的符号由空格隔开。
-f, --file=FILE
Speak text file. ('-' for stdin)
机翻:朗读文本文件。(标准输入为'-')
-o, --output=FILE
Output to file.
翻译:输出音频文件路径
-t, --type=OUTPUT_TYPE
Output type: wav(default), ogg or mp3
翻译:输出文件类型,默认为 wav 格式,支持输出 ogg 或 mp3 格式。
-p, --pitch=PITCH_DELTA
Set delta pitch. Value range from -100 to 100 (percent)
机翻:设置增量间距。值范围从 -100 到 100(百分比)
-a, --volume=VOLUME_DELTA
Set delta volume. Value range from -100 to 100 (percent)
机翻:设置音量值。值范围从 -100 到 100(百分比)
-s, --speed=SPEED
Set delta speed. Value range from -50 to 300 (percent)
机翻:设置语速。值范围从 -50 到 300(百分比)
--english-speed=SPEED
Set English delta speed. Value range from -50 to 150 (percent)
机翻:设置英语增量速度。值范围从 -50 到 150(百分比)
--server
Start Ekho TTS server.
机翻:启动服务
--request=TEXT
Send request to Ekho TTS server.
机翻:发送请求到Ekho TTS服务器。
--port
Set server port. Default is 2046.
翻译:设置服务端端口,默认端口 2046
--version
Show version number.
翻译:显示版本号
-h, --help
Display this help message.
翻译:显示帮助信息
Please report bugs to Cameron Wong (hgneng at gmail.com)
配置 mod_tts_commandline
tts_commandline.conf.xml
路径:/freeswitch/conf/autoload_configs/tts_commandline.conf.xml
<configuration name="tts_commandline.conf" description="TextToSpeech Commandline configuration"> <settings> <!-- Some variables will be replaced : ${text}: input text (quoted) 输入文本 ${rate}: sample rate (example: 8000) 采样率 ${voice}: voice_name passed to TTS(quoted) 传递给TTS的语音名称 ${file}: output file (quoted, including .wav extension) 输出文件(带引号的,包括.wav扩展名) Example commands can be found at: https://freeswitch.org/confluence/display/FREESWITCH/mod_tts_commandline#mod_tts_commandline-Examplecommands --> <!-- 注释掉该行,新建 ekho 命令用法 --> <!-- <param name="command" value="echo ${text} | text2wave -f ${rate} > ${file}"/> --> <param name="command" value="/usr/local/bin/ekho -v ${voice} -o ${file} ${text}"/> </settings> </configuration>
default.xml
路径:/freeswitch/dialplan/default.xml
<extension name="testing"> <condition field="destination_number" expression="^1234$"> <action application="answer"/> <!-- <action application="say" data="zh NUMBER ITERATED 1234567"/> --> <!-- 新建该行,使用 tts_commandline 朗读 --> <action application="speak" data="tts_commandline|Mandarin|改革春风吹满地,中国人民真争气。这个世界太疯狂,耗子都给猫当伴娘。齐德隆,齐东强。"/> <!-- 其他写法1 --> <!--<action application="playback" data="say:tts_commandline:Mandarin:改革春风吹满地,中国人民真争气。这个世界太疯狂,耗子都给猫当伴娘。齐德隆,齐东强。"/>--> <!-- 其他写法2 --> <!-- <action application="set" data="tts_engine=tts_commandline"/> <action application="set" data="tts_voice=Mandarin"/> <action application="playback" data="say:改革春风吹满地,中国人民真争气。这个世界太疯狂,耗子都给猫当伴娘。齐德隆,齐东强。"/> --> </condition> </extension>
处理逻辑如下:
- FreeSWITCH 收到呼叫,进入 dialplan。
- 执行 speak 功能,speak 功能是 FreeSWITCH 内置函数,代码在 switch_ivr_play_say.c 文件。
- speak 的 data 属性中,使用 tts_commandline 接口实现 TTS 功能。
- 在 tts_commandline 中调用 tts_commandline.conf.xml 配置的 command 生成语音文件。
- command 命令中,使用 ekho TTS 引擎生成语音文件。
- speak 播放 tts_commandline 生成的语音文件。
其他
音视频会议
知识科普
在 1 对 1 通信中,WebRTC 首先尝试两个终端之间是否可以通过 P2P 直接进行通信,如果无法直接通信的话,则会通过 STUN/TURN 服务器进行中转,如下图:
如果你想要通过 WebRTC 实现多对多通信,该如何做呢?
其实,基于 WebRTC 的多对多实时通信的开源项目也有很多,综合来看,多方通信架构无外乎以下三种方案:
Mesh 方案:即多个终端之间两两进行连接,形成一个网状结构。比如 A、B、C 三个终端进行多对多通信,当 A 想要共享媒体(比如音频、视频)时,它需要分别向 B 和 C 发送数据。同样的道理,B 想要共享媒体,就需要分别向 A、C 发送数据,依次类推。这种方案对各终端的带宽要求比较高。
在上图中,B1、B2、B3、B4 分别表示 4 个浏览器,它们之间两两相连,同时还分别与 STUN/TURN 服务器进行连接(此时的 STUN/TURN 服务器不能进行数据中转,否则情况会变得非常复杂),这样就形成了一个网格拓扑结构。
当某个浏览器想要共享它的音视频流时,它会将共享的媒体流分别发送给其他 3 个浏览器,这样就实现了多人通信。
这种结构的优势有:
- 不需要服务器中转数据,STUN/TUTN 只是负责 NAT 穿越,这样利用现有 WebRTC 通信模型就可以实现,而不需要开发媒体服务器。
- 充分利用了客户端的带宽资源。
- 节省了服务器资源,因为服务器带宽往往是专线,价格昂贵,所以这种方案可以很好地控制成本。
当然,有优势自然也有不足之处,主要表现在:
- 共享端共享媒体流的时候,需要给每一个参与人都转发一份媒体流,这样对上行带宽的占用很大。参与人越多,占用的带宽就越大。除此之外,对 CPU、Memory 等资源也是极大的考验。一般来说,客户端的机器资源、带宽资源往往是有限的,资源占用和参与人数是线性相关的。这样导致多人通信的规模非常有限,通过实践来看,这种方案在超过 4 个人时,就会有非常大的问题。
- 另一方面,在多人通信时,如果有部分人不能实现 NAT 穿越,但还想让这些人与其他人互通,就显得很麻烦,需要做出更多的可靠性设计。
MCU:该方案由一个服务器和多个终端组成一个星形结构。各终端将自己要共享的音视频流发送给服务器,服务器端会将在同一个房间中的所有终端的音视频流进行混合,最终生成一个混合后的音视频流再发给各个终端,这样各终端就可以看到 / 听到其他终端的音视频了。实际上服务器端就是一个音视频混合器,这种方案服务器的压力会非常大。
MCU 主要的处理逻辑是:接收每个共享端的音视频流,经过解码、与其他解码后的音视频进行混流、重新编码,之后再将混好的音视频流发送给房间里的所有人。
我们来假设一个条件,B1 与 B2 同时共享音视频流,它们首先将流推送给 MCU 服务器,MCU 服务器收到两路流后,分别将两路流进行解码,之后将解码后的两路流进行混流,然后再编码,编码后的流数据再分发给 B3 和 B4。
对于 B1 来说,因为它是其中的一个共享者,所以 MCU 给它推的是没有混合它的共享流的媒体流,在这个例子中就是直接推 B2 的流给它。同理,对于 B2 来说 MCU 给它发的是 B1 的共享流。但如果有更多的人共享音视频流,那情况就更加复杂。
MCU 主要的处理逻辑如下:
那 MCU 的优势有哪些呢?大致可总结为如下几点:
- 技术非常成熟,在硬件视频会议中应用非常广泛。
- 作为音视频网关,通过解码、再编码可以屏蔽不同编解码设备的差异化,满足更多客户的集成需求,提升用户体验和产品竞争力。
- 将多路视频混合成一路,所有参与人看到的是相同的画面,客户体验非常好。
同样,MCU 也有一些不足,主要表现为:
- 重新解码、编码、混流,需要大量的运算,对 CPU 资源的消耗很大。
- 重新解码、编码、混流还会带来延迟。
- 由于机器资源耗费很大,所以 MCU 所提供的容量有限,一般十几路视频就是上限了。
SFU:该方案也是由一个服务器和多个终端组成,但与 MCU 不同的是,SFU 不对音视频进行混流,收到某个终端共享的音视频流后,就直接将该音视频流转发给房间内的其他终端。它实际上就是一个音视频路由转发器。但可以实现服务端视频录制。
在这个图中,B1、B2、B3、B4 分别代表 4 个浏览器,每一个浏览器都会共享一路流发给 SFU,SFU 会将每一路流转发给共享者之外的 3 个浏览器。
下面这张图是从 SFU 服务器的角度展示的功能示意图:
相比 MCU,SFU 在结构上显得简单很多,只是接收流然后转发给其他人。然而,这个简单结构也给音视频传输带来了很多便利。比如,SFU 可以根据终端下行网络状况做一些流控,可以根据当前带宽情况、网络延时情况,选择性地丢弃一些媒体数据,保证通信的连续性。
目前许多 SFU 实现都支持 SVC 模式和 Simulcast 模式,用于适配 WiFi、4G 等不同网络状况,以及 Phone、Pad、PC 等不同终端设备。
SFU 的优势有哪些:
- 首先 由于是数据包直接转发,不需要编码、解码,对 CPU 资源消耗很小。
- 其次是 直接转发也极大地降低了延迟,提高了实时性。
- 最后 带来了很大的灵活性,能够更好地适应不同的网络状况和终端类型。
同样,SFU 有优势,也有不足,主要表现是:
- 由于是数据包直接转发,参与人观看多路视频的时候可能会出现不同步;相同的视频流,不同的参与人看到的画面也可能不一致。
- 参与人同时观看多路视频,在多路视频窗口显示、渲染等会带来很多麻烦,尤其对多人实时通信进行录制,多路流也会带来很多回放的困难。总之,整体在通用性、一致性方面比较差。
总结如下:
整体来看,由于各方面限制,Mesh 架构在真实的应用场景中几乎没有人使用,一般刚学习 WebRTC 会考虑使用这种架构来实现多方通信。
MCU 架构是非常成熟的技术,在硬件视频会议中应用非常广泛。像很多做音视频会议的公司之前都会购买一套 MCU 设备,这样一套设备价格不菲,最低都要 50 万,但随着互联网的发展以及音视频技术越来越成熟,硬件 MCU 已经逐步被淘汰。
当然现在也还有公司在使用软 MCU,比较有名的项目是 FreeSWITCH,但正如我们前面所分析的那样,由于 MCU 要进行解码、混流、重新编码的操作,这些操作对 CPU 消耗是巨大的。如果用硬件 MCU 还好,但软 MCU 这个劣势就很明显了,所以真正使用软 MCU 架构方案的公司也不多。
SFU 是最近几年流行的新架构,目前 WebRTC 多方通信媒体服务器都是 SFU 架构。从上面的介绍中你也可以了解到 SFU 这种架构非常灵活,性能也非常高,再配上视频的 Simulcast 模式或 SVC 模式,则使它更加如虎添翼,因此各个公司目前基本上都使用该方案。
开源方案:
会议配置
简单介绍
FreeSwitch 默认支持会议功能,有如下特点:
- 不需要创建一个会议室的操作,只需要通过 conference 拨码计划就可以实现;
- 会议室不真正存在, 直到有人呼入为止;
- 会议功能很强大,能实现灵活控制。
会议基础步骤:
- 运行 FreeSWITCH 服务器程序;
- 注册 1000、1001、1002 三部 IP 话机;
- 通过 1000 呼叫 3000,通话建立后, 1000 将听到一段保持音乐;
- 通过 1001 呼叫 3000,通话建立后, 1001 将能听到 1000 的声音,1000 也能听到 1001 的声音;
- 通过 1002 呼叫 3000,通话建立后, 1002 将能听到 1000 和 1001 的声音,1001 能听到 1000 和 1002 的声音,1000 也能听到 1001 和 1002 的声音。
注意:
目前 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=OPUS,G722,PCMU,PCMA,H264,VP8"/>
<X-PRE-PROCESS cmd="set" data="outbound_codec_prefs=OPUS,G722,PCMU,PCMA,H264,VP8"/>
拨号计划
配置文件
路径:/freeswitch/dialplan/default.xml
<!--
start a dynamic conference with the settings of the "default" conference profile in conference.conf.xml
机翻:用 conference.conf.xml 中 "默认"会议配置文件的设置启动一个动态会议。
-->
<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>
<extension name="wb_conferences">
<condition field="destination_number" expression="^(31\d{2})$">
<action application="answer"/>
<action application="conference" data="$1-${domain_name}@wideband"/>
</condition>
</extension>
<extension name="uwb_conferences">
<condition field="destination_number" expression="^(32\d{2})$">
<action application="answer"/>
<action application="conference" data="$1-${domain_name}@ultrawideband"/>
</condition>
</extension>
<!-- MONO 48kHz conferences -->
<extension name="cdquality_conferences">
<condition field="destination_number" expression="^(33\d{2})$">
<action application="answer"/>
<action application="conference" data="$1-${domain_name}@cdquality"/>
</condition>
</extension>
<!--
STEREO 48kHz conferences / Video MCU
机翻:立体声 48kHz 会议/视频 MCU
-->
<extension name="cdquality_stereo_conferences">
<condition field="destination_number" expression="^(35\d{2}).*?-screen$">
<action application="answer"/>
<action application="send_display" data="FreeSWITCH Conference|$1"/>
<action application="set" data="conference_member_flags=join-vid-floor"/>
<action application="conference" data="$1@video-mcu-stereo"/>
</condition>
</extension>
<extension name="conference-canvases" continue="true">
<condition field="destination_number" expression="(35\d{2})-canvas-(\d+)">
<action application="push" data="conference_member_flags=second-screen"/>
<action application="set" data="video_initial_watching_canvas=$2"/>
<action application="transfer" data="$1"/>
</condition>
</extension>
<extension name="conf mod">
<condition field="destination_number" expression="^6070-moderator$">
<action application="answer"/>
<action application="set" data="conference_member_flags=moderator"/>
<action application="conference" data="$1-${domain_name}@video-mcu-stereo"/>
</condition>
</extension>
<extension name="cdquality_conferences">
<condition field="destination_number" expression="^(35\d{2})$">
<action application="answer"/>
<action application="conference" data="$1@video-mcu-stereo"/>
</condition>
</extension>
<extension name="cdquality_conferences_720">
<condition field="destination_number" expression="^(36\d{2})$">
<action application="answer"/>
<action application="conference" data="$1@video-mcu-stereo-720"/>
</condition>
</extension>
<extension name="cdquality_conferences_480">
<condition field="destination_number" expression="^(37\d{2})$">
<action application="answer"/>
<action application="conference" data="$1@video-mcu-stereo-480"/>
</condition>
</extension>
<extension name="cdquality_conferences_320">
<condition field="destination_number" expression="^(38\d{2})$">
<action application="answer"/>
<action application="conference" data="$1@video-mcu-stereo-320"/>
</condition>
</extension>
会议密码 && 主持人
可以设置主持人以及会议密码。设置了主持人后,可以影响会议的开展;设置了会议密码后,与会成员必须输入正确密码才能入会。
主持人对会议的影响主要体现在以下两个方面:
- 直到主持人入会后,会议才开始;
- 主持人退出会议后,会议才结束。
设置会议主持人
<!-- 未设置主持人 -->
<action application="conference" data="$1@default"/>
<!-- 设置了主持人 -->
<action application="conference" data="$1@default+flags{moderator}"/>
设置会议密码
<!-- 设置入会密码为 1234 -->
<action application=”conference” data="$1@default+1234"/>
设置会议主持人和密码
<action application="conference" data="$1@default+1234+flags{moderator}">
视频会议
FreeSWITCH 在一对一视频通话时是使用 FreeSWITCH 来转发媒体,当然也有 Bypass Media 模式,这种模式下,媒体流不再经过 FreeSWITCH 服务器,而是 SIP 客户端之间直接建立 P2P 连接,直接将 RTP 流发送给对方。这种模式下在邀请对方通话的时候,SIP 携带的 SDP 信息会被 FreeSWITCH 透明转发到对方,而不会被修改为 FreeSWITCH 服务器的 SDP 信息。
FreeSWITCH 在视频会议的时候是使用 MCU 架构,客户端一路上行一路下行,FreeSWITCH 负责融屏并发送 RTP 流到客户端。FreeSWITCH 有命令(conference_config meetingID vid-layout 1up_top_left
)可以控制融屏的样式,是均分的 grid 还是有 floor 的其他样式。
布局方式参考:
<!-- 配置文件位置:/freeswitch/autoload_configs/conference_layouts.conf.xml -->
<configuration name="conference_layouts.conf" description="Audio Conference">
<layout-settings>
<layouts>
<layout name="1x1">
<image x="0" y="0" scale="360" floor="true"/>
</layout>
<layout name="1x2" auto-3d-position="true">
<image x="90" y="0" scale="180"/>
<image x="90" y="180" scale="180"/>
</layout>
<layout name="2x1" auto-3d-position="true">
<image x="0" y="90" scale="180"/>
<image x="180" y="90" scale="180"/>
</layout>
<!-- 省略...... -->
</layout-settings>
</configuration>
布局样式
1up_top_left+5
1up_top_left+7
1up_top_left+9
1x1
1x2
2up_bottom+8
2up_middle+8
2up_top+8
2x1
2x2
3up+4
3up+9
3x3
4x4
5x5
6x6
8x8
overlaps
presenter-dual-horizontal
presenter-dual-vertical
presenter-overlap-large-bot-right
presenter-overlap-large-top-right
presenter-overlap-small-bot-right
presenter-overlap-small-top-right
屏蔽语音激励设置主画面
由于 FreeSWITCH 目前不能对视频进行转码,因而只能是所有人共同看其中一个人的视频图像。那么,具体应该看谁的呢?
笔者经常开玩笑说:“谁的声音大就看谁的”(假设当前布局为 1up_top_left+5 ,那么说话的那个人将在 1up 的位置,使用指令调整会闪一下,无法调整成功)。
当然,这么说不完全准确。实际上,在会议的所有成员中,其中一个成员会有一个 vid_floor 标识,谁持有这个标识,大家就看到谁。这个标识是在会议中由 FreeSWITCH 根据发言者的声音能量值来动态分配的。一般来说,当有多个人轮流说话时,大家就轮流看到正在发言的人,在这种情况下它工作得很好。
修改 conference_layouts.conf.xml 配置文件,将 将 floor 设置为 false 即可解决该问题,操作如下:
路径:/freeswitch/autoload_configs/conference_layouts.conf.xml
<layout name="1up_top_left+5" auto-3d-position="true">
<!-- 将该行的 floor 设置为 false -->
<image x="0" y="0" scale="240" floor="false"/>
<image x="240" y="0" scale="120"/>
<image x="240" y="120" scale="120"/>
<image x="0" y="240" scale="120"/>
<image x="120" y="240" scale="120"/>
<image x="240" y="240" scale="120"/>
</layout>
多画布配置
简单介绍
举个例子:会议一般都在会议室举行。有的时候,即使是一个会议,也会有多张桌子(人数太多的时候)。
这个叫桌子,在 FreeSWITCH 中叫 Canvas 。FreeSWITCH 还能将用户分配到指定的 Canvas 或者让用户观看指定的 Canvas 。
配置操作
这里我们以 3500 会议(该视频会议用到 MCU 混流)为例。
首先找到
/freeswitch/dialplan/default.xml
配置文件<!-- STEREO 48kHz conferences / Video MCU --> <extension name="cdquality_stereo_conferences"> <condition field="destination_number" expression="^(35\d{2}).*?-screen$"> <action application="answer"/> <action application="send_display" data="FreeSWITCH Conference|$1"/> <action application="set" data="conference_member_flags=join-vid-floor"/> <!-- 这里表明所用的会议方案 --> <action application="conference" data="$1@video-mcu-stereo"/> </condition> </extension>
由拨号计划得知会议配置方案为
video-mcu-stereo
。接着找到/freeswitch/autoload_configs/conference.conf.xml
配置文件,加上如下关键配置项关键配置:
<!-- 指定 mux 模式下能够使用的画布数量 --> <param name="video-canvas-count" value="3"/>
完整示例:
<profile name="video-mcu-stereo"> <param name="domain" value="$${domain}"/> <param name="rate" value="48000"/> <param name="channels" value="2"/> <param name="interval" value="20"/> <param name="energy-level" value="200"/> <!-- <param name="tts-engine" value="flite"/> --> <!-- <param name="tts-voice" value="kal16"/> --> <param name="muted-sound" value="conference/conf-muted.wav"/> <param name="unmuted-sound" value="conference/conf-unmuted.wav"/> <param name="alone-sound" value="conference/conf-alone.wav"/> <param name="moh-sound" value="$${hold_music}"/> <param name="enter-sound" value="tone_stream://%(200,0,500,600,700)"/> <param name="exit-sound" value="tone_stream://%(500,0,300,200,100,50,25)"/> <param name="kicked-sound" value="conference/conf-kicked.wav"/> <param name="locked-sound" value="conference/conf-locked.wav"/> <param name="is-locked-sound" value="conference/conf-is-locked.wav"/> <param name="is-unlocked-sound" value="conference/conf-is-unlocked.wav"/> <param name="pin-sound" value="conference/conf-pin.wav"/> <param name="bad-pin-sound" value="conference/conf-bad-pin.wav"/> <param name="caller-id-name" value="$${outbound_caller_name}"/> <param name="caller-id-number" value="$${outbound_caller_id}"/> <param name="comfort-noise" value="false"/> <param name="conference-flags" value="livearray-json-status|json-events|video-floor-only|rfc-4579|livearray-sync|minimize-video-encoding|manage-inbound-video-bitrate|video-required-for-canvas|video-mute-exit-canvas|mute-detect"/> <param name="video-auto-floor-msec" value="1000"/> <param name="video-mode" value="mux"/> <param name="video-layout-name" value="3x3"/> <param name="video-layout-name" value="group:grid"/> <param name="video-canvas-size" value="1920x1080"/> <param name="video-canvas-bgcolor" value="#333333"/> <param name="video-layout-bgcolor" value="#000000"/> <param name="video-codec-bandwidth" value="3mb"/> <param name="video-fps" value="30"/> <!-- 指定 mux 模式下能够使用的画布数量 --> <param name="video-canvas-count" value="3"/> <!-- <param name="video-codec-config-profile-name" value="conference"/> --> </profile>
- 使用
reloadxml
命令重新加载配置
向会议朗读文本
- 安装 TTS 引擎,参考:"TTS 引擎"5
配置 conference.conf.xml
找到你要配置的会议所对应的方案(怎么查找参考 "多画布配置"1),修改 TTS 配置项
<profile name="video-mcu-stereo"> <param name="domain" value="$${domain}"/> <param name="rate" value="48000"/> <param name="channels" value="2"/> <param name="interval" value="20"/> <param name="energy-level" value="200"/> <!-- 配置tts引擎 --> <param name="tts-engine" value="tts_commandline"/> <param name="tts-voice" value="Mandarin"/> <!-- <param name="tts-engine" value="flite"/> --> <!-- <param name="tts-voice" value="kal16"/> --> <param name="muted-sound" value="conference/conf-muted.wav"/> <param name="unmuted-sound" value="conference/conf-unmuted.wav"/> <param name="alone-sound" value="conference/conf-alone.wav"/> <param name="moh-sound" value="$${hold_music}"/> <param name="enter-sound" value="tone_stream://%(200,0,500,600,700)"/> <param name="exit-sound" value="tone_stream://%(500,0,300,200,100,50,25)"/> <param name="kicked-sound" value="conference/conf-kicked.wav"/> <param name="locked-sound" value="conference/conf-locked.wav"/> <param name="is-locked-sound" value="conference/conf-is-locked.wav"/> <param name="is-unlocked-sound" value="conference/conf-is-unlocked.wav"/> <param name="pin-sound" value="conference/conf-pin.wav"/> <param name="bad-pin-sound" value="conference/conf-bad-pin.wav"/> <param name="caller-id-name" value="$${outbound_caller_name}"/> <param name="caller-id-number" value="$${outbound_caller_id}"/> <param name="comfort-noise" value="false"/> <param name="conference-flags" value="livearray-json-status|json-events|video-floor-only|rfc-4579|livearray-sync|minimize-video-encoding|manage-inbound-video-bitrate|video-required-for-canvas|video-mute-exit-canvas|mute-detect"/> <param name="video-auto-floor-msec" value="1000"/> <param name="video-mode" value="mux"/> <param name="video-layout-name" value="3x3"/> <param name="video-layout-name" value="group:grid"/> <param name="video-canvas-size" value="1920x1080"/> <param name="video-canvas-bgcolor" value="#333333"/> <param name="video-layout-bgcolor" value="#000000"/> <param name="video-codec-bandwidth" value="3mb"/> <param name="video-fps" value="30"/> <!-- 指定 mux 模式下能够使用的画布数量 --> <param name="video-canvas-count" value="3"/> <!-- <param name="video-codec-config-profile-name" value="conference"/> --> </profile>
测试
在
fs_cli
命令行中执行reloadxml
重新加载配置。注意:一定要关闭会议后再重新进入配置文件才会生效。
执行以下命令测试:
# 语法:conference <会议号> say <文本内容> conference 3500 say 666
媒体处理
代理模式
对媒体的处理有三种方式:
模式 | 说明 | 适用场景 |
---|---|---|
默认 | 媒体通过 FreeSWITCHRTP 被 FreeSWITCH 转发,FreeSWITCH 控制编码的协商并在协商不一致时提供语音编码转换能力 支持录音,二次拨号等 | 更适合呼叫中心等富功能应用,但性能相比其他两个也是最差的。 |
代理模式(Proxy Media) | 媒体通过 FreeSWITCH 转发,但是不处理媒体 RTP 通过 FreeSWITCH 转发(只改动 sdp c= ip) FreeSWITCH 不控制 sdp 参数,只是转发 通话的终端必须有一致的语音或者视频编码,因为 FreeSWITCH 此时不支持转码(适合视频编码) 不支持录音, 二次拨号等功能 | 更适合处理 nat 问题,可以考虑用这种模式做一个 session border controlor 也适合于外部 MCU 配合做为视频会议,性能也明显好于 默认方式 |
旁路模式(Bypass Media) | 不转发也不处理媒体,FreeSWITCH 不会对 SDP 控制,音视频也不走 FreeSWITCH 此模式下 FreeSWITCH 更像是一个信令 proxy,媒体不会通过 FreeSWITCH,sdp 消息体不做修改,没有录音,二次拨号等功能 | 更像是一个信令代理,性能最高,但提供的功能有限 |
模式配置方式:
代理模式(Proxy Media)
参考官方配置方式:Proxy Media
路径:/etc/freeswitch/sip_profiles/internal.xml
找到
inbound-proxy-media
取消注释启用,并且把其他模式都注释掉<!-- Uncomment to set all inbound calls to no media mode 机翻:取消注释,将所有呼入的电话设置为无媒体模式 --> <!--<param name="inbound-bypass-media" value="true"/>--> <!-- Uncomment to set all inbound calls to proxy media mode 机翻:取消注释,将所有呼入电话都设置为代理媒体模式 --> <param name="inbound-proxy-media" value="true"/> <!-- Let calls hit the dialplan before selecting codec for the a-leg 机翻:在选择A-Leg的编解码器之前,让电话打到拨号计划上 --> <!--<param name="inbound-late-negotiation" value="true"/>-->
或者,在 拨号计划(Dailplan)中 ,在打电话(bridge)之前设置
proxy_media=true
。<action application="set" data="proxy_media=true"/>
旁路模式(Bypass Media)
参考官方配置方式:Proxy Media
路径:/etc/freeswitch/sip_profiles/internal.xml
找到
inbound-proxy-media
取消注释启用,并且把其他模式都注释掉<!-- Uncomment to set all inbound calls to no media mode 机翻:取消注释,将所有呼入的电话设置为无媒体模式 --> <param name="inbound-bypass-media" value="true"/> <!-- Uncomment to set all inbound calls to proxy media mode 机翻:取消注释,将所有呼入电话都设置为代理媒体模式 --> <!--<param name="inbound-proxy-media" value="true"/>--> <!-- Let calls hit the dialplan before selecting codec for the a-leg 机翻:在选择A-Leg的编解码器之前,让电话打到拨号计划上 --> <!--<param name="inbound-late-negotiation" value="true"/>-->
或者,在 拨号计划(Dailplan)中 ,在打电话(bridge)之前设置
proxy_media=true
。<action application="set" data="bypass_media=true"/>
可控参数
拨号计划
Dialplan 一些常用的参数用于控制录音行为,参考如下:
参数 | 作用 | 默认值 | 使用示例 |
---|---|---|---|
RECORD_STEREO | 开启双声道录音 在一个立体声文件中,将腿 A 和腿 B 的数据流记录到不同的通道。 (即呼叫者记录在左通道,接收者记录在右通道) true:来话和去话会分别录制到两个声道 false:则来话和去话会录制到同一个声道。 | true | \<action application\="set" data\="RECORD\_STEREO\=true"/\> |
RECORD_STEREO_SWAP | 双声道互换声道 当 RECORD_STEREO 变量被设置为 "true "时,允许反转录音通道。 所以呼叫者被记录到右声道,而接收者被记录在左声道。 | false | \<action application\="set" data\="RECORD\_STEREO\_SWAP\=true"/\> |
RECORD_HANGUP_ON_ERROR | 录音错误时挂机 当设置为 true 时,如果尝试记录电话录音时出现错误,此通道变量将导致呼叫挂断。 这不是一个常见的功能,但是在必须记录电话录音的情况下,不可能有未记录的呼叫呼叫录音。 (在某些业务场景中很有用。) | false | \<action application\="set" data\="RECORD\_HANGUP\_ON\_ERROR\=true"/\> |
RECORD_WRITE_ONLY | 仅记录写入流。即 FreeSWITCH 发出,对端能够听到的声音 | \<action application\="set" data\="RECORD\_WRITE\_ONLY\=true"/\> | |
RECORD_READ_ONLY | 仅记录读取流。即 FreeSWITCH 能听到的声音 | \<action application\="set" data\="RECORD\_READ\_ONLY\=true"/\> | |
media\_bug\_answer\_req | 只有在频道被接通时才开始录音。 注意:RECORD_ANSWER_REQ 应该在 1.0.5 之前的版本。 | 无 | \<action application\="set" data\="media\_bug\_answer\_req\=true"/\> |
RECORD_BRIDGE_REQ | 只有在通道被桥接的情况下,才会记录会话。 | 无 | \<action application\="set" data\="RECORD\_BRIDGE\_REQ\=true"/\> |
RECORD_APPEND | 在通道上设置 RECORD_APPEND=true ,所有录制都将按照支持它的格式进行(目前是 WAV 的 mod_sndfile 等) | 无 | \<action application\="set" data\="RECORD\_APPEND\=true"/\> |
record_sample_rate | 设置录音的采样率 | 从 codec 中获取 | \<action application\="set" data\="record\_sample\_rate\=8000"/\> |
enable_file_write_buffering | 在录制文件时启用文件缓冲,如果没有设置,默认为 true。 缓冲区大小默认为 SWITCH_DEFAULT_FILE_BUFFER_LEN 但可以通过设置字节大小而不是 true 来覆盖。 | 65535 | \<action application\="set" data\="enable\_file\_write\_buffering\=false"/\> \<action application\="set" data\="enable\_file\_write\_buffering\=true"/\> \<action application\="set" data\="enable\_file\_write\_buffering\=65535"/\> |
sound_prefix | 录音保存文件的位置前缀 如果文件参数带的不是绝对地址,则会加上此前缀。 默认安装位置:/usr/local/freeswitch | base_dir | |
RECORD_TITLE | 录音文件中的 title 属性 | 无 | \<action application\="set" data\="RECORD\_TITLE\=MegaMusic"/\> |
RECORD_COPYRIGHT | 录音文件中的 copyright 属性 | 无 | \<action application\="set" data\="RECORD\_COPYRIGHT\=(c)2007-me"/\> |
RECORD_SOFTWARE | 录音文件中的 software 属性 | 无 | \<action application\="set" data\="RECORD\_SOFTWARE\=FreeSWITCH"/\> |
RECORD_ARTIST | 录音文件中的 artist 属性 | 无 | \<action application\="set" data\="RECORD\_ARTIST\=Unknown"/\> |
RECORD_COMMENT | 录音文件中的 comment 属性 | 无 | \<action application\="set" data\="RECORD\_COMMENT\=This is a blog spot"/\> |
RECORD_DATE | 录音文件中的 date 属性 | 无 | |
RECORD_MIN_SEC | 设置最小录音长度。通常情况下,一个录音必须至少有 3 秒长。 如果一个录音不符合最小长度,它将在被记录后被删除。 | \<action application\="set" data\="RECORD\_MIN\_SEC\=0"/\> | |
execute_on_answer | 当被叫方应答时执行应用程序(而不是一个 api)。 要设置 api,请使用 api_on_answer。 在无法忽略早期媒体的情况下,execute_on_answer 也将允许在处理无应答条件时有更多控制。 该命令仅在尚未应答的通道上执行。 只需使用 export 或 export with nolocal: prefix 来确保它在 b-leg 应答时执行。 在下面的第二个使用示例中,我们发起了一个呼出电话到本地分机,在那里我们将等待 30 秒, 同时手动忽略媒体。在这种情况下,我们使用'set'而不是'export'。 | "① execute_on_answer 使用示例"6 | |
api_on_answer | 当被叫方回答时,执行一个 api(而不是一个应用程序)。 要设置一个应用程序,请使用 execute_on_answer 。 | "② api_on_answer 使用示例"7 |
附:
① execute_on_answer 使用示例
<action application="export" data="nolocal:execute_on_answer=lua incrInUse.lua ${uuid}"/>
或者,在 "manually" 忽略早期媒体的情况下,等待 30 秒的回答。
originate {ignore_early_media=true}sofia/gateway/my_gateway/5551212 885551212
<extension name="exe_on_ans"> <condition field="destination_number" expression="^88(\d+)$"> <action application="set" data="execute_on_answer=transfer ANSWEREDCALL XML default"/> <action application="log" data="INFO Waiting 30 seconds for $1 to answer..."/> <action application="sleep" data="30000"/> <action application="log" data="INFO Call to $1 was not answered, taking alternative action..."/> <action application="transfer" data="UNANSWEREDCALL XML default"/> </condition> </extension>
② api_on_answer 使用示例
<action application="export" data="nolocal:api_on_answer=uuid_broadcast ${uuid} beep.wav both"/> <action application="bridge" data="{api_on_answer='uuid_broadcast ${uuid} beep.wav both'}sofia/gateway/provider/5551231234"/>
注:
RECORD_TITLE、RECORD_COPYRIGHT、RECORD_SOFTWARE、RECORD_ARTIST、RECORD_COMMENT、RECORD_DATE 这些信息设置后会写到文件标头里,需要使用 MediaInfo 软件才能看到
RECORD_STEREO
true
概览 CompleteName : E:\下载\New Folder\2023-03-06-15-46-44_F-1003_T-1002.wav Format/String : Wave FileSize/String : 575 KiB Duration/String : 18 秒 380 毫秒 OverallBitRate_Mode/String : 恒定码率 (CBR) OverallBitRate/String : 256 kb/s Track : Recording 1002 1003 2023-03-06 15:46 Director : FreeSWITCH Recorded_Date : 2023-03-06 15:46 Encoded_Application/String : FreeSWITCH (libsndfile-1.0.28) Copyright : (c) 2023 Comment : FreeSWITCH 音频 Format/String : PCM Format_Settings : Little / Signed CodecID : 1 Duration/String : 18 秒 380 毫秒 BitRate_Mode/String : 恒定码率 (CBR) BitRate/String : 256 kb/s Channel(s)/String : 2 声道 SamplingRate/String : 8 000 Hz BitDepth/String : 16 位 StreamSize/String : 574 KiB (100%)
false
概览 完整名称 : E:\下载\New Folder\2023-03-06-15-49-24_F-1003_T-1002.wav 格式 : Wave 文件大小 : 217 KiB 时长 : 13 秒 860 毫秒 总体码率模式 : 恒定码率 (CBR) 总体码率 : 128 kb/s 轨道名 : Recording 1002 1003 2023-03-06 15:49 导演 : FreeSWITCH 录制日期 : 2023-03-06 15:49 编码程序 : FreeSWITCH (libsndfile-1.0.28) 版权 : (c) 2023 注释 : FreeSWITCH 音频 格式 : PCM 格式设置 : Little / Signed 编解码器 ID : 1 时长 : 13 秒 860 毫秒 码率模式 : 恒定码率 (CBR) 码率 : 128 kb/s 声道数 : 1 声道 采样率 : 8 000 Hz 位深 : 16 位 流大小 : 217 KiB (100%)
会议
mod_conference 一些参数参考如下:
参数 | 作用 | 默认值 | 使用示例 |
---|---|---|---|
min-required-recording-participants | 录制音频和开始自动录制所需的最小会议参与者人数。这可以是 1(默认值)或 2。 | 1 | "① min-required-recording-participants 使用示例"8 |
auto-record | 为此参数设置文件名或流值,以便能够记录每个电话会议。 在 mod_conference 中,有一个名为 ${conference_name} 的特殊参数,可以用来形成记录文件名。 所有的通道变量也可以用来生成一个独特的文件名。 注意:自动录音要等到 min-require-recording-participants 中指定的会议人员数量加入后才开始录音。 | "② auto-record 使用示例"9 |
附:
① min-required-recording-participants 使用示例
<param name="min-required-recording-participants" value="2"/>
② auto-record 使用示例
<param name="auto-record" value="/var/myNFSshare/${conference_name}_${strftime(%Y-%m-%d-%H-%M-%S)}.wav"/> <param name="auto-record" value="/var/myNFSshare/${conference_name}_${strftime(%Y-%m-%d-%H-%M-%S)}.mp4"/> <!-- 这个mp3的格式有可能不对,官方只给了个 <shout://user:pass@server.com/live.mp3> 的示例 --> <param name="auto-record" value="<shout://user:pass@server.com/live.mp3>"/>
开启录音
注:FreeSWITCH 1.8.7 版本录音分析可参考 ChrisZhang 大佬的文章 【freeswitch 源码分析】录音机理
配置方式
路径:/freeswitch/dialplan/default.xml
<!--
dial the extension (1000-1019) for 30 seconds and go to voicemail if the
call fails (continue_on_fail=true), otherwise hang up after a successful
bridge (hangup_after_bridge=true)
机翻:拨打分机(1000-1019)30秒,如果呼叫失败,就转到语音信箱。呼叫失败(continue_on_fail=true),否则在桥接成功后挂断桥接后挂断(hangup_after_bridge=true)
-->
<extension name="Local_Extension">
<condition field="destination_number" expression="^(10[01][0-9])$">
<action application="export" data="dialed_extension=$1"/>
<!-- bind_meta_app can have these args <key> [a|b|ab] [a|b|o|s] <app> -->
<action application="bind_meta_app" data="1 b s execute_extension::dx XML features"/>
<action application="bind_meta_app" data="2 b s record_session::$${recordings_dir}/${caller_id_number}.${strftime(%Y-%m-%d-%H-%M-%S)}.wav"/>
<action application="bind_meta_app" data="3 b s execute_extension::cf XML features"/>
<action application="bind_meta_app" data="4 b s execute_extension::att_xfer XML features"/>
<action application="set" data="ringback=${us-ring}"/>
<action application="set" data="transfer_ringback=$${hold_music}"/>
<action application="set" data="call_timeout=30"/>
<!-- <action application="set" data="sip_exclude_contact=${network_addr}"/> -->
<action application="set" data="hangup_after_bridge=true"/>
<!--<action application="set" data="continue_on_fail=NORMAL_TEMPORARY_FAILURE,USER_BUSY,NO_ANSWER,TIMEOUT,NO_ROUTE_DESTINATION"/> -->
<action application="set" data="continue_on_fail=true"/>
<action application="hash" data="insert/${domain_name}-call_return/${dialed_extension}/${caller_id_number}"/>
<action application="hash" data="insert/${domain_name}-last_dial_ext/${dialed_extension}/${uuid}"/>
<action application="set" data="called_party_callgroup=${user_data(${dialed_extension}@${domain_name} var callgroup)}"/>
<action application="hash" data="insert/${domain_name}-last_dial_ext/${called_party_callgroup}/${uuid}"/>
<action application="hash" data="insert/${domain_name}-last_dial_ext/global/${uuid}"/>
<!--<action application="export" data="nolocal:rtp_secure_media=${user_data(${dialed_extension}@${domain_name} var rtp_secure_media)}"/>-->
<action application="hash" data="insert/${domain_name}-last_dial/${called_party_callgroup}/${uuid}"/>
<!-- 录音配置开始 -->
<!-- 注意!!!该段配置一定要放在 <action application="bridge" data="user/${dialed_extension}@${domain_name}"/> 前面 -->
<action application="set" data="RECORD_TITLE=Recording ${destination_number} ${caller_id_number} ${strftime(%Y-%m-%d %H:%M)}"/>
<action application="set" data="RECORD_COPYRIGHT=(c) 2023"/>
<action application="set" data="RECORD_SOFTWARE=FreeSWITCH"/>
<action application="set" data="RECORD_ARTIST=FreeSWITCH"/>
<action application="set" data="RECORD_COMMENT=FreeSWITCH"/>
<action application="set" data="RECORD_DATE=${strftime(%Y-%m-%d %H:%M)}"/>
<!--
true:来话和去话会分别录制到两个声道
false:则来话和去话会录制到同一个声道。
-->
<action application="set" data="RECORD_STEREO=false"/>
<!-- 设置为通话应答后才能进行录音 -->
<action application="set" data="media_bug_answer_req=true"/>
<!-- 设置最小录音秒数。。如果录音秒数时长少于该数,则删除录音文件-->
<action application="set" data="RECORD_MIN_SEC=4"/>
<action application="record_session" data="/home/recordings/${strftime(%Y-%m-%d-%H-%M-%S)}_F-${caller_id_number}_T-${destination_number}.wav"/>
<!-- 录音配置结束 -->
<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}"/>
</condition>
</extension>
注意:录音相关的配置一定要放在 bridge
参数上方!!!
其他配置请参考官方的 记录会话 示例
API 方式
单腿录音
originate user/1000 &record(/home/recordings/test_originate.wav)
上述命令是呼叫分机号 1000,1000 接听后即可以直接讲话并录音。由于这种录音方式仅涉及一条腿(leg,即一个 Channel),因而称为单腿录音。
在这种录音方式中,由于只有一个 Channel,所以录音文件是单声道的。我们可以直接用下面的方式使用 playback 来测试播放刚才的录音:
originate user/1000 &playback(/home/recordings/test_originate.wav)
两腿录音
一路正常的通话通常由两个 Channel 组成。而对于每个 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 仅是对一个 Channel 而言的,它仅支持一个声道。因而,FreeSWITCH 会先将两个声道混音,变成一个声道后再播放。该警告一般是无害的。只是,由于在播放时会进行混音,会多占用一些 CPU,因而在高并发的场合,可以
使用一些工具事先将声音文件混为一个声道。如我们可以使用 sox 命令行进行混音:
sox record.wav -c 1 record-1.wav
其中,“-c”指定 Channel 的数量。我们这里用的是“-c 1”,就是说将原来的声音文件混成一个声道。
可以随时用对应的 stop 指令停止某个录音,也可以使用特殊的文件名“all”同时停止所有录音,如:
uuid_record <channel_uuid> stop all
当然,如果在录音过程中电话挂断了,所有录音也就自动停止了。
另外,在 Dialplan 中,也可以通过 record_session 达到类似的效果(参考上面的"配置方式"10)。
总结
record_sesison 是一个 App,而 uuid_record 是一个 API。相同的是两者在执行时都会为当前的 Channel 添加一个 Media Bug,因而可以实时录音。
另外,record_session 是非阻塞的,它可以作用于单腿的呼叫也可以用于桥接(Bridge)的呼叫。
开启录像
知识科普
与录音相比,录像要复杂一些。录像数据要按一定的格式存储在文件中,而这些文件格式有好多种。不同的文件格式称为不同的容器(Container),在这些容器中,通常会包含多个音频轨道(Track)和视频轨道,有的还含有同步信息。
FreeSWITCH 中实现了一个简单的 mod_fsv 模块,提供 FreeSWITCH 中的录像及回放支持。它不依赖于任何其他的视频处理库,而是自己定义了一种私有的格式,将音频轨道用 L16 编码的数据保存,视频轨道则将整个 RTP 原始包都保存进去。
record_fsv 是一个黑客级的解决方案。话说那时候 FreeSWITCH 不能进行视频解码,因而,FreeSWITCH 就机械地把收到的视频 RTP 包放到一个文件里,等播放的时候,再用 play_fsv 从视频文件里取出来,直接通过 RTP 发出去。由于这是一个非通用的解决方案,所以,FreeSWITCH 使用了自己的视频文件格式,扩展名是.fsv,FreeSWITCH Video 的缩写。而且 record_fsv 是阻塞的。
在默认的 Dialplan 中,也提供了录像与回放的例子。拨打 9193 可以通过 record_fsv App 进行录像;录制完成后,就可以拨打 9194 播放刚刚录制的录像了,它是使用 play_fsv App 实现的 Dialplan 的。设置如下:
<extension name="video_record">
<condition field="destination_number" expression="^9193$">
<action application="answer"/>
<action application="record_fsv" data="$${temp_dir}/testrecord.fsv"/>
</condition>
</extension>
<extension name="video_playback">
<condition field="destination_number" expression="^9194$">
<action application="answer"/>
<action application="play_fsv" data="$${temp_dir}/testrecord.fsv"/>
</condition>
</extension>
说说 mp4 的故事,FreeSWITCH 官方版里的 mp4 录像,最早是在 mod_vlc 里实现的。但是,后来我们发现,用 mod_vlc 播放 mp4 还行,录像效果不好,很难控制,提供的 API 极其难用。后来,我就实现了 mod_av(最早叫 mod_ffmpeg)。这个模块我写了好几年,ffmpeg 的 API 变化很大,改来改去无数次,最后一次改动是他们分裂出了 libav,FreeSWITCH 里的 mod_av 是基于 libav 的,因为 Debian 上用的是 libav。当然,我们也希望它能跟 ffmpeg 兼容,但还没有收到报告谁在 ffmpeg 基础上编译通过了。好吧,这不是两个模块都支持 mp4 了么。其实还有一个,那就是人们念念不忘的 mod_mp4v2。
一对一通话
这里只做 mod_av 的介绍。
注意:只有呼叫方被录制了,被叫方录不到。(笔者也不知道怎么处理)
路径:/freeswitch/dialplan/default.xml
<extension name="Local_Extension">
<condition field="destination_number" expression="^(10[01][0-9])$">
<action application="export" data="dialed_extension=$1"/>
<!-- bind_meta_app can have these args <key> [a|b|ab] [a|b|o|s] <app> -->
<action application="bind_meta_app" data="1 b s execute_extension::dx XML features"/>
<action application="bind_meta_app" data="2 b s record_session::$${recordings_dir}/${caller_id_number}.${strftime(%Y-%m-%d-%H-%M-%S)}.wav"/>
<action application="bind_meta_app" data="3 b s execute_extension::cf XML features"/>
<action application="bind_meta_app" data="4 b s execute_extension::att_xfer XML features"/>
<action application="set" data="ringback=${us-ring}"/>
<action application="set" data="transfer_ringback=$${hold_music}"/>
<action application="set" data="call_timeout=30"/>
<!-- <action application="set" data="sip_exclude_contact=${network_addr}"/> -->
<action application="set" data="hangup_after_bridge=true"/>
<!--<action application="set" data="continue_on_fail=NORMAL_TEMPORARY_FAILURE,USER_BUSY,NO_ANSWER,TIMEOUT,NO_ROUTE_DESTINATION"/> -->
<action application="set" data="continue_on_fail=true"/>
<action application="hash" data="insert/${domain_name}-call_return/${dialed_extension}/${caller_id_number}"/>
<action application="hash" data="insert/${domain_name}-last_dial_ext/${dialed_extension}/${uuid}"/>
<action application="set" data="called_party_callgroup=${user_data(${dialed_extension}@${domain_name} var callgroup)}"/>
<action application="hash" data="insert/${domain_name}-last_dial_ext/${called_party_callgroup}/${uuid}"/>
<action application="hash" data="insert/${domain_name}-last_dial_ext/global/${uuid}"/>
<!--<action application="export" data="nolocal:rtp_secure_media=${user_data(${dialed_extension}@${domain_name} var rtp_secure_media)}"/>-->
<action application="hash" data="insert/${domain_name}-last_dial/${called_party_callgroup}/${uuid}"/>
<!-- 录像配置 -->
<!-- 在 A-Leg 上设置 execute_on_answer 即当应答时开始录像。-->
<action application="set" data="execute_on_answer=record_session av:///home/recordings/${strftime(%Y%m%d-%H%M%S)}_F-${caller_id_number}_T-${destination_number}.mp4"/>
<!-- 直接调用了record_session,而没有用execute_on_answer,也就是说,从最开始的 Early Media 阶段就开始录。 -->
<!--<action application="record_session" data="av:///home/recordings/${strftime(%Y%m%d-%H%M%S)}_F-${caller_id_number}_T-${destination_number}.mp4"/>-->
<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}"/>
</condition>
</extension>
效果如图:
视频会议
这里只做 mod_av 的介绍。
以 MCU 会议 3500 为例。
API 方式
拉流
conference 3500 play av://rtmp://172.16.30.53/20230309/screen async
注意:如果会议进来一个人或者退出一个人的情况下,该拉取流就消失了....
效果如下:
推流
conference 3500 record rtmp://172.16.30.53/live/3500
效果如下:
这是 FreeSWITCH 从 RTMP(Monibuca GO 媒体服务器) 拉流进行 MCU 后再推流用 VLC 播放器播放的延时情况。
配置方式
路径:/freeswitch/autoload_configs/conference.conf.xml
<profile name="video-mcu-stereo">
<param name="domain" value="$${domain}"/>
<param name="rate" value="48000"/>
<param name="channels" value="2"/>
<param name="interval" value="20"/>
<param name="energy-level" value="200"/>
<!-- 配置tts引擎 -->
<param name="tts-engine" value="tts_commandline"/>
<param name="tts-voice" value="Mandarin"/>
<param name="muted-sound" value="conference/conf-muted.wav"/>
<param name="unmuted-sound" value="conference/conf-unmuted.wav"/>
<param name="alone-sound" value="conference/conf-alone.wav"/>
<param name="moh-sound" value="$${hold_music}"/>
<param name="enter-sound" value="tone_stream://%(200,0,500,600,700)"/>
<param name="exit-sound" value="tone_stream://%(500,0,300,200,100,50,25)"/>
<param name="kicked-sound" value="conference/conf-kicked.wav"/>
<param name="locked-sound" value="conference/conf-locked.wav"/>
<param name="is-locked-sound" value="conference/conf-is-locked.wav"/>
<param name="is-unlocked-sound" value="conference/conf-is-unlocked.wav"/>
<param name="pin-sound" value="conference/conf-pin.wav"/>
<param name="bad-pin-sound" value="conference/conf-bad-pin.wav"/>
<param name="caller-id-name" value="$${outbound_caller_name}"/>
<param name="caller-id-number" value="$${outbound_caller_id}"/>
<param name="comfort-noise" value="false"/>
<param name="conference-flags" value="livearray-json-status|json-events|video-floor-only|rfc-4579|livearray-sync|minimize-video-encoding|manage-inbound-video-bitrate|video-required-for-canvas|video-mute-exit-canvas|mute-detect"/>
<param name="video-auto-floor-msec" value="1000"/>
<param name="video-mode" value="mux"/>
<param name="video-layout-name" value="3x3"/>
<param name="video-layout-name" value="group:grid"/>
<param name="video-canvas-size" value="1920x1080"/>
<param name="video-canvas-bgcolor" value="#333333"/>
<param name="video-layout-bgcolor" value="#000000"/>
<param name="video-codec-bandwidth" value="3mb"/>
<param name="video-fps" value="30"/>
<param name="video-canvas-count" value="3"/>
<!-- 配置自动录制 -->
<param name="min-required-recording-participants" value="2"/>
<param name="auto-record" value="/home/recordings/${conference_name}_${strftime(%Y-%m-%d-%H-%M-%S)}.mp4"/>
<!-- 推流到第三方服务 -->
<!--<param name="auto-record" value="rtmp://192.168.0.108/${conference_name}/${strftime(%Y%m%d)}"/>-->
<!-- <param name="video-codec-config-profile-name" value="conference"/> -->
</profile>
效果如下:
本地录制
推送到第三方平台
使用 mod_mariadb 连接 MySQL
安装依赖
# debian
apt install -y libmariadb-dev mariadb-client
# centos7(可能是)
yum install -y mariadb mariadb-devel
重新编译
编译 FreeSWITCH 时编辑 modules.conf 取消 mod_mariadb 的注释,然后再进行编译。
# 取消该行注释
databases/mod_mariadb
配置文件
准备数据库 free_switch (在 MySQL 建库),可以找一个已有 FreeSWITCH 库拷过去,也可以是空库,让 FreeSWITCH 启动时自行创建。这里只建了一个空库。
db.conf.xml
路径:/freeswitch/autoload_configs/db.conf.xml
<configuration name="db.conf" description="LIMIT DB Configuration"> <settings> <!--<param name="odbc-dsn" value="dsn:user:pass"/>--> <!-- 加入如下配置 --> <param name="odbc-dsn" value="mariadb://Server=192.168.0.112;Port=3306;Database=free_switch;Uid=root;Pwd=123456;" /> </settings> </configuration>
switch.conf.xml
路径:/freeswitch/autoload_configs/switch.conf.xml
<!-- Native PostgreSQL support was removed from the FreeSWITCH Core! ================================= NOTICE: You MUST enable mod_pgsql ================================= According to https://www.postgresql.org/docs/9.6/libpq-connect.html#LIBPQ-CONNSTRING There are two accepted formats for connection strings supported by the libpq library: * For plain keyword = value strings use pgsql:// pgsql://hostaddr=127.0.0.1 dbname=freeswitch user=freeswitch password='' options='-c client_min_messages=NOTICE' * For RFC 3986 URIs use postgresql:// or postgres:// postgresql:// postgresql://localhost postgresql://localhost:5433 postgresql://localhost/mydb postgresql://user@localhost postgresql://user:secret@localhost postgresql://other@localhost/otherdb?connect_timeout=10&application_name=myapp postgresql:///mydb?host=localhost&port=5433 --> <!-- <param name="core-db-dsn" value="pgsql://hostaddr=127.0.0.1 dbname=freeswitch user=freeswitch password='' options='-c client_min_messages=NOTICE'" /> --> <!-- <param name="core-db-dsn" value="postgresql://freeswitch:@127.0.0.1/freeswitch?options=-c%20client_min_messages%3DNOTICE" /> --> <!-- 取消该行 mariadb 注释 --> <param name="core-db-dsn" value="mariadb://Server=192.168.0.112;Port=3306;Database=free_switch;Uid=root;Pwd=123456;" /> <!-- <param name="core-db-dsn" value="dsn:username:password" /> --> <!-- <param name="odbc-skip-autocommit-flip" value="true" /> -->
voicemail.conf.xml
路径:/freeswitch/conf/autoload_configs/voicemail.conf.xml
<!--<param name="storage-dir" value="$${storage_dir}"/>--> <!--<param name="odbc-dsn" value="dsn:user:pass"/>--> <!-- 加入如下配置 --> <param name="odbc-dsn" value="mariadb://Server=192.168.0.112;Port=3306;Database=free_switch;Uid=root;Pwd=123456;" /> <!--<param name="record-comment" value="Your Comment"/>--> <!--<param name="record-title" value="Your Title"/>--> <!--<param name="record-copyright" value="Your Copyright"/>-->
callcenter.conf.xml
路径:/freeswitch/conf/autoload_configs/callcenter.conf.xml
<settings> <!--<param name="odbc-dsn" value="dsn:user:pass"/>--> <!-- 加入如下配置 --> <param name="odbc-dsn" value="mariadb://Server=192.168.0.112;Port=3306;Database=free_switch;Uid=root;Pwd=123456;" /> <!--<param name="dbname" value="/dev/shm/callcenter.db"/>--> <!--<param name="cc-instance-id" value="single_box"/>--> </settings>
external.xml
路径:/freeswitch/sip_profiles/external.xml
加一行配置
<!-- used to share presence info across sofia profiles manage-presence needs to be set to passive on this profile if you want it to behave as if it were the internal profile for presence. --> <!-- Name of the db to use for this profile --> <!--<param name="dbname" value="share_presence"/>--> <!--<param name="presence-hosts" value="$${domain}"/>--> <!--<param name="force-register-domain" value="$${domain}"/>--> <!--all inbound reg will stored in the db using this domain --> <!--<param name="force-register-db-domain" value="$${domain}"/>--> <!-- ************************************************* --> <!-- 加入如下配置 --> <param name="odbc-dsn" value="mariadb://Server=192.168.0.112;Port=3306;Database=free_switch;Uid=root;Pwd=123456;" />
internal.xml
路径:/freeswitch/sip_profiles/internal.xml
<!--If you don't want to pass through timestamps from 1 RTP call to another (on a per call basis with rtp_rewrite_timestamps chanvar)--> <!--<param name="rtp-rewrite-timestamps" value="true"/>--> <!--<param name="pass-rfc2833" value="true"/>--> <!--If you have ODBC support and a working dsn you can use it instead of SQLite--> <!--<param name="odbc-dsn" value="dsn:user:pass"/>--> <!-- Or, if you have PGSQL support, you can use that --> <!--<param name="odbc-dsn" value="pgsql://hostaddr=127.0.0.1 dbname=freeswitch user=freeswitch password='' options='-c client_min_messages=NOTICE' application_name='freeswitch'" />--> <!-- 加入如下配置 --> <param name="odbc-dsn" value="mariadb://Server=192.168.0.112;Port=3306;Database=free_switch;Uid=root;Pwd=123456;" />
internal-ipv6.xml
路径:/freeswitch/sip_profiles/internal-ipv6.xml
<!--If you don't want to pass through timestampes from 1 RTP call to another (on a per call basis with rtp_rewrite_timestamps chanvar)--> <!--<param name="rtp-rewrite-timestamps" value="true"/>--> <!--<param name="pass-rfc2833" value="true"/>--> <!--If you have ODBC support and a working dsn you can use it instead of SQLite--> <!--<param name="odbc-dsn" value="dsn:user:pass"/>--> <!-- 加入如下配置 --> <param name="odbc-dsn" value="mariadb://Server=192.168.0.112;Port=3306;Database=free_switch;Uid=root;Pwd=123456;" />
external-ipv6.xml
路径:/freeswitch/sip_profiles/external-ipv6.xml
加一行配置
<!-- used to share presence info across sofia profiles manage-presence needs to be set to passive on this profile if you want it to behave as if it were the internal profile for presence. --> <!-- Name of the db to use for this profile --> <!--<param name="dbname" value="share_presence"/>--> <!--<param name="presence-hosts" value="$${domain}"/>--> <!--<param name="force-register-domain" value="$${domain}"/>--> <!--all inbound reg will stored in the db using this domain --> <!--<param name="force-register-db-domain" value="$${domain}"/>--> <!-- ************************************************* --> <!-- 加入如下配置 --> <param name="odbc-dsn" value="mariadb://Server=192.168.0.112;Port=3306;Database=free_switch;Uid=root;Pwd=123456;" />
fifo.conf.xml
路径:/freeswitch/conf/autoload_configs/fifo.conf.xml
<configuration name="fifo.conf" description="FIFO Configuration"> <settings> <param name="delete-all-outbound-member-on-startup" value="false"/> <!-- 加入如下配置 --> <param name="odbc-dsn" value="mariadb://Server=192.168.0.112;Port=3306;Database=free_switch;Uid=root;Pwd=123456;" /> </settings> <fifos> <fifo name="cool_fifo@$${domain}" importance="0"> <!--<member timeout="60" simo="1" lag="20">{member_wait=nowait}user/1005@$${domain}</member>--> </fifo> </fifos> </configuration>
pre_load_modules.conf.xml
路径:/freeswitch/autoload_configs/pre_load_modules.conf.xml
<configuration name="pre_load_modules.conf" description="Modules"> <modules> <!-- Databases --> <!-- 取消 mod_mariadb 注释 --> <load module="mod_mariadb"/> <!-- 注释 mod_pgsql --> <!-- <load module="mod_pgsql"/> --> </modules> </configuration>
directory.conf.xml
路径:/freeswitch/conf/autoload_configs/directory.conf.xml
<settings> <!--<param name="odbc-dsn" value="dsn:user:pass"/>--> <param name="odbc-dsn" value="mariadb://Server=192.168.0.112;Port=3306;Database=free_switch;Uid=root;Pwd=123456;" /> <!--<param name="dbname" value="directory"/>--> </settings>
重启 FreeSWITCH 基本流程就完成了。
使用 ODBC 连接 MySQL
安装 ODBC
ODBC 介绍
unixODBC 是一个实现 ODBC API 的开源项目。该代码基于 GNU GPL/LGPL 授权,可以在大部分的 Unix、Linux、Mac OS、IBM OS/2 和微软的 Interix 上建构和使用 。
该项目的目标包括:
- 以最少的代码更改,为开发人员提供将 Microsoft Windows ODBC 应用程式移植到其他平台的工具。
- 将项目维护为供应商中立的接口数据库 SDK
- 为编写 ODBC 驱动程序的工程师提供将其驱动程序移植到非 Windows 平台的工具
- 为用户提供一组 GUI 和命令行工具来管理他们的资料库存取
- 保持自由软件社区和商业数据库供应商的关系,确保互用性
安装
# 方式1(笔者用这个)
yum install -y unixODBC unixODBC-devel mysql-connector-odbc
# 方式2
yum install -y unixODBC unixODBC-devel mysql-connector-odbc libtool-ltdl libtool-ltdl-devel
使用如下命令验证是否正确安装:
odbcinst -j
返回如下说明已正常安装:
[root@localhost etc]# odbcinst -j
unixODBC 2.3.1
DRIVERS............: /etc/odbcinst.ini
SYSTEM DATA SOURCES: /etc/odbc.ini
FILE DATA SOURCES..: /etc/ODBCDataSources
USER DATA SOURCES..: /root/.odbc.ini
SQLULEN Size.......: 8
SQLLEN Size........: 8
SQLSETPOSIROW Size.: 8
[root@localhost etc]#
配置驱动
libmyodbc5w.so
与libmyodbc5.so
的主要区别在于它们是使用的字符集不同。
- libmyodbc5.so:用的是
ASCII
字符集。这意味着,当您将libmyodbc5
用于连接到 MySQL 数据库时,您需要确保数据库中的字符集与 ASCII 兼容。否则,您可能会遇到字符集转换错误。- libmyodbc5w.so:用的是
Unicode
字符集。这意味着,它可以处理各种语言和字符集,包括中文、日文、韩文等。如果您需要处理多种语言的数据,建议使用libmyodbc5w.so
。需要注意的是,由于字符集的差异,使用
libmyodbc5.so
和libmyodbc5w.so
连接到数据库时需要使用不同的 DSN(数据源名称)。例如,对于libmyodbc5w.so
,您需要使用Unicode
DSN,而对于libmyodbc5.so
,您需要使用ANSI
DSN。
用以下命令打开配置文件:
vim /etc/odbcinst.ini
改为如下内容:
# Example driver definitions
# Driver from the postgresql-odbc package
# Setup from the unixODBC package
[PostgreSQL]
Description = ODBC for PostgreSQL
Driver = /usr/lib/psqlodbcw.so
Setup = /usr/lib/libodbcpsqlS.so
Driver64 = /usr/lib64/psqlodbcw.so
Setup64 = /usr/lib64/libodbcpsqlS.so
FileUsage = 1
# Driver from the mysql-connector-odbc package
# Setup from the unixODBC package
[MySQL]
Description = ODBC for MySQL
Driver = /usr/lib/libmyodbc5.so
Setup = /usr/lib/libodbcmyS.so
# 默认使用 libmyodbc5.so 这里注释掉换成 libmyodbc5w.so
# Driver64 = /usr/lib64/libmyodbc5.so
Driver64 = /usr/lib64/libmyodbc5w.so
Setup64 = /usr/lib64/libodbcmyS.so
FileUsage = 1
用以下命令查看已经配置的驱动:
odbcinst -q -d
一般返回如下:
[root@localhost etc]# odbcinst -q -d
[PostgreSQL]
[MySQL]
[root@localhost etc]#
配置数据源
用以下命令打开配置文件(没有文件,那就创建一个):
vim /etc/odbc.ini
添加如下配置:
[FreeSWITCH]
Description = FreeSWITCH Datasource
Driver = /usr/lib64/libmyodbc5w.so
Server = 127.0.0.1
Host = 127.0.0.1
Database = free_switch
Port = 3306
User = root
Password = 123456
CHARSET = UTF8
Trace = NO
OPTION = 67108864
其他数据库请参考官方文档
最终验证
执行连接命令:
isql -v FreeSWITCH
注意:这里的 FreeSWITCH
取至 数据源配置 文件中的 [FreeSWITCH]
定义。
返回如下,说明配置成功并能正常连接数据库:
[root@localhost etc]# isql -v FreeSWITCH
+---------------------------------------+
| Connected! |
| |
| sql-statement |
| help [tablename] |
| quit |
| |
+---------------------------------------+
SQL> show databases
+-----------------------------------------------------------------+
| Database |
+-----------------------------------------------------------------+
| information_schema |
+-----------------------------------------------------------------+
SQLRowCount returns 1
1 rows fetched
SQL>
编译
重新编译 FreeSWITCH ,加入 --enable-core-odbc-support
参数。
./configure --enable-portable-binary \
--prefix=/usr --localstatedir=/var --sysconfdir=/etc \
--with-gnu-ld --with-python --with-openssl \
--enable-core-odbc-support --enable-zrtp
make && make install
配置 XML
注意:
- dsn:数据源名称(在安装 ODBC 时配置的数据源名称,即 “[]” 中的名称。 )
- user:数据库账号
- pass:数据密码
编辑 db.conf.xml
路径:/freeswitch/autoload_configs/db.conf.xml
<configuration name="db.conf" description="LIMIT DB Configuration"> <settings> <!--<param name="odbc-dsn" value="dsn:user:pass"/>--> <!-- 加入如下配置 --> <param name="odbc-dsn" value="FreeSWITCH:root:1234"/> </settings> </configuration>
编辑 switch.conf.xml
路径:/freeswitch/autoload_configs/switch.conf.xml
<!-- <param name="core-db-dsn" value="pgsql://hostaddr=127.0.0.1 dbname=freeswitch user=freeswitch password='' options='-c client_min_messages=NOTICE'" /> --> <!-- <param name="core-db-dsn" value="postgresql://freeswitch:@127.0.0.1/freeswitch?options=-c%20client_min_messages%3DNOTICE" /> --> <!-- <param name="core-db-dsn" value="mariadb://Server=localhost;Database=freeswitch;Uid=freeswitch;Pwd=pass;" /> --> <!-- <param name="core-db-dsn" value="dsn:username:password" /> --> <!-- 加入如下配置 --> <param name="core-db-dsn" value="FreeSWITCH:root:1234" /> <param name="odbc-dsn" value="FreeSWITCH:root:1234"/>
编辑 voicemail.conf.xml
路径:/freeswitch/autoload_configs/voicemail.conf.xml
<!--<param name="storage-dir" value="$${storage_dir}"/>--> <!--<param name="odbc-dsn" value="dsn:user:pass"/>--> <!-- 加入如下配置 --> <param name="odbc-dsn" value="FreeSWITCH:root:1234"/> <!--<param name="record-comment" value="Your Comment"/>--> <!--<param name="record-title" value="Your Title"/>--> <!--<param name="record-copyright" value="Your Copyright"/>-->
编辑 callcenter.conf.xml
路径:/freeswitch/autoload_configs/callcenter.conf.xml
<settings> <!--<param name="odbc-dsn" value="dsn:user:pass"/>--> <!-- 加入如下配置 --> <param name="odbc-dsn" value="FreeSWITCH:root:1234"/> <!--<param name="dbname" value="/dev/shm/callcenter.db"/>--> <!--<param name="cc-instance-id" value="single_box"/>--> </settings>
编辑 external.xml
路径:/freeswitch/sip_profiles/external.xml
<!-- used to share presence info across sofia profiles manage-presence needs to be set to passive on this profile if you want it to behave as if it were the internal profile for presence. --> <!-- Name of the db to use for this profile --> <!--<param name="dbname" value="share_presence"/>--> <!--<param name="presence-hosts" value="$${domain}"/>--> <!--<param name="force-register-domain" value="$${domain}"/>--> <!--all inbound reg will stored in the db using this domain --> <!--<param name="force-register-db-domain" value="$${domain}"/>--> <!-- ************************************************* --> <!-- 加入如下配置 --> <param name="odbc-dsn" value="FreeSWITCH:root:1234"/>
编辑 internal.xml
路径:/freeswitch/sip_profiles/internal.xml
<!--If you don't want to pass through timestamps from 1 RTP call to another (on a per call basis with rtp_rewrite_timestamps chanvar)--> <!--<param name="rtp-rewrite-timestamps" value="true"/>--> <!--<param name="pass-rfc2833" value="true"/>--> <!--If you have ODBC support and a working dsn you can use it instead of SQLite--> <!--<param name="odbc-dsn" value="dsn:user:pass"/>--> <!-- 加入如下配置 --> <param name="odbc-dsn" value="FreeSWITCH:root:1234"/>
编辑 internal-ipv6.xml
路径:/freeswitch/sip_profiles/internal-ipv6.xml
<!--If you don't want to pass through timestampes from 1 RTP call to another (on a per call basis with rtp_rewrite_timestamps chanvar)--> <!--<param name="rtp-rewrite-timestamps" value="true"/>--> <!--<param name="pass-rfc2833" value="true"/>--> <!--If you have ODBC support and a working dsn you can use it instead of SQLite--> <!--<param name="odbc-dsn" value="dsn:user:pass"/>--> <!-- 加入如下配置 --> <param name="odbc-dsn" value="FreeSWITCH:root:1234"/>
编辑 external-ipv6.xml
路径:/freeswitch/sip_profiles/external-ipv6.xml
<!-- used to share presence info across sofia profiles manage-presence needs to be set to passive on this profile if you want it to behave as if it were the internal profile for presence. --> <!-- Name of the db to use for this profile --> <!--<param name="dbname" value="share_presence"/>--> <!--<param name="presence-hosts" value="$${domain}"/>--> <!--<param name="force-register-domain" value="$${domain}"/>--> <!--all inbound reg will stored in the db using this domain --> <!--<param name="force-register-db-domain" value="$${domain}"/>--> <!-- ************************************************* --> <!-- 加入如下配置 --> <param name="odbc-dsn" value="FreeSWITCH:root:1234"/>
编辑 fifo.conf.xml
路径:/freeswitch/autoload_configs/fifo.conf.xml
<configuration name="fifo.conf" description="FIFO Configuration"> <settings> <param name="delete-all-outbound-member-on-startup" value="false"/> <!-- 加入如下配置 --> <param name="odbc-dsn" value="FreeSWITCH:root:1234"/> </settings> <fifos> <fifo name="cool_fifo@$${domain}" importance="0"> <!--<member timeout="60" simo="1" lag="20">{member_wait=nowait}user/1005@$${domain}</member>--> </fifo> </fifos> </configuration>
编辑 vars.xml
路径:/freeswitch/vars.xml
<!-- 加入如下配置 --> <X-PRE-PROCESS cmd="set" data="json_db_handle=odbc://FreeSWITCH:root:123456"/>
directory.conf.xml
路径:/freeswitch/conf/autoload_configs/directory.conf.xml
<settings> <!--<param name="odbc-dsn" value="dsn:user:pass"/>--> <!-- 加入如下配置 --> <param name="odbc-dsn" value="FreeSWITCH:root:1234"/> <!--<param name="dbname" value="directory"/>--> </settings>
重启 FreeSWITCH 基本流程就完成了。
注意事项
MySQL 创建 channels 表时,字段超 65535 字节限制,将几个
VARCHAR(4096)
改为TEXT
类型就可以了。参考文章:MySQL 下因字段过多导致错误提示“Row size too large.....”
CREATE TABLE channels ( uuid VARCHAR(256), direction VARCHAR(32), created VARCHAR(128), created_epoch INTEGER, name VARCHAR(1024), state VARCHAR(64), cid_name VARCHAR(1024), cid_num VARCHAR(256), ip_addr VARCHAR(256), dest VARCHAR(1024), application VARCHAR(128), application_data VARCHAR(4096), dialplan VARCHAR(128), context VARCHAR(128), read_codec VARCHAR(128), read_rate VARCHAR(32), read_bit_rate VARCHAR(32), write_codec VARCHAR(128), write_rate VARCHAR(32), write_bit_rate VARCHAR(32), secure VARCHAR(64), hostname VARCHAR(256), presence_id VARCHAR(4096), presence_data VARCHAR(4096), accountcode VARCHAR(256), callstate VARCHAR(64), callee_name VARCHAR(1024), callee_num VARCHAR(256), callee_direction VARCHAR(5), call_uuid VARCHAR(256), sent_callee_name VARCHAR(1024), sent_callee_num VARCHAR(256), initial_cid_name VARCHAR(1024), initial_cid_num VARCHAR(256), initial_ip_addr VARCHAR(256), initial_dest VARCHAR(1024), initial_dialplan VARCHAR(128), initial_context VARCHAR(128) )
ESL 相关
拒绝连接
默认是只接受本机连接的,稍一改动就 内外都不能连接了,或者只能接受内外之一连接。
拒绝连接异常信息如下:
2023-03-14 16:00:12.329993 99.77% [WARNING] mod_event_socket.c:2676 IP ::ffff:172.16.30.53 Rejected by acl "loopback.auto"
解决方法:
event_socket.conf.xml
路径:/freeswitch/autoload_configs/event_socket.conf.xml
<configuration name="event_socket.conf" description="Socket Client"> <settings> <param name="nat-map" value="false"/> <!-- 允许远程ESL控制 --> <param name="listen-ip" value="::"/> <param name="listen-port" value="8021"/> <param name="password" value="ClueCon"/> <!-- 加入这行 --> <param name="apply-inbound-acl" value="lan"/> <!--<param name="apply-inbound-acl" value="loopback.auto"/>--> <!--<param name="stop-on-bind-error" value="true"/>--> </settings> </configuration>
重新加载
fs_cli reload mod_event_socket
Java 调用
参考开源项目 freeswitch-esl-all
功能特性:
- 支持连接 FreeSWITCH 大规模集群
- 更易于集成使用
- 与 spring boot 2.x 深度整合,提供 starter
- 可动态配置
使用说明:
获取实例
// 方式一 InboundClient.getInstance() // 方式二(Spring 注入) @Autowired private InboundClient inboundClient;
可动态配置添加或删除远端地址
添加远端地址
// 方式一 InboundClient.getInstance().option().addServerOption(new ServerOption(host, port)); // 方式二(Spring 注入后) inboundClient.option().addServerOption(new ServerOption(host, port));
删除远端地址
ServerOption serverOption = inboundClient.option().serverOptions().get(0); // 方式一 InboundClient.getInstance().option().removeServerOption(serverOption); // 方式二(Spring 注入后) inboundClient.option().removeServerOption(serverOption);
服务端连接监听器
// ServerConnectionListener 接口 inboundClient.option().serverConnectionListener(serverConnectionListenerImpl); // 两个方法 void onOpened(ServerOption serverOption); void onClosed(ServerOption serverOption);
配置动态获取或者删除
// 实现接口 InboundClientOptionHandler // 继承抽象类 AbstractInboundClientOptionHandler
使用示例:
/**
* <p>serverOptions.</p>
*
* @return a {@link java.lang.String} object.
*/
@GetMapping("/call")
public void call() {
String jobUUID = inboundClient.sendAsyncApiCommand("192.168.0.60:8021", "originate user/1000 1002", "");
LogFactory.get().info("---> 发送命令:{}", jobUUID);
}
/**
* @author song_jx
* @date 2023-03-14 014 16:57:38
*/
@Slf4j
@EslEventName(EslEventHandler.DEFAULT_ESL_EVENT_HANDLER)
@Component
public class DefaultHandle2 extends AbstractEslEventHandler {
@Override
public void handle(String addr, EslEvent event) {
Map<String, String> eventHeaders = event.getEventHeaders();
String jobUUID = eventHeaders.get(EslHeaders.Name.JOB_UUID.literal());
log.info("---> {} :{}", jobUUID, event.getEventBodyLines());
}
}
源码编译
填坑
lua-devel
错误提示: 找不到 lua.h 等 lua 的头文件
yum install lua lua-devel
opus-devel
错误提示:You must install libopus-dev to build mod_opus
yum -y install opus-devel
# 可能需要清理
make clean && ./configure && make
如果还是报这个错误,在 Makefile 就注释这两行:
# 大概是在第 896、897 行
vim freeswitch/src/mod/codecs/mod_opus/Makefile
#install: error
#all: error
sndfile
错误提示:You must install libsndfile-dev to build mod_sndfile
yum install libsndfile-devel
或者源代码编译安装:
# 下载包 libsndfile-x.x.xx.tar.gz from地址 http://www.mega-nerd.com/libsndfile/#Download, 然后
wget http://www.mega-nerd.com/libsndfile/files/libsndfile-1.0.28.tar.gz
tar zxvf libsndfile-1.0.28.tar.gz
./configure
make
make install
cp /usr/local/lib/pkgconfig/sndfile.pc /usr/lib64/pkgconfig
然后,重新编译 FreeSWITCH
如果还是报这个错误,就修改这两行,在 Makefile 末尾:
vim src/mod/formats/mod_sndfile/Makefile
# 修改这两行
install: install-am
all: install
然后,再编译
libyuv
简单介绍
libyuv 是 Google 开源的实现各种 YUV 与 RGB 之间相互转换、旋转、缩放的库。
它是跨平台的,可在 Windows、Linux、Mac、Android 等操作系统,x86、x64、arm 架构上进行编译运行,支持 SSE、AVX、NEON 等 SIMD 指令加速。
异常解决
错误提示:You must install libyuv-dev to build mod_fsv
cd freeswitch/libs
git clone https://freeswitch.org/stash/scm/sd/libyuv.gitcd libyuv
make -f linux.mk CXXFLAGS="-fPIC -O2 -fomit-frame-pointer -Iinclude/"
make install
cp /usr/lib/pkgconfig/libyuv.pc /usr/lib64/pkgconfig/
libvpx
简单介绍
libvpx 是开源的支持 VP8/VP9 编解码的 SDK。全面支持 WebM。x86 平台必须先安装 yasm 库。
异常解决
错误提示:You must install libvpx-dev to build ....
cd freeswitch/libs
git clone https://freeswitch.org/stash/scm/sd/libvpx.git
cd libvpx
./configure --enable-pic --disable-static --enable-shared
make
make install
cp /usr/local/lib/pkgconfig/vpx.pc /usr/lib64/pkgconfig/
libpng
简单介绍
PNG 的官方库,项目开发包的地址:官网 1 、官网 2。该项目依赖 zlib 库。
异常解决
- 错误提示:You must install libpng-dev to build ....
视频画面没有显示字体,只有视频标签背景色
有人回答,需要使用 使用 1.6+ 版本
issues:https://github.com/signalwire/freeswitch/issues/568 Q:I installed freeswitch 1.10.2 in CentOS. mod_av compiled by x264 and libav . When I hold a mixed conference, I call the conference API command: vid-banner, which returns that the call is successful, but the video screen does not display a font, only a video label background color 机翻:我在 CentOS 中安装了 freeswitch 1.10.2。 mod_av 由 x264 和 libav 编译。我开混合会议时,调用会议API命令:vid-banner,返回调用成功,但是视频画面没有显示字体,只有视频标签背景色 A:That usually means your libpng is too old. 机翻:这通常意味着您的 libpng 太旧了。
注意:头秃了,笔者怎么尝试都没解决....
wget https://download.sourceforge.net/libpng/libpng-1.6.39.tar.gz
tar zxvf libpng-1.6.39.tar.gz
cd libpng-1.6.39
./configure
make
make install
cp /usr/local/lib/pkgconfig/libpng* /usr/lib64/pkgconfig/
libav
简单介绍
Libav 是一个自由软件,可以执行音讯和视讯多种格式的录影、转档、串流功能。主要包含以下几个部分:
- libavcodec (一个用于多个专案中音讯和视讯的解码器函式库)
- libavformat (一个音讯与视讯格式转换函式库)
- libavutil(包含解压缩和各种实用功能库)
- libavfilter(提供一个改变解码音频和视频的拦截器链)
- libavdevice(提供捕获和重置设备的抽象访问能力)
- libavresample(实现音频混合和重采样能力)
- libswscale(实现颜色的转换和缩放能力)
异常解决
错误提示:You must install libav-dev to build mod_av
或者:You must install libavformat-dev to build mod_av
git clone https://freeswitch.org/stash/scm/sd/libav.git
# 或者 wget https://freeswitch.org/stash/rest/api/latest/projects/SD/repos/libav/archive?format=zip
cd libav
./configure # CFLAGS="-fPIC" ./configure --enable-pic --enable-shared
make # make CXXFLAGS="-fPIC"
make install
安装完成,如果 make 还是 提示这个错误,重新执行:
./bootstrap.sh -j
./configure
make
其他问题
错误提示:libs/spandsp
configure: loading cache ../../config.cache configure: error: `CFLAGS' was not set in the previous run configure: error: `CPPFLAGS' was not set in the previous run configure: error: in `/usr/src/freeswitch.git/libs/spandsp': configure: error: changes in the environment can compromise the build configure: error: run `make distclean' and/or `rm ../../config.cache' and start over
解决:重新 configure
./configure -C && make
基础环境
安装之前, 最好先安装这几个东西(如果有, 请忽略):
# 可能提示 libopus-dev 或 libopus-devel 等
opus-devel
lua-devel
libsndfile-devel
libtiff-devel
这里使用 yum-builddep 工具,基于 FreeSWITCH 的 yum 包依赖库批量安装,简单了很多:
yum install -y https://files.freeswitch.org/repo/yum/centos-release/freeswitch-release-repo-0-1.noarch.rpm epel-release yum-utils
yum-builddep -y freeswitch
yum install -y yum-plugin-ovl centos-release-scl rpmdevtools git
yum install -y devtoolset-4-gcc*
scl enable devtoolset-4 'bash'
补充 mod_av
直接用 Debian 系统安装 FreeSWITCH ,不需要自己苦逼的编译安装
mod_av 需要 libavformat-dev,但 CentOS 没有,通过第三方 yum 库安装 ffmpeg-devel 包实现(yum 的 FreeSWITCH 版本里面是没有 mod_av、mod_b64、mod_png 的,所以上面的安装也就没有):
rpm --import http://li.nux.ro/download/nux/RPM-GPG-KEY-nux.ro
rpm -Uvh http://li.nux.ro/download/nux/dextop/el7/x86_64/nux-dextop-release-0-5.el7.nux.noarch.rpm
yum install -y ffmpeg-devel
补充 mod_vlc
安装 VLC
软件包
apt-get install -y vlc
源码编译
安装所需的工具包:
apt-get install -y libavcodec-dev libavutil-dev libavformat-dev liba52-0.7.4-dev git
git clone https://ghproxy.com/https://github.com/videolan/vlc.git
cd vlc
./bootstrap
./configure --prefix=/usr/ --disable-vlc --disable-lua --disable-mad --disable-swscale --disable-postproc --disable-xcb --disable-alsa
make && make install
编译错误解决:
ERROR: flex is not installed.
解决方案:
apt-get install -y gettext flex
ERROR: GNU bison is not installed.
apt-get -y install bison
安装 mod_vlc
注意:笔者怎么编译都编译不成功,已经放弃挣扎了。以下的可以不用看了...
单纯记录下不成功的操作
使用命令搜索,看下是否有软件包
root@debian:/home/freeswitch/freeswitch# apt-cache search freeswitch-mod-vlc
freeswitch-mod-vlc - VLC streaming for FreeSWITCH
freeswitch-mod-vlc-dbg - VLC streaming for FreeSWITCH (debug)
如果有,直接用命令安装。
apt-get install -y aptitude
aptitude install freeswitch-mod-vlc
apt-get install -f -y freeswitch-mod-vlc --no-upgrade
如果碰到 freeswitch-mod-vlc : 依赖: vlc-nox 是虚拟软件包,未被任何可用软件包所提供 这个问题,那就配置下官方源吧。
结果笔者死活无法在线装不上,信息如下:
root@debian:/home/freeswitch/freeswitch# aptitude install freeswitch-mod-vlc
下列“新”软件包将被安装。
freeswitch-mod-vlc vlc-nox{ab}
0 个软件包被升级,新安装 2 个,0 个将被删除, 同时 2 个将不升级。
需要获取 266 kB 的存档。解包后将要使用 444 kB。
下列软件包存在未满足的依赖关系:
vlc-nox : 依赖: vlc-plugin-base (= 3.0.12-0+deb9u1) 但是 3.0.17.4-0+deb10u2 已安装
依赖: vlc-bin (= 3.0.12-0+deb9u1) 但是 3.0.17.4-0+deb10u2 已安装
下列动作将解决这些依赖关系:
保持 下列软件包于其当前版本:
1) freeswitch-mod-vlc [未安装的]
2) vlc-nox [未安装的]
root@debian:/home/vlc# sudo apt-get install vlc-nox -y --allow-downgrades
正在读取软件包列表... 完成
正在分析软件包的依赖关系树
正在读取状态信息... 完成
有一些软件包无法被安装。如果您用的是 unstable 发行版,这也许是
因为系统无法达到您要求的状态造成的。该版本中可能会有一些您需要的软件
包尚未被创建或是它们已被从新到(Incoming)目录移出。
下列信息可能会对解决问题有所帮助:
下列软件包有未满足的依赖关系:
vlc-nox : 依赖: vlc-plugin-base (= 3.0.12-0+deb9u1) 但是 3.0.17.4-0+deb10u2 正要被安装
依赖: vlc-bin (= 3.0.12-0+deb9u1) 但是 3.0.17.4-0+deb10u2 正要被安装
E: 无法修正错误,因为您要求某些软件包保持现状,就是它们破坏了软件包间的依赖关系。
这里提供离线安装方式,操作如下:
wget http://security.debian.org/debian-security/pool/updates/main/v/vlc/vlc-nox_3.0.12-0+deb9u1_amd64.deb
dpkg -i --force-all vlc-nox_3.0.12-0+deb9u1_amd64.deb
如果没有,自行尝试由 FreeSWITCH 源码中编译,模块路径 /freeswitch/src/mod/formats/mod_vlc
。
首先安装编译环境
apt-get build-dep freeswitch
接着开始准备编译 FreeSWITCH 源码
cd /usr/local/src
git clone https://ghproxy.com/https://github.com/signalwire/freeswitch.git
cd /usr/local/src/freeswitch
ldconfig
./bootstrap.sh -j
上述操作完后,会在源码根目录生成一个 modules.conf
文件,编辑它,将 formats/mod_vlc
前的 井号(#)注释去掉。接着开始正式编译
./configure --enable-portable-binary \
--prefix=/usr --localstatedir=/var --sysconfdir=/etc \
--with-gnu-ld --with-python --with-erlang --with-openssl \
--enable-core-odbc-support --enable-zrtp
编译完成后,我们进到 /freeswitch/src/mod/formats/mod_vlc
目录,着手编译 mod_vlc 。
make && make install
编译错误解决:
make: * 没有规则可制作目标“/home/freeswitch/freeswitch/libfreeswitch.la”,由“mod_vlc.la” 需求。 停止。
apt-get install libfreeswitch-dev
安装
软件包
TOKEN=你申请的 SIGNALWIRE 令牌(如:pat_EaomznC7sLrSWSvgeT5ktNnr)
apt-get update && apt-get install -y gnupg2 wget lsb-release
wget --http-user=signalwire --http-password=$TOKEN -O /usr/share/keyrings/signalwire-freeswitch-repo.gpg https://freeswitch.signalwire.com/repo/deb/debian-release/signalwire-freeswitch-repo.gpg
echo "machine freeswitch.signalwire.com login signalwire password $TOKEN" > /etc/apt/auth.conf
chmod 600 /etc/apt/auth.conf
echo "deb [signed-by=/usr/share/keyrings/signalwire-freeswitch-repo.gpg] https://freeswitch.signalwire.com/repo/deb/debian-release/ `lsb_release -sc` main" > /etc/apt/sources.list.d/freeswitch.list
echo "deb-src [signed-by=/usr/share/keyrings/signalwire-freeswitch-repo.gpg] https://freeswitch.signalwire.com/repo/deb/debian-release/ `lsb_release -sc` main" >> /etc/apt/sources.list.d/freeswitch.list
# you may want to populate /etc/freeswitch at this point.
# if /etc/freeswitch does not exist, the standard vanilla configuration is deployed
apt-get update && apt-get install -y freeswitch-meta-all
源码编译
cd /usr/local/src
git clone https://ghproxy.com/https://github.com/signalwire/freeswitch.git
cd /usr/local/src/freeswitch
ldconfig
./bootstrap.sh -j
./configure --enable-portable-binary \
--prefix=/usr --localstatedir=/var --sysconfdir=/etc \
--with-gnu-ld --with-python --with-erlang --with-openssl \
--enable-core-odbc-support --enable-zrtp
make
make -j install
不过还是习惯 所有 mod 、 conf 、 bin 、 log 、 db 等等 文件夹 都放在一起, 比如: /usr/local/freeswtich/
# 针对 Asterisk 服务器的一个命令,用于安装声音文件(sounds files),这些文件通常包括各种提示音、语音信箱欢迎词、忙音等等。
make -j cd-sounds-install
# 用于安装 FreeSWITCH 的音乐文件
make -j cd-moh-install
编译错误解决:
libtool not found. You need libtool version 1.5.14 or newer to build FreeSWITCH from source.
apt install -y libtool libtool-bin
error: no usable libodbc; please install unixodbc devel package or equivalent
apt-get install -y unixodbc-dev
Docker
基于曹亮大佬的笔记整理而来。
创建并启动 CentOS7 容器服务。接下来都在容器中操作。
docker run -itd --name=freeswitch_1.10.8 centos:7
去 FreeSWITCH 的官⽹申请 token
官⽹地址:https://voice9.signalwire.com/dashboard
# 将⽤户名和token写⼊⽂件 echo "voice9" > /etc/yum/vars/signalwireusername echo "PT699d3a10bf366b0294d4b8e318ff5885df92dae5beed5dcd" > /etc/yum/vars/signalwiretoken
更新系统镜像源
yum install -y wget wget -O /etc/yum.repos.d/CentOS-Base.repo http://mirrors.aliyun.com/repo/Centos-7.repo yum clean all yum makecache yum -y update yum install -y subversion autoconf automake libtool gcc-c++ ncurses-devel make yum install -y http://files.freeswitch.org/repo/yum/centos-release/freeswitch-release-repo-0-1.noarch.rpm epel-release yum install -y yum-plugin-ovl centos-release-scl rpmdevtools yum-utils git yum install -y alsa-lib-devel autoconf automake bison broadvoice-devel bzip2 centos-release-scl cmake3 curl-devel devtoolset-7 devtoolset-7
安装 noarch
cd /usr/local/src/ wget http://files.freeswitch.org/freeswitch-release-1-6.noarch.rpm yum install -y freeswitch-release-1-6.noarch.rpm yum install -y libatomic yum install -y git alsa-lib-devel autoconf automake bison broadvoice-devel bzip2 curl-devel libdb4-devel e2fsprogs-devel erlang flite-devel
安装 cmake
yum remove cmake wget https://cmake.org/files/v3.14/cmake-3.14.0.tar.gz tar -zxvf cmake-3.14.0.tar.gz cd cmake-3.14.0 ./configure make && make install
安装 libks
cd /usr/local/src/ git clone https://github.com/signalwire/libks.git cd libks cmake . make && make install
安装 signalwire-c
cd /usr/local/src/ git clone https://github.com/signalwire/signalwire-c.git cd signalwire-c/ cmake . make && make install ln -sf /usr/local/lib64/pkgconfig/signalwire_client.pc /usr/lib64/pkgconfig/signalwire_client.pc
安装 x264
cd /usr/local/src/ git clone http://git.videolan.org/git/x264.git cd x264 ./configure --disable-asm make && make install
安装 mod_av
wget -c http://files.freeswitch.org/downloads/libs/libx264.tar.bz2 tar -jxvf libx264.tar.bz2 cd libx264 ./configure --enable-static --enable-shared --prefix=/usr make && make install cp /usr/lib/pkgconfig/x264.pc /usr/lib64/pkgconfig/ cp /usr/lib/libx264.so /usr/lib64/ cp /usr/lib/libx264.a /usr/lib64/ # download and install libav wget -c http://files.freeswitch.org/downloads/libs/libav-12.tar.bz2 tar -jxvf libav-12.tar.bz2 cd libav ./configure --enable-pic --enable-shared --enable-libx264 --enable-gpl --extra-libs="-ldl" --extra-cflags=-I/usr/include --extra-ldflags=- make && make install # make CXXFLAGS="-fPIC" cp /usr/local/lib/pkgconfig/libavcodec.pc /usr/local/lib/pkgconfig/libavdevice.pc /usr/local/lib/pkgconfig/libavfilter.pc /usr/local/lib/pkg # 执⾏刷新,以让FreeSWITCH运⾏时可以找到库 ldconfig
安装 libpng
git clone https://freeswitch.org/stash/scm/sd/libpng.git cd libpng ./configure make && make install cp /usr/local/lib/pkgconfig/libpng* /usr/lib64/pkgconfig/
安装 opus
git clone https://freeswitch.org/stash/scm/sd/opus.git cd opus ./autogen.sh ./configure --libdir=$PWD/tmp make && make install
安装 sofia-sip
git clone https://github.com/freeswitch/sofia-sip cd sofia-sip ./bootstrap.sh ./configure make && make install
安装 spandsp
git clone https://github.com/freeswitch/spandsp cd spandsp ./bootstrap.sh ./configure make && make install export PKG_CONFIG_PATH=/usr/local/lib/pkgconfig
安装 libopus-devel rmp 包
vim /etc/yum.repos.d/linuxtech.repo [linuxtech] name=LinuxTECH baseurl=http://pkgrepo.linuxtech.net/el6/release/ enabled=1 gpgcheck=1 gpgkey=http://pkgrepo.linuxtech.net/el6/release/RPM-GPG-KEY-LinuxTECH.NET # 创建仓库,重新安装 yum install -y libopus-devel
安装 FreeSWITCH
cd /usr/local/src/ wget http://files.freeswitch.org/freeswitch-1.10.8.-release.tar.gz tar vzxf freeswitch-1.10.8.-release.tar.gz cd freeswitch-1.10.8.-release ./configure --prefix=/app/freeswitch make && make install ln -sf /app/freeswitch/bin/freeswitch /usr/bin/ ln -sf /app/freeswitch/bin/fs_cli /usr/bin/
在 load mod_av
时,如果出现 *libavformat.so.57 file not found ,执行以下命令后再进入控制台。
echo "/usr/local/lib" >> /etc/ld.so.conf
ldconfig
# 在宿主机上进⼊fs
docker exec -it freeswitch_x86 /usr/local/freeswitch/bin/fs_cli -H 172.17.0.2 -P 7400 -p voice9.com
# 在容器⾥⾯进⼊fs
fs_cli -H 127.0.0.1 -P 7400 -p voice9.com
# 启动、停⽌服务
freeswitch -nc -rp
freeswitch -stop
openSIPS
基础安装
apt install gnupg2
apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 049AD65B
echo "deb https://apt.opensips.org buster 3.3-releases" >/etc/apt/sources.list.d/opensips.list
echo "deb https://apt.opensips.org buster cli-nightly" >/etc/apt/sources.list.d/opensips-cli.list
apt update
# 安装opensips
apt install opensips
apt install opensips-cli
# 安装opensips所有其他模块
# apt-get install -y opensips-*
apt-get install -y opensips-mysql-module opensips-http-modules opensips-auth-modules opensips-presence-modules opensips-dialplan-module
修改配置文件
路径:/etc/opensips/opensips.cfg
# 将socket地址改为本地IP
socket=udp:<服务器IP地址>:5060 # CUSTOMIZE ME
启动 openSIPS
# 启动opensips并检查状态
systemctl start opensips
systemctl status opensips
数据库支持
OpenSIPS CLI 接受一个配置文件(格式可以是 ini 或 cfg ),它可以存储某些影响 OpenSIPS CLI 工具行为的参数。
如果 -f|--config
参数没有指定配置文件,OpenSIPS CLI 会在以下位置搜索一个。
- ~/.opensips-cli.cfg(最高优先级)
- /etc/opensips-cli.cfg
- /etc/opensips/opensips-cli.cfg(最低优先级)
如果没有找到文件,则以默认配置启动。
OpenSIPS CLI 核心可以使用以下参数:
- prompt_name:OpenSIPS CLI 提示符的名称(默认:
opensips-cli
) - prompt_intro:进入 OpenSIPS CLI 时的介绍信息
- prompt_emptyline_repeat_cmd:在空行上重复最后一条命令(默认:
False
) - history_file:历史记录文件的路径(默认:
~/.opensips-cli.history
) - history_file_size:历史文件的积压大小(默认:
1000
) - log_level:控制台日志记录的级别(默认:
WARNING
) - communication_type:OpenSIPS CLI 使用的通信传输方式(默认:
fifo
) - fifo_file:OpenSIPS 的 FIFO 文件,CLI 将把命令写入该文件中(默认:
/var/run/opensips/opensips_fifo
) - fifo_file_fallback:当找不到 fifo_file 时使用的后备 FIFO 文件,当默认的 fifo_file 从 /tmp/opensips_fifo 被改变时,为了向后兼容而引入了这个文件。 (默认:
/tmp/opensips_fifo
) - fifo_reply_dir:opensips-cli 将创建用于 OpenSIPS 回复的 fifo 的默认目录(默认:
/tmp
) - url:使用 http 通信类型时使用的默认 URL(默认:
http://127.0.0.1:8888/mi
)
数据库模块的几个参数:
- database_schema_path(可选):OpenSIPS DB 模式目录的绝对路径,如果是从软件包中安装,通常是 /usr/share/opensips ,如果使用 OpenSIPS 源树,则是 /path/to/opensips/scripts 。默认情况下:/usr/share/opensips
- database_admin_url(可选):具有特权(管理员)访问级别的数据库的连接字符串,它将被用于 创建 / 删除 数据库,以及创建或确保通过 database_url 提供的非特权数据库访问用户的访问。该 URL 结合了模式、用户名、密码、主机和端口。默认:mysql://root@localhost
- database_url(可选):数据库的连接字符串。一个好的做法是使用非管理员访问用户访问此 URL。默认值:mysql://opensips:opensipsrw@localhost
- database_name(可选):数据库的名称。如果您选择不安装所有模块,则可以将这些模块单独添加到此数据库中。默认值:opensips。
- database_modules(可选):接受 ALL 关键字,表示所有可用的模块都应该被安装,或者接受一个用空格分隔的模块名称列表。如果与创建命令一起处理,相应的表将被部署。默认值: acc alias_db auth_db avpops clusterer dialog dialplan dispatcher domain drouting group load_balancer msilo permissions rtpproxy rtpengine speeddial tls_mgm usrloc.
- database_force_drop(可选):表示 drop 命令是否会在没有用户交互的情况下删除数据库。默认值:false
路径:/etc/opensips-cli.cfg
[default]
log_level: WARNING
prompt_name: opensips-cli
prompt_intro: Welcome to OpenSIPS Command Line Interface!
prompt_emptyline_repeat_cmd: False
history_file: ~/.opensips-cli.history
history_file_size: 1000
output_type: pretty-print
communication_type: fifo
fifo_file: /run/opensips/opensips_fifo
# 数据库链接(一个不带密码,一个带密码)
database_admin_url: mysql://root@192.168.0.60
database_url: mysql://root:123456@192.168.0.60
database_modules: ALL
创建数据库指令:
# 使用 opensips-cli 创建数据库 opensips
opensips-cli -x database create opensips
常用指令
创建用户
# 语法:opensips-cli -x user add <用户名>@<地址> <密码>
opensips-cli -x user add username@domain.com S3cureP4s$
配合 FreeSWITCH
笔者没搞定,可自行参考以下几篇文章:
POC 对讲
前言
POC(PTT Over Cell 的简写),PTT 是按下讲话的意思,要么说,要么听别人说,不会出现同时讲话和听的情况,所以类似半双工的通信方式。
Janus
以下内容摘自呱牛大佬的 使用 Janus 作为对讲服务器的后台框架和业务流程 及 基于Janus房间服务器的POC对讲实现 两篇笔记。
通过 Nginx 负载 Janus http 服务器的 API 接口,通过该 API 接口可以获取可用 Janus 服务器的 IP 和端口;客户端拿到可用对讲服务器的 IP 和端口后,通过 WebSocket 连接到该服务器,并保持长连接,客户端进入会议室时,则复用这条长连接;如果进入其他会议室,则需要重新获取可用的 Janus 服务器 IP 和端口,并重复上述过程;多个对讲服务器之间通过 RabbitMQ 共享数据,三个服务器之间对等对外提供服务。
客户端加入会议室后,使用两个 peerconnection 分别用来做 publisher 角色和 subscriber 角色类型通信,publisher 主要用来讲话的通道,subscriber 则主要用来听对讲的通道;每次讲话前都需要申请 TBCP 讲话权限,获取成功,则开启录音,并发送数据,否则不开启录音;如果其他人在讲话,则打开播放器,开始播放声音,主要业务流程如下图:
详细的 对讲加入群组主要业务流程 如下图所示:
sequenceDiagram
participant a as 客户端
participant b as Nginx/Http Admin接口
participant c as Janus 服务器1
a ->> b: 通过房间号使用 http 接口<br/>获取可用的 Janus 服务器 IP 及 端口
b -->> a: 返回可用的 Janus 服务器 IP 及 端口
a ->> c : WebSocket 连接到 Janus 服务器
a ->> c : 创建 session
c -->> a: 返回创建成功
rect rgb(191, 223, 255)
a ->> c: 绑定对讲插件
c -->> a: 返回绑定成功
a ->> c: 创建房间
c -->> a: 返回房间已经存在 | 创建成功
a ->> c: 加入房间,publisher 角色是作为推流端
c -->> a: 返回加入成功 {"videoroom":"joined"} 以及房间中当前的人员列表
a ->> c: 配置房间
c -->> a: 返回配置成功 以及 服务器的 {"jsep":{"type":"answer","sdp":"v-0\r\no....."}}
a ->> c: 发送本端的 ICB 地址
c -->> a: 返回 ICB 配置成功
a ->> c: 讲通道建立成功
a ->> c: 申请 TBCP 权限
c -->> a: 返回申请成功 | 则打开 MIC 开始讲话 | 否则不讲话
end
rect rgb(200, 150, 255)
a ->> c: 绑定对讲插件
c -->> a: 返回绑定成功
a ->> c: 加入房间,subscriber 角色是作为拉流端
c -->> a: 返回加入成功 {"videoroom":"joined"} 以及房间中当前的人员列表
a ->> c: 发送本端 sdp
c -->> a: 返回配置成功 以及 服务器的 {"jsep":{"type":"answer","sdp":"v-0\r\no....."}}
a ->> c: 发送本端的 ICB 地址
c -->> a: 返回 ICB 配置成功
a ->> c: 听通道建立成功
c ->> a: TBCP 返回其他人在讲话,则开始声音播放
end
创建 session
{"janus" : "create", "apisecret":"123456", "transaction" :"1kqs0K1uzid9"}
绑定插件(同 19)
{"janus":"attach","plugin":"janus.plugin.pooroom","apisecret":"123456","session_id":1390233816776651,"transaction":"H4zNCmMmnFDv"}
创建房间
{"janus":"message","apisecret":"123456","body":{"request":"create","room":1542076668513,"secret":"1234","pin":"4321","display":"600555","permanent":false,"is_private":false}}
加入房间,publisher 角色作为推流端
{"janus":"message","apisecret":"123456","body":{"request":"join","ptype":"publisher","room":1542076668513,"pin":"4321","display":"600555"}}
配置房间
{"apisecret":"123456","body":{"audio":true,"audiocodec":"opus","data":true,"request":"configure","video":false},"janus":"message","jsep":{"sdp":"...."}}
发送本端的 ICB 地址 (同 25)
{"janus":"message", "apisecret":"123456","candioate":{"candidate":"candidate:...."}}
加入房间,subscriber 角色是作为拉流端
{"janus":"message","apisecret":"123456","body":{"request":"join","ptype":"subscriber","room":1542076668513,"pin":"xdja_4321","display":"600555"}}
发送本端 sdp
{"janus":"message","apisecret":"xdja_api_abc","body":{"request":"start","room":1542076668513},"jsep":{"sdp":"v-0\r\no..."}}
TBCP 返回其他人在讲话,则开始声音播放
{result:"tbcp", "tbcp_request_userid":"600555", "result_code":103,"tbcp_old_owner":"600555","transaction":"ODsCW2y4mbtd", "message":"release the tbcp burst success"}
退出会议室时,一定要保证两个 Peerconnection 的连接都能同服务器断开;
断网重连流程:如果碰到断网重连时,需要重新获取该房间的 janus 服务器的 IP 和端口,并在重连 WebSocket 成功后,重走 对讲加入群组主要业务流程 的流程,完成重新自动进入对讲房间的工作。
如果还沿用 janus_videoroom 的实现,如果有 N 个人参与的会议室,那么每个与会者都需要订阅其他 N-1 个人声音,会有 N-1 条下行信道,但每次只有一个信道可用,这对信道资源来说是明显的浪费,所以,我们希望改造成 MCU 模式,但又不需要做服务器端的混音操作,完成 POC 的业务对讲能力;
最后实现:
- 给房间增加一个公共的 publish 对象,所有对房间的订阅都是订阅该 publish 对象,达到支持 从 SFU 模式到支持 MCU 转发模式(适应 POC 对讲模式的 MCU 模式)
- 支持会场 TBCP 控制信息通过 DataChannel 通道传递
- 支持会场 TBCP 控制和会场通知 ;
部分逻辑:
- 给 janus_videoroom 结构体里面增加一个 janus_videoroom_publisher *room_publisher;
- 在创建房间成功后,既实例化这个对象 room_publisher;
- 初始进入房间或者枚举房间的 publisher 的时候,只返回这个对象即可,不返回 room 结构体里面的 participants 列表中的用户 id,这样每个 subscriber 都对应的是这个 publisher 对象;
- 然后在媒体包过来的时候,枚举 room_publisher 中的 subscriber,逐个分发即可。
修改 Janus 服务器,支持 datachannel 能力:Janus 的 datachannel 支持的协议主要是:DTLS/SCTP
、UDP/DTLS/SCTP
,而 RTP 通道使用的是 UDP/TLS/RTP/SAVPF
,修改的思路包括 SDP 返回的修改,RTP 数据包中提取 datachannel 包对应 SSRC 的包。
给 janus_ice_stream 结构体添加 data 的 ssrc 字段和初始化
guint32 data_ssrc_peer; guint32 data_ssrc, sequence_data; // int janus_sdp_process(void *ice_handle, janus_sdp *remote_sdp, gboolean update) 方法中增加如下逻辑: if(!strcasecmp(m->proto, "UDP/DTLS/SCTP") || !strcasecmp(m->proto, "UDP/TLS/RTP/SAVPF")) { stream->data_ssrc = janus_random_uint32(); /* FIXME Should we look for conflicts? */ stream->sequence_data = janus_random_uint32(); janus_flags_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_NEW_DATACHAN_SDP); janus_flags_clear(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_DATA_CHANNELS);//lyz@xdja.com add } else { janus_flags_clear(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_NEW_DATACHAN_SDP); }
SDP 返回数据
m->fmts = g_list_append(m->fmts, g_strdup("109")); janus_sdp_attribute *aa = janus_sdp_attribute_create("rtpmap", "109 google-data/90000"); m->attributes = g_list_append(m->attributes, aa); m->ptypes = g_list_append(m->ptypes, GINT_TO_POINTER(109));
接收 RTP 数据函数 janus_ice_cb_nice_recv 中,增加对 datachannel 的数据处理的逻辑
if (header->type == 109 || stream->data_ssrc_peer == packet_ssrc){ /* Pass the data to the responsible plugin */ janus_plugin *plugin = (janus_plugin *)handle->app; if(plugin && plugin->incoming_data && !g_atomic_int_get(&handle->app_handle->stopped) && !g_atomic_int_get(&handle->destroyed)){ plugin->incoming_data(handle->app_handle, (char *)buf + RTP_HEADER_SIZE, buflen - RTP_HEADER_SIZE); } return; }
发送 RTP 数据时,使用 RTP 通道发送 datachannel 的数据
void janus_plugin_relay_data(janus_plugin_session *plugin_session, char *buf, int len) { if((plugin_session < (janus_plugin_session *)0x1000) || g_atomic_int_get(&plugin_session->stopped) || buf == NULL || len < 1) return; janus_ice_handle *handle = (janus_ice_handle *)plugin_session->gateway_handle; if(!handle || janus_flags_is_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_STOP) || janus_flags_is_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_ALERT)){ return; } #ifdef HAVE_SCTP if (janus_flags_is_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_NEW_DATACHAN_SDP)){ janus_ice_relay_data_withrtp(handle, buf, len);//relay daa with rtp only. }else{ janus_ice_relay_data(handle, buf, len); } #else JANUS_LOG(LOG_WARN, "Asked to relay data, but Data Channels support has not been compiled...\n"); #endif } void janus_ice_relay_data_withrtp(janus_ice_handle *handle, char *buf, int len) { struct timeval now; if(!handle || handle->queued_packets == NULL || buf == NULL || len < 1){ return; } /* Queue this packet */ janus_ice_queued_packet *pkt = g_malloc(sizeof(janus_ice_queued_packet)); pkt->data = g_malloc(RTP_HEADER_SIZE+len+SRTP_MAX_TAG_LEN+4); janus_rtp_header *header = (janus_rtp_header *)pkt->data; header->ssrc = (handle->stream->data_ssrc); header->type = 109; header->version = 2; header->markerbit = 1; header->extension = 0; handle->stream->sequence_data++; header->seq_number = (handle->stream->sequence_data + 100); gettimeofday(&now,0); header->timestamp = htonl(now.tv_sec); memcpy((char *)pkt->data + RTP_HEADER_SIZE, buf, len); pkt->length = RTP_HEADER_SIZE + len; pkt->type = JANUS_ICE_PACKET_DATA_WITH_RTP; pkt->control = FALSE; pkt->encrypted = FALSE; pkt->retransmission = FALSE; pkt->added = janus_get_monotonic_time(); janus_ice_queue_packet(handle, pkt); } // 发送函数static gboolean janus_ice_outgoing_traffic_handle的逻辑中,对ssrc的处理需要修改为: // header->ssrc = htonl(video ? stream->video_ssrc : ((pkt->type == JANUS_ICE_PACKET_DATA_WITH_RTP)?stream->data_ssrc : stream->audio_ssrc));
广播方式
以下内容摘自呱牛大佬的 音频广播播放功能实现逻辑分享 笔记。
广播的业务还是挺好实现的,但业务链条比较长,作为练手项目绝对不错,主要涉及到几个点:
- 音频数据采集;
- 音频数据编码;
- 媒体流组包;
- (组播)UDP Socket 服务器和客户端,socket 接收和发送实现;
- 音频抖动缓冲区,及音频播放器实现;
从业务层面看,每个广播都可以通过广播按钮给同组的其他广播喊话。
从网络层面来看,每个广播都有一个信令监听端口,监听广播发起、广播结束的通知。
被喊话的广播接收到喊话通知后,开一个 udp 端口,接收广播的音频数据包。
录制和播放器参考 pjsip 的 audiotest.c 的代码实现。
录制:
//broadcast_record.c
/*
录制wav,编码? 通过组播发送出去
组播接收wav, 解码?播放wav
*/
#include <pjmedia-audiodev/audiodev.h>
#include <pjmedia.h>
#include <pjlib.h>
#include <pjlib-util.h>
#include "broadcast_app.h"
#define THIS_FILE "broadcast_record.c"
static pj_pool_t *pool = NULL;
static pjmedia_aud_param param;
static pjmedia_aud_stream *strm = NULL;
static unsigned playback_lat = PJMEDIA_SND_DEFAULT_PLAY_LATENCY;
static unsigned capture_lat = PJMEDIA_SND_DEFAULT_REC_LATENCY;
static volatile RecordCallback recordCallback = NULL;
int stop_record(void)
{
recordCallback = NULL;
if (strm) {
pjmedia_aud_stream_stop(strm);
pjmedia_aud_stream_destroy(strm);
}
if (pool){
pj_pool_release(pool);
}
strm = NULL;
pool = NULL;
return 0;
}
static pj_status_t wav_rec_cb(void *user_data, pjmedia_frame *frame)
{
//回调函数
//return pjmedia_port_put_frame((pjmedia_port*)user_data, frame);
pj_int16_t *pcm_in = (pj_int16_t*)frame->buf;
//编码?
//发送?
if (recordCallback != NULL){
recordCallback(user_data, pcm_in, (int)frame->size, frame->timestamp.u32.lo);
}
return 0;
}
int start_record(void *user_data, RecordCallback callback)
{
pj_status_t status;
pool = pj_pool_create(pjmedia_aud_subsys_get_pool_factory(), "wav",
1000, 1000, NULL);
status = pjmedia_aud_dev_default_param(0, ¶m);
if (status != PJ_SUCCESS) {
printf("pjmedia_aud_dev_default_param()", status);
goto on_return;
}
param.dir = PJMEDIA_DIR_CAPTURE;
param.clock_rate = 16000;
param.samples_per_frame = 320;
param.channel_count = 1;
param.bits_per_sample = 16;
/* Latency settings */
param.flags |= (PJMEDIA_AUD_DEV_CAP_INPUT_LATENCY |
PJMEDIA_AUD_DEV_CAP_OUTPUT_LATENCY);
param.input_latency_ms = capture_lat;
param.output_latency_ms = playback_lat;
if (strm) {
pjmedia_aud_stream_stop(strm);
pjmedia_aud_stream_destroy(strm);
}
status = pjmedia_aud_stream_create(¶m, &wav_rec_cb, NULL, user_data,
&strm);
if (status != PJ_SUCCESS) {
printf("Error opening the sound device", status);
goto on_failed;
}
status = pjmedia_aud_stream_start(strm);
if (status != PJ_SUCCESS) {
printf("Error starting the sound device", status);
goto on_failed;
}
//增加回调函数
recordCallback = callback;
PJ_LOG(3,(THIS_FILE, "Recording started"));
goto on_return;
on_failed:
if (strm) {
pjmedia_aud_stream_stop(strm);
pjmedia_aud_stream_destroy(strm);
}
if (pool){
pj_pool_release(pool);
}
on_return:
return 0;
}
播放器:
//broadcast_play.c
#include <pjmedia-audiodev/audiodev.h>
#include <pjmedia.h>
#include <pjlib.h>
#include <pjlib-util.h>
#include "broadcast_app.h"
#define THIS_FILE "broadcast_play.c"
static pj_pool_t *pool = NULL;
static pjmedia_aud_param param;
static pjmedia_aud_stream *strm = NULL;
static unsigned playback_lat = PJMEDIA_SND_DEFAULT_PLAY_LATENCY;
static unsigned capture_lat = PJMEDIA_SND_DEFAULT_REC_LATENCY;
static volatile RecordCallback playCallback = NULL;
static pj_status_t wav_play_cb(void *user_data, pjmedia_frame *frame)
{
//return pjmedia_port_get_frame((pjmedia_port*)user_data, frame);
if (playCallback != NULL){
playCallback(user_data, frame->buf, (int)frame->size, frame->timestamp.u32.lo);
}
return 0;
}
int stop_play(void){
playCallback = NULL;
if (strm) {
pjmedia_aud_stream_stop(strm);
pjmedia_aud_stream_destroy(strm);
}
if (pool){
pj_pool_release(pool);
}
return 0;
}
int start_play(void *user_data, PlayCallback callback)
{
pj_status_t status;
if (pool == NULL){
pool = pj_pool_create(pjmedia_aud_subsys_get_pool_factory(), "play",
1000, 1000, NULL);
}
status = pjmedia_aud_dev_default_param(0, ¶m);
if (status != PJ_SUCCESS) {
printf("pjmedia_aud_dev_default_param()", status);
goto on_return;
}
param.dir = PJMEDIA_DIR_PLAYBACK;
param.clock_rate = 16000;
param.samples_per_frame = 320;
param.channel_count = 1;
param.bits_per_sample = 16;
/* Latency settings */
param.flags |= (PJMEDIA_AUD_DEV_CAP_INPUT_LATENCY |
PJMEDIA_AUD_DEV_CAP_OUTPUT_LATENCY);
param.input_latency_ms = capture_lat;
param.output_latency_ms = playback_lat;
status = pjmedia_aud_stream_create(¶m, NULL, &wav_play_cb, user_data,
&strm);
if (status != PJ_SUCCESS) {
printf("Error opening the sound device", status);
goto on_failed;
}
status = pjmedia_aud_stream_start(strm);
if (status != PJ_SUCCESS) {
printf("Error starting the sound device", status);
goto on_failed;
}
playCallback = callback;
PJ_LOG(3,(THIS_FILE, "play started,strm:%08x",strm));
goto on_return;
on_failed:
if (strm) {
pjmedia_aud_stream_stop(strm);
pjmedia_aud_stream_destroy(strm);
}
if (pool){
pj_pool_release(pool);
}
on_return:
return 0;
}
头文件:
//broadcast_app.h
#ifndef __BROADCAST_APP__
#define __BROADCAST_APP__
#include <unistd.h>
#ifdef __cplusplus
extern "C"{
#endif
typedef int (*PlayCallback)(void *user_data,uint8_t *rawData, int len, uint32_t timestamp);
typedef int (*RecordCallback)(void *user_data,uint16_t *rawData, int len, uint32_t timestamp);
int start_play(void *user_data, PlayCallback callback);
int stop_play(void);
int start_record(void *user_data, RecordCallback callback);
int stop_record(void);
#ifdef __cplusplus
}
#endif
#endif//__BROADCAST_APP__
Yealink
话机设置
用网线连接 Internet 口与 PC 网口。
话机端静态 I 地址的设定:
- 按”Menu 键” -> Settings -> Advanced(密码:admin)-> Network -> WAN Port -> Static IP
- 在相应的区域中分别输入 IP 地址、子网掩码、默认网关、首选 DNS 服务器(参照相同网段内任意一台 PC)
- 按”OK 键”保存
后台配置
默认账号密码都是 admin 。
其他
sip-hub
sip-hub 是一个专注 sip 信令的搜索以及时序图可视化展示的服务。
相比于 Homer, sip-hub 做了大量的功能简化。同时也提供了一些个性化的查询,例如被叫后缀查询,仅域名查询等等。
做 sip-hub 的原因是 homer 太难用了,经常查不到想查的数据,查询的速度也蛮。
sip-hub 服务仅有 3 个页面:
- sip 消息搜索页面,用于按照主被叫、域名和时间范围搜索呼叫记录
- 时序图展示页面,用于展示 SIP 时序图和原始 SIP 消息
- 可以导入导出 SIP 消息
- 可以查找 A-Leg
- 监控功能
相关截图如下:
安装 sip-hub :
- 首先需要安装 MySql 数据库,并在其中建立一个名为 siphub 的数据库
运行
docker run -d -p 3000:3000 -p 9060:9060/udp \ --env NODE_ENV=production \ --env dbHost=1.2.3.4 \ --env dbUser=root \ --env dbPwd=123456 \ --env dbName=siphub \ --env dataKeepDays=3 \ --env logLevel=error \ --log-opt max-size=10M \ --log-opt max-file=3 \ --name siphub wangduanduan/siphub
参数介绍:
- dbHost:数据库地址
- dbUser:数据库用户
- dbPwd:数据库密码
- dbName:数据库名
- dataKeepDays:抓包保存天数
- logLevel:日志等级
- 3000:端口是 web 页面端口
- 9060:是 hep 消息收取端口
OpenSIPS 集成(在 OpenSIPS 2.4 进行测试):
# add hep listen
listen=hep_udp:your_ip:9061
loadmodule "proto_hep.so"
# replace SIP_HUB_IP_PORT with siphub‘s ip:port
modparam("proto_hep", "hep_id","[hep_dst] SIP_HUB_IP_PORT;transport=udp;version=3")
loadmodule "siptrace.so"
modparam("siptrace", "trace_id","[tid]uri=hep:hep_dst")
# add ite in request route();
if(!is_method("REGISTER") && !has_totag()){
sip_trace("tid", "d", "sip");
}
FreeSWITCH 集成(版本要高于 1.6.8+):
编辑配置文件 sofia.conf.xml
<!-- 用真实的 siphub ip:port 替换 SIP_HUB_IP_PORT -->
<param name="capture-server" value="udp:<SIP_HUB_IP_PORT>"/>
freeswitch@fsnode04> sofia global capture on
+OK Global capture on
freeswitch@fsnode04> sofia global capture off
+OK Global capture off
配合 SRS
杜老师已经做了 XSwitch 的对接,请参考 如何在XSwitch中对接SRS
原文 Issues 链接:WebRTC:支持 FS MCU 通过 WHIP 连接到 SIP 客户端的 SRS
以下是机翻:
如果您让 SIP 客户端加入会议,与 WebRTC 客户端进行通信,如何做到这一点?
由于 SIP 客户端仅支持 1 个视频流和 1 个音频流,有些客户端可能支持 1 个额外的屏幕流,因此您应该使用 MCU 合并房间中的所有流。
FS 或 Freeswitch 是用于 SIP 客户端的 MCU,也支持 WebRTC 客户端,因此您可以改用 FS。
有时,绝大多数房间都没有 SIP 客户端,只有一小部分房间应该支持 SIP 客户端。在这种情况下,SFU 是更好的解决方案,因为 MCU 需要大量的 CPU 资源进行编码。
如果您只有一个 SIP 客户端,其他客户端都是 WebRTC 客户端(如 Chrome 浏览器),您也可以使用 FS 作为 SIP 到 WebRTC 代理来连接到 SRS(如 WebRTC 客户端)。
以下是完整的架构:
。我认为工作流程应该是这样的:
- Chrome A 将 WebRTC 流推送到 SRS。
- SRS 通过 HTTP 回调调用 FS HTTP 服务器。
- FS 通过 WHIP 从 SRS 中提取 WebRTC 流。
- FS 向 SRS 发布一个混合流,其中包含 WebRTC 客户端和 SIP 客户端。
- Chrome A 从 SRS 中提取混合流。
除了这个解决方案,SRS 还可以通过 WHIP 将 WebRTC 流转发或推送到 FS,工作流应该是:
- Chrome A 将 WebRTC 流推送到 SRS。
- SIP 客户端连接到 FS,并将此事件通知 HTTP 服务器。
- HTTP 服务器通知 SRS 开始转发。
- SRS 通过 WHIP 将 WebRTC 流转发给 FS。
注意:我们强烈建议 MCU 通过 WHIP 或 WHEP 从 SRS 中提取流,这是 MCU 和 SFU 架构的常见解决方案。
要点翻译下:
a. Chrome A 推流到 SRS 后,回调到 FS。
b. FS 通过 recvonly WHIP 从 SRS 拉 Chrome A 的流。
c. Chrome B 和 C 和 A 一样,FS 都会把流拉过去。
1.1. FS 应该会有一个混流,通过 sendonly WHIP 送到 SRS,这个不一定是同一个 SRS,可以是另外一个 SRS。
1.2. Chrome A/B/C 可以拉这个混流,也可以互相拉流,看用户的策略了。
2.1. FS 应该还可以送一个 RTMP 出来,这就是连麦的直播流了。
一些项目
智能呼叫中心系统 voip、webrtc 。支持功能点:
- 支持 sdk 接入语音平台
- 支持 webrtc
- 支持 rest 接口接入语音平台
- 支持 http ivr 接入
- 支持 http NLP 响应
SoftSwitch-Gateway
基于 netty 4 对接的 Opensips Exported Event、Exported MI 和 FreeSWITCH Event Socket Library 、cdr 、xml_curl 等接口实现方案。
OV500 是一个开源软件解决方案,旨在为 Kamailio 和 FreeSWITCH 提供 VoIP 计费、交换和评级功能。
OV500 提供实时计费和评级、自动路由、动态负载平衡、高级报告和分析以及欺诈防范机制等功能。它可以被 VoIP 服务提供商、运营商和企业用于管理他们的语音流量和计费业务。
一些文章
- 《FreeSWITCH 权威指南》
- 怎么能少了 FreeSWITCH 官网呢?
- 音视频开发之路
- qzlink 大佬的 FreeSWITCH 专栏
- Younian 大佬的呼叫中心专栏
- 洞香春 大佬的博客
- Java_lilin 大佬的专栏
- FreeSwitch+Opensips+NFS 文件共享集群安装配置操作指南(百度文库,收费)
- softswitch-gateway(这里的文档蛮有价值的)
- 菩提树下的杨过 博客
- 杨虎成博客
- _Ong 博客
- 呱牛笔记 (改源码的大佬)
- 贾宝玉的玉宝贾的 CSDN 专栏(专栏收费)
- GUI 的专栏
- Freeswitch 源码分析
- FreeSWITCH 之视频录像
- Telecom R & D (外国网站,高大上,笔者看不懂,蛮收藏)
结语
笔者就是个菜鸡,上面的内容都是摘抄自各个平台,然后整理汇总的。大部分都亲自试过。
笔记 PDF 链接点这里 FreeSWITCH 常见问题(个人整理汇总)
<action application="export" data="nolocal:execute_on_answer=lua incrInUse.lua ${uuid}"/>
或者,在 "manually" 忽略早期媒体的情况下,等待 30 秒的回答。
```shell
originate {ignore_early_media=true}sofia/gateway/my_gateway/5551212 885551212
```
```xml
<extension name="exe_on_ans">
<condition field="destination_number" expression="^88(\d+)$">
<action application="set" data="execute_on_answer=transfer ANSWEREDCALL XML default"/>
<action application="log" data="INFO Waiting 30 seconds for $1 to answer..."/>
<action application="sleep" data="30000"/>
<action application="log" data="INFO Call to $1 was not answered, taking alternative action..."/>
<action application="transfer" data="UNANSWEREDCALL XML default"/>
</condition>
</extension>
```
<action application="export" data="nolocal:api_on_answer=uuid_broadcast ${uuid} beep.wav both"/>
<action application="bridge" data="{api_on_answer='uuid_broadcast ${uuid} beep.wav both'}sofia/gateway/provider/5551231234"/>
<param name="min-required-recording-participants" value="2"/>
<param name="auto-record" value="/var/myNFSshare/${conference_name}_${strftime(%Y-%m-%d-%H-%M-%S)}.wav"/>
<param name="auto-record" value="/var/myNFSshare/${conference_name}_${strftime(%Y-%m-%d-%H-%M-%S)}.mp4"/>
<!-- 这个mp3的格式有可能不对,官方只给了个 <shout://user:pass@server.com/live.mp3> 的示例 -->
<param name="auto-record" value="<shout://user:pass@server.com/live.mp3>"/>