FreeSWITCH 常见问题(个人整理汇总)

忠告

千万别使用 CentOS 部署 FreeSWITCH !!!

以下大部分都是笔者用 CentOS7 自编译踩得坑。

建议直接 Debian 安装官方编译好的包!!!可以少踩很多坑(严重怀疑官方歧视 CentOS)。

前言

FreeSWITCH 是一个自由开源的软件型电话交换机。它采用 Mozilla Public License(MPL)授权协议,MPL 是一个开源的软件协议。它的核心库 libfreeswitch 可以嵌入其它系统或产品中,也可以做一个单独的应用存在。

本文汇总 FreeSWITCH 配置时的一些常见问题。

端口介绍

防火墙端口网络协议应用协议描述
1719UDPH.323 Gatekeeper RAS port
1720TCPH.323 Call Signaling
2855-2856TCPMSRP用于短信通话
3478UDPSTUN service用于 NAT 穿透
3479UDPSTUN service用于 NAT 穿透
5002TCPMLP protocol server
5003UDPNeighborhood service
5060UDP & TCPSIP UAS用于 SIP 信令(标准 SIP 端口,用于默认的内部配置文件)
SIP 端口,用于处理 SIP 消息和呼叫控制协议。
5061UDP & TCPSIP UASSIP/TLS 端口,用于加密 SIP 信令。(笔者补充的,非官方文档描述!!!)
5070UDP & TCPSIP UAS用于 SIP 信令(用于默认的 "NAT "配置文件)
5080UDP & TCPSIP UAS用于 SIP 信令(用于默认的 "外部 "配置文件)
8021TCPESL用于 mod_event_socket
Event Socket 端口,用于与 FreeSWITCH API 进行交互。
16384-32768UDPRTP/ RTCP multimedia streaming用于 SIP、Verto 和其他协议中的音频/视频数据
RTP 端口范围,用于传输音频和视频数据。
5066TCPWebsocket用于 WebRTC(ws)
7443TCPWebsocket用于 WebRTC(wss)
8081-8082TCPWebsocket用于 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 信息
9195echo,回声测试,延迟 5 秒
9196echo,回声测试
9197使用 tone_stream 来播放一个连续的 1004hz 的音
9198使用 tone_stream 播放俄罗斯方块音乐
9664保持音乐
9888FreeSWITCH 公共会议
91616FreeSWITCH 公共会议16 kHz
93232FreeSWITCH 公共会议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 模块

基本概念

  1. Sofia-SIP FreeSwitch 的 SIP 功能是在 mod_sofia 模块中实现的。FreeSwitch 并没有自己开发新的 SIP 协议栈,而是使用了比较成熟的开源 SIP 协议栈 Sofia-SIP。
  2. Endpoint 在 FreeSwitch 中,实现一些互联网协议接口的模块称为 Endpoint。FreeSwitch 支持很多类型的 Endpoint,如 SIP,H232 等。这些不同的 Endpoint 主要是使用不同的控制协议跟其他的 Endpoint 通话。
  3. mod_sofia mod_sofia 实现了 SIP 中的注册服务器、重定向服务器、媒体服务器,呈现服务器、SBC 等各种功能。它的定位是一个 B2BUA。
  4. SIP Profile 在 mod_sofia 中,SIP Profile 相当于一个 SIPUA,通过各种不同的参数可以配置一个 UA 的行为。一个系统可以有多个 SIP Profile,每个 SIP Profile 都可以监听不同的 IP 地址和端口。
  5. Gateway 一个 SIP Profile 中有多个 Gateway(网关),它主要用于定义一个远程的 SIP 服务器,使 Freeswitch 可以与其他服务器通信。
  6. 本地 SIP 用户 FreeSwitch 可以作为注册服务器,这时候其他 SIP 客户端就可以向它注册。FreesWitch 将通过用户目录中(conf/Directory)中的配置信息对注册用户进行鉴权。这些 SIP 客户端锁代表的用户就称为本地 SIP 用户,简称本地用户
  7. 来电去话,中继来电,中继去话

配置文件

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。

  1. 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。
  2. 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>
  3. 查找 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
  4. 找到呼叫字符串后,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)。

概念

  1. Transaction 请求和所有的响应构成一个事务,一个完整的呼叫过程包括多个事件。
  2. UA 用户代理,是发起或接受呼叫的逻辑实体
  3. UAC 用户代理客户端,用于发起请求
  4. UAS 用户代理服务器,用于接受请求
  5. UAC 和 UAS 的划分是针对一个事务的,在一个呼叫的多个事务中,UAC 和 UAS 的角色是可以互相转换的
  6. B2BUA 是一个 SIP 中逻辑上的网络组件,用于操作不同会话的端点,它将 channel 划分为两路通话,在不同会话的端点直接通信。例如,当建立一通呼叫时,B2BUA 作为一个 UAS 接受所有用户请求,处理后以 UAC 角色转发至目标端。

