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