esl-client 2.0.0 发布:Java 项目接入 FreeSWITCH,从引入到使用

做呼叫中心、电话通知或语音机器人时,经常会遇到这些需求:

  • 电话接通、挂断后,通知业务系统更新状态。
  • 在 Java 中查询通话、发起呼叫、结束通话。
  • 接听来电、播放提示音,再根据用户按键执行不同操作。

esl-client 就是 Java 程序与 FreeSWITCH 之间的连接工具。2.0.0 已发布到 Maven 仓库,普通 Java 项目和 Spring Boot 项目都可以接入。

下面直接看怎么用。

一、先选对接入方式

你的需求 使用方式
监听接通、挂断事件,查询状态、发送命令 Inbound:Java 主动连接 FreeSWITCH
把某通电话交给 Java,执行接听、放音、收号等流程 Outbound:FreeSWITCH 将呼叫连接到 Java 服务

这里的 Inbound、Outbound 指的是 ESL 连接方向,不是电话的呼入、呼出方向。

运行环境:Java 17+ 。本文的 Spring Boot Starter 示例面向 Spring Boot 4.x。

二、Maven 项目怎么引入

普通 Java 项目,在 pom.xml 的 <dependencies> 中添加:

xml 复制代码
<dependency>
    <groupId>io.github.xxd763795151</groupId>
    <artifactId>esl-client</artifactId>
    <version>2.0.0</version>
</dependency>

Spring Boot 4.x 项目添加 Starter 即可,它已经包含核心库:

xml 复制代码
<dependency>
    <groupId>io.github.xxd763795151</groupId>
    <artifactId>esl-client-spring-boot-starter</artifactId>
    <version>2.0.0</version>
</dependency>

2.0.0 与 1.x 的 API 不兼容,老项目升级时需要调整调用代码。

三、连接前,先准备 FreeSWITCH

使用 Inbound 时,需要启用 FreeSWITCH 的 mod_event_socket,并确认三个信息:

  • FreeSWITCH 的 IP。
  • ESL 端口,通常为 8021。
  • ESL 密码。

下面是 Java 与 FreeSWITCH 在同一台机器上测试时的配置示例,文件为 FreeSWITCH 配置目录下的 autoload_configs/event_socket.conf.xml:

ini 复制代码
<configuration name="event_socket.conf" description="Socket Client">
    <settings>
        <param name="listen-ip" value="127.0.0.1"/>
        <param name="listen-port" value="8021"/>
        <param name="password" value="your-esl-password"/>
    </settings>
</configuration>

如果部署在不同机器上,需要把监听地址改成 FreeSWITCH 可访问的内网地址,并让 ACL 和防火墙允许 Java 服务访问。相关配置可参考 FreeSWITCH 官方文档。

下面示例中的地址、密码,都要替换成自己的实际配置。

四、非 Spring Boot 项目:连接、查状态、监听通话

适合普通 Java 程序、后台服务,以及自己管理客户端生命周期的项目。

新建 EslDemo.java:

csharp 复制代码
import io.github.xxd763795151.esl.event.EventSelector;
import io.github.xxd763795151.esl.inbound.InboundClient;

public class EslDemo {

    public static void main(String[] args) throws Exception {
        try (InboundClient client = InboundClient.builder()
                .address("127.0.0.1", 8021)
                .password("your-esl-password")
                .events("CHANNEL_ANSWER", "CHANNEL_HANGUP_COMPLETE")
                .build()) {

            client.addEventListener(
                    EventSelector.named(
                            "CHANNEL_ANSWER",
                            "CHANNEL_HANGUP_COMPLETE"),
                    event -> System.out.printf(
                            "事件=%s,通话ID=%s,主叫=%s%n",
                            event.name(),
                            event.uniqueId(),
                            event.header("Caller-Caller-ID-Number"))
            );

            client.connect().join();

            String status = client.api("status").join().body();
            System.out.println(status);

            // 演示监听 60 秒,期间可以拨打一通测试电话。
            Thread.sleep(60_000);
        }
    }
}

运行后,先看到 FreeSWITCH 状态;测试电话接通、挂断时,会打印对应事件。

这里的 .join() 表示等待操作完成,方便按顺序编写代码。

实际服务中,创建并复用一个长连接客户端,在应用退出时关闭,不用每发一条命令就重新连接。上面的 60 秒等待仅用于演示。

连接成功后,还能做什么?

以下代码放在客户端连接成功之后,根据业务需要调用:

