优化目录结构

This commit is contained in:
2026-05-25 20:37:36 +08:00
parent bbe7e71274
commit 40fd7f7e08
101 changed files with 108 additions and 110 deletions

187
client/src/core/peer.js Normal file
View 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}.`);
}
}
}

View 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=falseimpolite确保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
View 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);
}
}

View 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);
}
}