image

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 个头域:

  1. Call-ID 用于区分不同会话的唯一标志
  2. CSeq 序列号,用于在同一会话中区分事务
  3. From 说明请求来源
  4. To 说明请求接受方
  5. Max-Forwars 限制跳跃点数和最大转发次数
  6. Via 描述请求消息经过的路径

request 和 response 报文的 From 和 To 是完全一致的,尽管他们的方向是相反的。From 和 To 的值是根据 request 来定义的。 Via 中的 branch 标记腿的 id 当有使用 proxy 代理时,请求转发时用的还是同一个 branch,From 和 To 中的 tag 可以作为 id 标记是否为原始请求。

Via 的一个示例:

sip_Via.png

更多的请参考 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 命令

快捷键

快捷键命令备注
F1help
F2status
F3show channels
F4show calls
F5sofia status查看 sofia 状态
F6reloadxml重新加载配置
F7console loglevel 0
F8console loglevel 7
F9sofia status profile internal查看 profile 信息
F10sofia profile internal siptrace on
F11sofia profile internal siptrace off
F12version

查看 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 模式。效果如图:

 title=

更改用户在布局中的位置
# 命令格式:conference <会议号> vid-layer <会议中的用户号> <画布中的位置>

conference 3500 vid-layer 27 3

具体步骤解释如下:

  1. 先查出会议房间 3500 中的所有用户。(这里有两个用户 27 与 28)
  2. 设置用户 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> 

效果如图:

 title=

给会议室人员指定所在画布

画布配置方案请参考 "多画布配置"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

效果如图:

image

conference 3500 vid-watching-canvas 2 2

效果如图:

image

让指定人员在会议中对某人失聪或发言

官方解释:

  1. Mute or Deaf a specific member to another member

    ChatGPT 翻译:意思是将某个成员对另一个成员的声音或麦克风静音或关闭。

  2. 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

    1. nohear:member_id 将不再听到 other_member_id 的声音,也就是说,member_id 的听觉将被静音,以防止听到 other_member_id 的声音。换句话说,这是将一个会议成员静音,以使其无法听到另一个指定的会议成员的声音。
    2. nospeak:这句话的意思是,指定的参会者(member_id)不能再向被关联的参会者(other_member_id)说话。也就是说,该参会者的麦克风将被静音,无法在会议中向该被关联的参会者发言。
    3. 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'

效果如图:

image

拉流
# 语法: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 &lt;call_url&gt; &lt;exten&gt;|&amp;&lt;application_name&gt;(&lt;app_args&gt;) [&lt;dialplan&gt;] [&lt;context&gt;] [&lt;cid_name&gt;] [&lt;cid_num&gt;] [&lt;timeout_sec&gt;]

在使用过程中,call url [呼叫字符串] 常用常忘,这里就简单记录下常用的几种呼叫字符串,以示提醒(示例中 800000 是一个租户的 profile 名称):

  1. user/1000@default:其中 @default 可以省略,多租户情况不能省略。如:user/1000@800000,此时呼叫分机 1000
  2. group/sales@default:其中 @default 可以省略,多租户情况不能省略。如:group/sales@800000 , 此时呼叫全组,同时振铃,接听一个之后停止
  3. sofia/profile/1000:其中 profile 名称可以在 sip\_profiles 中找到,1000 是 profile 中的一个分机。 如:sofia/800000/1000,此时呼叫 800000 租户的 1000 分机
  4. sofia/gateway/***/159****:通过网关外呼电话

其它参数解析如下:

  1. exten:dialplan 中的一个规则号码
  2. application_name:application 的名称,可以使用 show application 查看所有的应用
  3. app_args:应用所需的参数,可以参考 show application
  4. dialplan:默认 XML
  5. context:dialplan 中的上下文 ,多租户情况中可以理解为租户号
  6. cid_name:call url 接通时,看到的主叫名。默认为:空
  7. cid_num:call url 接通时,看到的主叫号码。默认为:0000000000
  8. 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 字段,如图:

image

设置超时时间

这个超时不是指对方不接听的超时,而是对方不回复 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
参考文章
  1. freeswitch 基本技能——调试、抓包分析、originate 命令、更改主叫号码、呼叫流程 等
  2. FreeSwitch Originate API