csharp 复制代码
// 查询当前通话数量
String count = client.api("show", "calls count").join().body();

// 呼叫已配置并注册的分机 1000,接听后保持通话
String result = client.bgapi(
        "originate", "user/1000 &park").join().body();

System.out.println(result);

已经拿到某通电话的 UUID,也可以操作这通电话:

scss 复制代码
// uuid 来自通话事件中的 event.uniqueId()
client.channel(uuid).playback("/opt/voice/welcome.wav").join();

// 结束这通电话
client.channel(uuid).hangup().join();

语音文件路径指向 FreeSWITCH 所在机器上的文件。示例中的分机、文件路径,都需要提前准备。

五、Spring Boot 项目:写配置、注入使用

适合已有 Spring Boot 业务系统,希望将通话事件接入订单、坐席、客户管理等业务的场景。

引入 Starter 后,只需要两步。

第一步:配置 application.yml

yaml 复制代码
esl:
  inbound:
    enabled: true
    addresses:
      - 127.0.0.1:8021
    password: your-esl-password
    events:
      - CHANNEL_ANSWER
      - CHANNEL_HANGUP_COMPLETE

    connect-timeout: 10s
    command-timeout: 30s

    reconnect:
      strategy: exponential
      initial-delay: 1s
      max-delay: 30s

第二步:编写业务组件

把下面的类放在 Spring Boot 启动类所在包或其子包中:

csharp 复制代码
import io.github.xxd763795151.esl.event.EslEvent;
import io.github.xxd763795151.esl.inbound.InboundClient;
import io.github.xxd763795151.esl.spring.EslEventListener;
import org.springframework.stereotype.Service;

@Service
public class CallService {

    private final InboundClient client;

    public CallService(InboundClient client) {
        this.client = client;
    }

    public String queryStatus() {
        return client.api("status").join().body();
    }

    @EslEventListener("CHANNEL_ANSWER")
    public void onAnswer(EslEvent event) {
        System.out.println("电话已接通:" + event.uniqueId());
        // 在这里更新业务状态
    }

    @EslEventListener("CHANNEL_HANGUP_COMPLETE")
    public void onHangup(EslEvent event) {
        System.out.printf("电话已挂断:%s,原因:%s%n",
                event.uniqueId(),
                event.header("Hangup-Cause"));
        // 在这里记录通话结果
    }
}

启动应用后,Starter 会管理连接和关闭流程;其他业务组件注入 CallService,即可调用 queryStatus()。

注意:监听注解与事件订阅要配套。 写了 @EslEventListener,还要在 YAML 的 events 中订阅对应事件。

六、想用 Java 编写电话流程?使用 Outbound

例如:用户拨打 9999,由 Java 接听、播放欢迎语,然后挂断。

在 Spring Boot 项目中,添加配置:

yaml 复制代码
esl:
  outbound:
    port: 8084
    shutdown-timeout: 30s

spring:
  lifecycle:
    timeout-per-shutdown-phase: 40s

如果已有 esl.inbound 配置,把 outbound 合并到同一个 esl 节点下。只做 Outbound 时,无须开启 Inbound。

然后添加呼叫处理器:

kotlin 复制代码
import io.github.xxd763795151.esl.outbound.OutboundSessionHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class IvrConfig {

    @Bean
    public OutboundSessionHandler ivrHandler() {
        return session -> {
            var call = session.channel();

            call.answer().join();
            call.playback("/opt/voice/welcome.wav").join();
            call.hangup().join();
        };
    }
}

存在这个 Bean 时,Starter 会启动 Outbound 服务。

再在 FreeSWITCH 对应的拨号方案中加入:

ini 复制代码
<extension name="java-ivr">
    <condition field="destination_number" expression="^9999$">
        <action application="socket"
                data="127.0.0.1:8084 async full"/>
    </condition>
</extension>

这里的 127.0.0.1:8084 是 Java 服务的地址。如果两者不在同一台机器上,要改为 FreeSWITCH 能访问到的 Java 服务 IP。

加载拨号方案后,从匹配该拨号方案的分机拨打 9999,即可测试。

需要收集按键时,可以在接听后加入:

vbnet 复制代码
// 需要导入 io.github.xxd763795151.esl.app.PlayAndGetDigits
String digit = call.playAndGetDigits(
        PlayAndGetDigits.builder("/opt/voice/menu.wav")
                .digits(1, 1)
                .regex("[1-2]")
                .build()
).join();

