[mirotalksfu] - add collaborative whiteboard

main
Miroslav Pejic 4 years ago
parent a7e5be3289
commit 5ce76141a3

@ -16,6 +16,7 @@ Powered by `WebRTC` with [SFU](https://mediasoup.org) integrated server.
- Echo cancellation and noise suppression that makes your audio crystal clear
- Screen Sharing to present documents, slides, and more ...
- Chat with Emoji Picker to show you feeling and possibility to Save the conversations
- Collaborative whiteboard for the teachers
- Select Microphone - Speaker and Video source
- Recording your Screen, Audio or Video
- Full Screen Mode on mouse click on the Video element

@ -79,6 +79,7 @@
<script src="https://kit.fontawesome.com/d2f1016e6f.js" crossorigin="anonymous"></script>
<script src="https://cdn.rawgit.com/muaz-khan/DetectRTC/master/DetectRTC.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/qrious/4.0.2/qrious.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/460/fabric.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11.1.4"></script>
<script src="https://unpkg.com/emoji-picker-element@1" type="module"></script>
<script src="https://unpkg.com/@popperjs/core@2"></script>
@ -137,6 +138,22 @@ access to use this app.
<button id="stopVideoButton" class="hidden"><i class="fas fa-video"></i> Stop video</button>
<button id="startScreenButton" class="hidden"><i class="fas fa-desktop"></i> Start screen</button>
<button id="stopScreenButton" class="hidden"><i class="fas fa-stop-circle"></i> Stop screen</button>
<div class="dropdown">
<button id="whiteboardButton" class="hidden">
<i class="fas fa-chalkboard-teacher"></i> Whiteboard
</button>
<div id="whiteboardSettings" class="dropdown-content fadein">
<div id="whiteboardOptions">
<i class="fas fa-pencil-alt"></i> Line width
<input id="wbDrawingLineWidthEl" type="range" value="3" min="1" max="15" />
<br />
<i class="fas fa-palette"></i> Line color
<br />
<input id="wbDrawingColorEl" type="color" value="#FFFFFF" />
<br />
</div>
</div>
</div>
<button id="participantsButton" class="hidden"><i class="fas fa-users"></i> Participants</button>
<button id="lockRoomButton" class="hidden"><i class="fas fa-lock-open"></i> Lock room</button>
<button id="unlockRoomButton" class="hidden"><i class="fas fa-lock"></i> Unlock room</button>
@ -163,6 +180,22 @@ access to use this app.
</main>
</section>
<section id="whiteboard" class="fadein center hidden">
<header id="whiteboardHeader" class="whiteboard-header">
<div id="whiteboardTitle" class="whiteboard-header-title"></div>
<div class="whiteboard-header-options">
<button id="whiteboardUndoBtn" class="fas fa-undo"></button>
<button id="whiteboardRedoBtn" class="fas fa-redo"></button>
<button id="whiteboardSaveBtn" class="fas fa-save"></button>
<button id="whiteboardCleanBtn" class="fas fa-trash"></button>
<button id="whiteboardCloseBtn" class="fas fa-times"></button>
</div>
</header>
<main>
<canvas id="wbCanvas"></canvas>
</main>
</section>
<section id="chatRoom" class="chat-room center fadein">
<section id="msger" class="msger">
<header id="chatHeader" class="chat-header">

@ -38,6 +38,9 @@
--msger-height: 680px;
--msger-width: 420px;
--msger-bg: linear-gradient(to left, #1f1e1e, #000000);
--wb-width: 800px;
--wb-height: 600px;
--wb-bg: linear-gradient(to left, #1f1e1e, #000000);
--left-msg-bg: #222328;
--right-msg-bg: #0a0b0c;
--box-shadow: 0px 8px 16px 0px rgba(0, 0, 0, 0.2);
@ -50,7 +53,7 @@
}
html {
min-height: 100%;
height: 100%;
}
body {
@ -81,7 +84,7 @@ body {
--------------------------------------------------------------*/
#openNavButton {
z-index: 2;
z-index: 3;
position: absolute;
cursor: pointer;
padding: 10px;
@ -92,7 +95,7 @@ body {
}
.sidenav {
z-index: 3;
z-index: 4;
height: 100%;
width: 0;
position: fixed;
@ -149,7 +152,7 @@ body {
padding: 10px;
top: 0;
width: 250px;
background: black;
background: var(--body-bg);
}
#control button {
@ -162,6 +165,7 @@ body {
#settings {
position: relative;
background: var(--body-bg);
}
/*--------------------------------------------------------------
@ -207,6 +211,7 @@ body {
#recording {
position: relative;
background: var(--body-bg);
}
#recording button,
@ -222,7 +227,7 @@ body {
--------------------------------------------------------------*/
.chat-room {
z-index: 4;
z-index: 5;
display: none;
position: fixed;
height: var(--msger-height);
@ -528,7 +533,7 @@ button:hover {
--------------------------------------------------------------*/
#participants {
z-index: 5;
z-index: 6;
position: absolute;
margin: auto;
padding: 10px;
@ -572,6 +577,37 @@ button:hover {
}
*/
/*--------------------------------------------------------------
# Whiteboard
--------------------------------------------------------------*/
#whiteboardSettings {
position: relative;
background: var(--body-bg);
}
#whiteboard {
z-index: 2;
position: absolute;
margin: auto;
padding: 10px;
width: var(--wb-width);
height: var(--wb-height);
background: var(--wb-bg);
border-radius: 10px;
overflow: hidden;
}
.whiteboard-header {
display: flex;
justify-content: space-between;
background: rgb(0, 0, 0);
border-radius: 10px;
padding: 10px;
color: white;
cursor: move;
}
/*--------------------------------------------------------------
# Pulse class effect
--------------------------------------------------------------*/
@ -623,8 +659,10 @@ button:hover {
/*
z-index:
- 1 videoMediaContainer
- 2 buttonBar
- 3 sidenav
- 4 chat
- 5 participants
- 2 whiteboard
- 3 buttonBar
- 4 sidenav
- 4 whiteboard
- 5 chat
- 6 participants
*/

@ -41,6 +41,14 @@ let isVideoOn = true;
let recTimer = null;
let recElapsedTime = null;
const wbWidth = 800;
const wbHeight = 600;
let wbCanvas = null;
let wbIsDrawing = false;
let wbIsOpen = false;
var wbIsRedoing = false;
var wbPop = [];
const socket = io();
function getRandomNumber(length) {
@ -56,6 +64,11 @@ function getRandomNumber(length) {
function initClient() {
if (!DetectRTC.isMobileDevice) {
setTippy('closeNavButton', 'Close', 'right');
setTippy('whiteboardUndoBtn', 'Undo', 'top');
setTippy('whiteboardRedoBtn', 'Redo', 'top');
setTippy('whiteboardSaveBtn', 'Save', 'top');
setTippy('whiteboardCleanBtn', 'Clear', 'top');
setTippy('whiteboardCloseBtn', 'Close', 'top');
setTippy('participantsRefreshBtn', 'Refresh', 'top');
setTippy('participantsCloseBtn', 'Close', 'top');
setTippy('chatMessage', 'Press enter to send', 'top-start');
@ -66,6 +79,7 @@ function initClient() {
setTippy('chatCloseButton', 'Close', 'bottom');
setTippy('sessionTime', 'Session time', 'right');
}
setupWhiteboard();
initEnumerateDevices();
}
@ -394,6 +408,7 @@ function roomIsReady() {
} else {
rc.makeDraggable(chatRoom, chatHeader);
rc.makeDraggable(participants, participantsHeader);
rc.makeDraggable(whiteboard, whiteboardHeader);
if (navigator.getDisplayMedia || navigator.mediaDevices.getDisplayMedia) {
show(startScreenButton);
}
@ -408,6 +423,7 @@ function roomIsReady() {
show(raiseHandButton);
if (isAudioAllowed) show(startAudioButton);
if (isVideoAllowed) show(startVideoButton);
show(whiteboardButton);
show(participantsButton);
show(lockRoomButton);
show(aboutButton);
@ -579,6 +595,24 @@ function handleButtons() {
stopScreenButton.onclick = () => {
rc.closeProducer(RoomClient.mediaType.screen);
};
whiteboardButton.onclick = () => {
toggleWhiteboard();
};
whiteboardUndoBtn.onclick = () => {
whiteboardAction(getWhiteboardAction('undo'));
};
whiteboardRedoBtn.onclick = () => {
whiteboardAction(getWhiteboardAction('redo'));
};
whiteboardSaveBtn.onclick = () => {
wbCanvasSaveImg();
};
whiteboardCleanBtn.onclick = () => {
whiteboardAction(getWhiteboardAction('clear'));
};
whiteboardCloseBtn.onclick = () => {
whiteboardAction(getWhiteboardAction('close'));
};
participantsButton.onclick = () => {
getRoomParticipants();
};
@ -604,6 +638,7 @@ function handleButtons() {
// ####################################################
function handleSelects() {
// devices options
microphoneSelect.onchange = () => {
rc.closeThenProduce(RoomClient.mediaType.audio, microphoneSelect.value);
};
@ -613,6 +648,13 @@ function handleSelects() {
videoSelect.onchange = () => {
rc.closeThenProduce(RoomClient.mediaType.video, videoSelect.value);
};
// whiteboard options
wbDrawingLineWidthEl.onchange = () => {
wbCanvas.freeDrawingBrush.width = parseInt(wbDrawingLineWidthEl.value, 10) || 1;
};
wbDrawingColorEl.onchange = () => {
wbCanvas.freeDrawingBrush.color = wbDrawingColorEl.value;
};
}
// ####################################################
@ -749,7 +791,7 @@ function handleRoomClientEvents() {
}
// ####################################################
// SHOW LOG
// UTILITY
// ####################################################
function userLog(icon, message, position, timer = 3000) {
@ -766,6 +808,26 @@ function userLog(icon, message, position, timer = 3000) {
});
}
function saveDataToFile(dataURL, fileName) {
const a = document.createElement('a');
a.style.display = 'none';
a.href = dataURL;
a.download = fileName;
document.body.appendChild(a);
a.click();
setTimeout(() => {
document.body.removeChild(a);
window.URL.revokeObjectURL(dataURL);
}, 100);
}
function getDataTimeString() {
const d = new Date();
const date = d.toISOString().split('T')[0];
const time = d.toTimeString().split(' ')[0];
return `${date}-${time}`;
}
// ####################################################
// SOUND
// ####################################################
@ -780,6 +842,174 @@ async function sound(name) {
}
}
// ####################################################
// HANDLE WHITEBOARD
// ####################################################
function toggleWhiteboard() {
toggleWhiteboardSettings();
let whiteboard = rc.getId('whiteboard');
whiteboard.classList.toggle('show');
whiteboard.style.top = '50%';
whiteboard.style.left = '50%';
wbIsOpen = wbIsOpen ? false : true;
}
function toggleWhiteboardSettings() {
rc.getId('whiteboardSettings').classList.toggle('show');
}
function setupWhiteboard() {
setupWhiteboardCanvas();
setupWhiteboardCanvasSize();
setupWhiteboardLocalListners();
}
function setupWhiteboardCanvas() {
wbCanvas = new fabric.Canvas('wbCanvas');
wbCanvas.freeDrawingBrush.color = '#FFFFFF';
wbCanvas.freeDrawingBrush.width = 3;
wbCanvas.isDrawingMode = true;
}
function setupWhiteboardCanvasSize() {
let optimalSize = [wbWidth, wbHeight];
let scaleFactorX = window.innerWidth / optimalSize[0];
let scaleFactorY = window.innerHeight / optimalSize[1];
if (scaleFactorX < scaleFactorY && scaleFactorX < 1) {
wbCanvas.setWidth(optimalSize[0] * scaleFactorX);
wbCanvas.setHeight(optimalSize[1] * scaleFactorX);
wbCanvas.setZoom(scaleFactorX);
document.documentElement.style.setProperty('--wb-width', optimalSize[0] * scaleFactorX);
document.documentElement.style.setProperty('--wb-height', optimalSize[1] * scaleFactorX);
} else if (scaleFactorX > scaleFactorY && scaleFactorY < 1) {
wbCanvas.setWidth(optimalSize[0] * scaleFactorY);
wbCanvas.setHeight(optimalSize[1] * scaleFactorY);
wbCanvas.setZoom(scaleFactorY);
document.documentElement.style.setProperty('--wb-width', optimalSize[0] * scaleFactorY);
document.documentElement.style.setProperty('--wb-height', optimalSize[1] * scaleFactorY);
} else {
wbCanvas.setWidth(optimalSize[0]);
wbCanvas.setHeight(optimalSize[1]);
wbCanvas.setZoom(1);
document.documentElement.style.setProperty('--wb-width', optimalSize[0]);
document.documentElement.style.setProperty('--wb-height', optimalSize[1]);
}
wbCanvas.calcOffset();
wbCanvas.renderAll();
}
function setupWhiteboardLocalListners() {
wbCanvas.on('mouse:down', function () {
mouseDown();
});
wbCanvas.on('mouse:up', function () {
mouseUp();
});
wbCanvas.on('mouse:move', function () {
mouseMove();
});
wbCanvas.on('object:added', function () {
objectAdded();
});
}
function mouseDown() {
wbIsDrawing = true;
}
function mouseUp() {
wbIsDrawing = false;
wbCanvasToJson();
}
function mouseMove() {
if (!wbIsDrawing) return;
}
function objectAdded() {
if (!wbIsRedoing) wbPop = [];
wbIsRedoing = false;
}
function wbCanvasUndo() {
if (wbCanvas._objects.length > 0) {
wbPop.push(wbCanvas._objects.pop());
wbCanvas.renderAll();
}
}
function wbCanvasRedo() {
if (wbPop.length > 0) {
wbIsRedoing = true;
wbCanvas.add(wbPop.pop());
}
}
function wbCanvasSaveImg() {
const dataURL = wbCanvas.toDataURL({
width: wbCanvas.getWidth(),
height: wbCanvas.getHeight(),
left: 0,
top: 0,
format: 'png',
});
const dataNow = getDataTimeString();
const fileName = `whiteboard-${dataNow}.png`;
saveDataToFile(dataURL, fileName);
}
function wbCanvasToJson() {
if (rc.thereIsConsumers()) {
var wbCanvasJson = JSON.stringify(wbCanvas.toJSON());
rc.socket.emit('wbCanvasToJson', wbCanvasJson);
}
}
function JsonToWbCanvas(json) {
if (!wbIsOpen) toggleWhiteboard();
wbCanvas.loadFromJSON(json);
wbCanvas.renderAll();
}
function getWhiteboardAction(action) {
return {
peer_name: peer_name,
action: action,
};
}
function whiteboardAction(data, emit = true) {
if (emit) {
if (rc.thereIsConsumers()) {
rc.socket.emit('whiteboardAction', data);
}
} else {
userLog(
'info',
`${data.peer_name} <i class="fas fa-chalkboard-teacher"></i> whiteboard action: ${data.action}`,
'top-end',
);
}
switch (data.action) {
case 'undo':
wbCanvasUndo();
break;
case 'redo':
wbCanvasRedo();
break;
case 'clear':
wbCanvas.clear();
break;
case 'close':
if (wbIsOpen) toggleWhiteboard();
break;
//...
}
}
// ####################################################
// HANDLE PARTICIPANTS
// ####################################################

@ -393,6 +393,22 @@ class RoomClient {
}.bind(this),
);
this.socket.on(
'wbCanvasToJson',
function (data) {
console.log('Received whiteboard canvas JSON');
JsonToWbCanvas(data);
}.bind(this),
);
this.socket.on(
'whiteboardAction',
function (data) {
console.log('Whiteboard action', data);
whiteboardAction(data, false);
}.bind(this),
);
this.socket.on(
'disconnect',
function () {
@ -1029,7 +1045,6 @@ class RoomClient {
if (this.consumers.size > 0) {
return true;
}
this.userLog('info', 'No participants in the room', 'top-end');
return false;
}
@ -1156,6 +1171,7 @@ class RoomClient {
sendMessage() {
if (!this.thereIsConsumers()) {
chatMessage.value = '';
this.userLog('info', 'No participants in the room', 'top-end');
return;
}
let peer_msg = this.formatMsg(chatMessage.value);

@ -252,6 +252,17 @@ io.on('connection', (socket) => {
roomList.get(socket.room_id).broadCast(socket.id, 'updatePeerInfo', data);
});
socket.on('wbCanvasToJson', (data) => {
// let objLength = bytesToSize(Object.keys(data).length);
// log.debug('Send Whiteboard canvas JSON', { length: objLength });
roomList.get(socket.room_id).broadCast(socket.id, 'wbCanvasToJson', data);
});
socket.on('whiteboardAction', (data) => {
log.debug('Whiteboard', data);
roomList.get(socket.room_id).broadCast(socket.id, 'whiteboardAction', data);
});
socket.on('join', (data, cb) => {
if (!roomList.has(socket.room_id)) {
return cb({
@ -434,4 +445,11 @@ io.on('connection', (socket) => {
roomList.get(socket.room_id) && roomList.get(socket.room_id).getPeers().get(socket.id).peer_info.peer_name
);
}
function bytesToSize(bytes) {
let sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
if (bytes == 0) return '0 Byte';
let i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024)));
return Math.round(bytes / Math.pow(1024, i), 2) + ' ' + sizes[i];
}
});

Loading…
Cancel
Save