重新加载 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 穿越穿透。

  1. 路由器去掉 upnp 和 alg 功能。服务器的路由本身没进行 UPNP,没打开 NAT ALG

    阿里云服务器都没有!除非你自己去安装

  2. 修改配置文件 /freeswitch/sip_profiles/internal.xml

    1. 在 FS 端开启 rport 功能,这个配置默认是被注释掉了

      不开启的话,默认是从 Contact 头部字段中取对方地址的,这可能会导致 IP 和端口是对方在其内网中而不是其外网的,就送不到了。

      <!-- 默认注释的,得开启 -->
      <param name="NDLB-force-rport" value="true"/>
    2. 设置 rtp 自动调整功能

      这个配置默认被注释掉了,而且原来设置的是 true

      <!-- 开启,并改为false -->
      <!-- <param name="disable-rtp-auto-adjust" value="true"/> -->
      <param name="disable-rtp-auto-adjust" value="false"/>
    3. 设置 sip 和 rtp 的外网地址

      <param name="ext-rtp-ip" value="autonat:公网IP"/>
      <param name="ext-sip-ip" value="autonat:公网IP "/>
    4. 设置 acl 参数,以此来判断内外网呼叫

      <param name="local-network-acl" value="lan"/>
  3. acl 中配置 lan

    路径: /freeswitch/autoload_configs/acl.conf.xml

    <list name="lan"default="deny"/>
        <node type="allow"cidr="172.16.19.0/24" />
    </list>
  4. 在开放 FreeSWITCH 的 sip 端口和 rtp 端口(安全组和防火墙都需要设置开放相应的端口)

    实际上只开放 Profile 监听端口就行了,如 5060、5080; rtp 端口会自动 nat

  5. 针对没有 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 会话的建立。

  6. FreeSWITCH 用以下命令启动

    freeswitch -nc -nonat

配置 WebSocket

因为 WebRTC 需要 https ,对应的 WebSocket 也要 SSL 。freeSWITCH 支持 SSL 但默认没打开。

注意:先别停服务器!!!

wss 配置如下:

  1. /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"/>
  2. /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
  3. 局域网使用时得做的配置(不过笔者没做也可以用,蛮记录下)

    编辑 /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"/>
  4. 先重新加载 xml

    # 重新加载配置
    reloadxml
  5. 然后重启

    # 停止 freeswitch 
    freeswitch -stop
    
    # 启动 freeswitch 
    freeswitch -nc
  6. 最后进入 fs_cli 检查 wss 是否开启

    image

用户配置

创建新用户

在 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 中首先编译中文模块。