if ("1".equals(digit)) {
    call.playback("/opt/voice/business-hours.wav").join();
} else if ("2".equals(digit)) {
    call.bridge("user/1000").join();
}

提前准备提示音和分机,就可以扩展成"按 1 查询营业时间,按 2 转人工"的语音菜单。

非 Spring Boot 项目也能使用 Outbound,手动启动服务即可:

scss 复制代码
import io.github.xxd763795151.esl.outbound.OutboundServer;
import java.time.Duration;

public class IvrDemo {

    public static void main(String[] args) throws Exception {
        OutboundServer server = OutboundServer.builder()
                .port(8084)
                .handler(session -> {
                    var call = session.channel();
                    call.answer().join();
                    call.playback("/opt/voice/welcome.wav").join();
                    call.hangup().join();
                })
                .build();

        Runtime.getRuntime().addShutdownHook(new Thread(
                () -> server.shutdownGracefully(
                        Duration.ofSeconds(30)).join()));

        server.start().join();
        Thread.currentThread().join();
    }
}

FreeSWITCH 侧使用同样的拨号方案配置。

七、几个常用配置,按场景选择

以下属性位于 esl.inbound 下:

配置 用途
startup-wait: 10s 默认启动等待连接的时间;超时后允许应用继续启动
fail-fast: true 首次连接失败时,让应用启动失败
command-timeout: 30s 普通命令的回复超时时间
job-timeout: 5m bgapi 后台任务的等待超时时间
disconnected-command-policy: fail-fast 断线时,新命令直接失败,默认行为
disconnected-command-policy: queue-until-ready 断线时暂存新命令,等待恢复;仍受队列容量和超时限制

如果业务要求 FreeSWITCH 可用后才能提供服务,可以设置 fail-fast: true。

如果还有其他独立业务,希望 FreeSWITCH 暂时不可用时应用仍能启动,可以保留默认值,并在调用处处理连接不可用的情况。

需要同时连接多台 FreeSWITCH?

使用具名配置,每台一个客户端:

yaml 复制代码
esl:
  inbounds:
    fs1:
      addresses: ["10.0.1.10:8021"]
      password: password-for-fs1
      events: [CHANNEL_ANSWER, CHANNEL_HANGUP_COMPLETE]
    fs2:
      addresses: ["10.0.2.10:8021"]
      password: password-for-fs2
      events: [CHANNEL_ANSWER, CHANNEL_HANGUP_COMPLETE]

业务类通过构造器指定要使用的实例:

less 复制代码
public CallService(
        @Qualifier("fs1") InboundClient client) {
    this.client = client;
}

@Qualifier 的包名为 org.springframework.beans.factory.annotation.Qualifier。

只监听某台服务器时:

csharp 复制代码
@EslEventListener(value = "CHANNEL_ANSWER", client = "fs1")
public void onAnswer(EslEvent event) {
    System.out.println("fs1 电话接通:" + event.uniqueId());
}

同一个 addresses 中填写多个地址,用于重连时切换;同时连接多台服务器,要用 esl.inbounds。

相关推荐
牧瀬クリスだ1 小时前
Redis五大数据类型全解析:从入门到实战
java·数据库·redis·后端·缓存
二宝哥1 小时前
03.Spring AI Embedding 实战:从向量化原理到项目落地
人工智能·spring boot·向量·spring ai
Rain的Java大神之路2 小时前
北漂 13 年,我把 Java 家底钉成了这张知识图谱(全站免费)
java·后端·程序员
Amos_Web2 小时前
通过 Rspack 源码学 Rust(一):`mod`、`pub` 与 `use crate`/`use super`
后端
张彦峰ZYF2 小时前
拆得开不等于该拆开:微服务与模块化单体的边界判据、规模阈值、团队适配与一致性代价
后端·微服务·边界是否稳定·扩展天花板是否到顶·有无小组能独立值班·独立部署的自由会否被用上·这一刀是否切断不可降级的不变量
是江迪呀2 小时前
介绍一个开源项目 —— 烂笔头
前端·后端
Felaim2 小时前
【SLAM】多相机与动态场景 VSLAM 三篇调研:OpenMAVIS、cuVSLAM 与 DROID-W
开源·vslam·动态slam·光束法平差·多相机slam
captain3762 小时前
Spring Web MVC(2)
java·前端·spring boot·后端·spring·mvc
紧跟先前的步伐2 小时前
Go AI Agent 实战 17|模型适配层设计:让模型可替换
开发语言·人工智能·后端·ai·golang