- 像素流送插件 Pixel Streaming (接受信令服务器的控制命令信息 根据命令调用相应的功能进行渲染 将渲染结果发送给信令服务器)
- 信令服务器 Signalling and Web Server (基于nodejs 负责交涉浏览器和像素流送插件之间的连接,将播放媒体流送的HTML和JavaScript环境提供给浏览器。接受前端发过来的命令 并将命令转发到ue4服务端 同时接受ue4服务端发过来的视频流与其他信息给前端)
视频流连接过程:
1、启动所有像素流送组件时,在虚幻引擎中运行的像素流送插件首先将建立到信令和Web服务器的链接。
2、客户端会连接到信令服务器,服务器将对客户端提供一个HTML页面,其中包含播放器控件和以JavaScript编写的控制代码。
3、用户开始流送时,信令服务器将进行交涉,在客户端浏览器和虚幻引擎之间建立直接连接。
4、客户端和虚幻引擎之间的连接建立后,像素流送插件便会直接开始将媒体流送到浏览器。来自客户端的输入由播放器页面的JavaScript环境直接发送回虚幻引擎中。
5、即使媒体流送已经开始播放,信令和Web服务器仍会维持其与浏览器和虚幻引擎的连接,以便在必要时将用户从流送中移除,并处理浏览器造成的连接断开。
app.js作用
app.js
初始化调用load方法
(setupHtmlEvents)
注册浏览器窗口大小方向改变以及操作菜单的一些事件
(setupFreezeFrameOverlay)
创建了一个id为freezeFrameOverlay的div,该div有一个img子元素,定位子绝父绝 接收base64的图片格式
(registerKeyboardEvents)
监听键盘的按下弹起事件
(start)
调用window.WebSocket进行即时通讯
通过onmessage进行跨页面通信 app.js为接收方 通过Cirrus web服务器从WebRTC发送方接收配置数据
new了一个webRtcPlayer类(调用webRtcPlayer.js) 将接收到的配置数据传入 生成了一个webRtcPlayerObj对象
给webRtcPlayerObj对象下注册了很多给webRtcPlayer.js调用的事件
通过webRtcPlayerObj对象下的video属性在player主元素下生成一个video标签
将该标签作为返回值返回,在这个元素上,注册了鼠标跟播放器控件的交互事件
cirrus.js 作用
利用express框架搭建node服务器
读取config.js配置项 然后合并configFile跟defaultConfig配置项 合并后重新写入config.js
判断config对象中的每一个配置项是否为undefined,不是就单独提取出来重新赋值
为UE视频流创建Websocket服务器,以便与Cirrus通信
为浏览器创建Websocket服务器,以便与Cirrus进行通信。
记录操作了连接人数信息 players
与matchmaker进行通信
WebSocket
WebSocket是HTML5一种新的协议,这种协议是一种长连接,只需要通过一次请求来初始化链接,然后所有的请求和响应都是通过这个TCP链接进行通信。
客户端设置 app.js
var ws = new WebSocket(window.location.href.replace('http://', 'ws://').replace('https://', 'wss://'));
WebSocket()构造函数返回一个websocket对象,提供用于创建和管理与服务器的WebSocket连接的API 。
这个websocket对象将帮助我们建立与服务器的连接并创建双向数据流,即从两端发送和接收数据。
服务器设置 cirrus.js
// 创建服务器
var app = express();
var http = require('http').Server(app);
// 启动http服务 监听81端口
// httpport为config.js的配置项 81
http.listen(httpPort, function () {
console.logColor(logging.Green, 'Http listening on *: ' + httpPort);
});
// WebSocket正在关注81端口(当81端口可用时会立即尝试建立连接)
// playerServer对象将监听事件 建立连接或握手完成或连接关闭等
let playerServer = new WebSocket.Server({ server: config.UseHTTPS ? https : http });
从服务器发送数据到客户端 cirrus.js —— app.js
// 添加一个connection在握手完成时发出的事件。成功建立连接后,可以向客户端发送消息
playerServer.on('connection', function (ws, req) {
// 在事件“message”上接收来自客户端的消息
ws.on('message', function onStreamerMessage(msg) {
} catch (err) {
console.error(`ERROR: ws.on message error: ${err.message}`);
}
});
// 向客户端发送数据
var clientConfig = { type: 'config', peerConnectionOptions: {} };
ws.send(JSON.stringify(clientConfig));
})
app.js接收
ws.onmessage = function (event) {
console.log(`<- SS: ${event.data}`);
let msg = JSON.parse(event.data);
if (msg.type === 'config') {// 初始化 设置
onConfig(msg);
} else if (msg.type === 'playerCount') {// 玩家计数
updateKickButton(msg.count - 1);
} else if (msg.type === 'answer') {// 答案
onWebRtcAnswer(msg);
} else if (msg.type === 'iceCandidate') {// ICE候选人
onWebRtcIce(msg.candidate);
} else {
console.log(`invalid SS message type: ${msg.type}`);
}
};
客户端给信令服务器发送消息也是通过ws.send(),服务端通过监听message事件接收.
虚幻引擎的像素流插件与信令服务器之间的连接
cirrus.js
let streamer
// 启动http服务 监听8888端口 streamerPort:UE4实例的--streamerPort属性
var streamerHttpServer = require(config.UseHTTPS ? 'https' : 'http').Server({});
streamerHttpServer.listen(streamerPort);
// WebSocket正在关注8888端口(当8888端口可用时会立即尝试建立连接)
let streamerServer = new WebSocket.Server({ server: streamerHttpServer, backlog: 1 });
streamerServer.on('connection', function (ws, req) {
// 会先通知matchmaker,视频流已经连接了信令服务器 可以随时使用
matchmaker.write(JSON.stringify(message));
// 这里跟上面的一样 像素流插件跟信令服务器可以双向通信
streamer = ws;
})
websocket对象API
- 可读属性
ws.readyState // 只读属性返回WebSocket连接的当前状态
// CONNECTING:值为0,表示正在连接;
// OPEN:值为1,表示连接成功,可以通信了;
// CLOSING:值为2,表示连接正在关闭;
// CLOSED:值为3,表示连接已经关闭,或者打开连接失败;
ws.url // 返回WebSocket构造函数解析的绝对URL
- 方法
ws.close() // 关闭连接
ws.send() // 传输数据
- 可监听事件
close // 当WebSocket连接关闭时触发
error // 当与WebSocket的连接因错误而关闭时触发
message // 当通过WebSocket接收数据时触发
open // 当WebSocket连接打开时触发
players
在信令服务器连接上ue4实例之后,客户端开始连接信令服务器
let players = new Map();// Map 对象保存键值对。任何值(对象或者原始值) 都可以作为一个键或一个值。es6新增方法
let nextPlayerId = 100;
let playerId = (++nextPlayerId).toString();// 定义用户id 每多一个用户连接就加1
players.set(playerId, { ws: ws, id: playerId });
像素流送插件给信令服务器发送信息的时候,msg里面会携带playerId,然后根据用户的的id去查找对应的ws对象,再将发送过来的信息传给对应的用户
let playerId = msg.playerId;
let player = players.get(playerId);
player.ws.send(JSON.stringify(msg));
Matchmaker
matchmaker与信令服务器之间通过nodejs的net模块进行通信,http客户端与http服务端的通信均依赖于socket(net.Socket)
信令服务器与matchmaker的通信过程:
cirrus.js
// 如果设置了config.json的UseMatchmaker属性为true
var net = require('net');
var matchmaker = new net.Socket();
// 成功建立 socket 连接时触发。
matchmaker.on('connect', function () {
// 设置状态值 告诉mm当前是否有用户在连接状态
var playerConnected = false;
if (players && players.size > 0) {
playerConnected = true;
}
// 将新用户的连接状态发送给mm
message = {
type: 'connect',
address: typeof serverPublicIp === 'undefined' ? '127.0.0.1' : serverPublicIp,// 运行信令和 Web 服务器的计算机的公共 IP。(localhost)
port: httpPort,// 用于与客户端浏览器进行 HTTP 连接的端口。(81)
ready: streamer && streamer.readyState === 1,// ue4实例是否准备好了
playerConnected: playerConnected
};
matchmaker.write(JSON.stringify(message));
})
// 连接mm matchmakerPort(9999) matchmakerAddress(127.0.0.1)
matchmaker.connect(matchmakerPort, matchmakerAddress);
matchmaker.js
// 连接到Matchmaker的所有Cirrus服务器的列表。
var cirrusServers = new Map();
// 与信令服务器通信
const net = require('net');
const matchmaker = net.createServer((connection) => {
// 信令服务器连接到matchmaker
connection.on('data', (data) => {
message = JSON.parse(data);
if (message.type === 'connect') {
cirrusServer = {
address: message.address,
port: message.port,
numConnectedClients: 0,// 连接的人数
lastPingReceived: Date.now()
};
cirrusServer.ready = message.ready === true;
if (message.playerConnected == true) {
cirrusServer.numConnectedClients = 1;
}
// 查找是否已连接到ciruss服务器地址(可能发生重新连接)
let server = [...cirrusServers.entries()].find(([key, val]) => val.address === cirrusServer.address && val.port === cirrusServer.port);
if (!server || server.size <= 0) {
cirrusServers.set(connection, cirrusServer);
} else {
var foundServer = cirrusServers.get(server[0]);
// 如果找到了相同地址的服务器 删除(保留)上一个已连接的客户端
if (foundServer) {
cirrusServers.set(connection, cirrusServer);
cirrusServers.delete(server[0]);
} else {
cirrusServers.set(connection, cirrusServer);
}
}
}else if (message.type === 'streamerConnected') {
cirrusServer = cirrusServers.get(connection);
if (cirrusServer) {
cirrusServer.ready = true;
} else {
disconnect(connection);
}
}
})
})
// 监听来自cirrus.js的连接请求 MatchmakerPort(9999)
matchmaker.listen(config.MatchmakerPort, () => {
console.log('Matchmaker listening on *:' + config.MatchmakerPort);
});
matchmaker事件
信令服务器可以监听mm连接时的状态
cirrus.js
var matchmaker = new net.Socket();
matchmaker.onconnnect // 成功连接时触发
matchmaker.onerror // 失败时触发
matchmaker.onend // 结束时触发
matchmaker.onclose // 连接关闭时触发
matchmaker.js
const matchmaker = net.createServer((connection) => {
connection.ondata // 信令服务器发送消息时触发
connection.onerror // 信令服务器断开连接时触发
})
ue实例影响matchmaker
message = JSON.parse(data);
if(message.type === 'streamerConnected') {// 视频流连接成功触发
cirrusServer.ready = true;
}else if(message.type === 'streamerDisconnected') {// 视频流关闭触发
cirrusServer.ready = false;
}
浏览器客户端影响matchmaker
// 客户端ws对象
if(message.type === 'clientConnected'){// 连接成功后
cirrusServer.numConnectedClients++;
}else if(){// close跟error状态下触发
cirrusServer.numConnectedClients--;
}
连接状态
app.js