编译模块

  1. 在编译(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
  2. 补救方式,编译中文 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​ ,若无法播放声音,请仔细核对目录是否配置正确!!!

  1. FreeSWITCH 中文语音包(第三方:这么甜美的声音据说是某地 10086 的小姑娘)
  2. 官方 1
  3. 官方 2

创建 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

编译安装

  1. 在编译(configure)之前,编辑 modules.conf , 取消 mod_tts_commandline 这行的注释

    #asr_tts/mod_cepstral
    asr_tts/mod_flite
    #asr_tts/mod_pocketsphinx
    asr_tts/mod_tts_commandline
  2. 编译中文 mod_tts_commandline 模块

    # 模块存放路径 /freeswitch/src/mod/asr_tts/mod_tts_commandline
    cd /freeswitch/src/mod/asr_tts/mod_tts_commandline
    make && make install
  3. 在 FreeSWITCH 控制台上加载该模块

    load mod_tts_commandline
  4. 若想 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 实现。

  1. 首先,去 Ekho 官网下载源码

    注意:最新版,请去官网下载!!!(编辑时间 2023 年 2 月 24 日 11:19:45)

  2. 安装 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
编译错误
  1. checking for gcc... no 或 checking for cc... no 或 checking for cl.exe... no

    sudo apt-get install build-essential
  2. 在编译 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
  3. 在编译 espeak-ng 时发生错误:./autogen.sh: 14: ./autogen.sh: aclocal: not found

    sudo apt-get install make autoconf automake libtool pkg-config
  4. 在编译 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

  1. 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>
  2. 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>

处理逻辑如下:

  1. FreeSWITCH 收到呼叫,进入 dialplan。
  2. 执行 speak 功能,speak 功能是 FreeSWITCH 内置函数,代码在 switch_ivr_play_say.c 文件。
  3. speak 的 data 属性中,使用 tts_commandline 接口实现 TTS 功能。
  4. 在 tts_commandline 中调用 tts_commandline.conf.xml 配置的 command 生成语音文件。
  5. command 命令中,使用 ekho TTS 引擎生成语音文件。
  6. speak 播放 tts_commandline 生成的语音文件。

其他

  1. Freeswitch 智能语音开发之 TTS
  2. FreeSWITCH 折腾笔记 4——自己做一个 TTS 服务器

音视频会议

知识科普

在 1 对 1 通信中,WebRTC 首先尝试两个终端之间是否可以通过 P2P 直接进行通信,如果无法直接通信的话,则会通过 STUN/TURN 服务器进行中转,如下图:

 title=

如果你想要通过 WebRTC 实现多对多通信,该如何做呢?

其实,基于 WebRTC 的多对多实时通信的开源项目也有很多,综合来看,多方通信架构无外乎以下三种方案:

  • Mesh 方案:即多个终端之间两两进行连接,形成一个网状结构。比如 A、B、C 三个终端进行多对多通信,当 A 想要共享媒体(比如音频、视频)时,它需要分别向 B 和 C 发送数据。同样的道理,B 想要共享媒体,就需要分别向 A、C 发送数据,依次类推。这种方案对各终端的带宽要求比较高

     title=

    在上图中,B1、B2、B3、B4 分别表示 4 个浏览器,它们之间两两相连,同时还分别与 STUN/TURN 服务器进行连接(此时的 STUN/TURN 服务器不能进行数据中转,否则情况会变得非常复杂),这样就形成了一个网格拓扑结构。

    当某个浏览器想要共享它的音视频流时,它会将共享的媒体流分别发送给其他 3 个浏览器,这样就实现了多人通信。

    这种结构的优势有:

    1. 不需要服务器中转数据,STUN/TUTN 只是负责 NAT 穿越,这样利用现有 WebRTC 通信模型就可以实现,而不需要开发媒体服务器。
    2. 充分利用了客户端的带宽资源。
    3. 节省了服务器资源,因为服务器带宽往往是专线,价格昂贵,所以这种方案可以很好地控制成本。

    当然,有优势自然也有不足之处,主要表现在:

    1. 共享端共享媒体流的时候,需要给每一个参与人都转发一份媒体流,这样对上行带宽的占用很大。参与人越多,占用的带宽就越大。除此之外,对 CPU、Memory 等资源也是极大的考验。一般来说,客户端的机器资源、带宽资源往往是有限的,资源占用和参与人数是线性相关的。这样导致多人通信的规模非常有限,通过实践来看,这种方案在超过 4 个人时,就会有非常大的问题。
    2. 另一方面,在多人通信时,如果有部分人不能实现 NAT 穿越,但还想让这些人与其他人互通,就显得很麻烦,需要做出更多的可靠性设计。
  • MCU:该方案由一个服务器和多个终端组成一个星形结构。各终端将自己要共享的音视频流发送给服务器,服务器端会将在同一个房间中的所有终端的音视频流进行混合,最终生成一个混合后的音视频流再发给各个终端,这样各终端就可以看到 / 听到其他终端的音视频了。实际上服务器端就是一个音视频混合器,这种方案服务器的压力会非常大

     title=

    MCU 主要的处理逻辑是:接收每个共享端的音视频流,经过解码、与其他解码后的音视频进行混流、重新编码,之后再将混好的音视频流发送给房间里的所有人。

    我们来假设一个条件,B1 与 B2 同时共享音视频流,它们首先将流推送给 MCU 服务器,MCU 服务器收到两路流后,分别将两路流进行解码,之后将解码后的两路流进行混流,然后再编码,编码后的流数据再分发给 B3 和 B4。

    对于 B1 来说,因为它是其中的一个共享者,所以 MCU 给它推的是没有混合它的共享流的媒体流,在这个例子中就是直接推 B2 的流给它。同理,对于 B2 来说 MCU 给它发的是 B1 的共享流。但如果有更多的人共享音视频流,那情况就更加复杂。

    MCU 主要的处理逻辑如下:

    image

    那 MCU 的优势有哪些呢?大致可总结为如下几点:

    1. 技术非常成熟,在硬件视频会议中应用非常广泛。
    2. 作为音视频网关,通过解码、再编码可以屏蔽不同编解码设备的差异化,满足更多客户的集成需求,提升用户体验和产品竞争力。
    3. 将多路视频混合成一路,所有参与人看到的是相同的画面,客户体验非常好。

    同样,MCU 也有一些不足,主要表现为:

    1. 重新解码、编码、混流,需要大量的运算,对 CPU 资源的消耗很大。
    2. 重新解码、编码、混流还会带来延迟。
    3. 由于机器资源耗费很大,所以 MCU 所提供的容量有限,一般十几路视频就是上限了。
  • SFU:该方案也是由一个服务器和多个终端组成,但与 MCU 不同的是,SFU 不对音视频进行混流,收到某个终端共享的音视频流后,就直接将该音视频流转发给房间内的其他终端。它实际上就是一个音视频路由转发器。但可以实现服务端视频录制。

     title=

    在这个图中,B1、B2、B3、B4 分别代表 4 个浏览器,每一个浏览器都会共享一路流发给 SFU,SFU 会将每一路流转发给共享者之外的 3 个浏览器。

    下面这张图是从 SFU 服务器的角度展示的功能示意图:

     title=

    相比 MCU,SFU 在结构上显得简单很多,只是接收流然后转发给其他人。然而,这个简单结构也给音视频传输带来了很多便利。比如,SFU 可以根据终端下行网络状况做一些流控,可以根据当前带宽情况、网络延时情况,选择性地丢弃一些媒体数据,保证通信的连续性。

    目前许多 SFU 实现都支持 SVC 模式和 Simulcast 模式,用于适配 WiFi、4G 等不同网络状况,以及 Phone、Pad、PC 等不同终端设备。

    SFU 的优势有哪些:

    1. 首先 由于是数据包直接转发,不需要编码、解码,对 CPU 资源消耗很小。
    2. 其次是 直接转发也极大地降低了延迟,提高了实时性。
    3. 最后 带来了很大的灵活性,能够更好地适应不同的网络状况和终端类型。

    同样,SFU 有优势,也有不足,主要表现是:

    1. 由于是数据包直接转发,参与人观看多路视频的时候可能会出现不同步;相同的视频流,不同的参与人看到的画面也可能不一致。
    2. 参与人同时观看多路视频,在多路视频窗口显示、渲染等会带来很多麻烦,尤其对多人实时通信进行录制,多路流也会带来很多回放的困难。总之,整体在通用性、一致性方面比较差。

总结如下:

整体来看,由于各方面限制,Mesh 架构在真实的应用场景中几乎没有人使用,一般刚学习 WebRTC 会考虑使用这种架构来实现多方通信。

MCU 架构是非常成熟的技术,在硬件视频会议中应用非常广泛。像很多做音视频会议的公司之前都会购买一套 MCU 设备,这样一套设备价格不菲,最低都要 50 万,但随着互联网的发展以及音视频技术越来越成熟,硬件 MCU 已经逐步被淘汰。

当然现在也还有公司在使用软 MCU,比较有名的项目是 FreeSWITCH,但正如我们前面所分析的那样,由于 MCU 要进行解码、混流、重新编码的操作,这些操作对 CPU 消耗是巨大的。如果用硬件 MCU 还好,但软 MCU 这个劣势就很明显了,所以真正使用软 MCU 架构方案的公司也不多。

SFU 是最近几年流行的新架构,目前 WebRTC 多方通信媒体服务器都是 SFU 架构。从上面的介绍中你也可以了解到 SFU 这种架构非常灵活,性能也非常高,再配上视频的 Simulcast 模式或 SVC 模式,则使它更加如虎添翼,因此各个公司目前基本上都使用该方案。

开源方案:

​​image​​

会议配置

简单介绍

FreeSwitch 默认支持会议功能,有如下特点:

  • 不需要创建一个会议室的操作,只需要通过 conference 拨码计划就可以实现;
  • 会议室不真正存在, 直到有人呼入为止;
  • 会议功能很强大,能实现灵活控制。

会议基础步骤:

  1. 运行 FreeSWITCH 服务器程序;
  2. 注册 1000、1001、1002 三部 IP 话机;
  3. 通过 1000 呼叫 3000,通话建立后, 1000 将听到一段保持音乐;
  4. 通过 1001 呼叫 3000,通话建立后, 1001 将能听到 1000 的声音,1000 也能听到 1001 的声音;
  5. 通过 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>
会议密码 && 主持人

可以设置主持人以及会议密码。设置了主持人后,可以影响会议的开展;设置了会议密码后,与会成员必须输入正确密码才能入会。

主持人对会议的影响主要体现在以下两个方面:

  1. 直到主持人入会后,会议才开始;
  2. 主持人退出会议后,会议才结束。
设置会议主持人
<!-- 未设置主持人 -->
<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 的其他样式。

 title=

布局方式参考:

<!-- 配置文件位置:/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>

布局样式

  1. 1up_top_left+5

     title=

  2. 1up_top_left+7

     title=

  3. 1up_top_left+9

     title=

  4. 1x1

     title=

  5. 1x2

     title=

  6. 2up_bottom+8

     title=

  7. 2up_middle+8

    ​​ title=

  8. ​2up_top+8

    ​​ title=​​

  9. 2x1

     title=

  10. 2x2

     title=

  11. 3up+4

     title=

  12. ​3up+9

     title=

  13. ​3x3

     title=

  14. ​4x4

     title=

  15. 5x5

     title=

  16. 6x6

     title=

  17. 8x8

     title=

  18. ​overlaps

     title=

  19. presenter-dual-horizontal

     title=

  20. presenter-dual-vertical

     title=

  21. presenter-overlap-large-bot-right

     title=

  22. presenter-overlap-large-top-right

     title=

  23. presenter-overlap-small-bot-right

     title=

  24. presenter-overlap-small-top-right

     title=

屏蔽语音激励设置主画面

由于 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 混流)为例。

  1. 首先找到 /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>
  2. 由拨号计划得知会议配置方案为 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>
  3. 使用 reloadxml​​ 命令重新加载配置

