diff --git a/client/DataChannel.js b/client/DataChannel.js
index b29c2f503a290e5d4a41a213ce379fcbe0afea7e..c8578374bd0cb485b909731ecff3d1b0c11e5a07 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 77218a068ebb5b57ca857d454f15549db6eddf2c..c5e5cfbeffad16c06229cdac7ebc56cc8eb6bbdb 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 968bfdeec6beeecc22b843bfc2d8a50fc5ac66fd..aa11a8118f759157f8b6bfd173c3ac71985449d6 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);