尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

JsSIP+WebRTC:浏览器音视频通话Demo从零搭建指南

JsSIP+WebRTC:浏览器音视频通话Demo从零搭建指南 简介这是一份基于JSSIP库与FreeSWITCH服务器联调的Web音视频通信示例项目面向需要快速上手浏览器端VoIP接入的开发者与初学者。压缩包共385个文件总体积4.42MB以HTML页面、JavaScript逻辑、CSS样式为主体辅以LESS源文件、图片、字体及工程配置结构清晰便于本地运行和拆解学习。已有1429人学习使用。Demo覆盖SIP注册、音视频呼叫接听与挂断、媒体协商、信令交互、短信收发、WebSocket持久连接以及异常处理等关键环节并演示了如何与FreeSWITCH API对接扩展业务逻辑。通过实际操作该项目读者能直观理解SIP协议在Web环境下的工作流程获得可直接改写的代码基础适合作为构建Web实时通信应用的起始模板。 搞过 WebRTC 的开发者应该都清楚纯原生做音视频通话有多折腾——信令协商、媒体协商、STUN/TURN、ICE 重协商一堆底层细节等着你。更麻烦的是如果公司现有的通信系统跑的是 SIP 协议原生 WebRTC 完全不认识 SIP两边根本聊不到一块。JsSIP 就是解决这个问题的它是一个纯 JavaScript 实现的 SIP 客户端跑在浏览器里通过 WebSocket 连接 SIP 服务器把浏览器变成一个能注册、能拨打、能接听、能发消息的软电话终端。这篇文章把最近做的一个 JsSIP 音视频 demo 完整拆一遍从环境搭建到核心代码再到调试过程中踩过的坑适合刚接触 SIP WebRTC、想快速跑通一个可用的音视频 demo 的开发者参考。1. 项目定位JsSIP 到底能做什么这个 demo 该怎么拆1.1 为什么选 JsSIP 而不是原生 WebRTC先理清一个概念WebRTC 本身只解决媒体传输它不关心你的业务信令是什么格式。A 和 B 要通话双方得先约好“咱们用哪个编码”“网络路径怎么走”这一大坨协商逻辑就是信令。原生 WebRTC 官方只给了RTCPeerConnection这种底层能力信令怎么设计、怎么传输、怎么处理状态全部得你自己写。如果你们团队已经有一套基于 SIP 的语音网关、在线客服系统你总不能把整套信令体系推翻重来。JsSIP 的做法是把 SIP 协议栈整个搬进浏览器通过 WebSocket 传输 SIP 消息媒体部分仍然交给浏览器原生 WebRTC。这样业务侧可以继续用分机号、SIP 中继、IVR 这些老基础设施前端却获得了一个标准的现代 API。对于“要和现有 VoIP 系统打通”的场景这是比纯 WebRTC 务实得多的方案。1.2 demo 的整体架构与核心流程这个 demo 的整体架构不复杂核心就三段链路浏览器端JsSIP 负责 SIP 信令getUserMedia拿摄像头和麦克风RTCPeerConnection负责媒体传输。信令通道浏览器通过 WebSocket或加密的 WSS连接 SIP 服务器。JsSIP 会将 REGISTER、INVITE、ACK、BYE 等 SIP 消息封装成 WebSocket 帧发送。媒体通道通话建立后音视频流走 WebRTC 的 SRTP 通道不经过 SIP 服务器转发除非你强制走媒体中继。整个呼叫流程可以简单描述为注册分机号 → A 发起 INVITE 邀请 → 服务器找到 B 所在位置 → B 收到来电并应答 → 双方完成媒体协商 → RTP 流建立 → 通话中 → 任意一方挂断发送 BYE会话结束。demo 里我让两个浏览器页面分别注册为 1001 和 1002然后互相呼叫。这个设计的好处是不依赖任何外部 PSTN 线路纯内网就能验证全流程方便排查。2. 环境搭建先把信令链路跑通再谈音视频2.1 需要准备哪些组件做这个 demo 前先别急着写代码环境没搭好后面全是坑。我用的组件清单如下一台能跑 SIP 服务器的机器本机或局域网都行。我这边选的 FreeSWITCH社区活跃、WebSocket 对接资料多你也可以用 Asterisk 或 Kamailio原理大同小异。一个支持 WebSocket 的 SIP 服务端配置。FreeSWITCH 里常用mod_verto或者自己开一个 SIP over WebSocket 的 profile。一个域名或带 HTTPS 的服务地址。这是硬性条件浏览器只有在localhost或 HTTPS 环境下才允许调用摄像头和麦克风如果你的页面跑在局域网 IP 上还没上 HTTPSgetUserMedia会被直接拒绝。两个浏览器标签页分别登录两个分机账号。也可以一个标签页开两个页面只要能模拟主叫和被叫。前端工程我直接用 npm 装的jssip包不需要额外引什么复杂的 UI 框架原生 HTML JS 就能把 demo 跑起来。2.2 SIP 服务器端配置要点我用的 FreeSWITCH配置里需要开启 WebSocket 监听并放行分机注册。网上很多教程会让你直接改/etc/freeswitch/sip_profiles下的配置实际上如果你只想跑通 demo更快的路径是确认你的 FreeSWITCH 版本默认开了mod_verto。Verto 本身就是基于 WebSocket 的协议JsSIP 也可以通过它间接对接。但为了少绕弯子我推荐直接用标准 SIP over WebSocket在 profile 里加上param namews-binding value:5066/ param namewss-binding value:7443/端口号根据自己的环境改。如果你用的是 WSS务必把 SSL 证书路径配好。我最初偷懒想着内网测试不用加密结果页面非localhost环境全是 HTTP浏览器直接把媒体权限卡死这个坑后面细说。分机账号也就是目录号也要在directory/default.xml里添加。demo 里至少要两个分机不然没法互相呼叫user id1001 params param namepassword value123456/ param namevm-password value123456/ /params /user配完重启服务端然后用命令行客户端跑一下注册测试能通再进前端开发这一步能帮你把“服务器问题”和“前端问题”快速切割开。3. 核心代码实现一条龙打通注册、拨打、接听、挂断3.1 初始化 Ua 并完成注册JsSIP 的核心对象是Ua也就是 User Agent。它负责维护 SIP 注册状态、管理会话。初始化配置里最关键的几个字段如下const socket new JsSIP.WebSocketInterface(wss://your-server:7443); const ua new JsSIP.UA({ sockets: [socket], uri: sip:1001your-server, password: 123456, display_name: Demo User 1001, // 这两个字段负责自动注册 register: true, registrar_server: your-server, // 如果 uri 的用户名和 Authorization 用户名不同才需要单独配 authorization_user // authorization_user: 1001, // 打开 debug 日志调试期强烈建议设为 true // debug: true, }); ua.on(connected, () console.log(WebSocket 已连接)); ua.on(registered, () console.log(分机注册成功)); ua.on(registrationFailed, (e) console.error(注册失败, e)); ua.start();这里有个容易忽略的点uri里的sip:1001your-serveryour-server一定得是 SIP 服务器能解析到的地址别用localhost写死否则两个浏览器页面的注册都会指向本机逻辑上就分不清谁是谁了。register: true之后JsSIP 会自动发 REGISTER成功后会触发registered事件。debug: true这个配置如果你之前被各种疑难杂症卡住过会发现它比任何断点调试都好使。开启后控制台会打印所有 SIP 信令报文主叫发没发 INVITE、被叫回没回 200 OK一眼就能看明白。3.2 发起呼叫与绑定远端音视频流注册成功后发起呼叫就简单了。ua.call()会创建一个JsSIP.RTCSession同时触发newRTCSession事件。你需要在事件回调里拿到 session再绑定各种媒体事件。function call(callee) { const session ua.call(sip:${callee}your-server, { mediaConstraints: { audio: true, video: true, }, pcConfig: { iceServers: [ { urls: stun:stun.l.google.com:19302 }, // 生产环境这里务必填自己的 TURN不然公网通话大概率不通 ], }, // 这个回调也很有用可以提前拿到本地流做预览 localIdentity: Demo 1001, }); session.on(accepted, (e) { console.log(呼叫已被接听); // 远端流在 e.stream 里 const remoteVideo document.getElementById(remoteVideo); remoteVideo.srcObject e.stream; remoteVideo.play(); }); session.on(failed, (e) { console.error(呼叫失败:, e.cause); }); }注意e.stream是 JsSIP 帮你封装好的MediaStream对象直接赋给video标签的srcObject即可。新版浏览器基本都支持srcObject别再用老的createObjectURL了那套已经废弃。remoteVideo.play()建议调用一下因为有些浏览器自动播放策略严格不显式调用可能不出画面。如果你想做本地预览可以监听session.localStream或者直接在getUserMedia之后绑到本地视频标签。JsSIP 的mediaConstraints会在内部调用getUserMedia所以一般情况下不需要你手动再调用一次。3.3 接听、拒绝与挂断的完整处理被叫侧的逻辑稍微多点。当对端发起呼叫时本机会监听到newRTCSession事件并且事件的session参数是一个带direction: incoming的会话。此时你可以决定answer()还是terminate()。ua.on(newRTCSession, (e) { const session e.session; if (session.direction incoming) { // 自动接听你也可以弹窗让用户手动点 session.answer({ mediaConstraints: { audio: true, video: true, }, }); session.on(accepted, () { const remoteVideo document.getElementById(remoteVideo); // 接听后同样要绑定远端流 // 注意incoming 的 session 里远端流可能在 conn 事件或 confirmed 事件中 }); session.on(confirmed, (e) { const remoteVideo document.getElementById(remoteVideo); remoteVideo.srcObject e.stream; remoteVideo.play(); }); session.on(ended, () { console.log(通话已结束); cleanupMedia(); }); } }); function hangup(session) { if (session) { session.terminate(); } }接听后有人会把远端流绑定写在accepted里实际测下来confirmed事件更稳。因为accepted只代表对端发了 200 OK媒体协商可能还没完全完成confirmed表示整个会话真正建立此时e.stream通常已经有值。如果accepted里e.stream为 null先别急着改业务逻辑换个事件等一等很多黑屏问题就是这么来的。挂断很简单调用session.terminate()即可。但要记得把视频标签的srcObject置空play()停止别让摄像头灯一直亮着。我用的是一个cleanupMedia()工具函数function cleanupMedia() { const remoteVideo document.getElementById(remoteVideo); const localVideo document.getElementById(localVideo); if (remoteVideo.srcObject) { remoteVideo.srcObject.getTracks().forEach(track track.stop()); } if (localVideo.srcObject) { localVideo.srcObject.getTracks().forEach(track track.stop()); } remoteVideo.srcObject null; localVideo.srcObject null; }这一步容易被忽略但体验差距很大。不清理的话下次通话时摄像头灯还亮着麦克风还可能被占用后面所有音频采集都会出问题。4. 音视频调试中的常见坑与排查方法4.1 没有声音或黑屏的排查顺序demo 最常遇到的问题就是“显示已接通但没画面没声音”。我的排查顺序基本固定先看控制台有没有getUserMedia错误。如果提示NotFoundError说明浏览器没拿到摄像头权限如果是NotAllowedError就是用户拒绝了权限或者页面不是 HTTPS。看远端视频标签的srcObject是否真的有MediaStream。可以直接在控制台里执行document.getElementById(remoteVideo).srcObject看有没有值。检查浏览器自动播放策略。Chrome 很严格如果不在用户手势触发的处理函数里绑定流并调用play()可能被静默拦截。解决办法是在点击“接听”按钮的事件回调里完成绑定。最后才怀疑媒体协商问题查看pc.onconnectionstate状态。黑屏时先排除前两条90% 的 demo 问题都出在“流没绑上去”而不是网络问题。4.2 ICE 与 NAT 穿透问题注册正常、呼叫也显示已接通但双方就是看不到画面听不到声音这是典型的媒体通道没打通。原因通常是 ICE 协商失败尤其是两个浏览器不在同一个内网时只有 STUN 是不够的。STUN 只能拿到公网映射地址但对称型 NAT 下无法建立直接通路这时候必须上 TURN。TURN 是一个媒体中继服务器所有媒体流量都经过它转发虽然会增加延迟但胜在稳定。调试时别只看现象把 ICE 状态打出来pc.oniceconnectionstatechange () { console.log(ICE 状态:, pc.iceConnectionState); };如果状态一直停在checking或变成failed基本可以确认是 NAT 穿透问题。这时候把 TURN 地址配上问题立刻消失。还有一类情况是防火墙挡了 UDP 端口媒体流走的是 UDP很多企业网络默认不放行高位 UDP 端口这也是内网 demo 正常、一上公网就废的常见原因。4.3 回音与噪音处理这部分体验影响很大但经常被忽略。demo 阶段最容易出现的回音往往不是代码 bug而是扬声器外放导致麦克风重新采集了扬声器的声音。测试时强烈建议戴耳机能排除一半的回音问题。如果戴了耳机还有回音那就要检查 SIP 服务器或网关那边的回声消除配置。WebRTC 的echoCancellation默认开启但有些场景下 JS 端显式传参会失效我踩过时发现是mediaConstraints里忘了加mediaConstraints: { audio: { echoCancellation: true, noiseSuppression: true, autoGainControl: true, }, video: true, }这几项是浏览器音频处理的三大件看似基础但对通话质量影响巨大。另外如果你在 Chrome 里做软电话audio还可以给它指定deviceId用户切换耳机或麦克风时重新getUserMedia并绑定新设备即可JsSIP 在这块兼容性做得不错基本是原生的 WebRTC 能力直接透传。5. 从 demo 到生产还差哪些事5.1 呼叫状态机与异常兜底demo 里可以只监听accepted和confirmed但真实场景下一个呼叫可能经历振铃、拒接、超时、网络中断等各种状态。我建议把session上几个关键事件全部打点progress对端振铃中可以提示“正在呼叫对方”accepted对端已接听可以隐藏振铃提示failed呼叫失败e.cause会带原因比如busy、rejected、timeoutended通话结束做资源清理bye对端主动挂断connecting媒体通道建立中不要假设“发起呼叫后一定会走到 confirmed”生产环境里各种奇怪状况都有比如对方接了但 ICE 一直不通、对方手机没电自动挂断、呼叫超时被服务器掐断。给用户一个明确的状态提示比让用户对着白屏猜要好太多。5.2 安全、权限与体验细节生产环境比 demo 多的另一块是权限管理。浏览器权限被拒后页面要有能力引导用户重新授权。navigator.permissions.query({ name: camera })可以查询权限状态但这 API 的兼容性一般更通用的做法是捕获getUserMedia的错误弹一个带链接的提示让用户去浏览器设置里打开权限。还有两个小细节切换摄像头。如果用户想从前置切到后置需要先用navigator.mediaDevices.enumerateDevices()拿到设备列表然后用新的deviceId重建媒体流再通过 JsSIP 的session.unmute、session.mute或底层RTCRtpSender.replaceTrack替换轨道。这个功能 demo 可以不做但如果做的话建议用replaceTrack不要重新call()。断线重连。WebSocket 偶尔会断开尤其是移动端网络切换场景。JsSIP 的Ua提供了ua.on(disconnected)事件这里面做心跳检测和自动重连比用户手动刷新页面体验好得多。我一般会在disconnected后记录当前会话状态等 WebSocket 恢复后重新注册必要时提示用户“网络已断开通话可能中断”。写在最后的小建议这个 demo 做下来我最大的体会是JsSIP 本身并不难难的是把 SIP 信令和 WebRTC 媒体两条线理顺。调试时不要凭感觉猜先把debug: true打开看着控制台里的信令日志一步步推大部分问题都能定位。最后再分享一个我踩过几次坑后的习惯每次改完配置先重启 SIP 服务器再清浏览器缓存最后才跑页面。这三步做完还不行再看是不是浏览器权限或 HTTPS 的问题。按照这个顺序排查能省下不少瞎折腾的时间。本文还有配套的精品资源点击获取
返回列表