向会议朗读文本

  1. 安装 TTS 引擎,参考:"TTS 引擎"5
  2. 配置 ​​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>
  3. 测试

    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 消息体不做修改,没有录音,二次拨号等功能
更像是一个信令代理,性能最高,但提供的功能有限

模式配置方式:

  1. 代理模式(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"/>
  2. 旁路模式(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"/> 

注:

  1. RECORD_TITLE、RECORD_COPYRIGHT、RECORD_SOFTWARE、RECORD_ARTIST、RECORD_COMMENT、RECORD_DATE 这些信息设置后会写到文件标头里,需要使用 MediaInfo 软件才能看到

    image

  2. 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>

效果如图:

​​image

视频会议

这里只做 mod_av 的介绍。

以 MCU 会议 3500 为例。

API 方式
拉流
conference 3500 play av://rtmp://172.16.30.53/20230309/screen async

注意:如果会议进来一个人或者退出一个人的情况下,该拉取流就消失了....

效果如下:

image

推流
conference 3500 record rtmp://172.16.30.53/live/3500

效果如下:

这是 FreeSWITCH 从 RTMP(Monibuca GO 媒体服务器) 拉流进行 MCU 后再推流用 VLC 播放器播放的延时情况。

image

配置方式

路径:/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>

效果如下:

  1. 本地录制

    image

    image

  2. 推送到第三方平台

    ​​image​​

使用 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 启动时自行创建。这里只建了一个空库。

  1. 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>
  2. 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" /> -->
  3. 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"/>-->
  4. 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>
  5. 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;" />
  6. 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;" />
  7. 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;" />
  8. 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;" />
  9. 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>
  10. 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>
  11. 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:数据密码
  1. 编辑 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>
  2. 编辑 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"/>
  3. 编辑 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"/>-->
  4. 编辑 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>
  5. 编辑 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"/>
  6. 编辑 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"/>
  7. 编辑 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"/>
  8. 编辑 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"/>
  9. 编辑 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>
  10. 编辑 vars.xml

    路径:/freeswitch/vars.xml

    <!-- 加入如下配置 -->
    <X-PRE-PROCESS cmd="set" data="json_db_handle=odbc://FreeSWITCH:root:123456"/>
  11. 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 基本流程就完成了。

注意事项

  1. 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"

解决方法:

  1. 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>
  2. 重新加载

    fs_cli
    reload mod_event_socket

Java 调用

参考开源项目 freeswitch-esl-all

功能特性:

  1. 支持连接 FreeSWITCH 大规模集群
  2. 更易于集成使用
  3. 与 spring boot 2.x 深度整合,提供 starter
  4. 可动态配置

使用说明:

  1. 获取实例

    // 方式一
    InboundClient.getInstance()
    
    // 方式二(Spring 注入)
    @Autowired
    private InboundClient inboundClient;
  2. 可动态配置添加或删除远端地址

    • 添加远端地址

      // 方式一
      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);
  3. 服务端连接监听器

    // ServerConnectionListener 接口
    
    inboundClient.option().serverConnectionListener(serverConnectionListenerImpl);
    
    // 两个方法
    void onOpened(ServerOption serverOption);
    void onClosed(ServerOption serverOption);
  4. 配置动态获取或者删除

    // 实现接口
    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 库。

异常解决
  1. 错误提示:You must install libpng-dev to build ....
  2. 视频画面没有显示字体,只有视频标签背景色

    有人回答,需要使用 使用 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

其他问题

  1. 错误提示: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

编译错误解决:

  1. ERROR: flex is not installed.

    解决方案:

    apt-get install -y gettext flex
  2. 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

编译错误解决:

  1. 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

编译错误解决:

  1. libtool not found. You need libtool version 1.5.14 or newer to build FreeSWITCH from source.

    apt install -y libtool libtool-bin
  2. 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
  1. 去 FreeSWITCH 的官⽹申请 token

    官⽹地址:https://voice9.signalwire.com/dashboard

    # 将⽤户名和token写⼊⽂件
    
    echo "voice9" > /etc/yum/vars/signalwireusername
    echo "PT699d3a10bf366b0294d4b8e318ff5885df92dae5beed5dcd" > /etc/yum/vars/signalwiretoken
  2. 更新系统镜像源

    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
  3. 安装 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
  4. 安装 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
  5. 安装 libks

    cd /usr/local/src/
    git clone https://github.com/signalwire/libks.git
    cd libks
    cmake .
    make && make install
  6. 安装 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
  7. 安装 x264

    cd /usr/local/src/
    git clone http://git.videolan.org/git/x264.git
    cd x264
    ./configure --disable-asm
    make && make install
  8. 安装 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
  9. 安装 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/
  10. 安装 opus

    git clone https://freeswitch.org/stash/scm/sd/opus.git
    cd opus
    ./autogen.sh
    ./configure --libdir=$PWD/tmp
    make && make install
  11. 安装 sofia-sip

    git clone https://github.com/freeswitch/sofia-sip
    cd sofia-sip
    ./bootstrap.sh
    ./configure
    make && make install
  12. 安装 spandsp

    git clone https://github.com/freeswitch/spandsp
    cd spandsp
    ./bootstrap.sh
    ./configure
    make && make install
    export PKG_CONFIG_PATH=/usr/local/lib/pkgconfig
  13. 安装 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
  14. 安装 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

笔者没搞定,可自行参考以下几篇文章:

  1. softswitch-gateway
  2. FreeSWITCH 折腾笔记 8——使用 OpenSIPS 进行负载均衡
  3. Opensips + FreeSwitch 负载均衡

POC 对讲

前言

POC(PTT Over Cell 的简写),PTT 是按下讲话的意思,要么说,要么听别人说,不会出现同时讲话和听的情况,所以类似半双工的通信方式。

Janus

以下内容摘自呱牛大佬的 使用 Janus 作为对讲服务器的后台框架和业务流程基于Janus房间服务器的POC对讲实现 两篇笔记。

 title=

通过 Nginx 负载 Janus http 服务器的 API 接口,通过该 API 接口可以获取可用 Janus 服务器的 IP 和端口;客户端拿到可用对讲服务器的 IP 和端口后,通过 WebSocket 连接到该服务器,并保持长连接,客户端进入会议室时,则复用这条长连接;如果进入其他会议室,则需要重新获取可用的 Janus 服务器 IP 和端口,并重复上述过程;多个对讲服务器之间通过 RabbitMQ 共享数据,三个服务器之间对等对外提供服务。

客户端加入会议室后,使用两个 peerconnection 分别用来做 publisher 角色和 subscriber 角色类型通信,publisher 主要用来讲话的通道,subscriber 则主要用来听对讲的通道;每次讲话前都需要申请 TBCP 讲话权限,获取成功,则开启录音,并发送数据,否则不开启录音;如果其他人在讲话,则打开播放器,开始播放声音,主要业务流程如下图:

 title=

详细的 对讲加入群组主要业务流程 如下图所示:

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
  1. 创建 session

    {"janus" : "create", "apisecret":"123456", "transaction" :"1kqs0K1uzid9"}
  2. 绑定插件(同 19)

    {"janus":"attach","plugin":"janus.plugin.pooroom","apisecret":"123456","session_id":1390233816776651,"transaction":"H4zNCmMmnFDv"}
  3. 创建房间

    {"janus":"message","apisecret":"123456","body":{"request":"create","room":1542076668513,"secret":"1234","pin":"4321","display":"600555","permanent":false,"is_private":false}}
  4. 加入房间,publisher 角色作为推流端

    {"janus":"message","apisecret":"123456","body":{"request":"join","ptype":"publisher","room":1542076668513,"pin":"4321","display":"600555"}}
  5. 配置房间

    {"apisecret":"123456","body":{"audio":true,"audiocodec":"opus","data":true,"request":"configure","video":false},"janus":"message","jsep":{"sdp":"...."}}
  6. 发送本端的 ICB 地址 (同 25)

    {"janus":"message", "apisecret":"123456","candioate":{"candidate":"candidate:...."}}
  7. 加入房间,subscriber 角色是作为拉流端

    {"janus":"message","apisecret":"123456","body":{"request":"join","ptype":"subscriber","room":1542076668513,"pin":"xdja_4321","display":"600555"}}
  8. 发送本端 sdp

    {"janus":"message","apisecret":"xdja_api_abc","body":{"request":"start","room":1542076668513},"jsep":{"sdp":"v-0\r\no..."}}
  9. 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 的业务对讲能力;

最后实现:

  1. 给房间增加一个公共的 publish 对象,所有对房间的订阅都是订阅该 publish 对象,达到支持 从 SFU 模式到支持 MCU 转发模式(适应 POC 对讲模式的 MCU 模式)
  2. 支持会场 TBCP 控制信息通过 DataChannel 通道传递
  3. 支持会场 TBCP 控制和会场通知 ;

部分逻辑:

  1. 给 janus_videoroom 结构体里面增加一个 janus_videoroom_publisher *room_publisher;
  2. 在创建房间成功后,既实例化这个对象 room_publisher;
  3. 初始进入房间或者枚举房间的 publisher 的时候,只返回这个对象即可,不返回 room 结构体里面的 participants 列表中的用户 id,这样每个 subscriber 都对应的是这个 publisher 对象;
  4. 然后在媒体包过来的时候,枚举 room_publisher 中的 subscriber,逐个分发即可。

修改 Janus 服务器,支持 datachannel 能力:Janus 的 datachannel 支持的协议主要是:DTLS/SCTP​​ 、UDP/DTLS/SCTP​​,而 RTP 通道使用的是 UDP/TLS/RTP/SAVPF​​,修改的思路包括 SDP 返回的修改,RTP 数据包中提取 datachannel 包对应 SSRC 的包。

  1. 给 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);
    }
  2. 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));
  3. 接收 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;
    }
  4. 发送 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));

