优化目录结构
This commit is contained in:
187
client/src/core/peer.js
Normal file
187
client/src/core/peer.js
Normal file
@@ -0,0 +1,187 @@
|
||||
import * as Logger from "../utils/logger.js";
|
||||
|
||||
export default class Peer extends EventTarget {
|
||||
constructor(connectionId, polite, config, resendIntervalMsec = 5000) {
|
||||
super();
|
||||
const _this = this;
|
||||
this.connectionId = connectionId;
|
||||
this.polite = polite;
|
||||
this.config = config;
|
||||
this.pc = new RTCPeerConnection(this.config);
|
||||
this.makingOffer = false;
|
||||
this.waitingAnswer = false;
|
||||
this.ignoreOffer = false;
|
||||
this.srdAnswerPending = false;
|
||||
this.log = str => void Logger.log(`[${_this.polite ? 'POLITE' : 'IMPOLITE'}] ${str}`);
|
||||
this.warn = str => void Logger.warn(`[${_this.polite ? 'POLITE' : 'IMPOLITE'}] ${str}`);
|
||||
this.assert_equals = window.assert_equals ? window.assert_equals : (a, b, msg) => { if (a === b) { return; } throw new Error(`${msg} expected ${b} but got ${a}`); };
|
||||
this.interval = resendIntervalMsec;
|
||||
this.sleep = msec => new Promise(resolve => setTimeout(resolve, msec));
|
||||
|
||||
this.pc.ontrack = e => {
|
||||
_this.log(`ontrack:${e}`);
|
||||
_this.dispatchEvent(new CustomEvent('trackevent', { detail: e }));
|
||||
};
|
||||
this.pc.ondatachannel = e => {
|
||||
_this.log(`ondatachannel:${e}`);
|
||||
_this.dispatchEvent(new CustomEvent('adddatachannel', { detail: e }));
|
||||
};
|
||||
this.pc.onicecandidate = ({ candidate }) => {
|
||||
_this.log(`send candidate:${candidate}`);
|
||||
if (candidate == null) {
|
||||
return;
|
||||
}
|
||||
_this.dispatchEvent(new CustomEvent('sendcandidate', { detail: { connectionId: _this.connectionId, candidate: candidate.candidate, sdpMLineIndex: candidate.sdpMLineIndex, sdpMid: candidate.sdpMid } }));
|
||||
};
|
||||
|
||||
this.pc.onnegotiationneeded = this._onNegotiation.bind(this);
|
||||
|
||||
this.pc.onsignalingstatechange = () => {
|
||||
_this.log(`signalingState changed:${_this.pc.signalingState}`);
|
||||
};
|
||||
|
||||
this.pc.oniceconnectionstatechange = () => {
|
||||
_this.log(`iceConnectionState changed:${_this.pc.iceConnectionState}`);
|
||||
if (_this.pc.iceConnectionState === 'failed') {
|
||||
this.dispatchEvent(new Event('disconnect'));
|
||||
}
|
||||
};
|
||||
|
||||
this.pc.onicegatheringstatechange = () => {
|
||||
_this.log(`iceGatheringState changed:${_this.pc.iceGatheringState}'`);
|
||||
};
|
||||
|
||||
this.loopResendOffer();
|
||||
}
|
||||
|
||||
async _onNegotiation() {
|
||||
try {
|
||||
this.log(`SLD due to negotiationneeded`);
|
||||
this.assert_equals(this.pc.signalingState, 'stable', 'negotiationneeded always fires in stable state');
|
||||
this.assert_equals(this.makingOffer, false, 'negotiationneeded not already in progress');
|
||||
this.makingOffer = true;
|
||||
await this.pc.setLocalDescription();
|
||||
this.assert_equals(this.pc.signalingState, 'have-local-offer', 'negotiationneeded not racing with onmessage');
|
||||
this.assert_equals(this.pc.localDescription.type, 'offer', 'negotiationneeded SLD worked');
|
||||
this.waitingAnswer = true;
|
||||
this.dispatchEvent(new CustomEvent('sendoffer', { detail: { connectionId: this.connectionId, sdp: this.pc.localDescription.sdp } }));
|
||||
} catch (e) {
|
||||
this.log(e);
|
||||
} finally {
|
||||
this.makingOffer = false;
|
||||
}
|
||||
}
|
||||
|
||||
async loopResendOffer() {
|
||||
while (this.connectionId) {
|
||||
if (this.pc && this.waitingAnswer) {
|
||||
this.dispatchEvent(new CustomEvent('sendoffer', { detail: { connectionId: this.connectionId, sdp: this.pc.localDescription.sdp } }));
|
||||
}
|
||||
await this.sleep(this.interval);
|
||||
}
|
||||
}
|
||||
|
||||
close() {
|
||||
this.connectionId = null;
|
||||
if (this.pc) {
|
||||
this.pc.close();
|
||||
this.pc = null;
|
||||
}
|
||||
}
|
||||
|
||||
getTransceivers(connectionId) {
|
||||
if (this.connectionId != connectionId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.pc.getTransceivers();
|
||||
}
|
||||
|
||||
addTrack(connectionId, track) {
|
||||
if (this.connectionId != connectionId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.pc.addTrack(track);
|
||||
}
|
||||
|
||||
addTransceiver(connectionId, trackOrKind, init) {
|
||||
if (this.connectionId != connectionId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.pc.addTransceiver(trackOrKind, init);
|
||||
}
|
||||
|
||||
createDataChannel(connectionId, label) {
|
||||
if (this.connectionId != connectionId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.pc.createDataChannel(label);
|
||||
}
|
||||
|
||||
async getStats(connectionId) {
|
||||
if (this.connectionId != connectionId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return await this.pc.getStats();
|
||||
}
|
||||
|
||||
async onGotDescription(connectionId, description) {
|
||||
if (this.connectionId != connectionId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const _this = this;
|
||||
const isStable =
|
||||
this.pc.signalingState == 'stable' ||
|
||||
(this.pc.signalingState == 'have-local-offer' && this.srdAnswerPending);
|
||||
this.ignoreOffer =
|
||||
description.type == 'offer' && !this.polite && (this.makingOffer || !isStable);
|
||||
|
||||
if (this.ignoreOffer) {
|
||||
_this.log(`glare - ignoring offer`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.waitingAnswer = false;
|
||||
this.srdAnswerPending = description.type == 'answer';
|
||||
_this.log(`SRD(${description.type})`);
|
||||
await this.pc.setRemoteDescription(description);
|
||||
this.srdAnswerPending = false;
|
||||
|
||||
if (description.type == 'offer') {
|
||||
_this.dispatchEvent(new CustomEvent('ongotoffer', { detail: { connectionId: _this.connectionId } }));
|
||||
|
||||
_this.assert_equals(this.pc.signalingState, 'have-remote-offer', 'Remote offer');
|
||||
_this.assert_equals(this.pc.remoteDescription.type, 'offer', 'SRD worked');
|
||||
_this.log('SLD to get back to stable');
|
||||
await this.pc.setLocalDescription();
|
||||
_this.assert_equals(this.pc.signalingState, 'stable', 'onmessage not racing with negotiationneeded');
|
||||
_this.assert_equals(this.pc.localDescription.type, 'answer', 'onmessage SLD worked');
|
||||
_this.dispatchEvent(new CustomEvent('sendanswer', { detail: { connectionId: _this.connectionId, sdp: _this.pc.localDescription.sdp } }));
|
||||
|
||||
} else {
|
||||
_this.dispatchEvent(new CustomEvent('ongotanswer', { detail: { connectionId: _this.connectionId } }));
|
||||
|
||||
_this.assert_equals(this.pc.remoteDescription.type, 'answer', 'Answer was set');
|
||||
_this.assert_equals(this.pc.signalingState, 'stable', 'answered');
|
||||
this.pc.dispatchEvent(new Event('negotiated'));
|
||||
}
|
||||
}
|
||||
|
||||
async onGotCandidate(connectionId, candidate) {
|
||||
if (this.connectionId != connectionId) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.pc.addIceCandidate(candidate);
|
||||
} catch (e) {
|
||||
if (this.pc && !this.ignoreOffer)
|
||||
this.warn(`${this.pc} this candidate can't accept current signaling state ${this.pc.signalingState}.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
317
client/src/core/renderstreaming.js
Normal file
317
client/src/core/renderstreaming.js
Normal file
@@ -0,0 +1,317 @@
|
||||
import Peer from "./peer.js";
|
||||
import * as Logger from "../utils/logger.js";
|
||||
|
||||
function uuid4() {
|
||||
var temp_url = URL.createObjectURL(new Blob());
|
||||
var uuid = temp_url.toString();
|
||||
URL.revokeObjectURL(temp_url);
|
||||
return uuid.split(/[:/]/g).pop().toLowerCase();
|
||||
}
|
||||
|
||||
export class RenderStreaming {
|
||||
constructor(signaling, config) {
|
||||
this._peer = null; // participant端:单一peer
|
||||
this._peers = new Map(); // host端:多peer Map (participantId → Peer)
|
||||
this._connectionId = null;
|
||||
this._participantId = null; // 自己的participantId
|
||||
this._isHost = false;
|
||||
this.onConnect = function (connectionId, data) { Logger.log(`Connect peer on ${connectionId}.`); };
|
||||
this.onDisconnect = function (connectionId) { Logger.log(`Disconnect peer on ${connectionId}.`); };
|
||||
this.onGotOffer = function (connectionId) { Logger.log(`On got Offer on ${connectionId}.`); };
|
||||
this.onGotAnswer = function (connectionId) { Logger.log(`On got Answer on ${connectionId}.`); };
|
||||
this.onTrackEvent = function (data) { Logger.log(`OnTrack event peer with data:${data}`); };
|
||||
this.onAddChannel = function (data) { Logger.log(`onAddChannel event peer with data:${data}`); };
|
||||
this.onMessage = function (data) { Logger.log(`On message: ${data}`); };
|
||||
this.onParticipantLeft = function (participantId) { Logger.log(`Participant left: ${participantId}.`); };
|
||||
this.onParticipantJoined = function (participantId) { Logger.log(`Participant joined: ${participantId}.`); };
|
||||
this.onNewPeer = function (participantId) { Logger.log(`New peer created for ${participantId}.`); };
|
||||
this._config = config;
|
||||
this._signaling = signaling;
|
||||
this._signaling.addEventListener('connect', this._onConnect.bind(this));
|
||||
this._signaling.addEventListener('disconnect', this._onDisconnect.bind(this));
|
||||
this._signaling.addEventListener('offer', this._onOffer.bind(this));
|
||||
this._signaling.addEventListener('answer', this._onAnswer.bind(this));
|
||||
this._signaling.addEventListener('candidate', this._onIceCandidate.bind(this));
|
||||
this._signaling.addEventListener('on-message', this._onMessage.bind(this));
|
||||
this._signaling.addEventListener('participant-left', this._onParticipantLeft.bind(this));
|
||||
this._signaling.addEventListener('participant-joined', this._onParticipantJoined.bind(this));
|
||||
}
|
||||
|
||||
async _onConnect(e) {
|
||||
const data = e.detail;
|
||||
if (this._connectionId == data.connectionId) {
|
||||
this._participantId = data.participantId;
|
||||
this._isHost = data.role === 'host';
|
||||
|
||||
if (!this._isHost) {
|
||||
// participant端:立即创建单一peer并开始协商
|
||||
this._preparePeerConnection(this._connectionId, data.polite, null);
|
||||
}
|
||||
// host端:不在connect时创建peer,等participant加入后再创建
|
||||
|
||||
this.onConnect(data.connectionId, data);
|
||||
}
|
||||
}
|
||||
|
||||
async _onDisconnect(e) {
|
||||
const data = e.detail;
|
||||
if (this._connectionId == data.connectionId) {
|
||||
this.onDisconnect(data.connectionId);
|
||||
if (this._peer) {
|
||||
this._peer.close();
|
||||
this._peer = null;
|
||||
}
|
||||
// 关闭所有host端peers
|
||||
this._peers.forEach((peer, participantId) => {
|
||||
peer.close();
|
||||
});
|
||||
this._peers.clear();
|
||||
}
|
||||
}
|
||||
|
||||
async _onOffer(e) {
|
||||
const offer = e.detail;
|
||||
const participantId = offer.participantId;
|
||||
|
||||
if (this._isHost) {
|
||||
// host端:为该participant创建或复用peer
|
||||
// host端始终使用polite=false(impolite),确保perfect negotiation中host的offer优先
|
||||
let peer = this._peers.get(participantId);
|
||||
if (!peer || (peer.pc && peer.pc.iceConnectionState === 'disconnected')) {
|
||||
if (peer) peer.close();
|
||||
peer = this._preparePeerConnection(this._connectionId, false, participantId);
|
||||
}
|
||||
const desc = new RTCSessionDescription({ sdp: offer.sdp, type: "offer" });
|
||||
try {
|
||||
await peer.onGotDescription(this._connectionId, desc);
|
||||
} catch (error) {
|
||||
Logger.warn(`Error on GotDescription for participant ${participantId}: ${error}`);
|
||||
}
|
||||
} else {
|
||||
// participant端:使用单一peer,始终使用polite=true
|
||||
if (this._peer && this._peer.pc && this._peer.pc.iceConnectionState === 'disconnected') {
|
||||
this._peer.close();
|
||||
this._peer = null;
|
||||
}
|
||||
if (!this._peer) {
|
||||
this._preparePeerConnection(offer.connectionId, true, null);
|
||||
}
|
||||
const desc = new RTCSessionDescription({ sdp: offer.sdp, type: "offer" });
|
||||
try {
|
||||
await this._peer.onGotDescription(offer.connectionId, desc);
|
||||
} catch (error) {
|
||||
Logger.warn(`Error on GotDescription: ${error}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async _onAnswer(e) {
|
||||
const answer = e.detail;
|
||||
const participantId = answer.participantId;
|
||||
const desc = new RTCSessionDescription({ sdp: answer.sdp, type: "answer" });
|
||||
|
||||
if (this._isHost && participantId) {
|
||||
// host端:路由到对应participant的peer
|
||||
const peer = this._peers.get(participantId);
|
||||
if (peer) {
|
||||
try {
|
||||
await peer.onGotDescription(this._connectionId, desc);
|
||||
} catch (error) {
|
||||
Logger.warn(`Error on GotDescription answer for ${participantId}: ${error}`);
|
||||
}
|
||||
}
|
||||
} else if (this._peer) {
|
||||
// participant端
|
||||
try {
|
||||
await this._peer.onGotDescription(answer.connectionId, desc);
|
||||
} catch (error) {
|
||||
Logger.warn(`Error on GotDescription answer: ${error}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async _onIceCandidate(e) {
|
||||
const candidate = e.detail;
|
||||
const participantId = candidate.participantId;
|
||||
const iceCandidate = new RTCIceCandidate({ candidate: candidate.candidate, sdpMid: candidate.sdpMid, sdpMLineIndex: candidate.sdpMLineIndex });
|
||||
|
||||
if (this._isHost && participantId) {
|
||||
// host端:路由到对应participant的peer
|
||||
const peer = this._peers.get(participantId);
|
||||
if (peer) {
|
||||
await peer.onGotCandidate(this._connectionId, iceCandidate);
|
||||
}
|
||||
} else if (this._peer) {
|
||||
// participant端
|
||||
await this._peer.onGotCandidate(candidate.connectionId, iceCandidate);
|
||||
}
|
||||
}
|
||||
|
||||
async _onMessage(e) {
|
||||
const data = e.detail;
|
||||
this.onMessage(data);
|
||||
}
|
||||
|
||||
async _onParticipantLeft(e) {
|
||||
const data = e.detail;
|
||||
const participantId = data.participantId;
|
||||
Logger.log(`Participant left: ${participantId}`);
|
||||
|
||||
// 关闭该participant的peer
|
||||
if (this._peers.has(participantId)) {
|
||||
const peer = this._peers.get(participantId);
|
||||
peer.close();
|
||||
this._peers.delete(participantId);
|
||||
}
|
||||
|
||||
this.onParticipantLeft(participantId);
|
||||
}
|
||||
|
||||
async _onParticipantJoined(e) {
|
||||
const data = e.detail;
|
||||
const participantId = data.participantId;
|
||||
Logger.log(`Participant joined: ${participantId}`);
|
||||
|
||||
// host端:不在此处创建peer,等待participant的offer到达后在_onOffer中创建
|
||||
// 这样避免host和participant同时发offer导致的glare冲突
|
||||
// _onOffer会在收到participant的offer时自动创建peer(如果不存在)
|
||||
|
||||
this.onParticipantJoined(participantId);
|
||||
}
|
||||
|
||||
async createConnection(connectionId) {
|
||||
this._connectionId = connectionId ? connectionId : uuid4();
|
||||
await this._signaling.createConnection(this._connectionId);
|
||||
}
|
||||
|
||||
async deleteConnection() {
|
||||
await this._signaling.deleteConnection(this._connectionId);
|
||||
}
|
||||
|
||||
_preparePeerConnection(connectionId, polite, participantId) {
|
||||
// host端多peer模式:participantId标识目标participant
|
||||
// participant端单peer模式:participantId为null
|
||||
|
||||
const peer = new Peer(connectionId, polite, this._config);
|
||||
|
||||
// 保存peer
|
||||
if (participantId) {
|
||||
if (this._peers.has(participantId)) {
|
||||
const oldPeer = this._peers.get(participantId);
|
||||
oldPeer.close();
|
||||
}
|
||||
this._peers.set(participantId, peer);
|
||||
} else {
|
||||
if (this._peer) {
|
||||
this._peer.close();
|
||||
}
|
||||
this._peer = peer;
|
||||
}
|
||||
|
||||
// 事件处理:附加participantId用于路由
|
||||
peer.addEventListener('trackevent', (e) => {
|
||||
const data = e.detail;
|
||||
data.participantId = participantId;
|
||||
this.onTrackEvent(data);
|
||||
});
|
||||
|
||||
peer.addEventListener('adddatachannel', (e) => {
|
||||
const data = e.detail;
|
||||
this.onAddChannel(data);
|
||||
});
|
||||
|
||||
peer.addEventListener('ongotoffer', (e) => {
|
||||
const id = e.detail.connectionId;
|
||||
this.onGotOffer(id);
|
||||
});
|
||||
|
||||
peer.addEventListener('ongotanswer', (e) => {
|
||||
const id = e.detail.connectionId;
|
||||
this.onGotAnswer(id);
|
||||
});
|
||||
|
||||
peer.addEventListener('sendoffer', (e) => {
|
||||
const offer = e.detail;
|
||||
this._signaling.sendOffer(offer.connectionId, offer.sdp, participantId);
|
||||
});
|
||||
|
||||
peer.addEventListener('sendanswer', (e) => {
|
||||
const answer = e.detail;
|
||||
this._signaling.sendAnswer(answer.connectionId, answer.sdp, participantId);
|
||||
});
|
||||
|
||||
peer.addEventListener('sendcandidate', (e) => {
|
||||
const candidate = e.detail;
|
||||
this._signaling.sendCandidate(candidate.connectionId, candidate.candidate, candidate.sdpMid, candidate.sdpMLineIndex, participantId);
|
||||
});
|
||||
|
||||
this.onNewPeer(participantId || connectionId);
|
||||
return peer;
|
||||
}
|
||||
|
||||
async getStats(participantId) {
|
||||
if (this._isHost && participantId) {
|
||||
const peer = this._peers.get(participantId);
|
||||
return peer ? await peer.getStats(this._connectionId) : null;
|
||||
}
|
||||
return this._peer ? await this._peer.getStats(this._connectionId) : null;
|
||||
}
|
||||
|
||||
createDataChannel(label, participantId) {
|
||||
if (this._isHost && participantId) {
|
||||
const peer = this._peers.get(participantId);
|
||||
return peer ? peer.createDataChannel(this._connectionId, label) : null;
|
||||
}
|
||||
return this._peer ? this._peer.createDataChannel(this._connectionId, label) : null;
|
||||
}
|
||||
|
||||
addTrack(track, participantId) {
|
||||
if (this._isHost && participantId) {
|
||||
const peer = this._peers.get(participantId);
|
||||
return peer ? peer.addTrack(this._connectionId, track) : null;
|
||||
}
|
||||
return this._peer ? this._peer.addTrack(this._connectionId, track) : null;
|
||||
}
|
||||
|
||||
addTransceiver(trackOrKind, init, participantId) {
|
||||
if (this._isHost && participantId) {
|
||||
const peer = this._peers.get(participantId);
|
||||
return peer ? peer.addTransceiver(this._connectionId, trackOrKind, init) : null;
|
||||
}
|
||||
return this._peer ? this._peer.addTransceiver(this._connectionId, trackOrKind, init) : null;
|
||||
}
|
||||
|
||||
getTransceivers(participantId) {
|
||||
if (this._isHost && participantId) {
|
||||
const peer = this._peers.get(participantId);
|
||||
return peer ? peer.getTransceivers(this._connectionId) : null;
|
||||
}
|
||||
return this._peer ? this._peer.getTransceivers(this._connectionId) : null;
|
||||
}
|
||||
|
||||
sendMessage(message) {
|
||||
if (this._signaling && this._connectionId) {
|
||||
this._signaling.sendMessage(this._connectionId, message);
|
||||
}
|
||||
}
|
||||
|
||||
async start() {
|
||||
await this._signaling.start();
|
||||
}
|
||||
|
||||
async stop() {
|
||||
if (this._peer) {
|
||||
this._peer.close();
|
||||
this._peer = null;
|
||||
}
|
||||
this._peers.forEach((peer) => {
|
||||
peer.close();
|
||||
});
|
||||
this._peers.clear();
|
||||
|
||||
if (this._signaling) {
|
||||
await this._signaling.stop();
|
||||
this._signaling = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
208
client/src/core/sender.js
Normal file
208
client/src/core/sender.js
Normal file
@@ -0,0 +1,208 @@
|
||||
import {
|
||||
Mouse,
|
||||
Keyboard,
|
||||
Gamepad,
|
||||
Touchscreen,
|
||||
StateEvent,
|
||||
TextEvent
|
||||
} from "../input/inputdevice.js";
|
||||
|
||||
import { LocalInputManager } from "../input/inputremoting.js";
|
||||
import { GamepadHandler } from "../input/gamepadhandler.js";
|
||||
import { PointerCorrector } from "../input/pointercorrect.js";
|
||||
|
||||
export class Sender extends LocalInputManager {
|
||||
constructor(elem) {
|
||||
super();
|
||||
this._devices = [];
|
||||
this._elem = elem;
|
||||
this._corrector = new PointerCorrector(
|
||||
this._elem.videoWidth,
|
||||
this._elem.videoHeight,
|
||||
this._elem
|
||||
);
|
||||
|
||||
//since line 27 cannot complete resize initialization but can only monitor div dimension changes, line 26 needs to be reserved
|
||||
this._elem.addEventListener('resize', this._onResizeEvent.bind(this), false);
|
||||
const observer = new ResizeObserver(this._onResizeEvent.bind(this));
|
||||
observer.observe(this._elem);
|
||||
}
|
||||
|
||||
addMouse() {
|
||||
const descriptionMouse = {
|
||||
m_InterfaceName: "RawInput",
|
||||
m_DeviceClass: "Mouse",
|
||||
m_Manufacturer: "",
|
||||
m_Product: "",
|
||||
m_Serial: "",
|
||||
m_Version: "",
|
||||
m_Capabilities: ""
|
||||
};
|
||||
this.mouse = new Mouse("Mouse", "Mouse", 1, null, descriptionMouse);
|
||||
this._devices.push(this.mouse);
|
||||
|
||||
this._elem.addEventListener('click', this._onMouseEvent.bind(this), false);
|
||||
this._elem.addEventListener('mousedown', this._onMouseEvent.bind(this), false);
|
||||
this._elem.addEventListener('mouseup', this._onMouseEvent.bind(this), false);
|
||||
this._elem.addEventListener('mousemove', this._onMouseEvent.bind(this), false);
|
||||
this._elem.addEventListener('wheel', this._onWheelEvent.bind(this), false);
|
||||
}
|
||||
|
||||
addKeyboard() {
|
||||
const descriptionKeyboard = {
|
||||
m_InterfaceName: "RawInput",
|
||||
m_DeviceClass: "Keyboard",
|
||||
m_Manufacturer: "",
|
||||
m_Product: "",
|
||||
m_Serial: "",
|
||||
m_Version: "",
|
||||
m_Capabilities: ""
|
||||
};
|
||||
this.keyboard = new Keyboard("Keyboard", "Keyboard", 2, null, descriptionKeyboard);
|
||||
this._devices.push(this.keyboard);
|
||||
|
||||
document.addEventListener('keyup', this._onKeyEvent.bind(this), false);
|
||||
document.addEventListener('keydown', this._onKeyEvent.bind(this), false);
|
||||
}
|
||||
|
||||
addGamepad() {
|
||||
const descriptionGamepad = {
|
||||
m_InterfaceName: "RawInput",
|
||||
m_DeviceClass: "Gamepad",
|
||||
m_Manufacturer: "",
|
||||
m_Product: "",
|
||||
m_Serial: "",
|
||||
m_Version: "",
|
||||
m_Capabilities: ""
|
||||
};
|
||||
this.gamepad = new Gamepad("Gamepad", "Gamepad", 3, null, descriptionGamepad);
|
||||
this._devices.push(this.gamepad);
|
||||
|
||||
window.addEventListener("gamepadconnected", this._onGamepadEvent.bind(this), false);
|
||||
window.addEventListener("gamepaddisconnected", this._onGamepadEvent.bind(this), false);
|
||||
this._gamepadHandler = new GamepadHandler();
|
||||
this._gamepadHandler.addEventListener("gamepadupdated", this._onGamepadEvent.bind(this), false);
|
||||
}
|
||||
|
||||
addTouchscreen() {
|
||||
const descriptionTouch = {
|
||||
m_InterfaceName: "RawInput",
|
||||
m_DeviceClass: "Touch",
|
||||
m_Manufacturer: "",
|
||||
m_Product: "",
|
||||
m_Serial: "",
|
||||
m_Version: "",
|
||||
m_Capabilities: ""
|
||||
};
|
||||
this.touchscreen = new Touchscreen("Touchscreen", "Touchscreen", 4, null, descriptionTouch);
|
||||
this._devices.push(this.touchscreen);
|
||||
|
||||
this._elem.addEventListener('touchend', this._onTouchEvent.bind(this), false);
|
||||
this._elem.addEventListener('touchstart', this._onTouchEvent.bind(this), false);
|
||||
this._elem.addEventListener('touchcancel', this._onTouchEvent.bind(this), false);
|
||||
this._elem.addEventListener('touchmove', this._onTouchEvent.bind(this), false);
|
||||
this._elem.addEventListener('click', this._onTouchEvent.bind(this), false);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {InputDevice[]}
|
||||
*/
|
||||
get devices() {
|
||||
return this._devices;
|
||||
}
|
||||
|
||||
_onResizeEvent() {
|
||||
this._corrector.reset(
|
||||
this._elem.videoWidth,
|
||||
this._elem.videoHeight,
|
||||
this._elem
|
||||
);
|
||||
}
|
||||
_onMouseEvent(event) {
|
||||
this.mouse.queueEvent(event);
|
||||
this.mouse.currentState.position = this._corrector.map(this.mouse.currentState.position);
|
||||
this._queueStateEvent(this.mouse.currentState, this.mouse);
|
||||
}
|
||||
_onWheelEvent(event) {
|
||||
this.mouse.queueEvent(event);
|
||||
this._queueStateEvent(this.mouse.currentState, this.mouse);
|
||||
}
|
||||
_onKeyEvent(event) {
|
||||
if(event.type == 'keydown') {
|
||||
if(!event.repeat) { // StateEvent
|
||||
this.keyboard.queueEvent(event);
|
||||
this._queueStateEvent(this.keyboard.currentState, this.keyboard);
|
||||
}
|
||||
// TextEvent
|
||||
this._queueTextEvent(this.keyboard, event);
|
||||
}
|
||||
else if(event.type == 'keyup') {
|
||||
this.keyboard.queueEvent(event);
|
||||
this._queueStateEvent(this.keyboard.currentState, this.keyboard);
|
||||
}
|
||||
}
|
||||
_onTouchEvent(event) {
|
||||
this.touchscreen.queueEvent(event, this.timeSinceStartup);
|
||||
for(let touch of this.touchscreen.currentState.touchData) {
|
||||
let clone = touch.copy();
|
||||
clone.position = this._corrector.map(clone.position);
|
||||
this._queueStateEvent(clone, this.touchscreen);
|
||||
}
|
||||
}
|
||||
_onGamepadEvent(event) {
|
||||
switch(event.type) {
|
||||
case 'gamepadconnected': {
|
||||
this._gamepadHandler.addGamepad(event.gamepad);
|
||||
break;
|
||||
}
|
||||
case 'gamepaddisconnected': {
|
||||
this._gamepadHandler.removeGamepad(event.gamepad);
|
||||
break;
|
||||
}
|
||||
case 'gamepadupdated': {
|
||||
this.gamepad.queueEvent(event);
|
||||
this._queueStateEvent(this.gamepad.currentState, this.gamepad);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_queueStateEvent(state, device) {
|
||||
const stateEvent =
|
||||
StateEvent.fromState(state, device.deviceId, this.timeSinceStartup);
|
||||
const e = new CustomEvent(
|
||||
'event', {detail: { event: stateEvent, device: device}});
|
||||
super.onEvent.dispatchEvent(e);
|
||||
}
|
||||
_queueTextEvent(device, event) {
|
||||
const textEvent = TextEvent.create(device.deviceId, event, this.timeSinceStartup);
|
||||
const e = new CustomEvent(
|
||||
'event', {detail: { event: textEvent, device: device}});
|
||||
super.onEvent.dispatchEvent(e);
|
||||
}
|
||||
_queueDeviceChange(device, usage) {
|
||||
const e = new CustomEvent(
|
||||
'changedeviceusage', {detail: { device: device, usage: usage }});
|
||||
super.onEvent.dispatchEvent(e);
|
||||
}
|
||||
}
|
||||
|
||||
export class Observer {
|
||||
/**
|
||||
*
|
||||
* @param {RTCDataChannel} channel
|
||||
*/
|
||||
constructor(channel) {
|
||||
this.channel = channel;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @param {Message} message
|
||||
*/
|
||||
onNext(message) {
|
||||
if(this.channel == null || this.channel.readyState != 'open') {
|
||||
return;
|
||||
}
|
||||
this.channel.send(message.buffer);
|
||||
}
|
||||
}
|
||||
321
client/src/core/signaling.js
Normal file
321
client/src/core/signaling.js
Normal file
@@ -0,0 +1,321 @@
|
||||
import * as Logger from "../utils/logger.js";
|
||||
|
||||
export class Signaling extends EventTarget {
|
||||
|
||||
constructor(interval = 1000) {
|
||||
super();
|
||||
this.running = false;
|
||||
this.interval = interval;
|
||||
this.sleep = msec => new Promise(resolve => setTimeout(resolve, msec));
|
||||
}
|
||||
|
||||
headers() {
|
||||
if (this.sessionId !== undefined) {
|
||||
return { 'Content-Type': 'application/json', 'Session-Id': this.sessionId };
|
||||
}
|
||||
else {
|
||||
return { 'Content-Type': 'application/json' };
|
||||
}
|
||||
}
|
||||
|
||||
url(method, parameter = '') {
|
||||
let ret = location.origin + '/signaling';
|
||||
if (method)
|
||||
ret += '/' + method;
|
||||
if (parameter)
|
||||
ret += '?' + parameter;
|
||||
return ret;
|
||||
}
|
||||
|
||||
async start() {
|
||||
if (this.running) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.running = true;
|
||||
while (!this.sessionId) {
|
||||
const createResponse = await fetch(this.url(''), { method: 'PUT', headers: this.headers() });
|
||||
const session = await createResponse.json();
|
||||
this.sessionId = session.sessionId;
|
||||
|
||||
if (!this.sessionId) {
|
||||
await this.sleep(this.interval);
|
||||
}
|
||||
}
|
||||
|
||||
this.loopGetAll();
|
||||
}
|
||||
|
||||
async loopGetAll() {
|
||||
let lastTimeRequest = Date.now() - 30000;
|
||||
while (this.running) {
|
||||
const res = await this.getAll(lastTimeRequest);
|
||||
const data = await res.json();
|
||||
lastTimeRequest = data.datetime ? data.datetime : Date.now();
|
||||
|
||||
const messages = data.messages;
|
||||
|
||||
for (const msg of messages) {
|
||||
switch (msg.type) {
|
||||
case "connect":
|
||||
break;
|
||||
case "disconnect":
|
||||
this.dispatchEvent(new CustomEvent('disconnect', { detail: msg }));
|
||||
break;
|
||||
case "offer":
|
||||
this.dispatchEvent(new CustomEvent('offer', { detail: msg }));
|
||||
break;
|
||||
case "answer":
|
||||
this.dispatchEvent(new CustomEvent('answer', { detail: msg }));
|
||||
break;
|
||||
case "candidate":
|
||||
this.dispatchEvent(new CustomEvent('candidate', { detail: msg }));
|
||||
break;
|
||||
case "on-message":
|
||||
{
|
||||
let parsed = msg.data;
|
||||
if (typeof msg.data === 'string') {
|
||||
try { parsed = JSON.parse(msg.data); } catch(e) {
|
||||
Logger.error(`Signaling: on-message, error: ${e}`);
|
||||
}
|
||||
}
|
||||
this.dispatchEvent(new CustomEvent('on-message', { detail: parsed }));
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
await this.sleep(this.interval);
|
||||
}
|
||||
}
|
||||
|
||||
async stop() {
|
||||
this.running = false;
|
||||
await fetch(this.url(''), { method: 'DELETE', headers: this.headers() });
|
||||
this.sessionId = null;
|
||||
}
|
||||
|
||||
async createConnection(connectionId) {
|
||||
const data = { 'connectionId': connectionId };
|
||||
const res = await fetch(this.url('connection'), { method: 'PUT', headers: this.headers(), body: JSON.stringify(data) });
|
||||
const json = await res.json();
|
||||
Logger.log(`Signaling: HTTP create connection, connectionId: ${json.connectionId}, polite:${json.polite}`);
|
||||
|
||||
this.dispatchEvent(new CustomEvent('connect', { detail: json }));
|
||||
return json;
|
||||
}
|
||||
|
||||
async deleteConnection(connectionId) {
|
||||
const data = { 'connectionId': connectionId };
|
||||
const res = await fetch(this.url('connection'), { method: 'DELETE', headers: this.headers(), body: JSON.stringify(data) });
|
||||
const json = await res.json();
|
||||
this.dispatchEvent(new CustomEvent('disconnect', { detail: json }));
|
||||
return json;
|
||||
}
|
||||
|
||||
async sendOffer(connectionId, sdp) {
|
||||
const data = { 'sdp': sdp, 'connectionId': connectionId };
|
||||
Logger.log('sendOffer:' + data);
|
||||
await fetch(this.url('offer'), { method: 'POST', headers: this.headers(), body: JSON.stringify(data) });
|
||||
}
|
||||
|
||||
async sendAnswer(connectionId, sdp) {
|
||||
const data = { 'sdp': sdp, 'connectionId': connectionId };
|
||||
Logger.log('sendAnswer:' + data);
|
||||
await fetch(this.url('answer'), { method: 'POST', headers: this.headers(), body: JSON.stringify(data) });
|
||||
}
|
||||
|
||||
async sendCandidate(connectionId, candidate, sdpMid, sdpMLineIndex) {
|
||||
const data = {
|
||||
'candidate': candidate,
|
||||
'sdpMLineIndex': sdpMLineIndex,
|
||||
'sdpMid': sdpMid,
|
||||
'connectionId': connectionId
|
||||
};
|
||||
Logger.log('sendCandidate:' + data);
|
||||
await fetch(this.url('candidate'), { method: 'POST', headers: this.headers(), body: JSON.stringify(data) });
|
||||
}
|
||||
// 在 Signaling 类中添加
|
||||
async sendMessage(connectionId, message) {
|
||||
const data = {
|
||||
'message': message,
|
||||
'connectionId': connectionId
|
||||
};
|
||||
await fetch(this.url('on-message'), { method: 'POST', headers: this.headers(), body: JSON.stringify(data) });
|
||||
}
|
||||
async getAll(fromTime = 0) {
|
||||
return await fetch(this.url(``, `fromtime=${fromTime}`), { method: 'GET', headers: this.headers() });
|
||||
}
|
||||
}
|
||||
|
||||
export class WebSocketSignaling extends EventTarget {
|
||||
|
||||
constructor(interval = 1000) {
|
||||
super();
|
||||
this.interval = interval;
|
||||
this.sleep = msec => new Promise(resolve => setTimeout(resolve, msec));
|
||||
|
||||
let websocketUrl;
|
||||
if (location.protocol === "https:") {
|
||||
websocketUrl = "wss://" + location.host;
|
||||
} else {
|
||||
websocketUrl = "ws://" + location.host;
|
||||
}
|
||||
|
||||
this.websocket = new WebSocket(websocketUrl);
|
||||
this.connectionId = null;
|
||||
|
||||
this.websocket.onopen = () => {
|
||||
this.isWsOpen = true;
|
||||
};
|
||||
|
||||
this.websocket.onclose = () => {
|
||||
this.isWsOpen = false;
|
||||
};
|
||||
|
||||
this.websocket.onmessage = (event) => {
|
||||
const msg = JSON.parse(event.data);
|
||||
if (!msg || !this) {
|
||||
return;
|
||||
}
|
||||
|
||||
Logger.log(msg);
|
||||
|
||||
switch (msg.type) {
|
||||
case "connect":
|
||||
this.dispatchEvent(new CustomEvent('connect', { detail: msg }));
|
||||
break;
|
||||
case "disconnect":
|
||||
this.dispatchEvent(new CustomEvent('disconnect', { detail: msg }));
|
||||
break;
|
||||
case "offer":
|
||||
this.dispatchEvent(new CustomEvent('offer', { detail: { connectionId: msg.from, sdp: msg.data.sdp, polite: msg.data.polite, participantId: msg.participantId } }));
|
||||
break;
|
||||
case "answer":
|
||||
this.dispatchEvent(new CustomEvent('answer', { detail: { connectionId: msg.from, sdp: msg.data.sdp, participantId: msg.participantId } }));
|
||||
break;
|
||||
case "candidate":
|
||||
this.dispatchEvent(new CustomEvent('candidate', { detail: { connectionId: msg.from, candidate: msg.data.candidate, sdpMLineIndex: msg.data.sdpMLineIndex, sdpMid: msg.data.sdpMid, participantId: msg.participantId } }));
|
||||
break;
|
||||
case "on-message":
|
||||
{
|
||||
let parsed = msg.data;
|
||||
if (typeof msg.data === 'string') {
|
||||
try { parsed = JSON.parse(msg.data); } catch(e) {
|
||||
Logger.error(`Signaling: on-message, error: ${e}`);
|
||||
}
|
||||
}
|
||||
if (msg.participantId) {
|
||||
parsed.participantId = msg.participantId;
|
||||
}
|
||||
this.dispatchEvent(new CustomEvent('on-message', { detail: parsed }));
|
||||
}
|
||||
break;
|
||||
case "participant-left":
|
||||
this.dispatchEvent(new CustomEvent('participant-left', { detail: msg }));
|
||||
break;
|
||||
case "participant-joined":
|
||||
this.dispatchEvent(new CustomEvent('participant-joined', { detail: msg }));
|
||||
break;
|
||||
case "broadcast":
|
||||
this.dispatchEvent(new CustomEvent('on-message', { detail: msg.message }));
|
||||
break;
|
||||
case "invite-call":
|
||||
this.dispatchEvent(new CustomEvent('invite-call', { detail: msg.data }));
|
||||
break;
|
||||
case "invite-accepted":
|
||||
this.dispatchEvent(new CustomEvent('invite-accepted', { detail: msg.data }));
|
||||
break;
|
||||
case "invite-rejected":
|
||||
this.dispatchEvent(new CustomEvent('invite-rejected', { detail: msg.data }));
|
||||
break;
|
||||
case "invite-failed":
|
||||
this.dispatchEvent(new CustomEvent('invite-failed', { detail: msg.data }));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async start() {
|
||||
while (!this.isWsOpen) {
|
||||
await this.sleep(100);
|
||||
}
|
||||
}
|
||||
|
||||
async stop() {
|
||||
this.websocket.close();
|
||||
while (this.isWsOpen) {
|
||||
await this.sleep(100);
|
||||
}
|
||||
}
|
||||
|
||||
createConnection(connectionId) {
|
||||
const sendJson = JSON.stringify({ type: "connect", connectionId: connectionId });
|
||||
Logger.log(sendJson);
|
||||
this.websocket.send(sendJson);
|
||||
}
|
||||
|
||||
deleteConnection(connectionId) {
|
||||
const sendJson = JSON.stringify({ type: "disconnect", connectionId: connectionId });
|
||||
Logger.log(sendJson);
|
||||
this.websocket.send(sendJson);
|
||||
}
|
||||
|
||||
sendOffer(connectionId, sdp, participantId) {
|
||||
const data = { 'sdp': sdp, 'connectionId': connectionId };
|
||||
const sendJson = JSON.stringify({ type: "offer", from: connectionId, data: data, participantId: participantId || '' });
|
||||
Logger.log(sendJson);
|
||||
this.websocket.send(sendJson);
|
||||
}
|
||||
|
||||
sendAnswer(connectionId, sdp, participantId) {
|
||||
const data = { 'sdp': sdp, 'connectionId': connectionId };
|
||||
const sendJson = JSON.stringify({ type: "answer", from: connectionId, data: data, participantId: participantId || '' });
|
||||
Logger.log(sendJson);
|
||||
this.websocket.send(sendJson);
|
||||
}
|
||||
|
||||
sendCandidate(connectionId, candidate, sdpMLineIndex, sdpMid, participantId) {
|
||||
const data = {
|
||||
'candidate': candidate,
|
||||
'sdpMLineIndex': sdpMLineIndex,
|
||||
'sdpMid': sdpMid,
|
||||
'connectionId': connectionId
|
||||
};
|
||||
const sendJson = JSON.stringify({ type: "candidate", from: connectionId, data: data, participantId: participantId || '' });
|
||||
Logger.log(sendJson);
|
||||
this.websocket.send(sendJson);
|
||||
}
|
||||
// 在 WebSocketSignaling 类中添加
|
||||
sendMessage(connectionId, message) {
|
||||
const data = {
|
||||
'message': message,
|
||||
'senderId': message.senderId,
|
||||
'connectionId': connectionId
|
||||
};
|
||||
const sendJson = JSON.stringify({ type: "on-message", data: data });
|
||||
Logger.log(sendJson);
|
||||
this.websocket.send(sendJson);
|
||||
}
|
||||
|
||||
sendInviteCall(payload) {
|
||||
const sendJson = JSON.stringify({ type: 'invite-call', data: payload });
|
||||
Logger.log(sendJson);
|
||||
this.websocket.send(sendJson);
|
||||
}
|
||||
|
||||
sendInviteAccepted(payload) {
|
||||
const sendJson = JSON.stringify({ type: 'invite-accepted', data: payload });
|
||||
Logger.log(sendJson);
|
||||
this.websocket.send(sendJson);
|
||||
}
|
||||
|
||||
sendInviteRejected(payload) {
|
||||
const sendJson = JSON.stringify({ type: 'invite-rejected', data: payload });
|
||||
Logger.log(sendJson);
|
||||
this.websocket.send(sendJson);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user