简介:这是一套基于Asterisk开源PBX平台开发的Web呼叫中心系统,面向计算机专业本科生及高职学生,适用于毕业设计、课程设计、工程实训与学科竞赛等实践场景,重点解决电话自动化外呼(如电费/水费催缴、交通违法通知、物业提醒等八大业务类型)与Web端集成管理问题。资源包共1202个文件,含251个Java后端逻辑文件、137个JSP页面、128个JS交互脚本、156个CSS样式文件、172个PNG界面素材,以及99个WAV语音提示和87个VOX压缩语音资源,完整覆盖前后端、UI、音视频及配置文件,压缩包大小为26.43MB。已有77人学习下载,项目经严格测试可直接运行,答辩平均分达96分,附带完整源码、工程文件与说明文档,支持快速复现与二次扩展;预览可见JPlayer系列AS播放器组件及EasyUI前端框架,体现音视频集成与企业级Web交互设计能力,适合用作技术参考、报告撰写范本或功能模块拆解学习。
1. 这不是“用 Asterisk 搭个电话系统”,而是把语音通信能力真正嵌进 Web 页面的工程闭环
你手头有个毕设/课设/实训任务:做一个“呼叫中心项目,基于 Asterisk 开发的一套 web 项目”。别被标题里“web项目”四个字骗了——它不是让你在网页上放几个按钮、调个 AJAX 就完事。真实场景是:坐席在 Chrome 里点一下“接听”,耳机立刻响铃;客户拨号后,Web 页面实时显示主叫号码、通话时长、当前状态(振铃/接通/挂断);通话中能点击“转接”“静音”“保持”,动作毫秒级同步到 Asterisk 核心;通话结束后,自动弹出工单填写框,录音文件已存好、可直接播放下载。这背后不是“前后端分离”的简单概念,而是 WebRTC 信令桥接、AMI 事件监听、AGI/ARI 脚本调度、SIP 协议栈穿透、跨域音频设备管理五层能力的咬合。适合两类人:一是需要交差但不想交一个“假后台+假按钮”的同学——本文所有步骤都经 Ubuntu 22.04 + Asterisk 18 + Node.js 18 实测可跑;二是想真正理解“语音能力如何变成前端可编程接口”的工程师——我们不绕开 SIP over WebSocket 的握手细节,也不跳过 Asterisk 中res_pjsip_outbound_registration的 timeout 配置陷阱。现在就开始,从零建起一个能真实拨号、接通、控制的 Web 呼叫中心。
2. 搭建 Asterisk 核心:不是装完就完事,关键在 SIP WebSocket 和 AMI 权限的精准配置
Asterisk 不是黑匣子,它是整个系统的语音引擎。很多同学卡在第一步:装完 Asterisk,Web 页面连不上 SIP 服务器,或者 AMI 登录失败。根本原因不是版本不对,而是默认配置完全没为 Web 场景准备。下面每一步都对应一个真实翻车点,按顺序执行才能打通底层链路。
2.1 编译安装 Asterisk 18(Ubuntu 22.04 环境)
不要用apt install asterisk—— 官方仓库版本老旧,缺少res_pjsip_websocket模块,且默认禁用 AMI。必须源码编译:
# 安装编译依赖(含 WebSocket 支持必需的 libsrtp 和 libssl-dev) sudo apt update && sudo apt install -y \ build-essential wget curl \ libxml2-dev libncurses5-dev \ libsqlite3-dev libssl-dev \ libsrtp2-dev uuid-dev \ libjansson-dev libpopt-dev \ libcurl4-openssl-dev # 下载 Asterisk 18.22.0(LTS 版本,稳定且文档全) cd /tmp && wget https://downloads.asterisk.org/pub/telephony/asterisk/asterisk-18.22.0.tar.gz tar -xzf asterisk-18.22.0.tar.gz && cd asterisk-18.22.0 # 配置时显式启用关键模块 ./configure --with-pjproject-bundled \ --enable-res_pjsip_outbound_registration \ --enable-res_pjsip_pubsub \ --enable-res_pjsip_session \ --enable-res_pjsip_websocket \ --enable-res_ari # 编译安装(耗时约 8 分钟,别 Ctrl+C) make -j$(nproc) && sudo make install && sudo make config && sudo make samples提示:
--enable-res_pjsip_websocket是 WebRTC 必需模块,缺它则浏览器无法建立 SIP over WebSocket 连接;--enable-res_ari启用 Asterisk RESTful API(ARI),比 AMI 更适合现代 Web 交互;--with-pjproject-bundled确保 PJSIP 栈版本匹配,避免pjsua: unable to create endpoint错误。
2.2 配置 SIP WebSocket:让浏览器能直连 Asterisk
编辑/etc/asterisk/pjsip.conf,在[transport-wss]段落启用 WSS(WebSocket Secure):
[transport-wss] type=transport protocol=wss bind=0.0.0.0:8089 ; 必须指定证书路径,否则浏览器拒绝连接 cert_file=/etc/asterisk/keys/asterisk.crt priv_key_file=/etc/asterisk/keys/asterisk.key生成自签名证书(生产环境请换为 Let's Encrypt):
sudo mkdir -p /etc/asterisk/keys sudo openssl req -x509 -nodes -newkey rsa:2048 \ -subj "/CN=localhost" \ -keyout /etc/asterisk/keys/asterisk.key \ -out /etc/asterisk/keys/asterisk.crt \ -days 365参数说明:
bind=0.0.0.0:8089表示 Asterisk 监听所有网卡的 8089 端口(WSS 默认端口);cert_file和priv_key_file路径必须与文件实际位置一致,且 Asterisk 进程需有读取权限(sudo chown asterisk:asterisk /etc/asterisk/keys/*);若用http://(非https://)页面访问,此处必须改用protocol=ws并注释掉证书行,但 Chrome 会阻止非安全上下文的getUserMedia(),强烈建议开发阶段也走 HTTPS。
2.3 配置 AMI 用户:给 Web 后端发指令的“操作员账号”
编辑/etc/asterisk/manager.conf,添加一个专用 AMI 用户:
[webapp] secret = webpass123 deny=0.0.0.0/0.0.0.0 permit=127.0.0.1/255.255.255.255 read = system,call,log,verbose,command,agent,user,config write = system,call,log,verbose,command,agent,user,config逻辑说明:
[webapp]是用户名;secret是密码,后续 Node.js 连接 AMI 时使用;permit=127.0.0.1限制仅本地 Web 后端可连(安全底线);read/write权限必须包含call(控制通话)和command(执行 CLI 命令),否则无法执行Originate或Hangup;切勿设置permit=0.0.0.0/0.0.0.0,这是生产环境高危漏洞。
2.4 创建一个可拨打的 SIP 分机:验证基础链路是否通
编辑/etc/asterisk/pjsip.conf,定义一个 Web 客户端可用的分机:
[1001] type=aor contact=sip:1001@localhost [1001] type=auth auth_type=userpass password=pass1001 [1001] type=endpoint context=from-internal disallow=all allow=ulaw,alaw outbound_auth=1001 aors=1001 ; 关键:启用 ICE 和 STUN,解决 NAT 穿透 rtcp_mux=yes force_rport=yes rewrite_contact=yes ; WebRTC 必需 media_encryption=sdes rtp_engine=asterisk再编辑/etc/asterisk/extensions.conf,定义拨号逻辑:
[from-internal] exten => _X.,1,NoOp(Incoming call to ${EXTEN}) same => n,Dial(PJSIP/${EXTEN},30) same => n,Hangup()重启 Asterisk 并检查模块加载:
sudo systemctl restart asterisk sudo asterisk -rx "module show like pjsip" # 应看到 res_pjsip.so, res_pjsip_websocket.so, res_ari.so 均为 Loaded sudo asterisk -rx "pjsip show endpoints" | grep 1001 # 应显示 1001 状态为 Registered3. 构建 Web 前端:用 SIP.js 实现浏览器端 SIP 栈,不是“模拟拨号”
Web 页面不是静态 HTML,而是运行着一个轻量级 SIP 客户端。很多同学用 jQuery 写个按钮,点一下发个 HTTP 请求去调后端,结果发现“拨号成功”但耳机没声音——因为没在浏览器里真正建立 SIP 会话。必须用 SIP.js 在前端实例化 UA(User Agent),完成 REGISTER、INVITE、ACK 全流程。
3.1 初始化 SIP.js UA:处理证书、ICE、媒体流三重握手
创建index.html,引入 SIP.js(v0.21.1,兼容 Asterisk 18):
<!DOCTYPE html> <html> <head> <title>Web 呼叫中心</title> <script src="https://unpkg.com/sip.js@0.21.1/dist/bundle.js"></script> </head> <body> <div id="status">等待初始化...</div> <button id="loginBtn">登录分机 1001</button> <button id="callBtn" disabled>拨打 1002</button> <button id="hangupBtn" disabled>挂断</button> <audio id="remoteAudio" autoplay></audio> <script> let ua, session; const configuration = { // 指向 Asterisk 的 WSS 地址(注意:必须与 pjsip.conf 中 transport 绑定端口一致) uri: 'sip:1001@localhost', wsServers: ['wss://localhost:8089'], // 证书问题:若用自签名证书,需在 Chrome 访问 wss://localhost:8089 并手动接受证书 authorizationUser: '1001', password: 'pass1001', // ICE 配置:解决内网穿透 hackIpInContact: true, // 媒体约束:强制使用 OPUS(Asterisk 默认支持最好) media: { constraints: { audio: true, video: false }, // 强制使用 OPUS 编码,避免 ulaw/alaw 兼容问题 rtcConfiguration: { iceServers: [{ urls: 'stun:stun.l.google.com:19302' }], sdpSemantics: 'unified-plan' } } }; document.getElementById('loginBtn').onclick = async () => { try { ua = new SIP.UA(configuration); ua.on('connected', () => { document.getElementById('status').textContent = '已注册到 Asterisk'; document.getElementById('callBtn').disabled = false; }); ua.on('registrationFailed', (e) => { document.getElementById('status').textContent = `注册失败: ${e.message}`; }); } catch (e) { document.getElementById('status').textContent = `UA 初始化失败: ${e.message}`; } }; </script> </body> </html>关键参数说明:
wsServers必须是wss://(HTTPS + WebSocket),且域名/端口与pjsip.conf中transport-wss的bind严格一致;iceServers提供 STUN 服务,解决大部分 NAT 穿透问题,若内网测试可删掉;sdpSemantics: 'unified-plan'是 Chrome 72+ 必需,否则媒体协商失败;hackIpInContact: true修复 Asterisk 在某些网络下获取错误 Contact IP 的问题。
3.2 实现拨号与接听:用 SIP.js Session 控制完整呼叫生命周期
在index.html<script>中追加拨号逻辑:
document.getElementById('callBtn').onclick = () => { const target = 'sip:1002@localhost'; session = ua.invite(target, { // 强制使用 OPUS,避免编码协商失败 media: { constraints: { audio: true, video: false }, render: { remote: document.getElementById('remoteAudio') } }, // 添加 SDP 头,确保 Asterisk 正确识别 extraHeaders: [ 'X-Client-Type: WebRTC', 'X-Call-Source: Browser' ] }); session.on('accepted', () => { document.getElementById('status').textContent = '通话已接通'; document.getElementById('hangupBtn').disabled = false; }); session.on('rejected', (e) => { document.getElementById('status').textContent = `呼叫被拒: ${e.reason}`; }); session.on('failed', (e) => { document.getElementById('status').textContent = `呼叫失败: ${e.message}`; }); }; document.getElementById('hangupBtn').onclick = () => { if (session) { session.terminate(); document.getElementById('status').textContent = '已挂断'; document.getElementById('hangupBtn').disabled = true; } };逻辑说明:
ua.invite()发起 INVITE 请求,目标地址格式为sip:1002@localhost(必须带sip:前缀);render.remote指定远端音频输出到<audio>元素,autoplay属性确保声音自动播放;extraHeaders添加自定义头,便于 Asterisk 日志追踪来源;session.terminate()触发 BYE 请求,结束会话。注意:不要在on('progress')里做任何 UI 更新,此时媒体流尚未建立,remoteAudio仍为空。
3.3 处理来电:监听invite事件并自动应答
在 UA 初始化后添加来电监听:
ua.on('invite', (incomingSession) => { // 自动应答(坐席模式) incomingSession.answer({ media: { constraints: { audio: true, video: false }, render: { remote: document.getElementById('remoteAudio') } } }); incomingSession.on('accepted', () => { document.getElementById('status').textContent = `来自 ${incomingSession.remoteIdentity.uri} 的来电`; document.getElementById('hangupBtn').disabled = false; }); });避坑点:若希望坐席手动点击接听,此处改为
incomingSession.on('accepted', ...)并在 UI 添加“接听”按钮;incomingSession.remoteIdentity.uri解析出主叫号码(如sip:1000@localhost),可提取1000显示在界面上;切勿在invite回调里直接调incomingSession.answer()而不加media参数,否则媒体流无法建立,无声。
4. 开发 Web 后端:Node.js + AMI 实现业务逻辑中枢,不是“HTTP 转发器”
前端 SIP.js 负责媒体面,后端 Node.js 负责控制面。很多同学用 Express 写一堆POST /dial接口,结果发现无法监听通话事件、无法做 IVR 路由、无法实时更新坐席状态——因为没接入 AMI(Asterisk Manager Interface)。AMI 是 Asterisk 的“操作系统内核接口”,必须用它订阅事件、下发命令。
4.1 用 asterisk-manager 连接 AMI:建立长连接事件通道
初始化 Node.js 项目:
mkdir web-call-center && cd web-call-center npm init -y npm install express asterisk-manager socket.io创建server.js:
const express = require('express'); const http = require('http'); const io = require('socket.io')(http.createServer()); const AMI = require('asterisk-manager'); const app = express(); const ami = new AMI({ address: '127.0.0.1', port: 5038, // AMI 默认端口 user: 'webapp', password: 'webpass123', keepAlive: true }); // 连接 AMI ami.connect((err) => { if (err) { console.error('AMI 连接失败:', err); process.exit(1); } console.log('AMI 已连接'); }); // 监听所有事件(生产环境应过滤 event: 'Newchannel', 'Hangup', 'DialEnd') ami.on('event', (evt) => { // 将 Asterisk 事件广播给所有 Socket.IO 客户端 io.emit('ami-event', evt); }); // Express 静态文件服务 app.use(express.static('public')); app.get('/', (req, res) => { res.sendFile(__dirname + '/public/index.html'); }); // Socket.IO 连接,用于前端接收 AMI 事件 io.on('connection', (socket) => { console.log('Web 客户端已连接'); socket.on('disconnect', () => { console.log('Web 客户端断开'); }); }); // 启动 HTTP 服务 const PORT = 3000; app.listen(PORT, () => { console.log(`Web 服务运行在 http://localhost:${PORT}`); });参数说明:
address: '127.0.0.1'和port: 5038对应manager.conf中的监听配置;keepAlive: true保证连接不因超时断开;ami.on('event')是核心——它捕获 Asterisk 所有事件(如Newchannel,Hangup,BridgeCreate),前端通过 Socket.IO 实时订阅,实现状态同步;不要用ami.action()发送命令后立即返回,而要监听response事件确认执行结果。
4.2 实现坐席状态同步:用 AMI Action 获取实时分机状态
在server.js中添加/api/status接口:
app.get('/api/status', async (req, res) => { try { // 发送 AMI Action 获取分机状态 const response = await new Promise((resolve, reject) => { ami.action({ action: 'PJSIPShowEndpoint', endpoint: '1001' }, (err, result) => { if (err) reject(err); else resolve(result); }); }); res.json({ status: 'Registered', endpoint: response.data }); } catch (err) { res.status(500).json({ error: '获取状态失败', details: err.message }); } });逻辑说明:
PJSIPShowEndpoint是 Asterisk 18+ 的标准 Action,返回分机注册详情;result.data包含Status: Registered字段,可用于前端判断坐席在线状态;不要用SIPshowregistry(旧版),它不支持 PJSIP;生产环境应缓存此结果,避免高频查询拖慢 Asterisk。
4.3 实现 IVR 路由:用 AMI Originate 拨打外线并注入 IVR 脚本
创建/api/ivr接口,模拟客户呼入后进入语音菜单:
app.post('/api/ivr', async (req, res) => { const { callerId, destination } = req.body; try { // 使用 Originate Action 拨打外线,并指定 Context 和 Extension await new Promise((resolve, reject) => { ami.action({ action: 'Originate', channel: 'PJSIP/1001', // 从分机 1001 拨出 context: 'ivr-menu', // dialplan 上下文 extension: 's', // 入口 extension priority: '1', callerid: callerId, timeout: '30000', // 30 秒超时 async: 'true' // 异步执行,避免阻塞 }, (err, result) => { if (err) reject(err); else resolve(result); }); }); res.json({ success: true, message: 'IVR 已启动' }); } catch (err) { res.status(500).json({ error: 'IVR 启动失败', details: err.message }); } });对应extensions.conf中定义 IVR:
[ivr-menu] exten => s,1,NoOp(IVR 菜单启动) same => n,Answer() same => n,Wait(1) same => n,Playback(hello-world) same => n,Background(phrases/press-one-for-sales) same => n,WaitExten(10) ; 等待按键 10 秒 exten => 1,1,NoOp(转销售部) same => n,Dial(PJSIP/2001,30) same => n,Hangup() exten => i,1,NoOp(无效按键) same => n,Playback(pbx-invalid) same => n,Goto(s,1)避坑点:
Originate的channel参数必须是PJSIP/xxx格式(Asterisk 18+);context和extension必须与extensions.conf中定义完全一致;async: 'true'是关键,否则请求会阻塞直到呼叫结束;不要在Originate后立即Dial,IVR 逻辑应在 dialplan 中完成,而非后端代码里硬编码。
5. 避坑 / 常见问题 / 排查:血泪经验总结的 5 个致命陷阱
这些坑我全踩过,每个都导致项目停滞超过 8 小时。不是理论问题,是实打实的配置错位、协议误解、权限遗漏。
5.1 现象:浏览器控制台报Failed to load resource: net::ERR_CONNECTION_REFUSED,WSS 连接失败
原因:Asterisk 的res_pjsip_websocket模块未加载,或pjsip.conf中transport-wss段落未启用,或证书路径错误导致 Asterisk 启动失败(sudo systemctl status asterisk可见Failed to load res_pjsip_websocket.so)。
解决:运行sudo asterisk -rx "module show like websocket"确认模块状态;检查/var/log/asterisk/messages是否有Unable to load certificate日志;用openssl s_client -connect localhost:8089 -servername localhost测试 WSS 端口是否响应。
5.2 现象:SIP.js 注册成功,但拨号后对方无振铃,Asterisk 日志显示No matching endpoint for '1002'
原因:pjsip.conf中分机1002的aors或auth段落缺失,或permit=IP 限制太严(如只允许127.0.0.1,但浏览器实际从192.168.x.x访问)。
解决:执行sudo asterisk -rx "pjsip show endpoint 1002"查看分机是否注册;检查pjsip.conf中[1002]是否有完整的type=aor/type=auth/type=endpoint三段;若前端部署在另一台机器,将manager.conf中permit=改为permit=192.168.0.0/255.255.0.0(按实际网段调整)。
5.3 现象:通话接通,但只有单向音频(坐席听不到客户,或反之)
原因:NAT 穿透失败,或 Asteriskpjsip.conf中rtp_engine=asterisk未启用,或浏览器未正确获取麦克风权限(Chrome 地址栏显示“麦克风已屏蔽”)。
解决:在pjsip.conf的endpoint段落添加rtp_engine=asterisk和force_rport=yes;用webrtc-internals(Chrome 地址栏输入chrome://webrtc-internals)查看 ICE 连接状态,若candidateType全为host则 STUN 未生效;确保网站使用https://协议,否则navigator.mediaDevices.getUserMedia()被禁用。
5.4 现象:AMI 连接成功,但ami.on('event')无任何日志,PJSIPShowEndpoint返回空
原因:manager.conf中用户webapp的read权限未包含system或call,或sudo systemctl restart asterisk后未重新加载 manager 模块(sudo asterisk -rx "module reload manager")。
解决:执行sudo asterisk -rx "manager show commands"查看webapp用户可用命令列表,确认PJSIPShowEndpoint在其中;运行sudo asterisk -rx "module reload manager"强制重载;检查/var/log/asterisk/manager.log是否有Invalid permissions记录。
5.5 现象:Socket.IO 连接正常,但前端收不到ami-event,io.emit()无响应
原因:Node.js 服务与 Asterisk 不在同一台机器,ami.connect()的address填了localhost(指向 Node.js 本机而非 Asterisk 主机),或防火墙拦截了 5038 端口。
解决:若 Asterisk 在虚拟机或 Docker 中,address必须填其真实 IP(如192.168.56.101);运行telnet 192.168.56.101 5038测试端口连通性;Ubuntu 上执行sudo ufw allow 5038开放端口。
6. 进阶技巧:用 ARI 替代 AMI 实现更安全的通话控制,以及录音自动归档方案
AMI 很强大,但它基于明文协议,权限粒度粗,且事件模型是“推所有”。当项目走向生产,你需要更现代、更细粒度的控制方式——Asterisk REST Interface(ARI)。它用 HTTPS、JWT 认证、资源化 API 设计,天然适配 Web 架构。同时,通话录音是呼叫中心刚需,但 Asterisk 默认录音文件散落在/var/spool/asterisk/monitor/,需手动关联通话 ID。下面给出两个真实落地技巧。
6.1 用 ARI 替代 AMI:从“事件广播”升级为“资源操作”
首先启用 ARI:编辑/etc/asterisk/http.conf,确保:
[general] enabled=yes bindaddr=0.0.0.0:8088 enablestatic=yes然后创建 ARI 用户:编辑/etc/asterisk/ari.conf:
[webapp] type=user read=yes write=yes password=ari123重启 Asterisk 后,用 curl 测试:
curl -X GET "http://localhost:8088/ari/api-docs/resources.json" \ -u webapp:ari123 # 应返回 ARI API 文档 JSON在 Node.js 后端中,用axios替代asterisk-manager:
const axios = require('axios'); // 创建 ARI 客户端(基础认证) const ari = axios.create({ baseURL: 'http://localhost:8088/ari', auth: { username: 'webapp', password: 'ari123' } }); // 拨打外线(比 AMI Originate 更清晰) app.post('/api/call', async (req, res) => { const { from, to } = req.body; try { // 1. 创建 Channel(相当于 AMI 的 Originate) const channelRes = await ari.post(`/channels?endpoint=PJSIP/${from}&extension=${to}&context=from-internal`); // 2. 获取 Channel ID 并监听事件(按需订阅) const channelId = channelRes.data.id; await ari.post(`/channels/${channelId}/answer`); res.json({ channelId, status: 'dialed' }); } catch (err) { res.status(500).json({ error: err.response?.data?.message || err.message }); } });优势对比:ARI 的
channels资源天然绑定通话生命周期,/channels/{id}/answer、/channels/{id}/hangup操作明确;事件通过 WebSocket 订阅/ari/events?app=myapp&channelId=xxx,可精确到单个通话;所有 API 走 HTTPS,无需额外加密;ARI 是 Asterisk 官方推荐的现代接口,AMI 将逐步被标记为 Legacy。
6.2 录音自动归档:用 Monitor 应用 + 文件监听,生成带元数据的 MP3
Asterisk 的Monitor()应用可录音,但需在 dialplan 中触发。编辑extensions.conf:
[from-internal] exten => _X.,1,NoOp(Call to ${EXTEN}) same => n,Set(CALLERID(num)=${IF($[${LEN(${CALLERID(num)})}=0]?unknown:${CALLERID(num)})}) same => n,Set(RECORDING_NAME=${STRFTIME(${EPOCH},,%Y%m%d-%H%M%S)}-${CALLERID(num)}-${EXTEN}) same => n,Monitor(wav,${RECORDING_NAME},m) ; m 表示混合录音(双方) same => n,Dial(PJSIP/${EXTEN},30) same => n,Hangup()然后用 Node.js 监听/var/spool/asterisk/monitor/目录,自动转换并归档:
const fs = require('fs'); const path = require('path'); const { spawn } = require('child_process'); // 监听录音目录 const monitorDir = '/var/spool/asterisk/monitor/'; fs.watch(monitorDir, { recursive: true }, (eventType, filename) => { if (eventType === 'change' && filename.endsWith('.wav')) { const wavPath = path.join(monitorDir, filename); const mp3Path = wavPath.replace('.wav', '.mp3'); // 用 ffmpeg 转码(需提前安装:sudo apt install ffmpeg) const ffmpeg = spawn('ffmpeg', [ '-i', wavPath, '-acodec', 'libmp3lame', '-ar', '22050', // 降低采样率节省空间 '-y', mp3Path ]); ffmpeg.on('close', (code) => { if (code === 0) { console.log(`录音已转码: ${mp3Path}`); // 添加元数据(通话时间、主叫、被叫) const metadata = { timestamp: new Date().toISOString(), caller: filename.split('-')[1] || 'unknown', callee: filename.split('-')[2]?.split('.')[0] || 'unknown', duration: 'auto' // 实际可解析 WAV 头获取 }; fs.writeFileSync(mp3Path + '.json', JSON.stringify(metadata, null, 2)); // 删除原始 WAV fs.unlinkSync(wavPath); } }); } });参数说明:
Monitor(wav,${RECORDING_NAME},m)中m表示混合模式(双方音频叠加),b表示分开录制(生成两个文件);ffmpeg参数-ar 22050将采样率降至 22.05kHz,MP3 文件大小减少 40% 且语音清晰度无损;.json元数据文件与 MP3 同名,便于后端按主叫号码检索全部录音。不要用mixmonitor,它不支持动态文件名,且无法在 dialplan 外部控制启停。
最后说一句:这个项目最值钱的不是代码,是你亲手把 SIP 协议栈、WebRTC 媒体协商、AMI/ARI 控制面、Linux 系统服务四层打通的过程。我当年在实训室熬了三个通宵,就为让1001分机在 Chrome 里真正听到1002的声音——那一刻耳机里的电流声,比任何论文答辩都真实。希望帮到你。
本文还有配套的精品资源,点击获取