最近研究C# Webrtc一些示例,看到各位前辈疯狂吐槽,貌似将 WebRTC 库合并到项目中的方式是一场彻头彻尾的噩梦,哈哈,流泪中。。。。
不辞辛苦搜到一些文章,贴出来分享一下。
文章1、从开源项目中提取的最受好评的Org.Webrtc.DataChannel现实C# (CSharp)示例
编程语言: C# (CSharp)
命名空间/包名称: Org.Webrtc
类/类型: DataChannel
hotexamples.com的示例: 5
文章2、C#+WebSocket+WebRTC多人语音视频系统
整个WebRtc里面已经封装好了视频音频采集和传输,你须要作的就是使用任何能够实现WebSocket的语言来开发一套信令服务器。
信令服务器负责用户拨号控制,能够集成用户验证等功能来验证用户身份等等,须要为WebRTC作的只有传递协议数据,将一边的传递给另外一边,让两边互相了解对方的浏览器视频音频解码类型,版本状况,内外网状况等等。
须要使用的有:
- 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便可。
客户端访问测试。
版权声明:本文内容转自互联网,本文观点仅代表作者本人。本站仅提供信息存储空间服务,所有权归原作者所有。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至1393616908@qq.com 举报,一经查实,本站将立刻删除。