WebRTC + JsSIP + freeSWITCH一對一視頻聊天
2017年07月04日 14:19:52 foruok 閱讀數:10494 標籤: webrtc freeSWITCH Chrome Nodejs voip 更多
個人分類: 多媒體
版權聲明:本文爲foruok原創文章,轉載請通過訂閱號“程序視界”聯繫foruok獲取授權。 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>
- 1
配置 freeSWITCH
我們之前下載的 freeSWITCH ,默認是不處理音視頻編解碼的,所以,要設置它採用 media proxy 模式來代理轉發 WebRTC 的音視頻,這樣就可以基於 JsSIP 、 WebRTC 、 freeSWITCH 來一對一視頻聊天。
修改vars.xml,加入:
<X-PRE-PROCESS cmd=="set" data="proxy_media=true"/>
- 1
修改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"/>
- 1
- 2
- 3
- 4
- 5
這樣配置之後,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="js/jssip-3.0.13.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:[email protected])"/></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"/></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)"/></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:[email protected]:5060"></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="videoView" width="420px" height="320px" autoplay ></video>
</div>
</body>
<script type="text/javascript">
var outgoingSession = null;
var incomingSession = null;
var currentSession = null;
var videoView = document.getElementById('videoView');
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;
videoView.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_,
password: sip_password_,
register: true,
session_timers: false
};
userAgent = new JsSIP.UA(configuration);
userAgent.on('registered', function(data){
console.info("registered: ", data.response.status_code, ",", data.response.reason_phrase);
});
userAgent.on('registrationFailed', function(data){
console.log("registrationFailed, ", data);
//console.warn("registrationFailed, ", data.response.status_code, ",", data.response.reason_phrase, " cause - ", data.cause);
});
userAgent.on('registrationExpiring', function(){
console.warn("registrationExpiring");
});
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, mandatory: { maxWidth: 640, maxHeight: 360 } }, '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);
}
});
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);
});
data.session.on('progress', function(data){
console.info('onProgress - ', data.originator);
if(data.originator == 'remote'){
console.info('onProgress, response - ', data.response);
}
});
data.session.on('peerconnection', function(data){
console.info('onPeerconnection - ', data.peerconnection);
data.peerconnection.onaddstream = function(ev){
console.info('onaddstream from remote - ', ev);
videoView.src = URL.createObjectURL(ev.stream);
};
});
});
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");
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': true ,
mandatory: { maxWidth: 640, maxHeight: 360 }
},
'mediaStream': localStream
};
//outgoingSession = userAgent.call('sip:[email protected]:5060', options);
outgoingSession = userAgent.call(sip_phone_number_, options);
}
</script>
</html>
關於 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:[email protected]:5060
,然後點擊 Call 按鈕,即可呼叫 1002 ,接通後,可以看到對方視頻。效果如下:
我測試的攝像頭有問題,出來的是抽象視頻……
這是我們使用 JsSIP 和 freeSWITCH 構建視頻聊天的簡單 DEMO 。
想起來一點非常重要的:freeSWITCH 和 nodejs 實現的 https 服務器,使用的證書應該是一個,否則會報錯哦。可以先跑 freeSWITCH ,然後把 cert/wss.pem 文件的內容分拆給 nodejs 使用。