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

WebRTC是谷歌的开源的实时视频音频聊天技术,支持跨平台,Nat穿透技术(Stun,Turn,Ice),在部分支持Html5的浏览器里集成了这个功能。

至目前为止支持的PC浏览器有:Chrome 31+,opera 19+,FireFox 26+
至目前为止支持的Android浏览器有:Chrome,opera,FireFox
IE所有版本均不支持!!
IPhone手机暂不支持!!
整个WebRtc里面已经封装好了视频音频采集和传输,你需要做的就是使用任何可以实现WebSocket的语言来开发一套信令服务器

信令服务器负责用户拨号控制,可以集成用户验证等功能来验证用户身份等等,需要为WebRTC做的只有传递协议数据,将一边的传递给另一边,让两边互相了解对方的浏览器视频音频解码类型,版本情况,内外网情况等等,

需要使用的有:vs
```javascript
chrome
一个公网IP
CentOS
turnserver(https://code.google.com/p/rfc5766-turn-server/)

                    (这个版本集成了stun和turn,不需要分别再安装了)
需要使用的库:Fleck:一个.net的WebSocket库,百度可以搜得到。
              LitJson:一个小巧的Json解析库。
IWebSocketConnection类默认没有Args属性,是我后来修改源码添加的。
下面是我自己写的一个简单的WebRTC服务端,也就是信令服务器
```javascript
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即可。
客户端访问测试。

时间: 2024-10-05 17:31:55

C#+WebSocket+WebRTC多人语音视频系统的相关文章

发送语音/视频系统崩溃。

问题描述 发送语音或者视频系统崩溃.项目启动提示:12-12 11:01:26.111: E/dalvikvm(17326): dlopen("/data/app-lib/com.yiapp.helloworld-1/libeasemobservice.so") failed: Cannot load library: load_library(linker.cpp:761): not a valid ELF executable: /data/app-lib/com.yiapp.he

QQ的胆子有点大!腾讯QQ推多人语音通话或预示VOIP将开放

中介交易 SEO诊断 淘宝客 云主机 技术大厅 原标题:QQ的胆子有点大!腾讯QQ推多人语音通话或预示VOIP将开放 运营商向经营数据转型迫在眉睫 [IT时代周刊观察]最近,QQ在语音通话上迈的步子比微信要大很多,先是推出点对点语音通话,现在又推出50人多方语音通话.对此,业内人惊呼"QQ的胆子有点大!" 笔者认为,腾讯之所以在QQ上推出点对点的语音通话以及多人语音通话,第一个主要原因是QQ有语音通话市场的基础.从PC时代开始,QQ就拥有语音.视频功能,到后来的多人视频功能.此外,QQ

飞信(Fetion)语音视频聊天使用技巧

飞信为您提供语音和视频聊天功能,使您可以通过PC客户端与好友进行视频或语音聊天. 如果您想通过飞信与好友进行语音视频聊天,首先需要您与对方的PC都安装了视频和语音设备. 打开与好友的会话窗口,在工具栏内点击 可向好友发出视频对话请求,点击 可向好友发出语音对话请求.好友接受请求之后,您就可以和好友进行语音视频聊天了. 视频对话 在视频对话窗口中,右侧是您和好友的视频图像窗口,窗口右侧显示了通话时长,您还可以调节扬声器和麦克风的音量.音量调节上方,提供了个三个个性化按钮,分别为"浮动视频窗口&qu

网络语音视频技术浅议 Visual Studio 2010(转)

      我们在开发实践中常常会涉及到网络语音视频技术.诸如即时通讯.视频会议.远程医疗.远程教育.网络监控等等,这些网络多媒体应用系统都离不开网络语音视频技术.本人才疏学浅,对于网络语音视频技术也仅仅是略知皮毛,这里只想将自己了解到的一些最基础的知识分享给大家,管中窥豹,略见一斑,更重要的是抛砖引玉,希望更多的朋友们一起来探讨,同时,有讲得不正确的地方也希望大家批评指正.   一.基本流程    无论是即时通讯.视频会议,还是远程医疗.远程教育.网络监控等等系统,都需要获取到远程的语音.视频

使用超声波“无声”劫持语音助理系统:以Siri、Google Now为例

本文讲的是使用超声波"无声"劫持语音助理系统:以Siri.Google Now为例,近日,中国安全研究人员发明了一种可以不用说话就能巧妙的激活语音识别系统的方式.他们通过使用人类无法听到高频声波来对那些智能设备的语音助手发出命令. 这些安全研究人员来自浙江大学的一个研究小组,他们是在观察动物进行高频声波的相互沟通交流之后发现的这一方式,因此他们将这种技术称为DolphinAttack("海豚音攻击"因为海豚正是通过高频率的声波进行交流的).为了了解它是如何工作的,现

环信3.0 是否支持多人语音通话?

问题描述 环信3.0 是否支持多人语音通话? 解决方案 现在是单对单视频,没有多人视频

独家曝光了PC QQ新增多人语音功能的消息

之前我们独家曝光了PC QQ新增多人语音功能的消息,QQ希望借助多人语音功能,将QQ的使用场景拓展至音频会议等更加丰富的沟通场景之中.当时我们曾透露,QQ后续会将多人语音等功能平移直手机端,据36氪独家了解,正在内测的新版手机QQ已经内置了"多人语音功能". 我们提前拿到了新版手机QQ的安装包,和现有的手机QQ相比有非常大的改版.新版手机QQ新增了多人语音.相册共享.近距离文件传输.一键顺序阅读.特别关心等功能,比之前的应用场景要更为丰富. 新版手机QQ最大的功能点就是新增的"

启用电话诈骗语音提示系统常州模式可借鉴

启用电话诈骗语音提示系统 常州模式或具推广意义 防止电话诈骗并不难,只要对电话诈骗的种种骗术有所了解,大多数人都不会掉入陷阱.这也提醒有关部门,遏制电话诈骗的关键在于采取有效的预防措施,特别是在技术上健全防范体系至关重要 ■本报记者 陈丽容 电话诈骗居高不下成为头大难问题,针对这种情况,江苏省常州市警方与电信部门联合开发了一套"防电话诈骗语音提示系统",自10月15日该系统启用以来,已成功拦截诈骗电话近20000个,且提示系统启用后,常州市的固定电话诈骗案件仅有一起诈骗得逞. 在不断变

《极品飞车14》公布全新系统:“多人异步竞赛系统”

多玩网讯(编译/Drdarknight)今日,一名来自EA的制作人在本届科隆游戏展上公布了<极品飞车14:热力追踪>(NeedForSpeed:HotPursuit)的全新系统:"多人异步竞赛系统".该系统的雏形源于于某些社交游戏,玩家可以跟那些即便是不在线的好友们一同进行游戏.而在<极品飞车14>中,玩家可以通过这个系统随时挑战当前离线玩家的比赛记录. 在视频中,该制作人演示了<热力追踪>的竞赛信息(AutoLog)界面.玩家们可以通过该界面查阅和