
简介jssip音视频demo是基于JSSIP库的示例项目面向网页通信开发初学者演示如何通过JSSIP与FreeSWITCH服务器交互实现SIP注册、音视频通话及短信收发。压缩包约4.42MB共385个文件以HTML页面、JavaScript脚本、CSS样式及LESS预编译样式为主覆盖页面结构、交互逻辑与样式配置另有部分图片、字体与配置文件辅助界面展示。内容覆盖从信令流程、媒体协商、WebSocket连接与错误处理等关键环节运行后可直观理解一次呼叫从发起、接听到挂断的完整链路并可参考其中FreeSWITCH自定义API扩展实现业务逻辑。包内文件编排清晰适合结合目录逐模块阅读调试。已有1435人学习适合希望从零搭建网页音视频通信原型或对接FreeSWITCH的开发者。1. jssip音视频demo 是什么浏览器跑 SIP 的最短闭环jssip 音视频 demo 解决的是一个问题把一套完整的 SIP 软电话搬进浏览器不装客户端、不依赖特定操作系统打开网页就能注册分机、拨打音视频电话。它面向的是客服工作台、远程坐席、在线诊疗这类「业务系统里直接发起通话」的场景核心组件就是 JsSIP 这个纯 JavaScript 的 SIP 协议栈加上 WebRTC 做媒体传输。最反直觉的一点是这个 demo 真正花时间的部分往往不在 JS 代码而在证书、ICE 候选和浏览器媒体策略这三样东西上它们才是把 demo 拖进生产环境时最容易翻车的地方。适合谁看前端或全栈工程师想快速验证浏览器通话方案或者团队需要在两个浏览器之间做内部对讲验证。2. jssip demo 的服务端选型SIP 服务器与 WSS 信令的最小闭环一个 jssip demo 至少需要两段链路浏览器通过 WebSocket SecureWSS与 SIP 服务器交换注册信令、呼叫邀请媒体则走 WebRTC 的 RTP/RTCP 通道。换句话说WSS 只搬信令不搬音视频。很多人第一次搭的时候以为把音视频流推到 WSS 上就能通结果被浏览器安全上下文直接拦下来这个误解解开之后整个架构就清晰了。2.1 为什么服务端选 FreeSWITCH 而不是 Asterisk 或 Kamailio常见做法是拿 FreeSWITCH 当 demo 的 SIP 服务端理由很直接它自带了 WebSocket / Secure WebSocket 的 Sofia SIP 模块不需要额外装网关组件就能把 WSS 信令接进来同时它是 B2BUA浏览器端和浏览器端之间的媒体协商由它中转控制排查问题比纯 Router 型的 Kamailio 直观得多。我一般会这样对比选型控制在 30 分钟内能跑通服务端信令角色浏览器接入难度适合 demo 的理由主要代价FreeSWITCHB2BUA低自带 ws/wss-binding一个模块搞定信令自带回声消除和录音配置项多模块需要裁剪AsteriskB2BUA中需要额外配置 PJSIP WSS 传输老牌稳定文档多WSS 证书与传输配置比较绕Kamailio代理/路由高只做路由媒体绕行高并发压力测试用得上需要自己搭媒体服务器demo 阶段太重demo 阶段我永远选 FreeSWITCH。还有一个原因它的fs_cli命令行工具可以直接查看分机注册状态、拨号呼叫跟踪出问题时不用靠猜这对排错阶段的体验影响极大。2.2 建分机、开 WSS 与拨号规则三组最小配置搭建时一般先把 FreeSWITCH 装好。以 Debian/Ubuntu 系为例常见做法是直接通过官方包源安装安装完成后先确认fs_cli能进控制台再改三组配置。第一组是 Sofia SIP profile 的 WebSocket 监听。打开/etc/freeswitch/sip_profiles/external.xml确认以下参数没有被注释param namews-binding value:5066/ param namewss-binding value:7443/ws-binding是明文 WebSocketwss-binding是加密 WebSocket。浏览器端必须用wss://因为getUserMedia和 WebRTC 在非安全上下文里会被直接禁用所以明文ws://只适合内网调试。如果你用的是自签证书还要指定证书目录param nametls-cert-dir value/etc/freeswitch/tls/ param namewss-pem-file value/etc/freeswitch/tls/wss.pem/第二组是分机。在/etc/freeswitch/directory/default/下新建100.xmlinclude user id100 params param namepassword value100pass/ /params variables variable nameuser_context valuedefault/ variable nameeffective_caller_id_number value100/ /variables /user /include第三组是内部呼叫的拨号规则。在/etc/freeswitch/dialplan/default/下新建99_local_call.xml让任意位数分机都能互拨extension namelocal_call condition fielddestination_number expression^(\d)$ action applicationbridge datauser/${destination_number}/ /condition /extension改完之后 reload 配置并重启 Sofia 模块让 WSS 监听生效fs_cli -x reloadxml fs_cli -x sofia profile external restartreloadxml会重新加载目录和拨号规则sofia profile external restart则是让external.xml里的 ws/wss 监听端口真正生效。这两条命令在 demo 调试阶段会被反复使用每次改完配置都要跑一遍否则你看到的还是一个旧状态的 FreeSWITCH。验证是否成功可以看启动日志里是否有wss://监听端口出现或者直接用浏览器地址栏请求一次https://服务器IP:7443能看到证书握手反馈基本就证明 WSS 服务已经起来了。2.3 前端工程初始化demo 程序的前端骨架服务端就绪后前端工程反而是最简单的部分。我一般用一个 Vite 的 vanilla 模板起步避免 React 或 Vue 的框架代码干扰对 JsSIP 本身的理解。初始化命令npm create vitelatest jssip-demo -- --template vanilla cd jssip-demo npm install npm install jssip安装完成后把src/main.js拆成三个职责初始化 UA、注册事件、渲染媒体流。目录上不强求复杂分层一个main.js加一个index.html足够跑通首个 demo。提示demo 环境建议把浏览器和 FreeSWITCH 放在同一内网段先排除 NAT 干扰。等本地通了再引入 TURN 做跨网测试。3. 页面端 jssip 接入注册、发起呼叫与音视频渲染服务端链路通了之后页面端就是 JsSIP 的主场。注册、呼出、呼入、音视频渲染这四个动作其实都在围绕 UA 实例和 session 实例的事件做文章。把事件的时序理清楚代码自然就顺了。3.1 初始化 JsSIP UAsockets、URI 与 ICE 参数创建 UA 是第一步也是最容易埋坑的一步。下面是 demo 里最基础的初始化代码import { UA, WebSocketInterface } from jssip const socket new WebSocketInterface(wss://10.0.0.10:7443) const ua new UA({ sockets: [socket], uri: sip:10010.0.0.10, password: 100pass, display_name: 坐席-100, register: true, register_expires: 600, session_timers: false, ice_servers: [ { urls: [stun:stun.example.com:3478] }, { urls: turn:turn.example.com:3478, username: turnuser, credential: turnpass } ], log: { level: debug } }) ua.start()这段代码里最容易被忽略的是sockets是数组而且WebSocketInterface的地址必须是wss://。uri是分机的 SIP 地址它决定注册身份password对应分机密码register_expires控制注册周期建议和服务端允许的最大值保持一致设太大容易导致注册过期后服务端悄然剔除。session_timers在 demo 里我建议直接设成false避免每隔一段时间触发会话刷新逻辑干扰对核心链路的判断。ice_servers最终会映射到 RTCPeerConnection 的iceServers配置内网测试用 STUN 就够跨网再加 TURN。log.level设为debug是为了注册失败时能从控制台直接看到 SIP 信令的收发内容这比抓包快得多。3.2 呼出与来电session 生命周期上的关键事件UA 注册成功后会触发registered事件呼叫则围绕session展开。呼出代码如下const session ua.call(sip:20010.0.0.10, { mediaConstraints: { audio: true, video: true }, rtcOfferConstraints: { offerToReceiveAudio: 1, offerToReceiveVideo: 1 } }) session.on(connecting, () console.log(正在建立信令)) session.on(progress, () console.log(对方响铃中)) session.on(confirmed, () console.log(通话已建立)) session.on(failed, (e) console.warn(呼叫失败:, e.cause)) session.on(ended, () console.log(通话已结束))来电处理需要挂在 UA 层的事件上ua.on(newRTCSession, (data) { const session data.session if (session.direction incoming) { session.accept({ mediaConstraints: { audio: true, video: true }, rtcOfferConstraints: { offerToReceiveAudio: 1, offerToReceiveVideo: 1 } }) } })ua.call()返回的 session 对象带connecting、progress、confirmed等事件分别在信令发出、对方振铃、媒体通道建立时触发。failed事件的cause字段会直接告诉你失败原因常见值有401/407表示鉴权失败、404表示分机不存在、486表示对方忙。来电侧先判断direction再accept()是为了避免把自己发起的呼出也当成来电处理。3.3 音视频流处理从 RTCPeerConnection 到页面播放器媒体流的获取方式跟普通音视频播放器不一样播放器只管解码单一文件流而通话场景是实时双向流必须从 RTCPeerConnection 上取。JsSIP 会在媒体协商完成后暴露peerconnection事件最稳定的取流姿势如下session.on(peerconnection, (e) { const pc e.peerconnection pc.ontrack (event) { const [stream] event.streams if (event.track.kind audio) { remoteAudio.srcObject stream remoteAudio.play().catch((err) console.warn(自动播放被拦截:, err)) } if (event.track.kind video) { remoteVideo.srcObject stream remoteVideo.play().catch((err) console.warn(自动播放被拦截:, err)) } } })本地预览画面需要通过getUserMedia先拿一份流再把localVideo.srcObject指过去。注意一个细节ua.call()内部也会调用getUserMedia相当于本地预览和通话各自申请了一次媒体轨道这在多数浏览器里会自动授权但个别系统会再次弹窗询问体验上要提前说明。ontrack是现代 WebRTC 推荐的事件接口替代了老旧的onaddstream。拿到event.streams[0]后分别挂到 audio 和 video 标签的srcObject上即可。autoplay 策略是这里的高频坑浏览器禁止未经用户手势的自动播放如果用户在点击“拨打”后没有额外交互play()的 Promise 会被 reject表现为“通话已建立但没有声音”所以必须在 catch 里做降级处理。4. jssip demo 常见问题排查与避坑5 个把你的通话搞翻车的边界条件demo 跑通不难难的是那些看起来「差不多的配置」带来的血泪坑。以下问题我基本都亲手踩过每一条按现象、原因、解决三个层面来拆希望能帮你绕开。4.1 现象切到后台标签页后摄像头黑屏、麦克风没声音原因浏览器对非活动标签页的媒体设备访问有限制。页面在后台时getUserMedia拿到的视频轨道会被系统暂停麦克风也会被静音这是浏览器层面的资源节省策略。解决通话期间让用户保持页面在前台如果是集成到客服系统里确保通话组件所在的 iframe 标签包含allowcamera; microphone属性而不是让它在隐藏的 iframe 里运行。业务上需要后台通话的就要考虑做成独立的浏览器窗口而不是后台标签页。4.2 现象UA 注册成功但拨打 100 立即返回 404原因FreeSWITCH 里分机和拨号规则是两套东西。注册成功只能证明 Sofia 目录里有这个用户但呼叫能否接通取决于 dialplan 是否匹配以及 bridge 的目标用户是否存在。解决先确认分机 XML 真的放在了directory/default下然后确认 dialplan 的condition expression^(\d)$能匹配到拨打号码最后用 fs_cli 手动跟踪呼叫fs_cli -x sofia status profile internal fs_cli -x regex fs_cli -x originate user/100 echo()originate user/100 echo()是快速验证分机是否可被桥接的好办法如果这条命令都报错说明目录配置有问题跟浏览器端无关。404 大概率不是 JsSIP 的错先查服务端。4.3 现象一方能听到对方另一方完全没声音原因通话是双向独立通道单边无声要先定位是收不到还是发不出。收不到多半是自动播放策略拦截了play()发不出则可能是本地轨道被禁用、SDP 协商成了recvonly或者麦克风设备被系统独占。解决先判断方向。如果是对方听不到你打开浏览器的 WebRTC 后台页chrome://webrtc-internals看outbound-rtp里的packetsSent是否在增长。如果一直为零说明本地音频根本没进入 WebRTC 管道检查session.connection.getSenders()里音频轨道的readyState和enabled。如果是你听不到对方就在ontrack里提前用muted方式启动播放用户点击后取消静音remoteAudio.muted true remoteAudio.play().then(() { remoteAudio.muted false remoteAudio.volume 1 }).catch(() { // 仍被拦截时提示用户点击页面任意位置后重试 })这个方案比单纯 catch 更可靠能绕过大多数自动播放拦截。4.4 现象WSS 握手失败浏览器控制台报 ERR_CERT_AUTHORITY_INVALID原因demo 阶段几乎都在用自签证书浏览器对 WebSocket 的证书校验和对 HTTPS 一致遇到不信任的证书会直接拒绝连接。Firefox 比 Chrome 更严格Chrome 至少访问过 HTTPS 页面后可以手动点「继续访问」Firefox 有时会让你先把证书导入系统信任链。解决开发环境最省事的做法是先用浏览器访问一次https://服务器IP:7443在证书警告页手动信任如果要给多人联调就把自签证书的 CA 加入各操作系统信任库。不要一上来就关闭浏览器的证书校验开关——它会让后续排查变成一团乱麻。4.5 现象内网两台浏览器能正常通话跨网络一方始终只有单向或根本不通原因媒体流基于 ICE 打洞内网环境通常能用主机候选host直接互连跨网络时如果 NAT 类型是对称型STUN 拿到的srflx候选无法让对方连通必须有 TURN 服务器的relay候选兜底。解决在同一个网段验证业务逻辑跨网验证网络链路。给ice_servers加上 TURNice_servers: [ { urls: stun:stun.example.com:3478 }, { urls: turn:turn.example.com:3478, username: turnuser, credential: turnpass } ]部署 TURN 服务器是另一篇文章的深度但最小验证方式是起一个 coturn 进程配置好realm、lt-cred-mech和用户账号。重点不是记住 TURN 配置而是要知道WebRTC 的iceconnectionstatechange事件里一旦长期停在checking或出现failed一定要先去看 ICE candidate 是不是只有 host 和 srflx、没有 relay。5. 把 demo 推向可用的三个进阶动作5.1 用 webrtc-internals 验证媒体是否真的在传输我自己的习惯是所有「感觉没声音」「画面不动」的问题先打开chrome://webrtc-internals看inbound-rtp的packetsReceived和outbound-rtp的packetsSent。这两个计数器每秒都在跳说明媒体实际在流动如果数字不动说明问题在网络或协商层面而不是页面代码。这个页面还能直接看到完整 SDP方向属性sendonly/recvonly/inactive一目了然。5.2 加 TURN、DTMF 拨号和本地录音TURN 是跨网络可用性的底线没有它demo 永远只能给内网演示。想让它更接近产品形态至少还要加两样DTMF 拨号用于接通 IVR 按键按位发送session.sendDTMF(1)录音则用浏览器原生 MediaRecorder 录制远端媒体流配合ontrack拿到的 stream 就能直接存成 WebM 文件。这三样加起来一个 demo 才具备了对外展示的完整度。5.3 屏幕共享与 AI 音视频处理的延伸方向再往前走一步就是屏幕共享调用getDisplayMedia()拿到屏幕轨道后用pc.getSenders().find(s s.track.kind video).replaceTrack(displayStream.getVideoTracks()[0])替换视频轨道配合重协商即可实现。AI 音视频处理方面WebRTC 原生的echoCancellation和noiseSuppression已经能压住大部分环境噪声更专业的降噪和虚拟背景通常会把音频或视频轨道做中间层后处理再喂给RTCPeerConnection。我的教训是demo 阶段不要一口气把这些全堆进去先保证一条干净的音视频链路再逐个叠加否则出了问题你根本不知道是哪一层造成的。这个方向最值得投入的时间恰恰不是写代码而是把证书信任链、TURN 部署和浏览器媒体策略这三件基础设施钉死。我接到类似需求第一件事就是先确认这三样再谈 UI 和功能这个习惯帮我省掉过无数次「代码没动重启一下好了」的玄学调式。希望帮到你。本文还有配套的精品资源点击获取