广播方式

以下内容摘自呱牛大佬的 音频广播播放功能实现逻辑分享 笔记。

广播的业务还是挺好实现的,但业务链条比较长,作为练手项目绝对不错,主要涉及到几个点:

  1. 音频数据采集;
  2. 音频数据编码;
  3. 媒体流组包;
  4. (组播)UDP Socket 服务器和客户端,socket 接收和发送实现;
  5. 音频抖动缓冲区,及音频播放器实现;

 title=

从业务层面看,每个广播都可以通过广播按钮给同组的其他广播喊话。

从网络层面来看,每个广播都有一个信令监听端口,监听广播发起、广播结束的通知。

被喊话的广播接收到喊话通知后,开一个 udp 端口,接收广播的音频数据包。

 title=

录制和播放器参考 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, &param);
    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(&param, &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, &param);
    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(&param, 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 地址的设定:

  1. 按”Menu 键” -> Settings -> Advanced(密码:admin)-> Network -> WAN Port -> Static IP
  2. 在相应的区域中分别输入 IP 地址、子网掩码、默认网关、首选 DNS 服务器(参照相同网段内任意一台 PC)
  3. 按”OK 键”保存

后台配置

默认账号密码都是 admin 。

​​image

其他

sip-hub

sip-hub 是一个专注 sip 信令的搜索以及时序图可视化展示的服务。

相比于 Homer, sip-hub 做了大量的功能简化。同时也提供了一些个性化的查询,例如被叫后缀查询,仅域名查询等等。

做 sip-hub 的原因是 homer 太难用了,经常查不到想查的数据,查询的速度也蛮。

sip-hub 服务仅有 3 个页面:

  • sip 消息搜索页面,用于按照主被叫、域名和时间范围搜索呼叫记录
  • 时序图展示页面,用于展示 SIP 时序图和原始 SIP 消息
  • 可以导入导出 SIP 消息
  • 可以查找 A-Leg
  • 监控功能

相关截图如下:

image

安装 sip-hub :

  1. 首先需要安装 MySql 数据库,并在其中建立一个名为 siphub 的数据库
  2. 运行

    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 客户端)。

