一 、 概述
PeerJS 是一個基於瀏覽器WebRTC功能實現的js功能包,簡化了WebrRTC的開發過程,對底層的細節做了封裝,直接呼叫API即可,再配合surging 協議元件化從而做到穩定,高效可擴充套件的微服務,再利用RtmpToWebrtc 引擎元件可以做到不僅可以利用httpflv 觀看rtmp推流直播,還可以採用基於 Webrtc的peerjs 進行觀看,那麼今天要講的是如何結合開發語音視訊通話功能。放到手機和電腦上都可以實現語音視訊通話。
一鍵執行打包成品下載:https://pan.baidu.com/s/1MVsjKtVYpUonauAz9ZXtPg?pwd=1q2g
測試使用者:fanly
測試密碼:123456
為了讓大家節約時間,能儘快執行產品看到效果,上面有 一鍵執行打包成品可以進行下載測試執行。
二、如何測試執行
以下是目錄結構,
IDE:consul 註冊中心
kayak.client: 閘道器
kayak.server:微服務
apache-skywalking-apm:skywalking鏈路跟蹤
以上是目錄結構, 不需要進入管理介面配置網路元件,啟動後自帶埠96的ws協議主機,只要開啟video資料夾,裡面有兩個語音通話的html測試檔案,在同一一個區域網只要輸入對方的name就可以進行語音通話
開啟介面如圖
三、基於surging如何開發
以上是沒有開發環境的進行下載進行下載測試,那麼正在使用surging 的如何開發此功能呢?
1. 建立服務介面,繼承於IServiceKey
[ServiceBundle("Device/{Service}")] public interface IChatService : IServiceKey { }
2. 建立中間服務,繼承於WSBehavior, IChatService
internal class ChatService : WSBehavior, IChatService { private static readonly ConcurrentDictionary<string, string> _users = new ConcurrentDictionary<string, string>(); private static readonly ConcurrentDictionary<string, string> _clients = new ConcurrentDictionary<string, string>(); protected override void OnOpen() { var _name = Context.QueryString["name"]; if (!string.IsNullOrEmpty(_name)) { _clients[ID] = _name; _users[_name] = ID; } } protected override void OnError( WebSocketCore.ErrorEventArgs e) { var msg = e.Message; } protected override void OnMessage(MessageEventArgs e) { if (_clients.ContainsKey(ID)) { var message = JsonConvert.DeserializeObject<Dictionary<string, object>>(e.Data); //訊息型別 message.TryGetValue("type",out object @type); message.TryGetValue("toUser", out object toUser); message.TryGetValue("fromUser", out object fromUser); message.TryGetValue("msg", out object msg); message.TryGetValue("sdp", out object sdp); message.TryGetValue("iceCandidate", out object iceCandidate); Dictionary<String, Object> result = new Dictionary<String, Object>(); result.Add("type", @type); //呼叫的使用者不線上 if (!_users.ContainsKey(toUser?.ToString())) { result["type"]= "call_back"; result.Add("fromUser", "系統訊息"); result.Add("msg", "Sorry,呼叫的使用者不線上!"); this.Client().SendTo(JsonConvert.SerializeObject(result), ID); return; } //對方結束通話 if ("hangup".Equals(@type)) { result.Add("fromUser", fromUser); result.Add("msg", "對方結束通話!"); } //視訊通話請求 if ("call_start".Equals(@type)) { result.Add("fromUser", fromUser); result.Add("msg", "1"); } //視訊通話請求回應 if ("call_back".Equals(type)) { result.Add("fromUser", toUser); result.Add("msg", msg); } //offer if ("offer".Equals(type)) { result.Add("fromUser", toUser); result.Add("sdp", sdp); } //answer if ("answer".Equals(type)) { result.Add("fromUser", toUser); result.Add("sdp", sdp); } //ice if ("_ice".Equals(type)) { result.Add("fromUser", toUser); result.Add("iceCandidate", iceCandidate); } this.Client().SendTo(JsonConvert.SerializeObject(result), _users.GetValueOrDefault(toUser?.ToString())); } } protected override void OnClose(CloseEventArgs e) { if( _clients.TryRemove(ID, out string name)) _users.TryRemove (name, out string value); } }
3.設定surgingSettings的WSPort埠配置,預設96
以上就是利用websocket協議中轉訊息,下面是頁面如何編號,程式碼如下:
<!DOCTYPE> <!--解決idea thymeleaf 表示式模板報紅波浪線--> <!--suppress ALL --> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>WebRTC + WebSocket</title> <meta name="viewport" content="width=device-width,initial-scale=1.0,user-scalable=no"> <style> html,body{ margin: 0; padding: 0; } #main{ position: absolute; width: 370px; height: 550px; } #localVideo{ position: absolute; background: #757474; top: 10px; right: 10px; width: 100px; height: 150px; z-index: 2; } #remoteVideo{ position: absolute; top: 0px; left: 0px; width: 100%; height: 100%; background: #222; } #buttons{ z-index: 3; bottom: 20px; left: 90px; position: absolute; } #toUser{ border: 1px solid #ccc; padding: 7px 0px; border-radius: 5px; padding-left: 5px; margin-bottom: 5px; } #toUser:focus{ border-color: #66afe9; outline: 0; -webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6); box-shadow: inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6) } #call{ width: 70px; height: 35px; background-color: #00BB00; border: none; margin-right: 25px; color: white; border-radius: 5px; } #hangup{ width:70px; height:35px; background-color:#FF5151; border:none; color:white; border-radius: 5px; } </style> </head> <body> <div id="main"> <video id="remoteVideo" playsinline autoplay></video> <video id="localVideo" playsinline autoplay muted></video> <div id="buttons"> <input id="toUser" placeholder="輸入線上好友賬號"/><br/> <button id="call">視訊通話</button> <button id="hangup">結束通話</button> </div> </div> </body> <!-- 可引可不引 --> <!--<script th:src="@{/js/adapter-2021.js}"></script>--> <script type="text/javascript" th:inline="javascript"> let username = "fanly"; let localVideo = document.getElementById('localVideo'); let remoteVideo = document.getElementById('remoteVideo'); let websocket = null; let peer = null; WebSocketInit(); ButtonFunInit(); /* WebSocket */ function WebSocketInit(){ //判斷當前瀏覽器是否支援WebSocket if ('WebSocket' in window) { websocket = new WebSocket("ws://127.0.0.1:961/device/chat?name="+username); } else { alert("當前瀏覽器不支援WebSocket!"); } //連線發生錯誤的回撥方法 websocket.onerror = function (e) { alert("WebSocket連線發生錯誤!"); }; //連線關閉的回撥方法 websocket.onclose = function () { console.error("WebSocket連線關閉"); }; //連線成功建立的回撥方法 websocket.onopen = function () { console.log("WebSocket連線成功"); }; //接收到訊息的回撥方法 websocket.onmessage = async function (event) { let { type, fromUser, msg, sdp, iceCandidate } = JSON.parse(event.data.replace(/\n/g,"\\n").replace(/\r/g,"\\r")); console.log(type); if (type === 'hangup') { console.log(msg); document.getElementById('hangup').click(); return; } if (type === 'call_start') { let msg = "0" if(confirm(fromUser + "發起視訊通話,確定接聽嗎")==true){ document.getElementById('toUser').value = fromUser; WebRTCInit(); msg = "1" } websocket.send(JSON.stringify({ type:"call_back", toUser:fromUser, fromUser:username, msg:msg })); return; } if (type === 'call_back') { if(msg === "1"){ console.log(document.getElementById('toUser').value + "同意視訊通話"); //建立本地影片併傳送offer let stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true }) localVideo.srcObject = stream; stream.getTracks().forEach(track => { peer.addTrack(track, stream); }); let offer = await peer.createOffer(); await peer.setLocalDescription(offer); let newOffer = offer; newOffer["fromUser"] = username; newOffer["toUser"] = document.getElementById('toUser').value; websocket.send(JSON.stringify(newOffer)); }else if(msg === "0"){ alert(document.getElementById('toUser').value + "拒絕視訊通話"); document.getElementById('hangup').click(); }else{ alert(msg); document.getElementById('hangup').click(); } return; } if (type === 'offer') { let stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true }); localVideo.srcObject = stream; stream.getTracks().forEach(track => { peer.addTrack(track, stream); }); await peer.setRemoteDescription(new RTCSessionDescription({ type, sdp })); let answer = await peer.createAnswer(); let newAnswer = answer; newAnswer["fromUser"] = username; newAnswer["toUser"] = document.getElementById('toUser').value; websocket.send(JSON.stringify(newAnswer)); await peer.setLocalDescription(answer); return; } if (type === 'answer') { peer.setRemoteDescription(new RTCSessionDescription({ type, sdp })); return; } if (type === '_ice') { peer.addIceCandidate(iceCandidate); return; } } } /* WebRTC */ function WebRTCInit(){ peer = new RTCPeerConnection(); //ice peer.onicecandidate = function (e) { if (e.candidate) { websocket.send(JSON.stringify({ type: '_ice', toUser:document.getElementById('toUser').value, fromUser:username, iceCandidate: e.candidate })); } }; //track peer.ontrack = function (e) { if (e && e.streams) { remoteVideo.srcObject = e.streams[0]; } }; } /* 按鈕事件 */ function ButtonFunInit(){ //視訊通話 document.getElementById('call').onclick = function (e){ document.getElementById('toUser').style.visibility = 'hidden'; let toUser = document.getElementById('toUser').value; if(!toUser){ alert("請先指定好友賬號,再發起視訊通話!"); return; } if(peer == null){ WebRTCInit(); } websocket.send(JSON.stringify({ type:"call_start", fromUser:username, toUser:toUser, })); } //結束通話 document.getElementById('hangup').onclick = function (e){ document.getElementById('toUser').style.visibility = 'unset'; if(localVideo.srcObject){ const videoTracks = localVideo.srcObject.getVideoTracks(); videoTracks.forEach(videoTrack => { videoTrack.stop(); localVideo.srcObject.removeTrack(videoTrack); }); } if(remoteVideo.srcObject){ const videoTracks = remoteVideo.srcObject.getVideoTracks(); videoTracks.forEach(videoTrack => { videoTrack.stop(); remoteVideo.srcObject.removeTrack(videoTrack); }); //結束通話同時,通知對方 websocket.send(JSON.stringify({ type:"hangup", fromUser:username, toUser:document.getElementById('toUser').value, })); } if(peer){ peer.ontrack = null; peer.onremovetrack = null; peer.onremovestream = null; peer.onicecandidate = null; peer.oniceconnectionstatechange = null; peer.onsignalingstatechange = null; peer.onicegatheringstatechange = null; peer.onnegotiationneeded = null; peer.close(); peer = null; } localVideo.srcObject = null; remoteVideo.srcObject = null; } } </script> </html>
以上是頁面的程式碼,如需要新增其它賬號測試只要更改username ,或者ws地址也可以更改標記紅色的區域。
三、總結
本人正在開發平臺,如有疑問可以聯絡作者,QQ群:744677125