济南教育论坛网站建设,个人网站开发计划书,jsp 响应式网站模板下载,腾讯云如何建设网站首页整个P2P视频过程需要知道双方的媒体类型、流和候选者#xff0c;所以这里就会用到一下技术#xff1a;
信令服务器socket.io
状态机
ICE服务器
WebRTC框架
媒体协商
信令服务器Socket.io 信令服务器说白了作用就是发消息的中转站#xff0c;A把msg发到…整个P2P视频过程需要知道双方的媒体类型、流和候选者所以这里就会用到一下技术
信令服务器socket.io
状态机
ICE服务器
WebRTC框架
媒体协商
信令服务器Socket.io 信令服务器说白了作用就是发消息的中转站A把msg发到信令服务器然后信令服务器把msg发给B Socket.IO 是一个库可在客户端和服务器之间实现低延迟、双向和基于事件的通信。
它建立在 WebSocket 协议之上并提供额外的保证例如回退到 HTTP 长轮询或自动重新连接。
WebSocket 是一种通信协议它在服务器和浏览器之间提供全双工和低延迟通道。更多信息可以在这里找到。
有几种可用的 Socket.IO 服务器实现
JavaScript (可以在本网站上找到其文档) 安装步骤API源代码 Java: https://github.com/mrniko/netty-socketioJava: https://github.com/trinopoty/socket.io-server-javaPython: https://github.com/miguelgrinberg/python-socketio
大多数主要语言的客户端实现
JavaScript (可以在浏览器、Node.js 或 React Native 中运行) 安装步骤API源代码 Java: https://github.com/socketio/socket.io-client-javaC: https://github.com/socketio/socket.io-client-cppSwift: https://github.com/socketio/socket.io-client-swiftDart: https://github.com/rikulo/socket.io-client-dartPython: https://github.com/miguelgrinberg/python-socketio.Net: https://github.com/doghappy/socket.io-client-csharpGolang: https://github.com/googollee/go-socket.ioRust: https://github.com/1c3t3a/rust-socketioKotlin: https://github.com/icerockdev/moko-socket-io
这是一个使用普通 WebSocket 的基本示例
服务器 (基于 ws)
import { WebSocketServer } from ws;const server new WebSocketServer({ port: 3000 });server.on(connection, (socket) {// 向客户端发送消息socket.send(JSON.stringify({type: hello from server,content: [ 1, 2 ]}));// 从客户端接收消息socket.on(message, (data) {const packet JSON.parse(data);switch (packet.type) {case hello from server:// ...break;}});
});客户端
const socket new WebSocket(ws://localhost:3000);socket.addEventListener(open, () {// 向服务器发送消息socket.send(JSON.stringify({type: hello from server,content: [ 3, 4 ]}));
});// 从服务器接收消息
socket.addEventListener(message, ({ data }) {const packet JSON.parse(data);switch (packet.type) {case hello from server:// ...break;}
});这是与 Socket.IO 相同的示例
服务器
import { Server } from socket.io;const io new Server(3000);io.on(connection, (socket) {// 向客户端发送消息socket.emit(hello from server, 1, 2, { 3: Buffer.from([4]) });// 从客户端接收消息socket.on(hello from server, (...args) {// ...});
});客户端
import { io } from socket.io-client;const socket io(ws://localhost:3000);// 向服务器发送消息
socket.emit(hello from server, 5, 6, { 7: Uint8Array.from([8]) });// 从服务器接收消息
socket.on(hello from server, (...args) {// ...
});这两个示例看起来非常相似但实际上 Socket.IO 提供了附加功能这些功能隐藏了在生产环境中运行基于 WebSockets 的应用程序的复杂性。 下面列出了这些功能。
但首先让我们明确 Socket.IO 不是什么。
Socket.IO 不是什么
Socket.IO 不是 WebSocket实现。
尽管 Socket.IO 确实在可能的情况下使用 WebSocket 进行传输但它为每个数据包添加了额外的元数据。这就是为什么 WebSocket 客户端将无法成功连接到 Socket.IO 服务器而 Socket.IO 客户端也将无法连接到普通 WebSocket 服务器。
// 警告客户端将无法连接
const socket io(ws://echo.websocket.org);如果您正在寻找一个普通的 WebSocket 服务器请查看 ws 或 µWebSockets.js.
还有关于在 Node.js 核心中包含 WebSocket 服务器的讨论。在客户端您可能对robust-websocket感兴趣。
Socket.IO 并不打算在移动应用程序的后台服务中使用。
Socket.IO 库保持与服务器的开放 TCP 连接这可能会导致用户消耗大量电池。请为此用例使用请为此用例使用FCM等专用消息传递平台。
特点
以下是 Socket.IO 在普通 WebSockets 上提供的功能
HTTP 长轮询回退
如果无法建立 WebSocket 连接连接将回退到 HTTP 长轮询。
这个特性是人们在十多年前创建项目时使用 Socket.IO 的原因因为浏览器对 WebSockets 的支持仍处于起步阶段。
即使现在大多数浏览器都支持 WebSockets超过97%它仍然是一个很棒的功能因为我们仍然会收到来自用户的报告这些用户无法建立 WebSocket 连接因为他们使用了一些错误配置的代理。
自动重新连接
在某些特定情况下服务器和客户端之间的 WebSocket 连接可能会中断而双方都不知道链接的断开状态。
这就是为什么 Socket.IO 包含一个心跳机制它会定期检查连接的状态。
当客户端最终断开连接时它会以指数回退延迟自动重新连接以免使服务器不堪重负。
数据包缓冲
当客户端断开连接时数据包会自动缓冲并在重新连接时发送。
更多信息在这里.
收到后的回调
Socket.IO 提供了一种方便的方式来发送事件和接收响应
发件人
socket.emit(hello, world, (response) {console.log(response); // got it
});接收者
socket.on(hello, (arg, callback) {console.log(arg); // worldcallback(got it!);
});您还可以添加超时
socket.timeout(5000).emit(hello, world, (err, response) {if (err) {// 另一方未在给定延迟内确认事件} else {console.log(response); // got it}
});广播
在服务器端您可以向所有连接的客户端或客户端的子集发送事件
// 到所有连接的客户端
io.emit(hello);// 致“news”房间中的所有连接客户端
io.to(news).emit(hello);这在扩展到多个节点时也有效。
多路复用
命名空间允许您在单个共享连接上拆分应用程序的逻辑。例如如果您想创建一个只有授权用户才能加入的“管理员”频道这可能很有用。
io.on(connection, (socket) {// 普通用户
});io.of(/admin).on(connection, (socket) {// 管理员用户
});常见问题
现在还需要 Socket.IO 吗
这是一个很好的问题因为现在几乎所有地方 都支持 WebSocket 。
话虽如此我们相信如果您在应用程序中使用普通的 WebSocket您最终将需要实现 Socket.IO 中已经包含并经过实战测试的大部分功能例如重新连接确认或广播.
Socket.IO 协议的数据表大小
socket.emit(hello, world) 将作为单个 WebSocket 帧发送其中包含42[hello,world]
4 是 Engine.IO “消息”数据包类型2 是 Socket.IO “消息”数据包类型[hello,world]是JSON.stringify()参数数组的 -ed 版本
因此每条消息都会增加几个字节可以通过使用自定义解析器进一步减少。
浏览器包本身的大小是10.4 kB缩小和压缩。
开始
声明socket.io的版本不同用法不同这里用最新用法
第一步使用express框架搭建服务器路由
const express require(express); //引入express模块
const app express();
app.get(/, (req, res) {res.sendFile(__dirname /login.html);
});
/http/
const http require(http);//此处引用的是http但是我们的目的是创建P2P视频所以要开启https所以需要拥有ssl证书这个可以在各大云服务商免费申请推荐华为云和阿里云
const httpServer http.createServer(app).listen(8888, 0.0.0.0);
https
const https require(https);
var fs require(fs);
var opt {key:fs.readFileSync(./cert/ssl_server.key),cert:fs.readFileSync(./cert/ssl_server.pem)
}//引入ssl证书
const httpsServer http.createServer(app,opt).listen(8008, 0.0.0.0);
//这样一个服务器就搭好了直接node xx.js就可以启动第二步使用Socket.io服务端
服务端直接npm install socket.io就可以
老版本///
var io socketIo.listen(httpsServer);
/新版本///
const io new Server(httpsServer);io.sockets.on(connection, (socket){/操作内容///
})第三步使用Socket.io客户端
客户端可以使用CDN引入也可以下载相关的js库
script src/socket.io/socket.io.js/script
scriptvar url 120.78.xxx.xx:8008var socket io(url);});
/script这样一个客户端就已经连上一个服务端了接下来就是相关操作
第四步相关操作略
P2P加入房间流程
下图有三个客户端ABC它们同时连接信令服务器首先是A发起加入房间信号join然后信令服务器就把A加入房间然后回答joined信号给A然后B又来发起加入房间信号join然后信令服务器就把B加入房间而且回答joined信号给B同时给A其他人加入房间信号otherjoin。然后C就来发起加入房间的信号join但是信令服务器会对房间人数进行控制当房间人数等于2就给新请求加入的客户端回答full信号表示房间已满没有把你加入。这时候A和B就在一个房间里面接下来它们就可以在房间里面通讯。
状态机
利用状态机进行状态变换和判断
为什么要有状态机
先考虑一个客户端在一个聊天室会有几种状态: 未加入房间前或者离开房间后Init/Leave 加入房间后joined 第二个聊天者加入后joined_conn 第二个聊天者离开 后joined_unbind 通过上面的状态可以发现用户状态除非就是加入和离开但是用户进进出出房间会出什么情况呢。这里就要思考用户进入聊天室和离开聊天室会影响什么一个聊天室至少会有一个人存在那么这个人就是发起人那么怎么知道当前用户是发起人呢。就是通过用户状态来确定。接下来看下面这张图开始当前用户还未加入房间也就是处于离开状态但加入房间后就变成joined状态当第二个人加入就变成joined_conn状态而相对于第二个人是不会出现joined_conn状态所以就可以判断当前用户是不是第一个用户也就是发起者发起者的作用涉及媒体协商。最后就是当第二个用户离开就会变为joined_unbind状态。
ICE框架
首先来了解一下两个客户端是怎么点对点通讯的。
第一种自己知道host ip
第二种用到一个STUN serverA和B都访问这个STUN server就可以拿到对方的公网ip然后再利用信令服务器访问任何客户端都加入了信令服务器通过信令服务器交换信息就可以达到NAT穿透的效果
第三种用到一个中继服务器Relay serverTURN server
这三种方式就是三个候选者为什么要三种通讯方式呢因为信令服务器想尽量不参与通讯或者说信令服务器只想做一些简单的信息通讯。所以可知道我们音视频通讯利用的大概率就是Relay server和STUN server
现在host IP、Relay server和STUN server都集中到一个ICE服务器项目中现在只要搭建这个服务器就可以了。
搭建stun/turn服务器步骤
先安装依赖库
ubuntu:
apt(-get) install build-essential
apt(-get) install openssl libssl-dev
centos:
yum install libevent-devel openssl-devel下载4.5版本源码
wget https://github.com/coturn/coturn/archive/4.5.1.1.tar.ge
连不上github的查下资料改hosts解压
tar -zxvf 4.5.1.1.tar.gz进入到项目目录
cd coturn-4.5.1.1源码安装3连
./configure
make
make install复制配置文件
cp examoles/etc/turnserver.conf bin/turnserver.conf修改配置文件
#服务器监听端口3478为默认端口可修改为自己想要的端口号
listening-port3478
#想要监听的ip地址云服务器则是云服务器的内网ip
listening-ipxxx.xxx.xxx.xxx
#扩展ip地址云服务器则是云服务器的外网ip
extenal-ipxxx.xxx.xxx.xxx
#可以设置访问的用户及密码
userdemon:123启动服务
turnserver -v -r 外网ip:监听端口 -a -o验证
https://webrtc.github.io/samples/src/content/peerconnection/trickle-ice/host 本地连接
srflx nat映射后的结果
relay 中继服务器
WebRTC框架
RTCPeerConnection相关原理
pc new RTCPeerConnection([config])
pc的相关能力
媒体协商
流和轨的添加与停止
传输相关功能
统计相关功能
媒体协商
每个客户机都有自己支持的媒体格式信息所以为了统一双方媒体支持格式所以就想要媒体协商这是pc的功能。
过程如下 首先A是一个发起者通过状态机知道A首先Get一个offer这个Get过程收集A自己的媒体信息和候选者通过ICE就知道两个信息然后setLocalDescription这个信息然后把这个媒体信息发给信令服务器信令服务器发给房间内的另一个客户机B。然后B收到这个消息后就把这个消息setRemoteDescription同时B也要收集自己的媒体信息和候选者信息两个信息setLocalDescription后把answer发给信令服务器信令服务器转发给AA收到answer后也setRemoteDescription。这样双方都知道对方支持的媒体信息和候选者。 代码如下
function mediaNegociate() {if (status joined_conn) {//joined_conn代表我是连接的发起人if (pc) {var options {//要协商的内容如音频、视频...offerToReceiveVideo:true,offerToReceiveAudio:true}pc.createOffer(options).then(getOffer).catch(handleErr);}}
}socked.on(vgetdata, (room, data){console.log(vgetdata:, data);if (!data) {return ;}if (data.type candidata) {//拿到对方传过来的候选者信息//} else if(data.type offer) {//媒体协商默认有type值为offerconsole.log(get offer);pc.setRemoteDescription(new RTCSessionDescription(data));//把对方的媒体格式设置进来//查询自己的媒体格式信息并且应答给对方pc.createAnswer().then(getAnswer).catch(handleErr);} else if(data.type answer) {//媒体协商默认回应有type值为answerconsole.log(get answer);pc.setRemoteDescription(new RTCSessionDescription(data));//我把offer发给对方对方回的answer。offer和answer都是有媒体格式信息。所以offer和answer不会同时存在一个客户端第一个进来的会发offer第二个进来的会发answer。把对方的媒体格式设置进来}});P2P代码
html代码
!DOCTYPE html
html langen
headmeta charsetUTF-8title视频聊天/titlelink relicon href./2.jpg typeimage/x-icon
/head
bodydiv aligncentertabletrtd colspan2h1 idwelcom欢迎来到1v1的聊天室/h1input id roombutton id enterRoom进入房间/buttonbutton idleaveRoom 离开房间/button/td/trtrtdlable本地视频/lable/tdtdlabel远端视频/label/td/trtrtdvideo idlocalVideo autoplay playsinline/video/tdtdvideo idremoteVideo autoplay playsinline/video/td/tr/table/divscript srcjs/socket.io.js/scriptscript srcjs/videoRoom.js/script/body
/html信令服务器代码
use strict;var http require(http);
var https require(https);
var fs require(fs);//自己安装的模块
var express require(express);
var serveIndex require(serve-index);//文件目录
var sqlite3 require(sqlite3);
var log4js require(log4js);
var socketIo require(socket.io);var logger log4js.getLogger();
logger.level info;var appexpress();
app.use(serveIndex(./zhangyangsong));
app.use(express.static(./zhangyangsong));var opt {key:fs.readFileSync(./cert/ssl_server.key),cert:fs.readFileSync(./cert/ssl_server.pem)
}// var httpServerhttp.createServer(app)
// .listen(8888, 0.0.0.0);
var httpsServerhttps.createServer(opt, app).listen(8008, 0.0.0.0);var db null;
var sql ;
// var io socketIo.listen(httpServer);
var io socketIo.listen(httpsServer);
io.sockets.on(connection, (socket){logger.info(connection:, socket.id);//处理1v1聊天室的消息socket.on(vjoin, (room, uname){logger.info(vjoin, room, uname);socket.join(room);var myRoom io.sockets.adapter.rooms[room];var users Object.keys(myRoom.sockets).length;logger.info(room user users);if (users 2) {socket.leave(room);socket.emit(vfull, room);} else {socket.emit(vjoined, room);if (users 1) {socket.to(room).emit(votherjoined, room, uname);}}});socket.on(vdata, (room, data){logger.info(vdata, data);socket.to(room).emit(vgetdata, room, data);});socket.on(vleave, (room, uname){if (room ) {logger.info(room is empty string);} else if (room undefined) {logger.info(room is undefine);} else if (room null) {logger.info(room is null);}var myRoom io.sockets.adapter.rooms[room];var users Object.keys(myRoom.sockets).length;logger.info(vleave users (users - 1));socket.leave(room);socket.emit(vleft, room);socket.to(room).emit(votherleft, room, uname);});
});function handleErr(e) {logger.info(e);
}具体操作js代码
use strict
//整个P2P过程需要知道双方的媒体类型、流和候选者
var hWelcom document.querySelector(h1#welcom);var url location.href;
var uname url.split(?)[1].split()[1];hWelcom.textContent 欢迎来到1v1视频聊天室: uname;var iptRoom document.querySelector(input#room);
var btnEnterRoom document.querySelector(button#enterRoom);
var btnLeaveRoom document.querySelector(button#leaveRoom);var videoLocal document.querySelector(video#localVideo);
var videoRemote document.querySelector(video#remoteVideo);var localStream null;
var remoteStream null;var socked null;
var room null;
var status init;
var pc null;
var url 120.78.130.50:8008
function getMedia(stream) {localStream stream;videoLocal.srcObject stream;
}function start() {var constraints {video:true,audio:true};//打开摄像头navigator.mediaDevices.getUserMedia(constraints).then(getMedia).catch(handleErr);conn();
}function conn() {socked io.connect(url);//监听来自服务器的信号socked.on(vfull, (room){status leaved;alert(房间已满: room);console.log(vfull, status);});socked.on(vjoined, (room){//创建视频连接类alert(成功加入房间: room);createPeerConnection();status joined;console.log(vjoined:, status);});socked.on(votherjoined, (room, uname){//建立视频连接alert(有人进来了: uname);if (status joined_unbind) {createPeerConnection();}status joined_conn;//当第二个人进来就要发起媒体协商了媒体协商就是双方互相知道和设置对方的媒体格式mediaNegociate();console.log(votherjoined:, status);});socked.on(vgetdata, (room, data){console.log(vgetdata:, data);if (!data) {return ;}if (data.type candidata) {//拿到对方传过来的候选者信息console.log(get other candidata);//候选者信息var cddt new RTCIceCandidate({sdpMLineIndex:data.label,candidate:data.candidate});pc.addIceCandidate(cddt);//把候选者对象加入pc} else if(data.type offer) {//媒体协商默认有type值为offerconsole.log(get offer);pc.setRemoteDescription(new RTCSessionDescription(data));//把对方的媒体格式设置进来//查询自己的媒体格式信息并且应答给对方pc.createAnswer().then(getAnswer).catch(handleErr);} else if(data.type answer) {//媒体协商默认回应有type值为answerconsole.log(get answer);pc.setRemoteDescription(new RTCSessionDescription(data));//我把offer发给对方对方回的answer。offer和answer都是有媒体格式信息。所以offer和answer不会同时存在一个客户端第一个进来的会发offer第二个进来的会发answer。把对方的媒体格式设置进来}});socked.on(vleft, (room){status leaved;console.log(vleft:, status);});socked.on(votherleft, (room, uname){status joined_unbind;closePeerConnection();console.log(votherleft:, status);});
}function getAnswer(decs) {pc.setLocalDescription(decs);//设置一下本地的媒体格式信息sendMessage(decs);
}function mediaNegociate() {if (status joined_conn) {//joined_conn代表我是连接的发起人if (pc) {var options {//要协商的内容如音频、视频...offerToReceiveVideo:true,offerToReceiveAudio:true}pc.createOffer(options).then(getOffer).catch(handleErr);}}
}function getOffer(desc) {//收到的媒体格式pc.setLocalDescription(desc);sendMessage(desc);//把我需要的媒体格式发给对方
}function createPeerConnection() {if (!pc) {var pcConfig {//ICE服务器iceServers:[{urls:turn:120.78.130.xx:3478, //指定中继服务器turnusername:zhangyangsong,credential:123}]}pc new RTCPeerConnection(pcConfig); //pc作用媒体协商流和轨的添加和停止传输相关功能统计相关功能pc.onicecandidate (e){ //得到了ICE服务器选择的候选者返回的事件if (e.candidate) {//先判断是不是候选者事件回来的console.log(CANDIDATE, e.candidate);sendMessage({//把候选者信息发给对方会发给信令服务器然后转发给对方type:candidata,label:e.candidate.sdpMLineIndex,//候选者标签id:e.candidate.sdpMid,//候选者idcandidate:e.candidate.candidate//候选者数据});}}//当媒体到达的时候做什么pc.ontrack (e){//ontrack收到远程音视频轨e时//alert(连接成功)remoteStream e.streams[0];videoRemote.srcObject remoteStream;//把远程媒体流放到远程音频标签里面显示出来}}if (localStream) {localStream.getTracks().forEach((track){pc.addTrack(track, localStream);//将本地的媒体流轨加到pc里面})}
}start();function sendMessage(data) {if (socked) {socked.emit(vdata, room, data);}
}function handleErr(e) {console.log(e);
}function enterRoom() {room iptRoom.value.trim();if (room ) {alert(请输入房间号);return;}socked.emit(vjoin, room, uname);
}function leaveRoom() {socked.emit(vleave, room, uname);closePeerConnection();
}function closePeerConnection() {console.log(close RTCPeerConnection);if (pc) {pc.close();pc null;}
}btnEnterRoom.onclick enterRoom;
btnLeaveRoom.onclick leaveRoom;到这里整个WebRTCP2P聊天室就完成了WebRTC可以开发的功能还有很多但基本原理都是这几个内容。
注意记得打开chrome://flags/这个网站搜索platform然后打开