以下是完整的架构:

。我认为工作流程应该是这样的:

  1. Chrome A 将 WebRTC 流推送到 SRS。
  2. SRS 通过 HTTP 回调调用 FS HTTP 服务器。
  3. FS 通过 WHIP 从 SRS 中提取 WebRTC 流。
  4. FS 向 SRS 发布一个混合流,其中包含 WebRTC 客户端和 SIP 客户端。
  5. Chrome A 从 SRS 中提取混合流。

除了这个解决方案,SRS 还可以通过 WHIP 将 WebRTC 流转发或推送到 FS,工作流应该是:

  1. Chrome A 将 WebRTC 流推送到 SRS。
  2. SIP 客户端连接到 FS,并将此事件通知 HTTP 服务器。
  3. HTTP 服务器通知 SRS 开始转发。
  4. 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 出来,这就是连麦的直播流了。

一些项目

  1. contact-center

    智能呼叫中心系统 voip、webrtc 。支持功能点:

    • 支持 sdk 接入语音平台
    • 支持 webrtc
    • 支持 rest 接口接入语音平台
    • 支持 http ivr 接入
    • 支持 http NLP 响应
  2. SoftSwitch-Gateway

    基于 netty 4 对接的 Opensips Exported Event、Exported MI 和 FreeSWITCH Event Socket Library 、cdr 、xml_curl 等接口实现方案。

  3. OV500

    OV500 是一个开源软件解决方案,旨在为 Kamailio 和 FreeSWITCH 提供 VoIP 计费、交换和评级功能。

    OV500 提供实时计费和评级、自动路由、动态负载平衡、高级报告和分析以及欺诈防范机制等功能。它可以被 VoIP 服务提供商、运营商和企业用于管理他们的语音流量和计费业务。

