C#+WebSocket+WebRTC多人语音视频系统

jopen 10年前

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='www.deekj.com'/>    <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即可。

客户端访问测试。

来自:http://my.oschina.net/u/858881/blog/293751