WebRTC是谷歌的开源的实时视频音频聊天技术,支持跨平台,Nat穿透技术(Stun,Turn,Ice),在部分支持Html5的浏览器里集成了这个功能。
至目前为止支持的PC浏览器有:Chrome 31+,opera 19+,FireFox 26+
至目前为止支持的Android浏览器有:Chrome,opera,FireFox
IE所有版本均不支持!!
IPhone手机暂不支持!!
整个WebRtc里面已经封装好了视频音频采集和传输,你需要做的就是使用任何可以实现WebSocket的语言来开发一套信令服务器
信令服务器负责用户拨号控制,可以集成用户验证等功能来验证用户身份等等,需要为WebRTC做的只有传递协议数据,将一边的传递给另一边,让两边互相了解对方的浏览器视频音频解码类型,版本情况,内外网情况等等,
需要使用的有:vs
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即可。
客户端访问测试。