-
Ben Eltschig authoredUnverifiedd196a3c0
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
Signaler.js 4.31 KiB
import {SocketDataChannel} from "./DataChannel.js";
export const baseURL = `ws://${document.location.hostname}:443/skribbl`;
/**
* @typedef {{connection:RTCPeerConnection,dataChannel:RTCDataChannel}} ConnectionData
*/
/**
* A connection to the signaling server used to start and join games.
*/
export default class Signaler {
/**
* Starts a new game, returning a promise with its ID. After that, calls the given callback with a new RTCPeerConnection whenever someone joins the game.
* @param {(connectionData:ConnectionData)=>void} onConnect
*/
static async host(onConnect){
const socket = new SocketDataChannel(baseURL+"/host");
//await socket.waitUntilReady();
let response = await socket.next();
console.log("response:",response);
if (!response.startsWith("started ")){
throw new Error(`Unexpected first response or something: "${response}"`);
}
let id = response.split(" ")[1];
socket.onMessage(async message=>{
if (message.startsWith("join ")){
let playerID = message.split(" ",2)[1];
console.log("new player joined: ",playerID);
/** @type {((message:string)=>void)[]} */
let callbacks = [];
socket.onMessage(message=>{
if (message.startsWith("message "+playerID+" ")){
let data = message.split(" ").slice(2).join(" ");
console.log("message from player "+playerID+": ",data);
callbacks.forEach(callback=>callback(data));
}
});
onConnect(await perfectNegotiation(callback=>callbacks.push(callback),message=>{
console.log("message to player "+playerID+": ",message);
socket.send("message "+playerID+" "+message);
},false));
}
});
return id;
}
/**
* Joins a new game, returning an RTCPeerConnection to its host.
* @param {string} id
*/
static async join(id){
const socket = new SocketDataChannel(baseURL+"/join/"+id);
await socket.waitUntilReady();
/** @type {((message:string)=>void)[]} */
let callbacks = [];
socket.onMessage(message=>{
console.log("new message: ",message);
callbacks.forEach(callback=>callback(message.split(" ").slice(1).join(" ")));
});
return await perfectNegotiation(callback=>callbacks.push(callback),message=>{
console.log("send message:",message);
socket.send("message "+message);
},true);
}
}
/**
* An implementation of "perfect negotiaton" as described [here](https://developer.mozilla.org/en-US/docs/Web/API/WebRTC_API/Perfect_negotiation).
* @param {(callback:(message:string)=>void)=>void} onMessage
* @param {(message:string)=>void} sendMessage
* @param {boolean} polite
* @returns {Promise<ConnectionData>}
*/
export async function perfectNegotiation(onMessage,sendMessage,polite){
const connection = new RTCPeerConnection();
const dataChannel = connection.createDataChannel("channel",{id:0,negotiated:true});
let makingOffer = false;
connection.onnegotiationneeded = async()=>{
makingOffer = true;
// @ts-ignore
await connection.setLocalDescription();
//console.log("local description:",connection.localDescription)
sendMessage(JSON.stringify({description:connection.localDescription}));
makingOffer = false;
};
connection.onicecandidate = ({candidate})=>{
//console.log("candidate:",candidate);
sendMessage(JSON.stringify({candidate}));
};
let ignoreOffer = false;
onMessage(async message=>{
console.log("message to negotiator:",message);
/** @type {{description:RTCSessionDescriptionInit,candidate:undefined}|{candidate:RTCIceCandidateInit,description:undefined}} */
let data = JSON.parse(message);
if (data.description){
let description = data.description;
ignoreOffer = !polite&&(description.type=="offer"&&(makingOffer||connection.signalingState!="stable"));
if(!ignoreOffer){
await connection.setRemoteDescription(description);
if (description.type=="offer"){
// @ts-ignore
await connection.setLocalDescription();
sendMessage(JSON.stringify({description:connection.localDescription}));
}
}
}else if(data.candidate){
try {
await connection.addIceCandidate(data.candidate);
}catch(e){
if (!ignoreOffer){
throw e;
}
}
}
});
/** @type {Promise<ConnectionData>} */
return new Promise(resolve=>{
connection.onsignalingstatechange = ()=>{
console.log("Signaling state changed to:",connection.signalingState);
if (connection.signalingState=="stable"){
resolve({connection,dataChannel});
}
};
});
}