【webrtc】websocket交换sdp实现ice链接
前言上一次进行了手动交换sdp成功进行了ice连接,但是正常情况下,不可能是让你手动交换,因为你能手动交换,说明你们之间已经有了传输通道,不然怎么获取对方的sdp。所以一般情况下,需要有个中间的服务器用来交换sdp,两个客户都通过中间服务器交换了sdp后实现ice连接。貌似这个过程的专业名词叫信令转发。服务器搭建首先初始化个npm项目,安装ws起个ws到8001const webSocket =
![](https://csdnimg.cn/release/devpress/public/img/ic-book.4f347164.png)
一键AI生成摘要,助你高效阅读
问答
·
前言
- 上一次进行了手动交换sdp成功进行了ice连接,但是正常情况下,不可能是让你手动交换,因为你能手动交换,说明你们之间已经有了传输通道,不然怎么获取对方的sdp。所以一般情况下,需要有个中间的服务器用来交换sdp,两个客户都通过中间服务器交换了sdp后实现ice连接。貌似这个过程的专业名词叫信令转发。
服务器搭建
- 首先初始化个npm项目,安装ws
- 起个ws到8001
const webSocket = require("ws");
const wss = new webSocket.Server({ port: 8001 });
const code2ws = new Map();
wss.on("connection", function connection(ws, request) {
//ws端
//随机码 六位数
let code = Math.floor(Math.random() * (999999 - 100000)) + 100000;
code2ws.set(code, ws); //形成一个映射
ws.sendData = (event, data) => {
//封装数据成字符串格式
ws.send(JSON.stringify({ event, data }));
};
ws.sendError = (msg) => {
ws.sendData("error", { msg });
};
ws.on("message", function incoming(message) {
console.log("incoming", message); //传过来的数据类型是:{event,data}
let parsedMessage = {};
try {
//通过event判断类型,交换到两端不同的函数里去
//防止服务器会崩溃
parsedMessage = JSON.parse(message);
} catch (e) {
ws.sendError("message invalid");
console.log("parse message error", e);
return;
}
let { event, data } = parsedMessage;
if (event === "login") {
ws.sendData("logined", { code });
} else if (event === "control") {
let remote = +data.remote; //转换成数据类型
if (code2ws.has(remote)) {
//相当于把这个客户端的发送方法与另一个客户端发送方法统一起来。
// 远端发送方法都为sendRemote,同时发送被控制和已控制事件
ws.sendData("controlled", { remote });
ws.sendRemote = code2ws.get(remote).sendData;
code2ws.get(remote).sendRemote = ws.sendData;
ws.sendRemote("be-controlled", { remote: code });
}
} else if (event === "forward") {
//实现信令转发需求
if (ws.sendRemote) {
ws.sendRemote(data.event, data.data);
}
}
});
ws.on("close", () => {
//清理事件
code2ws.delete(code);
});
});
- http://websocket.org/echo.html 这个网址可以快速测试ws有效性,直接填自己地址进行连接,连上说明ok。
- 下面的message可以模拟客户端发送请求:
{"event":"login"}
- 发送login,则触发login
- 打开另一个网页,模拟另一个客户端,同样连接ws,发送:
{"event":"control","data":{"remote":"664497"}}
- remote内容为login收到的code,其实就是每多一个链接在map上存一个。
- forward则是直发,发送“data”:{“data”:“xxx”}即可收到xxx的内容。
electron
- 我们可以用electron来模拟制作个远程控制交换sdp。
- 每个拿到electron客户端的会去连接websocket服务交换sdp,利用electron自带的node服务获取桌面流,然后进行通信。
- 原理和上面差不多,在启动时监听主窗口以及ws服务器传来的消息:
const { ipcMain } = require("electron"); //主进程
const { send: sendMainWindow } = require("./windows/main"); //向主窗口发送信息
const {
create: createControlWindow,
send: sendControlWindow,
} = require("./windows/control"); //创建新的窗口
const signal = require("./signal");
const robot = require("./robot");
module.exports = function () {
ipcMain.handle("login", async () => {
let { code } = await signal.invoke("login", null, "logined");
console.log("login--data", code);
return code;
});
ipcMain.on("control", async (e, remote) => {
signal.send("control", { remote });
});
signal.on("controlled", (data) => {
createControlWindow();
sendMainWindow("control-state-change", data.remote, 1);
});
signal.on("be-controlled", (data) => {
sendMainWindow("control-state-change", data.remote, 2);
});
ipcMain.on("forward", (e, event, data) => {
signal.send("forward", { event, data });
});
signal.on("offer", (data) => {
sendMainWindow("offer", data);
});
signal.on("answer", (data) => {
sendControlWindow("answer", data);
});
signal.on("puppet-candidate", (data) => {
sendControlWindow("candidate", data);
});
signal.on("control-candidate", (data) => {
sendMainWindow("candidate", data);
});
robot();
};
const { ipcMain } = require("electron");
const robot = require("robotjs");
const vkey = require("vkey");
function handleMouse(data) {
//传过来的数据:data:{clientX,clientY,screen:{width,height},video:{width,height}}
let { clientX, clientY, screen, video } = data;
let x = (clientX * screen.width) / video.width;
let y = (clientY * screen.height) / video.height;
console.log(x, y);
robot.moveMouse(x, y);
robot.mouseClick();
console.log("mouse", data);
}
function handleKey(data) {
//传过来的数据:data:{keyCode,meta,alt,ctrl,shift}
const modifiers = []; //修饰键
if (data.meta) modifiers.push("meta");
if (data.shift) modifiers.push("shift");
if (data.alt) modifiers.push("alt");
if (data.ctrl) modifiers.push("ctrl");
let key = vkey[data.keyCode].toLowerCase(); //拿到对应的键值
if (key[0] !== "<") {
//排除<shift>特殊字符
robot.keyTap(key, modifiers);
}
console.log("key", data);
}
module.exports = function () {
ipcMain.on("robot", (e, type, data) => {
if (type === "mouse") {
//鼠标类型
handleMouse(data);
} else if (type === "key") {
//键盘类型
handleKey(data);
}
});
};
- 启动应用则是创建了一个react应用:
const { BrowserWindow } = require("electron");
const isDev = require("electron-is-dev"); //判断是生产环境还是开发环境
const path = require("path");
let win;
function create() {
win = new BrowserWindow({
//创建一个窗口
width: 600,
height: 600,
webPreferences: {
//可以使用node相关的
nodeIntegration: true,
},
});
if (isDev) {
win.loadURL("http://localhost:3000");
} else {
win.loadFile(
//这里是react打包后的目录
path.resolve(__dirname, "../../renderer/pages/main/index.html")
);
}
}
function send(channel, ...args) {
win.webContents.send(channel, ...args);
}
module.exports = { create, send };
- 在react应用中,可以调用electron的能力与主进程通信:
import React, { useState, useEffect } from "react"; //使用hooks
import "./controll";
const { ipcRenderer } = window.require("electron"); //引入渲染进程
function App() {
const [remoteCode, setRemoteCode] = useState(""); //控制的控制码
const [localCode, setLocalCode] = useState(""); //本身的控制码
const [controlText, setControlText] = useState(""); //控制码的文案
const login = async () => {
let code = await ipcRenderer.invoke("login");
setLocalCode(code);
};
useEffect(() => {
login();//加载完成发送登录获取随机码
ipcRenderer.on("control-state-change", handleControlState); //监听状态变更
return () => {
ipcRenderer.removeListener(
"control-state-change",
handleControlState
);
};
}, []);
const startControl = (remoteCode) => {
ipcRenderer.send("control", remoteCode);
};
const handleControlState = (e, name, type) => {
let text = "";
if (type === 1) {
//控制别人
text = `正在远程控制${name}`;
} else if (type === 2) {
//被别人控制
text = `被${name}控制`;
}
setControlText(text); //当前页面的文本
};
return (
<div className="App">
{controlText === "" ? (
<>
<div>你的控制码{localCode}</div>
<input
type="text"
value={remoteCode}
onChange={(e) => setRemoteCode(e.target.value)}
/>
<button onClick={() => startControl(remoteCode)}>
确认
</button>
</>
) : (
<div>{controlText}</div>
)}
</div>
);
}
export default App;
- 当我们输入远端的随机码,即可触发control,也就是前面监听的control,这个control会发送输入的随机码给ws服务器,在ws服务器上,2个客户端的通信方法则被改写,ws会给2方发送被控制和已控制事件,监听到已控制事件的控制方会打开新窗口,并创建rtc链接,新窗口用来播放从被控制端获取的视频码流:
const { ipcRenderer } = require("electron");
const EventEmitter = require("events");
const peer = new EventEmitter();
const video = document.getElementById("screen-video");
function play(stream) {
video.srcObject = stream;
video.onloadedmetadata = () => video.play();
}
peer.on("add-stream", (stream) => {
play(stream);
});
window.onkeydown = function (e) {
console.log(e);
var data = {
keyCode: e.keyCode,
shift: e.shiftKey,
meta: e.metaKey,
control: e.controlKey,
alt: e.altKey,
};
peer.emit("robot", "key", data); //返回一个键盘类型的事件的结果
};
window.onmouseup = function (e) {
console.log(e);
var data = {
clientX: e.clientX,
clientY: e.clientY,
video: {
width: video.getBoundingClientRect().width,
height: video.getBoundingClientRect().height,
},
};
peer.emit("robot", "mouse", data); 返回一个鼠标类型的事件的结果
};
peer.on("robot", (type, data) => {
if (type === "mouse") {
data.screen = {
width: window.screen.width,
height: window.screen.height,
};
}
setTimeout(() => {
ipcRenderer.send("robot", type, data); //********通信**渲染进程
}, 2000);
});
//创建一个远程连接
const pc = new window.RTCPeerConnection({});
async function createOffer() {
//创造一个远程端点
const offer = await pc.createOffer({
//只需要视频
offerToReceiveAudio: false,
offerToReceiveVideo: true,
});
await pc.setLocalDescription(offer);
console.log("pc offer", JSON.stringify(offer));
return pc.localDescription;
}
createOffer().then((offer) => {
ipcRenderer.send("forward", "offer", { type: offer.type, sdp: offer.sdp });
});
async function setRemote(answer) {
await pc.setRemoteDescription(answer);
}
ipcRenderer.on("answer", (e, answer) => {
setRemote(answer);
});
pc.onaddstream = function (e) {
peer.emit("add-stream", e.stream);
};
pc.onicecandidate = function (e) {
if (e.candidate) {
ipcRenderer.send(
"forward",
"control-candidate",
JSON.stringify(e.candidate)
);
}
};
let candidates = [];
ipcRenderer.on("candidate", (e, candidate) => {
addIceCandidate(candidate);
});
async function addIceCandidate(candidate) {
if (candidate) {
candidates.push(candidate);
}
if (pc.remoteDescription && pc.remoteDescription.type) {
for (var i = 0; i < candidates.length; i++) {
await pc.addIceCandidate(
new RTCIceCandidate(JSON.parse(candidates[i]))
);
}
candidates = []; //清空数据
}
}
- 监听到被控制方事件会触发control-state-change事件,改变文案显示,同时自身早已创建好监听,等待offer。
- 控制方会发送
ipcRenderer.send("forward", "offer", { type: offer.type, sdp: offer.sdp });
,用来再次转发给ws,将offer发给被控制方。 - 被控制方收到offer后会发送answer给控制方:
const { desktopCapturer, ipcRenderer } = window.require("electron");
function getScreenStream() {
return new Promise((resolve, reject) => {
desktopCapturer
.getSources({ types: ["window", "screen"] })
.then(async (sources) => {
for (const source of sources) {
try {
const stream = await navigator.mediaDevices.getUserMedia(
{
audio: false,
video: {
mandatory: {
chromeMediaSource: "desktop",
chromeMediaSourceId: source.id,
maxWidth: window.screen.width,
maxHeight: window.screen.height,
},
},
}
);
resolve(stream);
} catch (reject) {
console.error(reject);
}
}
});
});
}
const pc = new window.RTCPeerConnection({});
async function createAnswer(offer) {
let screenStream = await getScreenStream(); //获取媒体流
pc.addStream(screenStream); //添加媒体流
await pc.setRemoteDescription(offer);
await pc.setLocalDescription(await pc.createAnswer());
return pc.localDescription;
}
ipcRenderer.on("offer", async (e, offer) => {
let answer = await createAnswer(offer);
ipcRenderer.send("forward", "answer", {
type: answer.type,
sdp: answer.sdp,
});
});
pc.onicecandidate = function (e) {
if (e.candidate) {
ipcRenderer.send(
"forward",
"puppet-candidate",
JSON.stringify(e.candidate)
);
}
};
let candidates = []; //缓存的效果
async function addIceCandidate(candidate) {
if (candidate) {
candidates.push(candidate);
}
if (pc.remoteDescription && pc.remoteDescription.type) {
for (var i = 0; i < candidates.length; i++) {
await pc.addIceCandidate(new RTCIceCandidate(candidates[i]));
}
candidates = []; //清空数据
}
}
ipcRenderer.on("candidate", (e, candidate) => {
addIceCandidate(candidate);
});
- 然后控制方会拿到answer用setRemoteDescription存起来。
- 这时2端的候选地址就会产生,一个发送
control-candidate
事件,一个发送puppet-candidate
事件。 - 然后2端通过
addIceCandidate
保存对方的候选地址。这样2端就通了。 - 前面控制端使用rtc监听
onaddstream
事件则被被控制端触发,收到视频码流,最后使用播放即可输出:
function play(stream) {
video.srcObject = stream;
video.onloadedmetadata = () => video.play();
}
peer.on("add-stream", (stream) => {
play(stream);
});
- 注意:robotjs的远端点击在win10下可能由于缩放问题不准,需要将win10的缩放调整至100%(一般的笔记本默认是125%甚至有150%的)。
- 通过rtc链接后可以使用datachannel来进行rtc级别的消息通信,但是我实测无法使用,暂时不知道哪里出了问题。有兴趣的可以看看mdn :https://developer.mozilla.org/zh-CN/docs/Web/API/RTCPeerConnection/createDataChannel
- 本篇代码地址:https://github.com/yehuozhili/learn-webrtc
更多推荐
所有评论(0)