最近研究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 举报,一经查实,本站将立刻删除。