C#+WebSocket+WebRTC多人語音視頻系統

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便可。

客戶端訪問測試。

相關文章
相關標籤/搜索