From f66c98443b84411bcd5f1d5492a66492e089c3b9 Mon Sep 17 00:00:00 2001 From: Ben Eltschig <43812953+peabrainiac@users.noreply.github.com> Date: Thu, 4 Mar 2021 00:18:55 +0100 Subject: [PATCH] DataChannel-Klasse weiter ausgebaut & auf RTCPeerConnections verallgemeinert --- client/DataChannel.js | 80 +++++++++++++++++++++++++++++++++++++++++-- client/Signaler.js | 60 ++++++++++++-------------------- client/script.js | 24 +++++-------- 3 files changed, 108 insertions(+), 56 deletions(-) diff --git a/client/DataChannel.js b/client/DataChannel.js index b29c2f5..c857837 100644 --- a/client/DataChannel.js +++ b/client/DataChannel.js @@ -10,7 +10,7 @@ export default class DataChannel { */ /** * Constructs a new DataChannel endpoint from the given callbacks once the promise resolves. - * @param {()=>Promise<{send:(data:string)=>void,onMessage:(callback:MessageCallback)=>void,close:()=>void}>} callback + * @param {()=>Promise<{send:(data:string)=>void,onMessage:(callback:MessageCallback)=>void,close?:()=>void}>} callback */ constructor(callback){ /** @type {MessageCallback[]} */ @@ -26,7 +26,7 @@ export default class DataChannel { c.onMessage(message=>{ this._onMessageCallbacks.slice().forEach(callback=>callback(message)); }); - this._close = c.close; + this._close = c.close||(()=>{}); resolve(); }); } @@ -86,6 +86,32 @@ export default class DataChannel { close(){ this._close(); } + + /** + * Creates a new DataChannel that send messages via an already-existing DataChannel. + * @param {DataChannel} dataChannel DataChannel to send messages over + * @param {(message:string)=>string} modifier modifier to outgoing messages + * @param {(message:string)=>string} filter modifier to incoming messages, can act as a filter by returning `null` + */ + static from(dataChannel,modifier,filter){ + return new DataChannel(async()=>{ + await dataChannel.waitUntilReady(); + /** @type {(message:string)=>void} */ + let send = message=>{ + dataChannel.send(modifier(message)); + }; + /** @type {(callback:(message:string)=>void)=>void} */ + let onMessage = callback=>{ + dataChannel.onMessage(message=>{ + let m = filter(message); + if (m!==null){ + callback(m); + } + }); + }; + return {send,onMessage}; + }); + } } /** * Class representing a connection to a server via a WebSocket. @@ -117,4 +143,54 @@ export class SocketDataChannel extends DataChannel { return {send,onMessage,close}; }); } +} +/** + * Class representing a connection to another browser via an `RTCPeerConnection` with a single `RTCDataChannel`. + * + * Not responsible for maintaining the connection, only + */ +export class PeerDataChannel extends DataChannel { + /** + * Creates a new PeerDataChannel object wrapping the given `RTCPeerConnection` and `RTCDataChannel`. + * @param {RTCPeerConnection} connection + * @param {RTCDataChannel} dataChannel + */ + constructor(connection,dataChannel){ + super(async()=>{ + this._connection = connection; + this._dataChannel = dataChannel; + /** @type {(message:string)=>void} */ + let send = message=>{ + this._dataChannel.send(message); + }; + /** @type {(callback:(message:string)=>void)=>void} */ + let onMessage = callback=>{ + this._dataChannel.addEventListener("message",e=>{ + callback(e.data+""); + }); + }; + let close = ()=>{ + this._connection.close(); + } + /*console.log("Signaling state:",this._connection.signalingState); + if (this._connection.signalingState!=="stable"){ + await new Promise(resolve=>{ + this._connection.addEventListener("signalingstatechange",e=>{ + console.log("Signaling state changed to:",connection.signalingState); + if (this._connection.signalingState==="stable"){ + resolve(); + } + }); + }); + }*/ + console.log("DataChannel state:",this._dataChannel.readyState); + if (this._dataChannel.readyState!=="open"){ + await new Promise(resolve=>{ + dataChannel.onopen = resolve; + }); + console.log("Channel opened!"); + } + return {send,onMessage,close}; + }); + } } \ No newline at end of file diff --git a/client/Signaler.js b/client/Signaler.js index 77218a0..c5e5cfb 100644 --- a/client/Signaler.js +++ b/client/Signaler.js @@ -1,4 +1,4 @@ -import {SocketDataChannel} from "./DataChannel.js"; +import DataChannel, {PeerDataChannel, SocketDataChannel} from "./DataChannel.js"; export const baseURL = `ws://${document.location.hostname}:443/skribbl`; /** @@ -11,7 +11,7 @@ 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 + * @param {(dataChannel:PeerDataChannel)=>void} onConnect */ static async host(onConnect){ const socket = new SocketDataChannel(baseURL+"/host"); @@ -26,19 +26,12 @@ export default class Signaler { 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=>{ + let signalingChannel = DataChannel.from(socket,message=>`message ${playerID} ${message}`,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)); + return message.split(" ").slice(2).join(" "); } }); - onConnect(await perfectNegotiation(callback=>callbacks.push(callback),message=>{ - console.log("message to player "+playerID+": ",message); - socket.send("message "+playerID+" "+message); - },false)); + onConnect(await perfectNegotiation(signalingChannel,false)); } }); return id; @@ -50,27 +43,24 @@ export default class Signaler { */ 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(" "))); + let signalingChannel = DataChannel.from(socket,message=>"message "+message,message=>{ + if (message.startsWith("message ")){ + return message.split(" ").slice(1).join(" "); + }else{ + throw new Error(`Unexpected message: "${message}"`); + } }); - return await perfectNegotiation(callback=>callbacks.push(callback),message=>{ - console.log("send message:",message); - socket.send("message "+message); - },true); + return await perfectNegotiation(signalingChannel,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 {DataChannel} signalingChannel * @param {boolean} polite - * @returns {Promise<ConnectionData>} + * @returns {Promise<PeerDataChannel>} */ -export async function perfectNegotiation(onMessage,sendMessage,polite){ +export async function perfectNegotiation(signalingChannel,polite){ + await signalingChannel.waitUntilReady(); const connection = new RTCPeerConnection(); const dataChannel = connection.createDataChannel("channel",{id:0,negotiated:true}); let makingOffer = false; @@ -79,15 +69,15 @@ export async function perfectNegotiation(onMessage,sendMessage,polite){ // @ts-ignore await connection.setLocalDescription(); //console.log("local description:",connection.localDescription) - sendMessage(JSON.stringify({description:connection.localDescription})); + signalingChannel.send(JSON.stringify({description:connection.localDescription})); makingOffer = false; }; connection.onicecandidate = ({candidate})=>{ //console.log("candidate:",candidate); - sendMessage(JSON.stringify({candidate})); + signalingChannel.send(JSON.stringify({candidate})); }; let ignoreOffer = false; - onMessage(async message=>{ + signalingChannel.onMessage(async message=>{ console.log("message to negotiator:",message); /** @type {{description:RTCSessionDescriptionInit,candidate:undefined}|{candidate:RTCIceCandidateInit,description:undefined}} */ let data = JSON.parse(message); @@ -99,7 +89,7 @@ export async function perfectNegotiation(onMessage,sendMessage,polite){ if (description.type=="offer"){ // @ts-ignore await connection.setLocalDescription(); - sendMessage(JSON.stringify({description:connection.localDescription})); + signalingChannel.send(JSON.stringify({description:connection.localDescription})); } } }else if(data.candidate){ @@ -112,13 +102,5 @@ export async function perfectNegotiation(onMessage,sendMessage,polite){ } } }); - /** @type {Promise<ConnectionData>} */ - return new Promise(resolve=>{ - connection.onsignalingstatechange = ()=>{ - console.log("Signaling state changed to:",connection.signalingState); - if (connection.signalingState=="stable"){ - resolve({connection,dataChannel}); - } - }; - }); + return new PeerDataChannel(connection,dataChannel); } \ No newline at end of file diff --git a/client/script.js b/client/script.js index 968bfde..aa11a81 100644 --- a/client/script.js +++ b/client/script.js @@ -5,15 +5,14 @@ document.addEventListener("DOMContentLoaded",async()=>{ if (document.location.hash){ document.body.innerHTML = ""; const gameID = document.location.hash.substring(1); - const {connection,dataChannel} = await Signaler.join(gameID); + const dataChannel = await Signaler.join(gameID); console.group("Joined game "+gameID+"!"); - console.log("connection:",connection); console.log("dataChannel:",dataChannel); console.groupEnd(); - dataChannel.onmessage = (e)=>{ - console.log("Message through DataChannel:",e.data); + dataChannel.onMessage(message=>{ + console.log("Message through DataChannel:",message); dataChannel.send("Got ya message, many thanks <3"); - } + }); }else{ /** @type {HTMLButtonElement} *///@ts-ignore const button = document.getElementById("button"); @@ -21,19 +20,14 @@ document.addEventListener("DOMContentLoaded",async()=>{ button.disabled = true; resolve(); })}); - let id = await Signaler.host(async({connection,dataChannel})=>{ + let id = await Signaler.host(async dataChannel=>{ console.group("New connection!"); - console.log("connection:",connection); console.log("dataChannel:",dataChannel); console.groupEnd(); - dataChannel.onmessage = (e)=>{ - console.log("Message through DataChannel:",e.data); - } - if (dataChannel.readyState!="open"){ - await new Promise(resolve=>{ - dataChannel.onopen = resolve; - }); - } + dataChannel.onMessage(message=>{ + console.log("Message through DataChannel:",message); + }); + await dataChannel.waitUntilReady(); dataChannel.send("Oh hello there :3"); }); alert(document.location.host+document.location.pathname+"#"+id); -- GitLab