From aefd377687f55482ed737fabbaf2d1c02059e697 Mon Sep 17 00:00:00 2001 From: Norbert Szulc Date: Mon, 13 Apr 2020 20:19:46 +0200 Subject: [PATCH 1/6] Add firebase config and comments --- database.rules.json | 7 ++++ firebase.json | 17 ++++++++++ src/sessions.js | 82 ++++++++++++++++++++++++++------------------- 3 files changed, 71 insertions(+), 35 deletions(-) create mode 100644 database.rules.json create mode 100644 firebase.json diff --git a/database.rules.json b/database.rules.json new file mode 100644 index 000000000..76c9e6a2e --- /dev/null +++ b/database.rules.json @@ -0,0 +1,7 @@ +{ + /* Visit https://firebase.google.com/docs/database/security to learn more about security rules. */ + "rules": { + ".read": false, + ".write": false + } +} \ No newline at end of file diff --git a/firebase.json b/firebase.json new file mode 100644 index 000000000..0c1d5b90f --- /dev/null +++ b/firebase.json @@ -0,0 +1,17 @@ +{ + "database": { + "rules": "database.rules.json" + }, + "hosting": { + "public": "./", + "ignore": [ + "firebase.json", + "**/.*", + "**/node_modules/**" + ], + "rewrites": [{ + "source": "**", + "destination": "/index.html" + }] + } +} \ No newline at end of file diff --git a/src/sessions.js b/src/sessions.js index 25be2da22..645fa4ef9 100644 --- a/src/sessions.js +++ b/src/sessions.js @@ -1,38 +1,49 @@ - +'use strict'; (() => { - const log = (...args)=> { - window.console && console.log(...args); - }; - - let localStorageAvailable = false; - try { - localStorage._available = true; - localStorageAvailable = localStorage._available; - delete localStorage._available; - // eslint-disable-next-line no-empty - } catch (e) {} - - // @TODO: keep other data in addition to the image data - // such as the file_name and other state - // (maybe even whether it's considered saved? idk about that) - // I could have the image in one storage slot and the state in another - - - const canvas_has_any_apparent_image_data = ()=> - canvas.ctx.getImageData(0, 0, canvas.width, canvas.height).data.some((v)=> v > 0); - - let $recovery_window; - function show_recovery_window(no_longer_blank) { - $recovery_window && $recovery_window.close(); - const $w = $recovery_window = $FormToolWindow(); - $w.on("close", ()=> { - $recovery_window = null; - }); - $w.title("Recover Document"); - let backup_impossible = false; - try{window.localStorage}catch(e){backup_impossible = true;} - $w.$main.append($(` + const log = (...args) => { + window.console && console.log(...args); + }; + + const configuration = { + iceServers: [{ + urls: [ + 'stun:stun1.l.google.com:19302', + 'stun:stun2.l.google.com:19302', + ], + }, ], + iceCandidatePoolSize: 10, + }; + + let localStorageAvailable = false; + try { + localStorage._available = true; + localStorageAvailable = localStorage._available; + delete localStorage._available; + // eslint-disable-next-line no-empty + } catch (e) {} + + // @TODO: keep other data in addition to the image data + // such as the file_name and other state + // (maybe even whether it's considered saved? idk about that) + // I could have the image in one storage slot and the state in another + + + const canvas_has_any_apparent_image_data = () => + canvas.ctx.getImageData(0, 0, canvas.width, canvas.height).data.some((v) => v > 0); + + let $recovery_window; + + function show_recovery_window(no_longer_blank) { + $recovery_window && $recovery_window.close(); + const $w = $recovery_window = $FormToolWindow(); + $w.on("close", () => { + $recovery_window = null; + }); + $w.title("Recover Document"); + let backup_impossible = false; + try { window.localStorage } catch (e) { backup_impossible = true; } + $w.$main.append($(`

Woah!

Your browser may have cleared the canvas due to memory usage.

Undo to recover the document, and remember to save with File > Save!

@@ -250,7 +261,7 @@ }; // Get Firebase references this.fb = MultiUserSession.fb_root.child(this.id); - this.fb_data = this.fb.child("data"); + this.fb_data = this.fb.child("data"); // TODO: gtfo to peer to peer this.fb_users = this.fb.child("users"); if (user_id) { this.fb_user = this.fb_users.child(user_id); @@ -332,6 +343,7 @@ }); let previous_uri; // let pointer_operations = []; // the multiplayer syncing stuff is a can of worms, so this is disabled + // TODO: this should be peer to peer const write_canvas_to_database = debounce(() => { const save_paused = handle_data_loss(); if (save_paused) { @@ -589,4 +601,4 @@ // @TODO: Indicate when there is no session! // Probably in app.js so as to handle the possibility of sessions.js failing to load. -})(); +})(); \ No newline at end of file From abbe169882fb34527907cb0df42579b1b9fdba32 Mon Sep 17 00:00:00 2001 From: Norbert Szulc Date: Mon, 13 Apr 2020 21:50:07 +0200 Subject: [PATCH 2/6] Use FirebaseRTC example --- src/sessions.js | 200 +++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 191 insertions(+), 9 deletions(-) diff --git a/src/sessions.js b/src/sessions.js index 645fa4ef9..ae1dc4792 100644 --- a/src/sessions.js +++ b/src/sessions.js @@ -106,7 +106,7 @@ class LocalSession { constructor(session_id) { - this.id = session_id; + this.session_id = session_id; const lsid = `image#${session_id}`; log(`Local storage ID: ${lsid}`); // save image to storage @@ -186,6 +186,7 @@ lightness: ~~(Math.random() * 40) + 50, }; + // TODO: make this a method or something // The main cursor color user.color = `hsla(${user.hue}, ${user.saturation}%, ${user.lightness}%, 1)`; // Unused @@ -201,18 +202,18 @@ class MultiUserSession { constructor(session_id) { - this.id = session_id; + this.session_id = session_id; this._fb_listeners = []; - file_name = `[Loading ${this.id}]`; + file_name = `[Loading ${this.session_id}]`; update_title(); const on_firebase_loaded = () => { - file_name = `[${this.id}]`; + file_name = `[${this.session_id}]`; update_title(); this.start(); }; if (!MultiUserSession.fb_root) { - $.getScript("lib/firebase.js") + $.getScript("lib/firebase.js") // is this lazy loading? this makes init complicated .done(() => { const config = { apiKey: "AIzaSyBgau8Vu9ZE8u_j0rp-Lc044gYTX5O3X9k", @@ -223,12 +224,14 @@ messagingSenderId: "63395010995" }; firebase.initializeApp(config); + // why MultiUserSession and not this? MultiUserSession.fb_root = firebase.database().ref("/"); + this.db = firebase.database(); // lets rewrite fb_root and others to use this one on_firebase_loaded(); }) .fail(() => { show_error_message("Failed to load Firebase; the document will not load, and changes will not be saved."); - file_name = `[Failed to load ${this.id}]`; + file_name = `[Failed to load ${this.session_id}]`; update_title(); }); } @@ -260,9 +263,9 @@ fb.on(event_type, callback, error_callback); }; // Get Firebase references - this.fb = MultiUserSession.fb_root.child(this.id); + this.fb = MultiUserSession.fb_root.child(this.session_id); this.fb_data = this.fb.child("data"); // TODO: gtfo to peer to peer - this.fb_users = this.fb.child("users"); + this.fb_users = this.fb.child("users"); // this contins cursors of other players? if (user_id) { this.fb_user = this.fb_users.child(user_id); } @@ -425,7 +428,7 @@ } }, error => { show_error_message("Failed to retrieve data from Firebase. The document will not load, and changes will not be saved.", error); - file_name = `[Failed to load ${this.id}]`; + file_name = `[Failed to load ${this.session_id}]`; update_title(); }); // Update the cursor status @@ -492,6 +495,165 @@ // Reset to "untitled" reset_file(); } + + async createRoom() { + // document.querySelector('#createBtn').disabled = true; + // document.querySelector('#joinBtn').disabled = true; + const db = this.db; + const roomRef = await db.collection('rooms').doc(); + + console.log('Create PeerConnection with configuration: ', configuration); + peerConnection = new RTCPeerConnection(configuration); + + registerPeerConnectionListeners(); + + localStream.getTracks().forEach(track => { + peerConnection.addTrack(track, localStream); + }); + + // Code for collecting ICE candidates below + const callerCandidatesCollection = roomRef.collection('callerCandidates'); + + peerConnection.addEventListener('icecandidate', event => { + if (!event.candidate) { + console.log('Got final candidate!'); + return; + } + console.log('Got candidate: ', event.candidate); + callerCandidatesCollection.add(event.candidate.toJSON()); + }); + // Code for collecting ICE candidates above + + // Code for creating a room below + const offer = await peerConnection.createOffer(); + await peerConnection.setLocalDescription(offer); + console.log('Created offer:', offer); + + const roomWithOffer = { + 'offer': { + type: offer.type, + sdp: offer.sdp, + }, + }; + await roomRef.set(roomWithOffer); + roomId = roomRef.id; + console.log(`New room created with SDP offer. Room ID: ${roomRef.id}`); + // document.querySelector( + // '#currentRoom').innerText = `Current room is ${roomRef.id} - You are the caller!`; + // Code for creating a room above + + peerConnection.addEventListener('track', event => { + console.log('Got remote track:', event.streams[0]); + event.streams[0].getTracks().forEach(track => { + console.log('Add a track to the remoteStream:', track); + remoteStream.addTrack(track); + }); + }); + + // Listening for remote session description below + roomRef.onSnapshot(async snapshot => { + const data = snapshot.data(); + if (!peerConnection.currentRemoteDescription && data && data.answer) { + console.log('Got remote description: ', data.answer); + const rtcSessionDescription = new RTCSessionDescription(data.answer); + await peerConnection.setRemoteDescription(rtcSessionDescription); + } + }); + // Listening for remote session description above + + // Listen for remote ICE candidates below + roomRef.collection('calleeCandidates').onSnapshot(snapshot => { + snapshot.docChanges().forEach(async change => { + if (change.type === 'added') { + let data = change.doc.data(); + console.log(`Got new remote ICE candidate: ${JSON.stringify(data)}`); + await peerConnection.addIceCandidate(new RTCIceCandidate(data)); + } + }); + }); + // Listen for remote ICE candidates above + } + + + joinRoom() { + document.querySelector('#createBtn').disabled = true; + document.querySelector('#joinBtn').disabled = true; + + document.querySelector('#confirmJoinBtn'). + addEventListener('click', async() => { + roomId = document.querySelector('#room-id').value; + console.log('Join room: ', roomId); + document.querySelector( + '#currentRoom').innerText = `Current room is ${roomId} - You are the callee!`; + await joinRoomById(roomId); + }, { once: true }); + roomDialog.open(); + } + + async joinRoomById(roomId) { + const db = firebase.firestore(); + const roomRef = db.collection('rooms').doc(`${roomId}`); + const roomSnapshot = await roomRef.get(); + console.log('Got room:', roomSnapshot.exists); + + if (roomSnapshot.exists) { + console.log('Create PeerConnection with configuration: ', configuration); + peerConnection = new RTCPeerConnection(configuration); + registerPeerConnectionListeners(); + localStream.getTracks().forEach(track => { + peerConnection.addTrack(track, localStream); + }); + + // Code for collecting ICE candidates below + const calleeCandidatesCollection = roomRef.collection('calleeCandidates'); + peerConnection.addEventListener('icecandidate', event => { + if (!event.candidate) { + console.log('Got final candidate!'); + return; + } + console.log('Got candidate: ', event.candidate); + calleeCandidatesCollection.add(event.candidate.toJSON()); + }); + // Code for collecting ICE candidates above + + peerConnection.addEventListener('track', event => { + console.log('Got remote track:', event.streams[0]); + event.streams[0].getTracks().forEach(track => { + console.log('Add a track to the remoteStream:', track); + remoteStream.addTrack(track); + }); + }); + + // Code for creating SDP answer below + const offer = roomSnapshot.data().offer; + console.log('Got offer:', offer); + await peerConnection.setRemoteDescription(new RTCSessionDescription(offer)); + const answer = await peerConnection.createAnswer(); + console.log('Created answer:', answer); + await peerConnection.setLocalDescription(answer); + + const roomWithAnswer = { + answer: { + type: answer.type, + sdp: answer.sdp, + }, + }; + await roomRef.update(roomWithAnswer); + // Code for creating SDP answer above + + // Listening for remote ICE candidates below + roomRef.collection('callerCandidates').onSnapshot(snapshot => { + snapshot.docChanges().forEach(async change => { + if (change.type === 'added') { + let data = change.doc.data(); + console.log(`Got new remote ICE candidate: ${JSON.stringify(data)}`); + await peerConnection.addIceCandidate(new RTCIceCandidate(data)); + } + }); + }); + // Listening for remote ICE candidates above + } + } } @@ -601,4 +763,24 @@ // @TODO: Indicate when there is no session! // Probably in app.js so as to handle the possibility of sessions.js failing to load. + + function registerPeerConnectionListeners() { + peerConnection.addEventListener('icegatheringstatechange', () => { + console.log( + `ICE gathering state changed: ${peerConnection.iceGatheringState}`); + }); + + peerConnection.addEventListener('connectionstatechange', () => { + console.log(`Connection state change: ${peerConnection.connectionState}`); + }); + + peerConnection.addEventListener('signalingstatechange', () => { + console.log(`Signaling state change: ${peerConnection.signalingState}`); + }); + + peerConnection.addEventListener('iceconnectionstatechange ', () => { + console.log( + `ICE connection state change: ${peerConnection.iceConnectionState}`); + }); + } })(); \ No newline at end of file From 77b54b48a6c19d063f49574f69bb19649316ee13 Mon Sep 17 00:00:00 2001 From: Norbert Szulc Date: Mon, 13 Apr 2020 22:43:18 +0200 Subject: [PATCH 3/6] More comments, and disabling code --- src/sessions.js | 371 +++++++++++++++++++++++++----------------------- 1 file changed, 197 insertions(+), 174 deletions(-) diff --git a/src/sessions.js b/src/sessions.js index ae1dc4792..c45149544 100644 --- a/src/sessions.js +++ b/src/sessions.js @@ -1,3 +1,13 @@ +// REFACTORING +// glossary: +// session_id has 2 meanings +// in local session is a documentId +// in multi session is a roomId +// documentId should be generated localy +// roomId should be generated by server +// +// + 'use strict'; (() => { @@ -105,11 +115,12 @@ } class LocalSession { + // this class handles saving/loading to local storage, and nothing else constructor(session_id) { this.session_id = session_id; const lsid = `image#${session_id}`; log(`Local storage ID: ${lsid}`); - // save image to storage + // hookup autosave for image to local storage const save_image_to_storage = debounce(() => { const save_paused = handle_data_loss(); if (save_paused) { @@ -179,6 +190,7 @@ away: true, }, // Currently selected tool (@TODO) + // not7cd: move to cursor, hell everything ihere is a cursor property tool: "Pencil", // Color components hue: ~~(Math.random() * 360), @@ -201,6 +213,7 @@ class MultiUserSession { + // this is hell constructor(session_id) { this.session_id = session_id; this._fb_listeners = []; @@ -213,20 +226,21 @@ this.start(); }; if (!MultiUserSession.fb_root) { - $.getScript("lib/firebase.js") // is this lazy loading? this makes init complicated + $.getScript("lib/firebase.js") // TODO: remove this, load in html, no lazy .done(() => { - const config = { - apiKey: "AIzaSyBgau8Vu9ZE8u_j0rp-Lc044gYTX5O3X9k", - authDomain: "jspaint.firebaseapp.com", - databaseURL: "https://jspaint.firebaseio.com", - projectId: "firebase-jspaint", - storageBucket: "", - messagingSenderId: "63395010995" - }; - firebase.initializeApp(config); + // TODO: Move outide ;-; + // const config = { + // apiKey: "AIzaSyBgau8Vu9ZE8u_j0rp-Lc044gYTX5O3X9k", + // authDomain: "jspaint.firebaseapp.com", + // databaseURL: "https://jspaint.firebaseio.com", + // projectId: "firebase-jspaint", + // storageBucket: "", + // messagingSenderId: "63395010995" + // }; + // firebase.initializeApp(configuration); // why MultiUserSession and not this? - MultiUserSession.fb_root = firebase.database().ref("/"); - this.db = firebase.database(); // lets rewrite fb_root and others to use this one + // MultiUserSession.fb_root = firebase.database().ref("/"); + this.db = firebase.firestore(); // lets rewrite fb_root and others to use this one on_firebase_loaded(); }) .fail(() => { @@ -258,92 +272,92 @@ // Wrap the Firebase API because they don't // provide a great way to clean up event listeners - const _fb_on = (fb, event_type, callback, error_callback) => { - this._fb_listeners.push({ fb, event_type, callback, error_callback }); - fb.on(event_type, callback, error_callback); - }; + // const _fb_on = (fb, event_type, callback, error_callback) => { + // this._fb_listeners.push({ fb, event_type, callback, error_callback }); + // fb.on(event_type, callback, error_callback); + // }; // Get Firebase references - this.fb = MultiUserSession.fb_root.child(this.session_id); - this.fb_data = this.fb.child("data"); // TODO: gtfo to peer to peer - this.fb_users = this.fb.child("users"); // this contins cursors of other players? - if (user_id) { - this.fb_user = this.fb_users.child(user_id); - } - else { - this.fb_user = this.fb_users.push(); - user_id = this.fb_user.key; - } - // Remove the user from the session when they disconnect - this.fb_user.onDisconnect().remove(); - // Make the user present in the session - this.fb_user.set(user); + // this.fb = MultiUserSession.fb_root.child(this.session_id); + // this.fb_data = this.fb.child("data"); // TODO: gtfo to peer to peer + // this.fb_users = this.fb.child("users"); // this contins cursors of other players? + // if (user_id) { + // this.local_user = this.fb_users.child(user_id); + // } + // else { + // this.local_user = this.fb_users.push(); + // user_id = this.local_user.key; + // } + // // Remove the user from the session when they disconnect + // this.local_user.onDisconnect().remove(); + // // Make the user present in the session + // this.local_user.set(user); // @TODO: Execute the above two lines when .info/connected // For each existing and new user - _fb_on(this.fb_users, "child_added", snap => { - // Is this you? - if (snap.key === user_id) { - // You already have a cursor. - return; - } - // Get the Firebase reference for this user - const fb_other_user = snap.ref; - // Get the user object stored on the server - let other_user = snap.val(); - // @TODO: display other cursor types? - // @TODO: display pointer button state? - // @TODO: display selections - const cursor_canvas = make_canvas(32, 32); - // Make the cursor element - const $cursor = $(cursor_canvas).addClass("user-cursor").appendTo($app); - $cursor.css({ - display: "none", - position: "absolute", - left: 0, - top: 0, - opacity: 0, - zIndex: 5, // @#: z-index - pointerEvents: "none", - transition: "opacity 0.5s", - }); - // When the cursor data changes - _fb_on(fb_other_user, "value", snap => { - other_user = snap.val(); - // If the user has left - if (other_user == null) { - // Remove the cursor element - $cursor.remove(); - } - else { - // Draw the cursor - const draw_cursor = () => { - cursor_canvas.width = cursor_image.width; - cursor_canvas.height = cursor_image.height; - const cctx = cursor_canvas.ctx; - cctx.fillStyle = other_user.color; - cctx.fillRect(0, 0, cursor_canvas.width, cursor_canvas.height); - cctx.globalCompositeOperation = "multiply"; - cctx.drawImage(cursor_image, 0, 0); - cctx.globalCompositeOperation = "destination-atop"; - cctx.drawImage(cursor_image, 0, 0); - }; - if (cursor_image.complete) { - draw_cursor(); - } - else { - $(cursor_image).one("load", draw_cursor); - } - // Update the cursor element - const canvas_rect = canvas_bounding_client_rect; - $cursor.css({ - display: "block", - position: "absolute", - left: canvas_rect.left + magnification * other_user.cursor.x, - top: canvas_rect.top + magnification * other_user.cursor.y, - opacity: 1 - other_user.cursor.away, - }); - } - }); - }); + // _fb_on(this.fb_users, "child_added", snap => { + // // Is this you? + // if (snap.key === user_id) { + // // You already have a cursor. + // return; + // } + // // Get the Firebase reference for this user + // const fb_other_user = snap.ref; + // // Get the user object stored on the server + // let other_user = snap.val(); + // // @TODO: display other cursor types? + // // @TODO: display pointer button state? + // // @TODO: display selections + // const cursor_canvas = make_canvas(32, 32); + // // Make the cursor element + // const $cursor = $(cursor_canvas).addClass("user-cursor").appendTo($app); + // $cursor.css({ + // display: "none", + // position: "absolute", + // left: 0, + // top: 0, + // opacity: 0, + // zIndex: 5, // @#: z-index + // pointerEvents: "none", + // transition: "opacity 0.5s", + // }); + // // When the cursor data changes + // _fb_on(fb_other_user, "value", snap => { + // other_user = snap.val(); + // // If the user has left + // if (other_user == null) { + // // Remove the cursor element + // $cursor.remove(); + // } + // else { + // // Draw the cursor + // const draw_cursor = () => { + // cursor_canvas.width = cursor_image.width; + // cursor_canvas.height = cursor_image.height; + // const cctx = cursor_canvas.ctx; + // cctx.fillStyle = other_user.color; + // cctx.fillRect(0, 0, cursor_canvas.width, cursor_canvas.height); + // cctx.globalCompositeOperation = "multiply"; + // cctx.drawImage(cursor_image, 0, 0); + // cctx.globalCompositeOperation = "destination-atop"; + // cctx.drawImage(cursor_image, 0, 0); + // }; + // if (cursor_image.complete) { + // draw_cursor(); + // } + // else { + // $(cursor_image).one("load", draw_cursor); + // } + // // Update the cursor element + // const canvas_rect = canvas_bounding_client_rect; + // $cursor.css({ + // display: "block", + // position: "absolute", + // left: canvas_rect.left + magnification * other_user.cursor.x, + // top: canvas_rect.top + magnification * other_user.cursor.y, + // opacity: 1 - other_user.cursor.away, + // }); + // } + // }); + // }); let previous_uri; // let pointer_operations = []; // the multiplayer syncing stuff is a can of worms, so this is disabled // TODO: this should be peer to peer @@ -358,7 +372,7 @@ // log("clear pointer operations to set data", pointer_operations); // pointer_operations = []; log("Write canvas data to Firebase"); - this.fb_data.set(uri); + // this.fb_data.set(uri); previous_uri = uri; } else { @@ -374,74 +388,76 @@ write_canvas_to_database(); }); // Any time we change or recieve the image data - _fb_on(this.fb_data, "value", snap => { - log("Firebase data update"); - const uri = snap.val(); - if (uri == null) { - // If there's no value at the data location, this is a new session - // Sync the current data to it - write_canvas_to_database(); - } - else { - previous_uri = uri; - saved = true; // hopefully - // Load the new image data - const img = new Image(); - img.onload = () => { - // Cancel any in-progress pointer operations - // if (pointer_operations.length) { - // $G.triggerHandler("pointerup", "cancel"); - // } - - const test_canvas = make_canvas(img); - const image_data_remote = test_canvas.ctx.getImageData(0, 0, test_canvas.width, test_canvas.height); - const image_data_local = ctx.getImageData(0, 0, canvas.width, canvas.height); + // _fb_on(this.fb_data, "value", snap => { + // log("Firebase data update"); + // const uri = snap.val(); + // if (uri == null) { + // // If there's no value at the data location, this is a new session + // // Sync the current data to it + // write_canvas_to_database(); + // } + // else { + // previous_uri = uri; + // saved = true; // hopefully + // // Load the new image data + // const img = new Image(); + // img.onload = () => { + // // Cancel any in-progress pointer operations + // // if (pointer_operations.length) { + // // $G.triggerHandler("pointerup", "cancel"); + // // } + + // const test_canvas = make_canvas(img); + // const image_data_remote = test_canvas.ctx.getImageData(0, 0, test_canvas.width, test_canvas.height); + // const image_data_local = ctx.getImageData(0, 0, canvas.width, canvas.height); - if (!image_data_match(image_data_remote, image_data_local, 5)) { - ignore_session_update = true; - undoable({ - name: "Sync Session", - icon: get_help_folder_icon("p_database.png"), - }, ()=> { - // Write the image data to the canvas - ctx.copy(img); - $canvas_area.trigger("resize"); - }); - ignore_session_update = false; - } - - // (detect_transparency() here would not be ideal - // Perhaps a better way of syncing transparency - // and other options will be established) - /* - // Playback recorded in-progress pointer operations - log("Playback", pointer_operations); - - for (const e of pointer_operations) { - // Trigger the event at each place it is listened for - $canvas.triggerHandler(e, ["synthetic"]); - $G.triggerHandler(e, ["synthetic"]); - } - */ - }; - img.src = uri; - } - }, error => { - show_error_message("Failed to retrieve data from Firebase. The document will not load, and changes will not be saved.", error); - file_name = `[Failed to load ${this.session_id}]`; - update_title(); - }); + // if (!image_data_match(image_data_remote, image_data_local, 5)) { + // ignore_session_update = true; + // undoable({ + // name: "Sync Session", + // icon: get_help_folder_icon("p_database.png"), + // }, ()=> { + // // Write the image data to the canvas + // ctx.copy(img); + // $canvas_area.trigger("resize"); + // }); + // ignore_session_update = false; + // } + + // // (detect_transparency() here would not be ideal + // // Perhaps a better way of syncing transparency + // // and other options will be established) + // /* + // // Playback recorded in-progress pointer operations + // log("Playback", pointer_operations); + + // for (const e of pointer_operations) { + // // Trigger the event at each place it is listened for + // $canvas.triggerHandler(e, ["synthetic"]); + // $G.triggerHandler(e, ["synthetic"]); + // } + // */ + // }; + // img.src = uri; + // } + // }, error => { + // show_error_message("Failed to retrieve data from Firebase. The document will not load, and changes will not be saved.", error); + // file_name = `[Failed to load ${this.session_id}]`; + // update_title(); + // }); // Update the cursor status $G.on("pointermove.session-hook", e => { const m = to_canvas_coords(e); - this.fb_user.child("cursor").update({ + const c = { x: m.x, y: m.y, away: false, - }); + } + this._sendCursor(c) + // this.local_user.child("cursor").update(c); }); $G.on("blur.session-hook", ()=> { - this.fb_user.child("cursor").update({ + this._sendCursor({ away: true, }); }); @@ -483,15 +499,15 @@ $G.off(".session-hook"); // $canvas_area.off("pointerdown.session-hook"); // Remove collected Firebase event listeners - this._fb_listeners.forEach(({ fb, event_type, callback/*, error_callback*/ }) => { - log(`Remove listener for ${fb.path.toString()} .on ${event_type}`); - fb.off(event_type, callback); - }); - this._fb_listeners.length = 0; - // Remove the user from the session - this.fb_user.remove(); - // Remove any cursor elements - $app.find(".user-cursor").remove(); + // this._fb_listeners.forEach(({ fb, event_type, callback/*, error_callback*/ }) => { + // log(`Remove listener for ${fb.path.toString()} .on ${event_type}`); + // fb.off(event_type, callback); + // }); + // this._fb_listeners.length = 0; + // // Remove the user from the session + // this.local_user.remove(); + // // Remove any cursor elements + // $app.find(".user-cursor").remove(); // Reset to "untitled" reset_file(); } @@ -503,18 +519,19 @@ const roomRef = await db.collection('rooms').doc(); console.log('Create PeerConnection with configuration: ', configuration); - peerConnection = new RTCPeerConnection(configuration); + this._peerConnection = new RTCPeerConnection(configuration); registerPeerConnectionListeners(); - - localStream.getTracks().forEach(track => { - peerConnection.addTrack(track, localStream); - }); + + const dataChannelParams = {ordered: true}; + // create datachannel to share position + this.cursorChannel = this._peerConnection + .createDataChannel('messaging-channel', dataChannelParams); // Code for collecting ICE candidates below const callerCandidatesCollection = roomRef.collection('callerCandidates'); - peerConnection.addEventListener('icecandidate', event => { + this._peerConnection.addEventListener('icecandidate', event => { if (!event.candidate) { console.log('Got final candidate!'); return; @@ -525,8 +542,8 @@ // Code for collecting ICE candidates above // Code for creating a room below - const offer = await peerConnection.createOffer(); - await peerConnection.setLocalDescription(offer); + const offer = await this._peerConnection.createOffer(); + await this._peerConnection.setLocalDescription(offer); console.log('Created offer:', offer); const roomWithOffer = { @@ -654,10 +671,14 @@ // Listening for remote ICE candidates above } } + _sendCursor(c) { + log('sending cursor pos ', c) + this.cursorChannel.send(c); + } } - + // Handle the starting, switching, and ending of sessions from the location.hash let current_session; @@ -696,6 +717,8 @@ log(`Starting a new LocalSession, ID: ${session_id}`); current_session = new LocalSession(session_id); }else{ + // Multiuser session shouldn't get predefined session_id + // log(`Starting a new MultiUserSession, ID: ${session_id}`); current_session = new MultiUserSession(session_id); } From ca6db789d4b5a682b15ed64d22e26fe8f61426a6 Mon Sep 17 00:00:00 2001 From: Norbert Szulc Date: Tue, 14 Apr 2020 02:09:54 +0200 Subject: [PATCH 4/6] MVP Data Connection, autoforatting --- index.html | 567 ++++++++++++++++++++++++------------------------ src/sessions.js | 373 ++++++++++++------------------- 2 files changed, 420 insertions(+), 520 deletions(-) diff --git a/index.html b/index.html index fc6048da8..713d74d7e 100644 --- a/index.html +++ b/index.html @@ -1,298 +1,292 @@ - - - JS Paint - - - - - - - - - - - + + + + + + - - - - - - - + + + + + + + - + - - - - - - - - - - - - - + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + - -