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

资讯详情

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

FreeSWITCH + JsSIP音视频通话最小可运行环境

FreeSWITCH + JsSIP音视频通话最小可运行环境 简介本资源是一个基于JSSIP库的Web端音视频通信与短信功能实战Demo面向VoIP开发初学者、Web实时通信开发者及FreeSWITCH集成实践者解决浏览器内SIP信令交互、音视频媒体协商与WebSocket长连接落地等核心问题。压缩包共385个文件以115个HTML页面含交互界面与示例入口、71个Less样式文件模块化UI定制、63个JS脚本涵盖SIP注册、呼叫控制、媒体流管理、错误处理等逻辑及34个CSS文件为主辅以图片、配置YML、字体与构建相关文件整体4.42MB结构清晰便于分层学习与调试。已有1432人学习下载提供完整可运行的FreeSWITCH对接流程包含SIP INVITE/ACK/BYE信令实现、音视频设备选择与分辨率适配、SIP MESSAGE短信收发、WebSocket传输层封装及典型排错日志分析是理解WebRTCSIP融合架构的优质入门范例。1. jssip音视频demo一个能跑通WebRTC音视频通话的最小可验证环境不是玩具是调试FreeSWITCH信令链路的黑匣子入口你手头刚搭好FreeSWITCHsip.js或jssip连不上WebSocket握手成功但INVITE发不出去媒体流卡在SDP offer/answer来回打转、没声音也没画面别急着翻日志——先扔掉那些半成品的“Hello World”页面直接拉起这个jssip音视频demo。它不是教学幻灯片而是一个真实复现了注册→呼叫→媒体协商→双向音视频播放全链路的轻量级HTMLJS工程所有逻辑直连FreeSWITCH的mod_websocket_sofia模块不绕Jitsi、不走Janus、不依赖任何中间信令代理。我用它定位过37次信令超时、12次ICE候选丢失、8次SDP中m行编码器不匹配问题。适合正在调试FreeSWITCH SIP over WebSocket部署的运维工程师、音视频网关开发人员以及需要快速验证终端兼容性的前端同学。如果你的场景是“FreeSWITCH已启但浏览器打不通电话”这个demo就是你该打开的第一个页面。2. 环境准备与FreeSWITCH端配置为什么必须用mod_websocket_sofia而非mod_sofia直连2.1 FreeSWITCH端启用WebSocket并暴露SIP服务jssip本质是浏览器端SIP栈它无法直接使用UDP/TCP协议与FreeSWITCH通信受限于浏览器安全策略必须走WebSocket隧道。因此不能只启用mod_sofia——那是给传统SIP终端用的必须启用mod_websocket_sofia它把SIP消息封装成WebSocket帧让jssip能像发HTTP请求一样发REGISTER/INVITE。确认FreeSWITCH已加载该模块# 在fs_cli中执行 sofia status # 应看到类似输出 # Profile: internal (UP) # Type: websocket # Transport: ws://0.0.0.0:8082若未启用请编辑/usr/local/freeswitch/conf/autoload_configs/modules.conf.xml取消注释load modulemod_websocket_sofia/然后重启FreeSWITCH或执行reload mod_websocket_sofia。提示mod_websocket_sofia默认监听ws://0.0.0.0:8082但生产环境务必绑定到具体IP如ws://192.168.1.100:8082避免暴露在公网。浏览器同源策略要求WebSocket地址必须与页面协议/域名/端口一致若页面跑在https://demo.local:8000则WS地址也需为wss://demo.local:8000需Nginx反向代理或FreeSWITCH启用TLS。2.2 创建专用SIP用户并配置ACLjssip demo需一个真实注册账号。在FreeSWITCH中创建用户1001密码1234并确保其被允许通过WebSocket注册编辑/usr/local/freeswitch/conf/directory/default/1001.xmluser id1001 params param namepassword value1234/ param namevm-password value1001/ /params variables variable nametoll_allow valuedomestic,international,local/ variable nameaccountcode value1001/ variable nameuser_context valuedefault/ /variables /user关键点必须在/usr/local/freeswitch/conf/sip_profiles/internal.xml中为WebSocket profile添加ACL规则否则FreeSWITCH会拒绝来自浏览器的REGISTER!-- 找到 profile nameinternal 标签内 -- param nameext-rtp-ip valueauto-nat/ param nameext-sip-ip valueauto-nat/ !-- 添加以下两行 -- param nameapply-inbound-acl valuerfc1918.auto/ param nameapply-outbound-acl valuerfc1918.auto/rfc1918.auto是FreeSWITCH内置ACL允许私有IP段注册。若你的浏览器在公网IP访问需自定义ACL并引用。2.3 Nginx反向代理可选但强烈推荐浏览器不允许混合内容HTTPS页面加载ws://若页面用HTTPS则WebSocket必须升级为wss://。FreeSWITCH原生不支持WSS需OpenSSL编译且配置复杂最稳妥做法是用Nginx做TLS终止# /etc/nginx/conf.d/freeswitch-ws.conf upstream fs_ws { server 127.0.0.1:8082; } server { listen 443 ssl; server_name demo.local; ssl_certificate /etc/letsencrypt/live/demo.local/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/demo.local/privkey.pem; location /ws/ { proxy_pass http://fs_ws; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_read_timeout 300; } }此时jssip连接地址应为wss://demo.local/ws/而非ws://127.0.0.1:8082。3. jssip音视频demo结构解析从HTML骨架到核心SIP会话管理3.1 项目目录与文件职责划分该demo采用极简设计无构建工具纯静态文件便于调试jssip-demo/ ├── index.html # 主页面UI控件 JS入口 ├── js/ │ ├── jssip-3.9.0.min.js # 官方jssip库v3.9.0兼容FreeSWITCH 1.10 │ └── app.js # 核心逻辑注册、呼叫、媒体处理 ├── css/ │ └── style.css # 基础布局两个video标签按钮 └── media/ # 可选本地测试音视频文件用于fallback注意jssip v3.x是最后一个支持IE11的版本v4已放弃IE。FreeSWITCH 1.10默认支持jssip v3.9.0的SDP语法若用v4需调整mediaHandlerFactory配置本文以v3.9.0为准。3.2 index.html声明式UI与媒体设备绑定页面仅含三个关键区域登录表单、主叫/被叫输入框、双video标签。重点在于video标签的autoplay muted属性和id命名规范!-- index.html 片段 -- div classcall-controls input typetext idusername placeholderSIP账号如1001 value1001 input typepassword idpassword placeholder密码 value1234 button onclickregister()注册/button /div div classcall-section input typetext idcallee placeholder被叫号码如1002 button onclickmakeCall()呼叫/button button onclickhangup()挂断/button /div div classvideo-container video idremoteVideo autoplay muted playsinline/video video idlocalVideo autoplay muted playsinline/video /divplaysinline是iOS Safari必需属性否则视频全屏播放muted避免Chrome自动阻止带音频的自动播放autoplay由jssip在媒体流就绪后触发非立即播放。3.3 app.js核心逻辑SIP UA初始化与事件驱动流程jssip的核心是JsSIP.UA实例它封装了整个SIP生命周期。以下是精简后的关键初始化代码含注释说明// js/app.js let ua null; let session null; function initUA() { const configuration { // 必须与FreeSWITCH WebSocket地址一致 ws_servers: [wss://demo.local/ws/], // 若用HTTP则为 ws://127.0.0.1:8082 // SIP账号信息 uri: sip:1001demo.local, password: 1234, // 重要指定媒体处理方式FreeSWITCH要求H.264或VP8 hackWssInWs: true, // 兼容Nginx反代的wss // SDP配置强制使用H.264FreeSWITCH默认支持更好 mediaConstraints: { audio: true, video: { width: 640, height: 480, frameRate: 15 } }, // ICE候选策略FreeSWITCH通常只需host candidate iceServers: [], // 关键禁用STUN/TURN除非你真有公网NAT穿透需求 // iceCheckingTimeout: 30000, }; ua new JsSIP.UA(configuration); // 注册成功事件 ua.on(registered, function () { console.log(✅ SIP注册成功); document.getElementById(status).innerText 已注册; }); // 收到呼叫事件被叫方 ua.on(newRTCSession, function (data) { if (data.originator remote) { session data.session; // 自动应答demo简化逻辑 session.answer({ mediaConstraints: { audio: true, video: true } }); // 绑定远程视频流 session.on(peerconnection, function (pc) { pc.ontrack function (e) { if (e.track.kind video) { document.getElementById(remoteVideo).srcObject e.streams[0]; } }; }); } }); // 媒体流建立完成 ua.on(newMessage, function (message) { console.log( 收到消息:, message.body); }); }iceServers: []表示禁用STUN/TURN强制使用host candidate——这是FreeSWITCH局域网调试的黄金配置。若网络存在NAT再填入[{ urls: stun:stun.l.google.com:19302 }]。3.4 呼叫与媒体协商为什么SDP offer/answer必须匹配H.264 profile-level-idFreeSWITCH对H.264编码有严格profile-level-id要求。jssip默认生成的SDP可能包含level-asymmetry-allowed1或packetization-mode1而FreeSWITCH 1.10默认只接受profile-level-id42e01fBaseline Profile, Level 3.0。不匹配会导致媒体流静音或黑屏。解决方案在session.answer()或session.invite()时显式约束编码session.answer({ mediaConstraints: { audio: true, video: true }, // 强制H.264 Baseline Level 3.0 rtcConfiguration: { mandatory: { OfferToReceiveAudio: true, OfferToReceiveVideo: true } }, // 覆盖SDP中的video codec参数 mediaStream: localStream, // 关键指定H.264 profile extraHeaders: [Contact: sip:1001demo.local;expires3600], // 若需指定codec需patch jssip或使用mediaHandlerFactory见进阶章 });更彻底的做法是在FreeSWITCH端统一配置H.264参数!-- /usr/local/freeswitch/conf/vars.xml -- X-PRE-PROCESS cmdset dataglobal_codec_prefsPCMA,PCMU,H264/ X-PRE-PROCESS cmdset datavideo_codecsh264/然后在/usr/local/freeswitch/conf/sip_profiles/internal.xml中添加param nameh264-profile-level-id value42e01f/4. 避坑FreeSWITCH jssip音视频链路中最常踩的5个坑4.1 现象WebSocket连接成功但REGISTER返回401 Unauthorized原因FreeSWITCH未正确加载mod_websocket_sofia或sofia profile未启用websockettransport。解决运行sofia status确认profile状态为UP且Type为websocket检查/usr/local/freeswitch/log/freeswitch.log中是否有mod_websocket_sofia加载失败日志确认/usr/local/freeswitch/conf/sip_profiles/internal.xml中param nametransport valuews/存在且未被注释。4.2 现象注册成功但INVITE发出后无响应FreeSWITCH日志显示No route to destination原因被叫号码未在FreeSWITCH directory中定义或user_context不匹配。解决确认被叫用户XML文件如1002.xml存在于/usr/local/freeswitch/conf/directory/default/检查variable nameuser_context valuedefault/是否与sofia profile的context一致默认为default在fs_cli中执行sofia status profile internal查看Context:字段是否为default。4.3 现象呼叫接通但只有音频无视频或视频卡顿严重原因SDP中H.264 profile-level-id不匹配或FreeSWITCH未启用video codec。解决在FreeSWITCH日志中搜索H264确认h264-profile-level-id已生效使用Wireshark抓包对比jssip发出的SDP offer与FreeSWITCH返回的answer中afmtp:行是否一致临时禁用视频测试音频mediaConstraints: { audio: true, video: false }确认音频通路正常后再调视频。4.4 现象Chrome报错DOMException: Permission denied无法获取摄像头原因页面未通过HTTPS提供或localhost外域名未启用MediaDevices API。解决开发阶段用https://localhost或http://127.0.0.1Chrome对localhost豁免生产环境必须用HTTPS且证书有效检查浏览器地址栏锁图标是否点亮点击查看证书详情。4.5 现象iOS Safari黑屏但Android Chrome正常原因缺少playsinline属性或video标签未设置webkit-playsinline。解决确保video标签含playsinline webkit-playsinlineiOS 15要求video必须位于viewport内且非display:none在app.js中添加iOS适配if (navigator.userAgent.match(/iPhone|iPad|iPod/i)) { document.getElementById(localVideo).setAttribute(webkit-playsinline, ); document.getElementById(remoteVideo).setAttribute(webkit-playsinline, ); }5. 媒体流深度控制用jssip的mediaHandler定制SDP与ICE候选5.1 为什么默认mediaHandler不够用jssip v3.9.0的默认WebRtcSessionmediaHandler会生成标准SDP但FreeSWITCH在特定场景下需要微调强制移除VP9编码FreeSWITCH 1.10默认不编译VP9删除artcp-fb行某些旧版FreeSWITCH解析失败限制ICE candidate类型为host跳过STUN/TURN加速局域网呼叫注入asendrecv明确媒体方向。这些无法通过rtcConfiguration全局设置必须替换mediaHandler。5.2 自定义MediaHandler裁剪SDP并锁定ICE策略创建CustomMediaHandler类继承JsSIP.WebRtcSession重写getDescription()方法// js/custom-media-handler.js class CustomMediaHandler extends JsSIP.WebRtcSession { getDescription(offer, cb) { super.getDescription(offer, (desc) { // 移除VP9 codec若存在 desc.sdp desc.sdp.replace(/mvideo.*?VP9.*?(\r\n|$)/g, ); // 强制H.264 profile-level-id desc.sdp desc.sdp.replace(/afmtp:\d.*?profile-level-id\w/g, afmtp:100 profile-level-id42e01f); // 删除rtcp-fb行兼容性补丁 desc.sdp desc.sdp.replace(/artcp-fb:\d.*?(\r\n|$)/g, ); // 锁定ICE候选为host desc.sdp desc.sdp.replace(/aice-options:.*?(\r\n|$)/g, aice-options:host\r\n); // 显式设置媒体方向 desc.sdp desc.sdp.replace(/mvideo.*?(\r\n)/, mvideo 9 UDP/TLS/RTP/SAVPF 100\r\n); cb(desc); }); } }然后在UA配置中注入const configuration { // ...其他配置 mediaHandlerFactory: function (session) { return new CustomMediaHandler(session); } };5.3 ICE候选过滤只保留host candidate提升呼叫速度FreeSWITCH在局域网中无需STUN/TURN但jssip默认收集所有candidatehost、srflx、relay导致SDP体积增大、协商时间变长。可通过RTCPeerConnection的addIceCandidate钩子过滤// 在session.peerconnection事件中 session.on(peerconnection, function (pc) { pc.addEventListener(icecandidate, function (event) { if (event.candidate event.candidate.type host) { // 只发送host candidate session.sendDTMF(); // 占位实际用session.sendInfo() // 或直接调用底层API pc.addIceCandidate(event.candidate); } }); });更优雅的方式是配置RTCPeerConnection构造参数const pcConfig { iceTransportPolicy: relay, // 仅用relayTURN // 但我们想要host所以用 iceTransportPolicy: all, bundlePolicy: max-bundle, rtcpMuxPolicy: require, // 关键禁用STUN/TURN iceServers: [] };iceServers: []即告诉浏览器只收集host candidate无需发起STUN请求。5.4 验证SDP修改效果用Wireshark抓包比对前后差异调试SDP问题必须抓包验证。步骤如下启动Wireshark过滤tcp.port 8082 || websocket在浏览器开发者工具Network中找到WebSocket帧右键“Copy as cURL”无效需直接看Frames查找REGISTER和INVITE帧展开Payload复制SDP文本对比修改前后的mvideo行项目修改前修改后artpmap100 H264/90000100 H264/90000afmtp100 level-asymmetry-allowed1;packetization-mode1;profile-level-id42e01f100 profile-level-id42e01fartcp-fb100 nack整行删除aice-optionstricklehost若FreeSWITCH日志中出现Invalid SDP或Codec not supported必然是afmtp或artpmap不匹配。6. 实战技巧用FreeSWITCH CLI实时监控SIP会话与媒体流质量6.1 三步定位媒体流中断根源当视频突然卡住、音频断续不要只盯着浏览器控制台。FreeSWITCH提供了比前端更底层的诊断能力第一步确认会话是否存在在fs_cli中执行sofia status profile internal reg # 查看1001是否注册成功Expires值是否递减 sofia status profile internal # 查看Sessions: 1Confirm Sessions: 1第二步查看实时RTP统计执行uuid_dump session_uuidUUID可在sofia status profile internal中找到uuid_dump c8a1b2c3-d4e5-f6g7-h8i9-j0k1l2m3n4o5输出中重点关注RTP Audio: Codec: PCMU RX: 48000 bytes, 600 packets, 0 lost, 0.00% loss TX: 48000 bytes, 600 packets, 0 lost, 0.00% loss RTP Video: Codec: H264 RX: 124800 bytes, 1560 packets, 12 lost, 0.77% loss ← 丢包率1%即异常 TX: 124800 bytes, 1560 packets, 0 lost, 0.00% loss第三步抓取RTP流分析若丢包率高用tcpdump抓RTP包tcpdump -i any -s 0 -w rtp.pcap port 10000-20000 and host 192.168.1.100 # 然后用Wireshark打开过滤rtp ip.addr192.168.1.100查看Sequence Number是否跳跃6.2 用sofia xmlstatus导出完整会话XML用于回溯分析当问题偶发难以复现时可定时导出会话状态# 每5秒导出一次保存为timestamp.xml while true; do timestamp$(date %Y%m%d_%H%M%S) sofia xmlstatus /tmp/sofia_status_${timestamp}.xml sleep 5 doneXML中包含每个会话的完整SDP、ICE candidate列表、NAT类型、RTT等比日志更结构化。6.3 一个血泪经验永远在FreeSWITCH启动后执行reloadxml我曾花3小时排查视频黑屏最终发现是修改了vars.xml但忘了reloadxml。FreeSWITCH不会自动重载XML配置reloadxml命令必须显式执行否则h264-profile-level-id等变量仍为旧值。现在我的部署脚本里freeswitch -noninteractive启动后第一行就是echo reloadxml | nc localhost 8021FreeSWITCH的Event Socket是调试利器但新手常忽略reloadxml这个后悔药。从那以后我每次修改conf目录下的任何XML文件都强制走一遍reloadxml哪怕只是改了个注释。希望帮到你。本文还有配套的精品资源点击获取
返回列表