一些文章

  1. 《FreeSWITCH 权威指南》
  2. 怎么能少了 FreeSWITCH 官网呢?
  3. 音视频开发之路
  4. qzlink 大佬的 FreeSWITCH 专栏
  5. Younian 大佬的呼叫中心专栏
  6. 洞香春 大佬的博客
  7. Java_lilin 大佬的专栏
  8. FreeSwitch+Opensips+NFS 文件共享集群安装配置操作指南(百度文库,收费)
  9. softswitch-gateway(这里的文档蛮有价值的)
  10. 菩提树下的杨过 博客
  11. 杨虎成博客
  12. _Ong 博客
  13. 呱牛笔记 (改源码的大佬)
  14. 贾宝玉的玉宝贾的 CSDN 专栏(专栏收费)
  15. GUI 的专栏
  16. Freeswitch 源码分析
  17. FreeSWITCH 之视频录像
  18. 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>"/>

  1. ### 多画布配置
  2. ### 知识科普
  3. ### 向会议朗读文本
  4. ## 服务器部署语音不通
  5. ## TTS 引擎
  6. * ① execute_on_answer 使用示例
  7. * ② api_on_answer 使用示例
  8. * ① min-required-recording-participants 使用示例
  9. * ② auto-record 使用示例
  10. #### 配置方式
posted @ 2023-03-30 10:09:00 猎隼丶止戈 阅读(76) 评论(0)
发表评论
昵称
邮箱
网址