WebRTC是谷歌的開源的實時視頻音頻聊天技術,支持跨平臺,Nat穿透技術(Stun,Turn,Ice),在部分支持Html5的瀏覽器裏集成了這個功能。css
至目前爲止支持的PC瀏覽器有:Chrome 31+,opera 19+,FireFox 26+html
至目前爲止支持的Android瀏覽器有:Chrome,opera,FireFoxgit
IE全部版本均不支持!!github
IPhone手機暫不支持!!web
整個WebRtc裏面已經封裝好了視頻音頻採集和傳輸,你須要作的就是使用任何能夠實現WebSocket的語言來開發一套信令服務器
chrome
信令服務器負責用戶撥號控制,能夠集成用戶驗證等功能來驗證用戶身份等等,須要爲WebRTC作的只有傳遞協議數據,將一邊的傳遞給另外一邊,讓兩邊互相瞭解對方的瀏覽器視頻音頻解碼類型,版本狀況,內外網狀況等等,shell
須要使用的有:vsc#
chrome瀏覽器
一個公網IP服務器
CentOS
turnserver(https://code.google.com/p/rfc5766-turn-server/)
(這個版本集成了stun和turn,不須要分別再安裝了)
須要使用的庫:Fleck:一個.net的WebSocket庫,百度能夠搜獲得。
LitJson:一個小巧的Json解析庫。
IWebSocketConnection類默認沒有Args屬性,是我後來修改源碼添加的。
下面是我本身寫的一個簡單的WebRTC服務端,也就是信令服務器
using Fleck; using System; using System.Collections.Generic; using System.Linq; using System.Net; using System.Text; using System.Reflection; using LitJson; namespace WebRtc { public class Work { public Dictionary<string, IWebSocketConnection> ClientList = new Dictionary<string, IWebSocketConnection>(); public string Id = null; public IWebSocketConnection Master = null; public string WorkName = null; public void start() { foreach (WebSocketConnection suser in ClientList.Values) { foreach (WebSocketConnection duser in ClientList.Values) { if (suser == duser) continue; JsonData jd = JsonHelper.GetJson("conn", "main"); jd["wname"] = this.Id; jd["duser"] = duser.Args["username"].ToString(); jd["suser"] = suser.Args["username"].ToString(); jd["type"] = "start"; suser.Send(jd.ToJson()); } } } } public class Str { public const string Falid = "falid"; public const string Success = "success"; public const string Exist = "exist"; } public class Command { public const string CreateWork = "createWork"; public const string Login = "login"; public const string Join = "join"; public const string Sec = "sec"; public const string Conn = "conn"; public const string Start = "start"; } class WebRTCServer : IDisposable { public Dictionary<string, Work> WorkList = new Dictionary<string, Work>(); //聲明會議室列表 public Dictionary<string, IWebSocketConnection> UserList = new Dictionary<string, IWebSocketConnection>(); //聲明已登陸的用戶列表 private WebSocketServer server; //聲明WebSocket服務類 public WebRTCServer(int port) : this("ws://0.0.0.0:" + port) { } public WebRTCServer(string URL) { server = new WebSocketServer(URL); server.Start(socket => { socket.OnMessage = message => { OnReceive(socket, message); }; socket.OnClose = () => { OnDisconnect(socket); }; }); } private void OnConnected(IWebSocketConnection context) { } private void OnDisconnect(IWebSocketConnection context) { if (UserList.Count == 0) return; string key = null; foreach (string i in UserList.Keys) if (UserList[i] == context) key = i; if (key != null) UserList.Remove(key); key = null; foreach (string i in WorkList.Keys) { foreach(string u in WorkList[i].ClientList.Keys) if (WorkList[i].ClientList[u] == context) key = u; if (key != null) WorkList[i].ClientList.Remove(key); } key = null; foreach (string i in WorkList.Keys) { if (WorkList[i].Master == context) key = i; } if (key != null) WorkList.Remove(key); context = null; } private void OnReceive(IWebSocketConnection context,string msg) { if (!msg.Contains("command")) return; //若是沒有命令字符跳出 JsonData jd = JsonMapper.ToObject(msg); string command = jd["command"].ToString(); if (!UserList.ContainsValue(context)) //判斷是否登陸 { switch (command) //未登陸狀況下的處理 { case Command.Login : //登陸處理 try { string username = jd["username"].ToString(); context.Args.Add("username", username); UserList.Add(username, context); context.Send(JsonHelper.GetJsonStr( Command.Login, null, Str.Success)); } catch { context.Send(JsonHelper.GetJsonStr( Command.Login, null, Str.Falid)); } break; default: //未登陸狀況下的默認處理 context.Send(JsonHelper.GetJsonStr( Command.Sec, null, Str.Falid)); break; } } else { switch (command) //登陸以後的處理 { case Command.CreateWork: //建立聊天室,這裏是工做 try { string wname = jd["wname"].ToString(); if (!WorkList.ContainsKey(wname)) { WorkList.Add(wname, new Work() { Master = context, Id = wname, WorkName = wname } ); context.Send(JsonHelper.GetJsonStr( Command.CreateWork, wname, Str.Success)); } else context.Send(JsonHelper.GetJsonStr( Command.CreateWork, wname, Str.Exist)); } catch { context.Send(JsonHelper.GetJsonStr( Command.CreateWork, null, Str.Falid)); } break; case Command.Join: //用戶加入 try { string wname = jd["wname"].ToString(); string username = jd["username"].ToString(); if (!WorkList[wname].ClientList.ContainsKey(username)) { WorkList[wname].ClientList.Add(username, context); context.Send(JsonHelper.GetJsonStr( Command.Join, wname, Str.Success)); } else context.Send(JsonHelper.GetJsonStr( Command.Join, wname, Str.Exist)); } catch { context.Send(JsonHelper.GetJsonStr( Command.Join, null, Str.Falid)); } break; case Command.Start: //正式開始,發起鏈接 try { string wname = jd["wname"].ToString(); if (WorkList[wname].Master == context) { WorkList[wname].start(); } else { context.Send(JsonHelper.GetJsonStr( Command.Sec, null, Str.Falid)); } } catch { context.Send(JsonHelper.GetJsonStr( Command.Start, null, Str.Falid)); } break; case Command.Conn: //WebRtc命令轉發 try { string dname = jd["duser"].ToString(); UserList[dname].Send(msg); } catch { } break; } } } public void Dispose() { try { foreach (IWebSocketConnection i in UserList.Values) { i.Close(); } server.Dispose(); UserList.Clear(); WorkList.Clear(); } catch { } } } public class JsonHelper { public static JsonData GetJson(string command, string ret) { JsonData jd = new JsonData(); jd["command"] = command; jd["ret"] = ret; return jd; } public static string GetJsonStr(string command, string data, string ret) { JsonData jd = new JsonData(); jd["command"] = command; jd["data"] = data; jd["ret"] = ret; return jd.ToJson(); } } }
下面是網頁端的Js代碼,算是客戶端,rtc_main.js
var socket; var PeerConnection = (window.PeerConnection || window.webkitPeerConnection00 || window.webkitRTCPeerConnection || window.mozRTCPeerConnection); navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia; var localstream = null; var rpc = new Array(); var dpc = new Array(); var vrpc = new Array(); var camer_stream = {audio:true, video:{ mandatory: { maxWidth: 640, maxHeight: 360 } }} var rconn_count = 1; var servers = {"iceServers": [ {"url":"stun:1.1.1.1"}, //這裏1.1.1.1對應你的公網IP {"url":"turn:1.1.1.1?transport=tcp", "credential":"user", "username":"passwd"}, ] }; window.onload = function() { console.log("獲取本地視頻源..."); navigator.getUserMedia(camer_stream, getUMsuccess, function() {}); } function getUMsuccess(stream){ console.log("獲取本地視頻源成功!"); vid1.src = webkitURL.createObjectURL(stream); //本地視頻顯示 localstream = stream; //本地流 } function connect () { socket = new WebSocket("ws://" + server.value + ":8889"); setSocketEvents(socket); //設置WebSocket監聽事件 } function setSocketEvents(Socket) { Socket.onopen = function() { //鏈接成功處理方法 console.log("Socket已鏈接!"); send(JSON.stringify({"command":"login", "username":username.value})) }; Socket.onmessage = function(Message) { //接收信息處理方法 var obj = JSON.parse(Message.data); var command = obj.command; switch(command) { case "createWork" : { if (obj.ret == "success") console.log("建立會議室成功!"); else if(obj.ret == "exist") console.log("會議室已存在!"); else console.log("建立會議室失敗!"); break; } case "login" : { obj.ret == "success" ? console.log("登陸成功!") : console.log("登陸失敗!"); break; } case "join" : { obj.ret == "success" ? console.log("加入會議室成功!") : console.log("加入會議室失敗!"); break; } case "sec" : { console.log("沒有權限!"); break; } case "conn" : { Conn(obj); break; } default : { console.log(Message.data); } } }; Socket.onclose = function() { console.log("Socket鏈接已斷開!"); } } function createWork() { console.log("建立會議室:" + work.value); var obj = JSON.stringify({"command":"createWork", "wname":work.value}); send(obj); } function join() { console.log("加入會議室:" + work.value); var obj = JSON.stringify({"command":"join", "wname":work.value, "username":username.value}); send(obj); } function startwork(){ console.log("會議開始:" + work.value); var obj = JSON.stringify({"command":"start", "wname":work.value}); send(obj); } function Conn(jd){ ///////////////////////// // 發起端代碼 // ///////////////////////// if (jd.ret == "main") { if (jd.type=="start"){ console.log("發起鏈接:wname:" + jd.wname + ",sname:" + jd.suser + ",dname:" + jd.duser); rpc[jd.duser] = new webkitRTCPeerConnection(servers); var trpc = rpc[jd.duser]; vrpc[jd.duser] = ++rconn_count; trpc.addStream(localstream); trpc.onaddstream = function(e){ try{ document.getElementById('vid' + vrpc[jd.duser]).src = webkitURL.createObjectURL(e.stream); console.log("鏈接遠程媒體成功!"); }catch(ex){ console.log("鏈接遠程媒體失敗!",ex); } }; trpc.onicecandidate = function(event){ if (event.candidate) { var obj = JSON.stringify({ "command":"conn", "type":"ice_data", "suser":jd.suser, "duser":jd.duser, "wname":jd.wname, "ret":"msg", "data":JSON.stringify(event.candidate) }); send(obj); } }; trpc.createOffer(function(desc){ trpc.setLocalDescription(desc); var obj = JSON.stringify({ "command":"conn", "type":"offer", "suser":jd.suser, "duser":jd.duser, "wname":jd.wname, "ret":"msg", "data":JSON.stringify(desc) }); send(obj); }); }else if(jd.type=="answer"){ rpc[jd.suser].setRemoteDescription( new RTCSessionDescription(JSON.parse(jd.data)) ); }else if(jd.type=="ice_data"){ console.log("main_candidate",jd.data); rpc[jd.suser].addIceCandidate( new RTCIceCandidate(JSON.parse(jd.data)) ); } ///////////////////////// // 接收端代碼 // ///////////////////////// }else if(jd.ret == "msg"){ if (jd.type=="offer"){ console.log("接受鏈接:wname:" + jd.wname + ",sname:" + jd.suser + ",dname:" + jd.duser); dpc[jd.suser] = new webkitRTCPeerConnection(servers); var trpc = dpc[jd.suser]; trpc.setRemoteDescription( new RTCSessionDescription(JSON.parse(jd.data)) ); trpc.addStream(localstream); trpc.onicecandidate = function(event){ if (event.candidate) { var obj = JSON.stringify({ "command":"conn", "type":"ice_data", "suser":jd.duser, "duser":jd.suser, "wname":jd.wname, "ret":"main", "data":JSON.stringify(event.candidate) }); send(obj); } }; trpc.createAnswer(function(desc){ trpc.setLocalDescription(desc); var obj = JSON.stringify({ "command":"conn", "type":"answer", "suser":jd.duser, "duser":jd.suser, "wname":jd.wname, "ret":"main", "data":JSON.stringify(desc) }); send(obj); }); }else if(jd.type=="ice_data"){ console.log("client_candidate",jd.data); dpc[jd.suser].addIceCandidate( new RTCIceCandidate(JSON.parse(jd.data)) ); } } } function send(data){ try{ socket.send(data); }catch(ex){ console.log("消息發送失敗!"); } }
網頁前臺代碼。。。很簡陋,vid可無限擴展
<!doctype html> <html> <head> <meta charset="UTF-8"> <title>視頻會議</title> <link rel="stylesheet" href="css/main.css" /> <style> div#container { max-width: 90%; } video { margin: 0 0.5em 1.5em 0; } @media screen and (min-width: 800px) { video { width: 45%; } } </style> <script src="js/rtc_main.js"></script> </head> <body> <div id="container"> <video id="vid1" width="640" height="480" autoplay></video> <video id="vid2" width="640" height="480" autoplay></video> <div> <input type="text" id="server" size="30" value='1.1.1.1'/> <input type="text" id="work" size="30" value='work1'/> <input type="text" id="username" size="30" value='user1'/> <button id="btn1" onclick="connect()">鏈接服務器</button> <button id="btn2" onclick="createWork()">建立工做區</button> <button id="btn3" onclick="join()">鏈接到工做區</button> <button id="btn4" onclick="startwork()">開始會議</button> </div> </div> </body> </html>
main.css
a { color: #77aaff; text-decoration: none; } a:hover { color: #88bbff; text-decoration: underline; } a#viewSource { display: block; margin: 1.3em 0 0 0; border-top: 1px solid #999; padding: 1em 0 0 0; } #server{ margin: 0 0.5em 0 0; width: 7.5em; color: #aaa; } div#links a { display: block; line-height: 1.3em; margin: 0 0 1.5em 0; } @media screen and (min-width: 1000px) { /* hack! to detect non-touch devices */ div#links a { line-height: 0.8em; } } audio { max-width: 100%; } body { background: #9999; font-family: Arial, sans-serif; padding: 20px; word-break: break-word; } button { margin: 0 0.5em 0 0; width: 9em; height: 5em; } button[disabled] { color: #aaa; } code { font-family: 'Courier New', monospace; letter-spacing: -0.1em; } div#container { background: #000; margin: 0 auto 0 auto; max-width: 40em; padding: 1em 1.5em 1.3em 1.5em; } div#links { padding: 0.5em 0 0 0; } h1 { border-bottom: 1px solid #aaa; color: white; font-family: Arial, sans-serif; margin: 0 0 0.8em 0; padding: 0 0 0.4em 0; } h2 { color: #ccc; font-family: Arial, sans-serif; margin: 1.8em 0 0.6em 0; } html { /* avoid annoying page width change when moving from the home page */ overflow-y: scroll; } img { border: none; max-width: 100%; } p { color: #eee; line-height: 1.6em; } p#data { border-top: 1px dotted #666; font-family: Courier New, monospace; line-height: 1.3em; max-height: 800px; overflow-y: auto; padding: 1em 0 0 0; } p.borderBelow { border-bottom: 1px solid #aaa; padding: 0 0 20px 0; } video { background: #222; width: 100%; } @media screen and (min-width: 800px) { video { } } @media screen and (max-width: 800px) { video { } }
下面是Linux配置Stun和Turn服務端
先下載依賴包libevent編譯安裝
wget https://cloud.github.com/downloads/libevent/libevent/libevent-2.0.21-stable.tar.gz tar -xvf libevent-2.0.21-stable.tar.gz cd libevent* ./configure make && make install
再下載服務端turnserver編譯安裝
wget http://turnserver.open-sys.org/downloads/v3.2.3.96/turnserver-3.2.3.96.tar.gz tar -xvf turnserver-3.2.3.96.tar.gz cd turnserver* ./configure make && make install
修改服務端配置文件
cd /usr/local/etc/ cp -p turnserver.conf.default turnserver.conf cp -p turnuserdb.conf.default turnuserdb.conf vi turnserver.conf
查找修改如下內容,保存退出。
listening-device=eth1 服務器監聽哪塊網卡 listening-ip=1.1.1.1 服務器監聽哪個IP 這裏1.1.1.1對應你的公網IP
其餘選項根據狀況設置,有詳細的解釋
下一步生成用戶Key,用來驗證用戶,(不包含中括號)
turnadmin -k -u [用戶名] -r [登陸域(例:baidu.com)] -p [密碼]
這個命令會產生一個0x開頭的字符串,這即是用戶的Key。
而後把用戶名和Key保存在turnuserdb.conf裏
vi turnuserdb.conf
下面是寫入內容,保存退出。
[用戶名]:[Key]
如今服務器配置完成,可啓動服務了。直接運行turnserver便可。
客戶端訪問測試。