版權聲明:本文為CSDN博主「foruok」的原創文章,遵循 CC 4.0 BY-SA 版權協議,轉載請附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/foruok/article/details/74321214
之前幾篇文件介紹了 freeSWITCH 和 WebRTC 結合在一起需要的各種環境,現在到了最關鍵的一篇,使用 JsSIP 來創建一個 DEMO 。這次我們需要寫點 JS 代碼。
准備 JsSIP 庫文件
可以從 http://www.jssip.net/download/ 下載一個 min 版的 js 文件,我用的是 3.0.13 ,文件名是 jssip-3.0.13.min.js ,把它放在我們之前用 Node.js 建立的 https 服務器的 public/js 目錄下,我們將在 html 文件內引用它。類似:
<script src="js/jssip-3.0.13.min.js" type="text/javascript"></script>
配置 freeSWITCH
我們之前下載的 freeSWITCH ,默認是不處理音視頻編解碼的,所以,要設置它采用 media proxy 模式來代理轉發 WebRTC 的音視頻,這樣就可以基於 JsSIP 、 WebRTC 、 freeSWITCH 來一對一視頻聊天。
修改vars.xml,加入:
<X-PRE-PROCESS cmd=="set" data="proxy_media=true"/>
修改sip_profiles/internal.xml,設置inbound-proxy-media和inbound-late-negotiation為true,類似下面:
<!--Uncomment to set all inbound calls to proxy media mode--> <param name="inbound-proxy-media" value="true"/> <!-- Let calls hit the dialplan before selecting codec for the a-leg --> <param name="inbound-late-negotiation" value="true"/>
這樣配置之后,freeSWITCH 會進入代理模式,不對media 做任何處理,直接在兩個 end peer 之間轉發(RTP包)。
JsSIP DEMO
JsSIP 的 API 文檔參考下面鏈接:
http://www.jssip.net/documentation/3.0.x/api/
http://www.jssip.net/documentation/3.0.x/api/session/
http://www.jssip.net/documentation/3.0.x/api/ua/
注意, JsSIP 對 SIP 和 WebRTC 做了封裝,比如你不需要自己調用 getUserMedia 來捕獲音視頻了, JsSIP 會根據你傳給JsSIP.UA.call方法的參數來自己調用,用起來比較方便。
但是,你還是要了解 SIP 呼叫的流程和WebRTC的各種限制以及如何處理 RTCPeerConnection 發過來的音視頻流。
關於 WebRTC JS 側 API,看這里好了:http://w3c.github.io/webrtc-pc/。
想看更多資料,可以看我搜集的這些鏈接:WebRTC學習資料大全。
關於 SIP 的流程,參考《freeSWITCH權威指南》這本書吧,講得很明白。
網上關於 JsSIP + freeSWITCH 的 demo 很少,而且基本跑不起來……我這個是驗證過的啦!
直接給出我們的 demo.html 的所有代碼:
<!DOCTYPE html> <html> <head> <title>JsSIP + WebRTC + freeSWITCH</title> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <meta name="Author" content="foruok" /> <meta name="description" content="JsSIP based example web application." /> <script src="./jssip-3.3.11.min.js" type="text/javascript"></script> <style type="text/css"> </style> </head> <body> <div id="login-page" style="width: 424px; height: 260px; background-color: #f2f4f4; border: 1px solid grey; padding-top: 4px"> <table border="0" frame="void" width="418px"> <tr> <td class="td_label" width="160px" align="right"><label for="sip_uri">SIP URI:</label></td> <td width="258px"><input style="width:250px" id="sip_uri" type="text" placeholder="SIP URI (i.e: sip:alice@example.com)" value=""/></td> </tr> <tr> <td class="td_label" align="right"><label for="sip_password">SIP Password:</label></td> <td><input style="width:250px" id="sip_password" type="password" placeholder="SIP password" value=""/></td> </tr> <tr> <td class="td_label" align="right"><label for="ws_uri">WSS URI:</label></td> <td><input style="width:250px" id="ws_uri" class="last unset" type="text" placeholder="WSS URI (i.e: wss://example.com)" value=""/></td> </tr> <tr> <td class="td_label" align="right"><label class="input_label" for="sip_phone_number">SIP Phone Info:</label></td> <td><input style="width:250px" id="sip_phone_number" type="text" placeholder="sip:3000@192.168.40.96:5060" value=""></td> </tr> <tr> <td colspan="2" align="center"><button onclick="testStart()"> Initialize </button></td> </tr> <tr> <td colspan="2" align="center"><button onclick="testCall()"> Call </button></td> </tr> <tr> <td colspan="2" align="center"><button onclick="captureLocalMedia()"> Capture Local Media</button></td> </tr> </table> </div> <div style="width: 424px; height: 324px;background-color: #333333; border: 2px solid blue; padding:0px; margin-top: 4px;"> <video id="video" width="420px" height="320px" autoplay ></video> <audio id="audio" controls></audio> </div> </body> <script type="text/javascript"> var outgoingSession = null; var incomingSession = null; var currentSession = null; var audio = document.getElementById('audio'); var constraints = { audio: true, video: true, mandatory: { maxWidth: 640, maxHeight: 360 } }; URL = window.URL || window.webkitURL; var localStream = null; var userAgent = null; function gotLocalMedia(stream) { console.info('Received local media stream'); localStream = stream; audio.src = URL.createObjectURL(stream); } function captureLocalMedia() { console.info('Requesting local video & audio'); navigator.webkitGetUserMedia(constraints, gotLocalMedia, function(e){ alert('getUserMedia() error: ' + e.name); }); } function testStart(){ var sip_uri_ = document.getElementById("sip_uri").value.toString(); var sip_password_ = document.getElementById("sip_password").value.toString(); var ws_uri_ = document.getElementById("ws_uri").value.toString(); console.info("get input info: sip_uri = ", sip_uri_, " sip_password = ", sip_password_, " ws_uri = ", ws_uri_); var socket = new JsSIP.WebSocketInterface(ws_uri_); var configuration = { sockets: [ socket ], outbound_proxy_set: ws_uri_, uri: sip_uri_,//與用戶代理關聯的SIP URI(字符串)。這是您的提供商提供給您的SIP地址 password: sip_password_,//SIP身份驗證密碼 register: true,//指示啟動時JsSIP用戶代理是否應自動注冊 session_timers: false//啟用會話計時器(根據RFC 4028) }; userAgent = new JsSIP.UA(configuration); //成功注冊成功,data:Response JsSIP.IncomingResponse收到的SIP 2XX響應的實例 userAgent.on('registered', function(data){ console.info("registered: ", data.response.status_code, ",", data.response.reason_phrase); }); //由於注冊失敗而被解雇,data:Response JsSIP.IncomingResponse接收到的SIP否定響應的實例,如果失敗是由這樣的響應的接收產生的,否則為空 userAgent.on('registrationFailed', function(data){ console.log("registrationFailed, ", data); //console.warn("registrationFailed, ", data.response.status_code, ",", data.response.reason_phrase, " cause - ", data.cause); }); //1.在注冊到期之前發射幾秒鍾。如果應用程序沒有為這個事件設置任何監聽器,JsSIP將像往常一樣重新注冊。 // 2.如果應用程序訂閱了這個事件,它負責ua.register()在registrationExpiring事件中調用(否則注冊將過期)。 // 3.此事件使應用程序有機會在重新注冊之前執行異步操作。對於那些在REGISTER請求中的自定義SIP頭中使用外部獲得的“令牌”的環境很有用。 userAgent.on('registrationExpiring', function(){ console.warn("registrationExpiring"); }); //為傳入或傳出會話/呼叫激發。data: // originator:'remote',新消息由遠程對等方生成;'local',新消息由本地用戶生成。 // session:JsSIP.RTCSession 實例。 // request:JsSIP.IncomingRequest收到的MESSAGE請求的實例;JsSIP.OutgoingRequest傳出MESSAGE請求的實例 userAgent.on('newRTCSession', function(data){ console.info('onNewRTCSession: ', data); if(data.originator == 'remote'){ //incoming call console.info("incomingSession, answer the call"); incomingSession = data.session; //回答傳入會話。此方法僅適用於傳入會話。 data.session.answer({'mediaConstraints' : { 'audio': true, 'video': true }, // 'mediaStream': localStream }); }else{ console.info("outgoingSession"); outgoingSession = data.session; outgoingSession.on('connecting', function(data){ console.info('onConnecting - ', data.request); currentSession = outgoingSession; outgoingSession = null; }); } //接受呼叫時激發 data.session.on('accepted', function(data){ console.info('onAccepted - ', data); if(data.originator == 'remote' && currentSession == null){ currentSession = incomingSession; incomingSession = null; console.info("setCurrentSession - ", currentSession); } }); //確認呼叫后激發 data.session.on('confirmed', function(data){ console.info('onConfirmed - ', data); if(data.originator == 'remote' && currentSession == null){ currentSession = incomingSession; incomingSession = null; console.info("setCurrentSession - ", currentSession); } }); //在將遠程SDP傳遞到RTC引擎之前以及在發送本地SDP之前激發。此事件提供了修改傳入和傳出SDP的機制。 data.session.on('sdp', function(data){ console.info('onSDP, type - ', data.type, ' sdp - ', data.sdp); //data.sdp = data.sdp.replace('UDP/TLS/RTP/SAVPF', 'RTP/SAVPF'); //console.info('onSDP, changed sdp - ', data.sdp); }); //接收或生成對邀請請求的1XX SIP類響應(>100)時激發。該事件在SDP處理之前觸發(如果存在),以便在需要時對其進行微調,甚至通過刪除數據對象中響應參數的主體來刪除它 data.session.on('progress', function(data){ console.info('onProgress - ', data.originator); if(data.originator == 'remote'){ console.info('onProgress, response - ', data.response); } }); //創建基礎RTCPeerConnection后激發。應用程序有機會通過在peerconnection上添加RTCDataChannel或設置相應的事件偵聽器來更改peerconnection。 data.session.on('peerconnection', function(data){ console.info('onPeerconnection - ', data.peerconnection); data.peerconnection.onaddstream = function(ev){ console.info('onaddstream from remote - ', ev); audio.src = URL.createObjectURL(ev.stream); audio.onloadstart = () => { audio.play(); }; audio.onerror = () => { alert('錄音加載失敗...'); }; }; }); }); //為傳入或傳出消息請求激發。data: // originator:'remote',新消息由遠程對等方生成;'local',新消息由本地用戶生成。 // message:JsSIP.Message 實例。 // request:JsSIP.IncomingRequest收到的MESSAGE請求的實例;JsSIP.OutgoingRequest傳出MESSAGE請求的實例 userAgent.on('newMessage', function(data){ if(data.originator == 'local'){ console.info('onNewMessage , OutgoingRequest - ', data.request); }else{ console.info('onNewMessage , IncomingRequest - ', data.request); } }); console.info("call register"); //連接到信令服務器,並恢復以前的狀態,如果以前停止。重新開始時,如果UA配置中的參數設置為register:true,則向SIP域注冊。 userAgent.start(); } // Register callbacks to desired call events var eventHandlers = { 'progress': function(e) { console.log('call is in progress'); }, 'failed': function(e) { console.log('call failed: ', e); }, 'ended': function(e) { console.log('call ended : ', e); }, 'confirmed': function(e) { console.log('call confirmed'); } }; function testCall(){ var sip_phone_number_ = document.getElementById("sip_phone_number").value.toString(); var options = { 'eventHandlers' : eventHandlers, 'mediaConstraints' : { 'audio': true, 'video': false , }, //'mediaStream': localStream }; //outgoingSession = userAgent.call('sip:3000@192.168.40.96:5060', options); /* * 撥打多媒體電話。不需要自己調用 getUserMedia 來捕獲音視頻了, JsSIP 會根據你傳給JsSIP.UA.call方法的參數來自己調用 參數 Target 通話的目的地。String表示目標用戶名或完整的SIP URI或JsSIP.URI實例。 Options 可選Object附加參數(見下文)。 options對象中的字段; mediaConstraints Object有兩個有效的字段(audio和video)指示會話是否打算使用音頻和/或視頻以及要使用的約束。默認值是audio並且video設置為true。 mediaStream MediaStream 傳送到另一端。 eventHandlers Object事件處理程序的可選項將被注冊到每個呼叫事件。為每個要通知的事件定義事件處理程序。 */ outgoingSession = userAgent.call(sip_phone_number_, options); } </script> </html>
注意:我用的3.3.11和原作者有點不同
session.on('peerconnection')事件沒有觸發
//我又添加了另一個事件,能拿到遠程的音頻流
data.session.connection.addEventListener("addstream", function (ev) {
console.info('onaddstream from remote - ', ev.stream);
audio.srcObject = ev.stream;
});
關於 JsSIP.UA 和 JsSIP.RTCSession 等類和 API 的使用,參考代碼,對照 JsSIP 的官方文檔,即可理解。
注意我在代碼里調用 RTCSession 的 answer 方法做了自動接聽。實際開發中,你需要彈出一個提示框,讓用戶選擇是否接聽。
一對一視頻聊天的效果:
先運行 freeSWITCH , 驗證啟動正常(參看freeSWITCH安裝、配置與局域網測試),再運行 npm start啟動 https 服務器,最后 Chrome 內訪問 https://192.168.40.131:8080/demo.html ,看到下面的結果:
注意,必須填寫有效信息,然后先點擊 Initialize 來初始化,代碼中會到 freeSWITCH 那里注冊 SIP 號碼,然后才可以呼叫別人。
被呼叫方只需要填寫信息,點擊 Initialize 按鈕,不需要點擊 Call 按鈕。
填寫所有參數,點擊 Initialize 按鈕,可以注冊一個 SIP 號碼 1000(使用開發者工具可查看我輸出的日志)。
然后到另一台電腦訪問同一個鏈接,注冊一個不同的賬號 1002。
再回到初始的那台電腦,在 SIP Phone Info 后輸入 sip:1002@192.168.40.131:5060,然后點擊 Call 按鈕,即可呼叫 1002 ,接通后,可以看到對方視頻。效果如下:
這是我們使用 JsSIP 和 freeSWITCH 構建視頻聊天的簡單 DEMO 。
想起來一點非常重要的:freeSWITCH 和 nodejs 實現的 https 服務器,使用的證書應該是一個,否則會報錯哦。可以先跑 freeSWITCH ,然后把 cert/wss.pem 文件的內容分拆給 nodejs 使用。