Files
mirotalk/www/js/client.js
T
2021-06-03 14:32:02 +02:00

4062 lines
114 KiB
JavaScript

/*
██████ ██  ██ ███████ ███  ██ ████████ 
██      ██  ██ ██      ████  ██    ██    
██  ██  ██ █████  ██ ██  ██  ██ 
██  ██  ██ ██     ██  ██ ██  ██ 
██████ ███████ ██ ███████ ██   ████  ██ 
*/
"use strict"; // https://www.w3schools.com/js/js_strict.asp
const welcomeImg = "../images/illustration-section-01.svg";
const shareUrlImg = "../images/illustration-section-01.svg";
const leaveRoomImg = "../images/illustration-section-01.svg";
const confirmImg = "../images/illustration-section-01.svg";
const fileSharingImg = "../images/illustration-section-01.svg";
const aboutImg = "../images/about.png";
const peerLoockupUrl = "https://extreme-ip-lookup.com/json/";
const avatarApiUrl = "https://eu.ui-avatars.com/api";
const notifyBySound = true; // turn on - off sound notifications
const notifyAddPeer = "../audio/addPeer.mp3";
const notifyDownload = "../audio/download.mp3";
const notifyKickedOut = "../audio/kickedOut.mp3";
const notifyRemovePeer = "../audio/removePeer.mp3";
const notifyNewMessage = "../audio/newMessage.mp3";
const notifyRecStart = "../audio/recStart.mp3";
const notifyRecStop = "../audio/recStop.mp3";
const notifyRaiseHand = "../audio/raiseHand.mp3";
const notifyError = "../audio/error.mp3";
const fileSharingInput = "*"; // allow all file extensions
// "image/*,.mp3,.doc,.docs,.txt,.pdf,.xls,.xlsx,.csv,.pcap,.xml,.json,.md,.html,.js,.css,.php,.py,.sh,.zip,.rar,.tar"; // "*"
const isWebRTCSupported = DetectRTC.isWebRTCSupported;
const isMobileDevice = DetectRTC.isMobileDevice;
var leftChatAvatar = "../images/friend.svg";
var rightChatAvatar = "../images/programmer.svg";
var callStartTime;
var callElapsedTime;
var recStartTime;
var recElapsedTime;
var mirotalkTheme = "neon"; // neon - dark - forest - ghost ...
var swalBackground = "rgba(0, 0, 0, 0.7)"; // black - #16171b - transparent ...
var signalingServerPort = 3000; // must be same of server PORT
var signalingServer = getServerUrl();
var roomId = getRoomId();
var peerInfo = getPeerInfo();
var peerGeo;
var peerConnection;
var myPeerName;
var useAudio = true;
var useVideo = true;
var camera = "user";
var myVideoChange = false;
var myHandStatus = false;
var myVideoStatus = true;
var myAudioStatus = true;
var isScreenStreaming = false;
var isChatRoomVisible = false;
var isChatEmojiVisible = false;
var isButtonsVisible = false;
var isMySettingsVisible = false;
var isVideoOnFullScreen = false;
var isDocumentOnFullScreen = false;
var signalingSocket; // socket.io connection to our webserver
var localMediaStream; // my microphone / webcam
var remoteMediaStream; // peers microphone / webcam
var remoteMediaControls = false; // enable - disable peers video player controls (default false)
var peerConnections = {}; // keep track of our peer connections, indexed by peer_id == socket.io id
var chatDataChannels = {}; // keep track of our peer chat data channels
var fileSharingDataChannels = {}; // keep track of our peer file sharing data channels
var useRTCDataChannel = true; // https://developer.mozilla.org/en-US/docs/Web/API/RTCDataChannel
var peerMediaElements = {}; // keep track of our peer <video> tags, indexed by peer_id
var chatMessages = []; // collect chat messages to save it later if want
var iceServers = [{ urls: "stun:stun.l.google.com:19302" }]; // backup iceServers
var chatInputEmoji = {
"<3": "\u2764\uFE0F",
"</3": "\uD83D\uDC94",
":D": "\uD83D\uDE00",
":)": "\uD83D\uDE03",
";)": "\uD83D\uDE09",
":(": "\uD83D\uDE12",
":p": "\uD83D\uDE1B",
";p": "\uD83D\uDE1C",
":'(": "\uD83D\uDE22",
":+1:": "\uD83D\uDC4D",
}; // https://github.com/wooorm/gemoji/blob/main/support.md
var countTime;
// init audio-video
var initAudioBtn;
var initVideoBtn;
// left buttons
var leftButtons;
var shareRoomBtn;
var audioBtn;
var videoBtn;
var swapCameraBtn;
var screenShareBtn;
var recordStreamBtn;
var fullScreenBtn;
var chatRoomBtn;
var myHandBtn;
var whiteboardBtn;
var fileShareBtn;
var mySettingsBtn;
var aboutBtn;
var leaveRoomBtn;
// chat room elements
var msgerDraggable;
var msgerHeader;
var msgerTheme;
var msgerCPBtn;
var msgerClean;
var msgerSaveBtn;
var msgerClose;
var msgerChat;
var msgerEmojiBtn;
var msgerInput;
var msgerSendBtn;
// chat room connected peers
var msgerCP;
var msgerCPHeader;
var msgerCPCloseBtn;
var msgerCPList;
// chat room emoji picker
var msgerEmojiPicker;
var msgerEmojiHeader;
var msgerCloseEmojiBtn;
var emojiPicker;
// my settings
var mySettings;
var mySettingsHeader;
var mySettingsCloseBtn;
var myPeerNameSet;
var myPeerNameSetBtn;
var audioInputSelect;
var audioOutputSelect;
var videoSelect;
var themeSelect;
var selectors;
// my video element
var myVideo;
// name && hand video audio status
var myVideoParagraph;
var myHandStatusIcon;
var myVideoStatusIcon;
var myAudioStatusIcon;
// record Media Stream
var mediaRecorder;
var recordedBlobs;
var isStreamRecording = false;
// whiteboard init
var whiteboardCont;
var whiteboardColorPicker;
var whiteboardBlackColor;
var whiteboardWhiteColor;
var whiteboardCloseBtn;
var whiteboardCleanBtn;
var whiteboardEraserBtn;
var isWhiteboardVisible = false;
var canvas;
var ctx;
// whiteboard settings
var isDrawing = 0;
var x = 0;
var y = 0;
var color = "#000000";
var drawsize = 3;
// room actions btns
var muteEveryoneBtn;
var hideEveryoneBtn;
// file transfer settings
var fileToSend;
var fileReader;
var receiveBuffer = [];
var receivedSize = 0;
var incomingFileInfo;
var incomingFileData;
var sendFileDiv;
var sendFileInfo;
var sendProgress;
var sendAbortBtn;
var sendInProgress = false;
var fsDataChannelOpen = false;
const chunkSize = 16 * 1024; //16kb
/**
* Load all Html elements by Id
*/
function getHtmlElementsById() {
countTime = getId("countTime");
myVideo = getId("myVideo");
// left buttons
leftButtons = getId("leftButtons");
shareRoomBtn = getId("shareRoomBtn");
audioBtn = getId("audioBtn");
videoBtn = getId("videoBtn");
swapCameraBtn = getId("swapCameraBtn");
screenShareBtn = getId("screenShareBtn");
recordStreamBtn = getId("recordStreamBtn");
fullScreenBtn = getId("fullScreenBtn");
chatRoomBtn = getId("chatRoomBtn");
whiteboardBtn = getId("whiteboardBtn");
fileShareBtn = getId("fileShareBtn");
myHandBtn = getId("myHandBtn");
mySettingsBtn = getId("mySettingsBtn");
aboutBtn = getId("aboutBtn");
leaveRoomBtn = getId("leaveRoomBtn");
// chat Room elements
msgerDraggable = getId("msgerDraggable");
msgerHeader = getId("msgerHeader");
msgerTheme = getId("msgerTheme");
msgerCPBtn = getId("msgerCPBtn");
msgerClean = getId("msgerClean");
msgerSaveBtn = getId("msgerSaveBtn");
msgerClose = getId("msgerClose");
msgerChat = getId("msgerChat");
msgerEmojiBtn = getId("msgerEmojiBtn");
msgerInput = getId("msgerInput");
msgerSendBtn = getId("msgerSendBtn");
// chat room connected peers
msgerCP = getId("msgerCP");
msgerCPHeader = getId("msgerCPHeader");
msgerCPCloseBtn = getId("msgerCPCloseBtn");
msgerCPList = getId("msgerCPList");
// chat room emoji picker
msgerEmojiPicker = getId("msgerEmojiPicker");
msgerEmojiHeader = getId("msgerEmojiHeader");
msgerCloseEmojiBtn = getId("msgerCloseEmojiBtn");
emojiPicker = getSl("emoji-picker");
// my settings
mySettings = getId("mySettings");
mySettingsHeader = getId("mySettingsHeader");
mySettingsCloseBtn = getId("mySettingsCloseBtn");
myPeerNameSet = getId("myPeerNameSet");
myPeerNameSetBtn = getId("myPeerNameSetBtn");
audioInputSelect = getId("audioSource");
audioOutputSelect = getId("audioOutput");
videoSelect = getId("videoSource");
themeSelect = getId("mirotalkTheme");
// my conference name, hand, video - audio status
myVideoParagraph = getId("myVideoParagraph");
myHandStatusIcon = getId("myHandStatusIcon");
myVideoStatusIcon = getId("myVideoStatusIcon");
myAudioStatusIcon = getId("myAudioStatusIcon");
// my whiteboard
whiteboardCont = getSl(".whiteboard-cont");
whiteboardCloseBtn = getId("whiteboardCloseBtn");
whiteboardColorPicker = getId("whiteboardColorPicker");
whiteboardBlackColor = getId("whiteboardBlackColor");
whiteboardWhiteColor = getId("whiteboardWhiteColor");
whiteboardEraserBtn = getId("whiteboardEraserBtn");
whiteboardCleanBtn = getId("whiteboardCleanBtn");
canvas = getId("whiteboard");
ctx = canvas.getContext("2d");
// room actions buttons
muteEveryoneBtn = getId("muteEveryoneBtn");
hideEveryoneBtn = getId("hideEveryoneBtn");
// file send progress
sendFileDiv = getId("sendFileDiv");
sendFileInfo = getId("sendFileInfo");
sendProgress = getId("sendProgress");
sendAbortBtn = getId("sendAbortBtn");
}
/**
* Using tippy aka very nice tooltip!
* https://atomiks.github.io/tippyjs/
*/
function setButtonsTitle() {
// not need for mobile
if (isMobileDevice) return;
// left buttons
tippy(shareRoomBtn, {
content: "Invite people to join",
placement: "right-start",
});
tippy(audioBtn, {
content: "Click to audio OFF",
placement: "right-start",
});
tippy(videoBtn, {
content: "Click to video OFF",
placement: "right-start",
});
tippy(screenShareBtn, {
content: "START screen sharing",
placement: "right-start",
});
tippy(recordStreamBtn, {
content: "START recording",
placement: "right-start",
});
tippy(fullScreenBtn, {
content: "VIEW full screen",
placement: "right-start",
});
tippy(chatRoomBtn, {
content: "OPEN the chat",
placement: "right-start",
});
tippy(myHandBtn, {
content: "RAISE your hand",
placement: "right-start",
});
tippy(whiteboardBtn, {
content: "OPEN the whiteboard",
placement: "right-start",
});
tippy(fileShareBtn, {
content: "SHARE the file",
placement: "right-start",
});
tippy(mySettingsBtn, {
content: "Show settings",
placement: "right-start",
});
tippy(aboutBtn, {
content: "Show about",
placement: "right-start",
});
tippy(leaveRoomBtn, {
content: "Leave this room",
placement: "right-start",
});
// chat room buttons
tippy(msgerTheme, {
content: "Ghost theme",
});
tippy(msgerCPBtn, {
content: "All Participants",
});
tippy(msgerClean, {
content: "Clean messages",
});
tippy(msgerSaveBtn, {
content: "Save messages",
});
tippy(msgerClose, {
content: "Close the chat",
});
tippy(msgerEmojiBtn, {
content: "Emoji",
});
tippy(msgerSendBtn, {
content: "Send",
});
// emoji picker
tippy(msgerCloseEmojiBtn, {
content: "Close emoji",
});
// settings
tippy(mySettingsCloseBtn, {
content: "Close settings",
});
tippy(myPeerNameSetBtn, {
content: "Change name",
});
// whiteboard btns
tippy(whiteboardCloseBtn, {
content: "CLOSE the whiteboard",
placement: "right-start",
});
tippy(whiteboardColorPicker, {
content: "COLOR picker",
placement: "right-start",
});
tippy(whiteboardEraserBtn, {
content: "ERASE the board",
placement: "right-start",
});
tippy(whiteboardCleanBtn, {
content: "CLEAN the board",
placement: "right-start",
});
// room actions btn
tippy(muteEveryoneBtn, {
content: "MUTE everyone except yourself",
placement: "top",
});
tippy(hideEveryoneBtn, {
content: "HIDE everyone except yourself",
placement: "top",
});
// Suspend File transfer btn
tippy(sendAbortBtn, {
content: "ABORT file transfer",
placement: "right-start",
});
}
/**
* Get peer info using DetecRTC
* https://github.com/muaz-khan/DetectRTC
* @return Json peer info
*/
function getPeerInfo() {
return {
detectRTCversion: DetectRTC.version,
isWebRTCSupported: DetectRTC.isWebRTCSupported,
isMobileDevice: DetectRTC.isMobileDevice,
osName: DetectRTC.osName,
osVersion: DetectRTC.osVersion,
browserName: DetectRTC.browser.name,
browserVersion: DetectRTC.browser.version,
};
}
/**
* Get approximative peer geolocation
* @return json
*/
function getPeerGeoLocation() {
fetch(peerLoockupUrl)
.then((res) => res.json())
.then((outJson) => {
peerGeo = outJson;
})
.catch((err) => console.error(err));
}
/**
* Get Signaling server url
* @return Signaling server Url
*/
function getServerUrl() {
return (
"http" +
(location.hostname == "localhost" ? "" : "s") +
"://" +
location.hostname +
(location.hostname == "localhost" ? ":" + signalingServerPort : "")
);
}
/**
* Generate random Room id
* @return Room Id
*/
function getRoomId() {
// skip /join/
let roomId = location.pathname.substring(6);
// if not specified room id, create one random
if (roomId == "") {
roomId = makeId(12);
const newurl = signalingServer + "/join/" + roomId;
window.history.pushState({ url: newurl }, roomId, newurl);
}
return roomId;
}
/**
* Generate random Id
* @param {*} length
* @returns random id
*/
function makeId(length) {
var result = "";
var characters =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
var charactersLength = characters.length;
for (var i = 0; i < length; i++) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
}
return result;
}
/**
* Check if there is peer connections
* @return true, false otherwise
*/
function thereIsPeerConnections() {
if (Object.keys(peerConnections).length === 0) {
return false;
}
return true;
}
/**
* On body load Get started
*/
function initPeer() {
// set mirotalk theme
setTheme(mirotalkTheme);
// check if peer is done for WebRTC
if (!isWebRTCSupported) {
console.error("isWebRTCSupported: false");
userLog("error", "This browser seems not supported WebRTC!");
return;
}
// peer ready for WebRTC! :)
console.log("Connecting to signaling server");
signalingSocket = io(signalingServer);
/**
* Once the user has given us access to their
* microphone/camcorder, join the channel
* and start peering up
*/
signalingSocket.on("connect", function () {
console.log("Connected to signaling server");
if (localMediaStream) joinToChannel();
else
setupLocalMedia(function () {
whoAreYou();
});
});
/**
* set your name 4 conference
*/
function whoAreYou() {
playSound("newMessage");
Swal.fire({
allowOutsideClick: false,
background: swalBackground,
position: "center",
imageAlt: "mirotalk-name",
imageUrl: welcomeImg,
title: "Enter your name",
input: "text",
html: `<br>
<button id="initAudioBtn" class="fas fa-microphone" onclick="handleAudio(event, true)"></button>
<button id="initVideoBtn" class="fas fa-video" onclick="handleVideo(event, true)"></button>
`,
confirmButtonText: `Join meeting`,
showClass: {
popup: "animate__animated animate__fadeInDown",
},
hideClass: {
popup: "animate__animated animate__fadeOutUp",
},
inputValidator: (value) => {
if (!value) {
return "Please enter your name";
}
myPeerName = value;
myVideoParagraph.innerHTML = myPeerName + " (me)";
setPeerAvatarImgName("myVideoAvatarImage", myPeerName);
setPeerChatAvatarImgName("right", myPeerName);
joinToChannel();
},
}).then(function () {
welcomeUser();
});
// not need for mobile
if (isMobileDevice) return;
// init audio-video
initAudioBtn = getId("initAudioBtn");
initVideoBtn = getId("initVideoBtn");
// popup text
tippy(initAudioBtn, {
content: "Click to audio OFF",
placement: "top",
});
tippy(initVideoBtn, {
content: "Click to video OFF",
placement: "top",
});
}
/**
* join to chennel and send some peer info
*/
function joinToChannel() {
console.log("join to channel", roomId);
signalingSocket.emit("join", {
channel: roomId,
peerInfo: peerInfo,
peerGeo: peerGeo,
peerName: myPeerName,
peerVideo: myVideoStatus,
peerAudio: myAudioStatus,
peerHand: myHandStatus,
});
}
/**
* welcome message
*/
function welcomeUser() {
const myRoomUrl = window.location.href;
playSound("newMessage");
copyRoomURL();
Swal.fire({
background: swalBackground,
position: "center",
title: "<strong>Welcome " + myPeerName + "</strong>",
imageAlt: "mirotalk-welcome",
imageUrl: welcomeImg,
html:
`
<br/>
<p style="color:white;">Share this meeting invite others to join.</p>
<p style="color:rgb(8, 189, 89);">` +
myRoomUrl +
`</p>`,
confirmButtonText: `Copy meeting URL`,
showClass: {
popup: "animate__animated animate__fadeInDown",
},
hideClass: {
popup: "animate__animated animate__fadeOutUp",
},
});
}
/**
* Tear down all of our peer connections
* and remove all the media divs when we disconnect
*/
signalingSocket.on("disconnect", function () {
console.log("Disconnected from signaling server");
for (var peer_id in peerMediaElements) {
document.body.removeChild(peerMediaElements[peer_id].parentNode);
resizeVideos();
}
for (var peer_id in peerConnections) {
peerConnections[peer_id].close();
msgerRemovePeer(peer_id);
}
if (useRTCDataChannel) chatDataChannels = {};
fileSharingDataChannels = {};
peerConnections = {};
peerMediaElements = {};
});
/**
* When we join a group, our signaling server will send out 'addPeer' events to each pair
* of users in the group (creating a fully-connected graph of users, ie if there are 6 people
* in the channel you will connect directly to the other 5, so there will be a total of 15
* connections in the network).
*/
signalingSocket.on("addPeer", function (config) {
// console.log("addPeer", JSON.stringify(config));
var peer_id = config.peer_id;
var peers = config.peers;
if (peer_id in peerConnections) {
// This could happen if the user joins multiple channels where the other peer is also in.
console.log("Already connected to peer", peer_id);
return;
}
if (config.iceServers) iceServers = config.iceServers;
console.log("iceServers", iceServers[0]);
// https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection
peerConnection = new RTCPeerConnection({ iceServers: iceServers });
// collect peer connections
peerConnections[peer_id] = peerConnection;
// add peer to msger lists 4 private msgs
msgerAddPeers(peers);
playSound("addPeer");
// https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/onicecandidate
peerConnections[peer_id].onicecandidate = function (event) {
if (event.candidate) {
signalingSocket.emit("relayICE", {
peer_id: peer_id,
ice_candidate: {
sdpMLineIndex: event.candidate.sdpMLineIndex,
candidate: event.candidate.candidate,
address: event.candidate.address,
},
});
}
};
/**
* WebRTC: onaddstream is deprecated! Use peerConnection.ontrack instead (done)
* https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/onaddstream
* https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/ontrack
*/
let ontrackCount = 0;
peerConnections[peer_id].ontrack = function (event) {
ontrackCount++;
if (ontrackCount === 2) {
console.log("ontrack", event);
remoteMediaStream = event.streams[0];
const videoWrap = document.createElement("div");
// handle peers name video audio status
const remoteStatusMenu = document.createElement("div");
const remoteVideoParagraph = document.createElement("h4");
const remoteHandStatusIcon = document.createElement("button");
const remoteVideoStatusIcon = document.createElement("button");
const remoteAudioStatusIcon = document.createElement("button");
const remotePeerKickOut = document.createElement("button");
const remoteVideoFullScreenBtn = document.createElement("button");
const remoteVideoAvatarImage = document.createElement("img");
// menu Status
remoteStatusMenu.setAttribute("id", peer_id + "_menuStatus");
remoteStatusMenu.className = "statusMenu";
// remote peer name element
remoteVideoParagraph.setAttribute("id", peer_id + "_name");
remoteVideoParagraph.className = "videoPeerName";
tippy(remoteVideoParagraph, {
content: "Participant name",
});
const peerVideoText = document.createTextNode(
peers[peer_id]["peer_name"]
);
remoteVideoParagraph.appendChild(peerVideoText);
// remote hand status element
remoteHandStatusIcon.setAttribute("id", peer_id + "_handStatus");
remoteHandStatusIcon.style.setProperty("color", "rgb(0, 255, 0)");
remoteHandStatusIcon.className = "fas fa-hand-paper pulsate";
tippy(remoteHandStatusIcon, {
content: "Participant hand is RAISED",
});
// remote video status element
remoteVideoStatusIcon.setAttribute("id", peer_id + "_videoStatus");
remoteVideoStatusIcon.className = "fas fa-video";
tippy(remoteVideoStatusIcon, {
content: "Participant video is ON",
});
// remote audio status element
remoteAudioStatusIcon.setAttribute("id", peer_id + "_audioStatus");
remoteAudioStatusIcon.className = "fas fa-microphone";
tippy(remoteAudioStatusIcon, {
content: "Participant audio is ON",
});
// remote peer kick out
remotePeerKickOut.setAttribute("id", peer_id + "_kickOut");
remotePeerKickOut.className = "fas fa-sign-out-alt";
tippy(remotePeerKickOut, {
content: "Kick out",
});
// remote video full screen mode
remoteVideoFullScreenBtn.setAttribute("id", peer_id + "_fullScreen");
remoteVideoFullScreenBtn.className = "fas fa-expand";
tippy(remoteVideoFullScreenBtn, {
content: "Full screen mode",
});
// my video avatar image
remoteVideoAvatarImage.setAttribute("id", peer_id + "_avatar");
remoteVideoAvatarImage.className = "videoAvatarImage pulsate";
// add elements to remoteStatusMenu div
remoteStatusMenu.appendChild(remoteVideoParagraph);
remoteStatusMenu.appendChild(remoteHandStatusIcon);
remoteStatusMenu.appendChild(remoteVideoStatusIcon);
remoteStatusMenu.appendChild(remoteAudioStatusIcon);
remoteStatusMenu.appendChild(remotePeerKickOut);
remoteStatusMenu.appendChild(remoteVideoFullScreenBtn);
// add elements to videoWrap div
videoWrap.appendChild(remoteStatusMenu);
videoWrap.appendChild(remoteVideoAvatarImage);
const remoteMedia = document.createElement("video");
videoWrap.className = "video";
videoWrap.appendChild(remoteMedia);
remoteMedia.setAttribute("id", peer_id + "_video");
remoteMedia.setAttribute("playsinline", true);
remoteMedia.mediaGroup = "remotevideo";
remoteMedia.autoplay = true;
isMobileDevice
? (remoteMediaControls = false)
: (remoteMediaControls = remoteMediaControls);
remoteMedia.controls = remoteMediaControls;
peerMediaElements[peer_id] = remoteMedia;
document.body.appendChild(videoWrap);
// attachMediaStream is a part of the adapter.js library
attachMediaStream(remoteMedia, remoteMediaStream);
resizeVideos();
if (!isMobileDevice) {
handleVideoPlayerFs(peer_id + "_video", peer_id + "_fullScreen");
}
// handle kick out button event
handlePeerKickOutBtn(peer_id);
// refresh remote peers avatar name
setPeerAvatarImgName(peer_id + "_avatar", peers[peer_id]["peer_name"]);
// refresh remote peers hand icon status and title
setPeerHandStatus(peer_id, peers[peer_id]["peer_hand"]);
// refresh remote peers video icon status and title
setPeerVideoStatus(peer_id, peers[peer_id]["peer_video"]);
// refresh remote peers audio icon status and title
setPeerAudioStatus(peer_id, peers[peer_id]["peer_audio"]);
// show status menu
toggleClassElements("statusMenu", "inline");
}
};
if (useRTCDataChannel) {
/**
* Secure Data Channel (production mode)
* https://developer.mozilla.org/en-US/docs/Web/API/RTCDataChannel
* https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/ondatachannel
* https://developer.mozilla.org/en-US/docs/Web/API/RTCDataChannel/onmessage
*/
peerConnections[peer_id].ondatachannel = function (event) {
console.log("Datachannel event " + peer_id, event);
event.channel.onmessage = (msg) => {
switch (event.channel.label) {
case "mirotalk_chat_channel":
let dataMessage = {};
try {
dataMessage = JSON.parse(msg.data);
handleDataChannelChat(dataMessage);
} catch (err) {
console.log(err);
}
break;
case "mirotalk_file_sharing_channel":
handleDataChannelFileSharing(msg.data);
break;
}
};
};
// https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/createDataChannel
createChatDataChannel(peer_id);
createFileSharingDataChannel(peer_id);
// ...
} else {
// show chat messages dev mode
signalingSocket.on("onMessage", function (config) {
console.log("Receive msg", { msg: config.msg });
if (!isChatRoomVisible) {
showChatRoomDraggable();
chatRoomBtn.className = "fas fa-comment-slash";
}
playSound("newMessage");
setPeerChatAvatarImgName("left", config.name);
appendMessage(
config.name,
leftChatAvatar,
"left",
config.msg,
config.privateMsg
);
});
}
/**
* peerConnections[peer_id].addStream(localMediaStream); // no longer raccomanded
* https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/addStream
* https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/addTrack
*/
localMediaStream.getTracks().forEach(function (track) {
peerConnections[peer_id].addTrack(track, localMediaStream);
});
/**
* Only one side of the peer connection should create the
* offer, the signaling server picks one to be the offerer.
* The other user will get a 'sessionDescription' event and will
* create an offer, then send back an answer 'sessionDescription' to us
*/
if (config.should_create_offer) {
console.log("Creating RTC offer to", peer_id);
// https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/createOffer
peerConnections[peer_id]
.createOffer()
.then(function (local_description) {
console.log("Local offer description is", local_description);
// https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/setLocalDescription
peerConnections[peer_id]
.setLocalDescription(local_description)
.then(function () {
signalingSocket.emit("relaySDP", {
peer_id: peer_id,
session_description: local_description,
});
console.log("Offer setLocalDescription done!");
})
.catch((e) => {
console.error("[Error] offer setLocalDescription", e);
userLog(
"error",
"Offer setLocalDescription failed: " + e.message
);
});
})
.catch((e) => {
console.error("[Error] sending offer", e);
});
} // end [if offer true]
}); // end [addPeer]
/**
* Peers exchange session descriptions which contains information
* about their audio / video settings and that sort of stuff. First
* the 'offerer' sends a description to the 'answerer' (with type
* "offer"), then the answerer sends one back (with type "answer").
*/
signalingSocket.on("sessionDescription", function (config) {
console.log("Remote Session-description", config);
var peer_id = config.peer_id;
var remote_description = config.session_description;
// https://developer.mozilla.org/en-US/docs/Web/API/RTCSessionDescription
var description = new RTCSessionDescription(remote_description);
// https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/setRemoteDescription
peerConnections[peer_id]
.setRemoteDescription(description)
.then(function () {
console.log("setRemoteDescription done!");
if (remote_description.type == "offer") {
console.log("Creating answer");
// https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/createAnswer
peerConnections[peer_id]
.createAnswer()
.then(function (local_description) {
console.log("Answer description is: ", local_description);
// https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/setLocalDescription
peerConnections[peer_id]
.setLocalDescription(local_description)
.then(function () {
signalingSocket.emit("relaySDP", {
peer_id: peer_id,
session_description: local_description,
});
console.log("Answer setLocalDescription done!");
})
.catch((e) => {
console.error("[Error] answer setLocalDescription", e);
userLog(
"error",
"Answer setLocalDescription failed: " + e.message
);
});
})
.catch((e) => {
console.error("[Error] creating answer", e);
});
} // end [if type offer]
})
.catch((e) => {
console.error("[Error] setRemoteDescription", e);
});
}); // end [sessionDescription]
/**
* The offerer will send a number of ICE Candidate blobs to the answerer so they
* can begin trying to find the best path to one another on the net.
*/
signalingSocket.on("iceCandidate", function (config) {
var peer_id = config.peer_id;
var ice_candidate = config.ice_candidate;
// https://developer.mozilla.org/en-US/docs/Web/API/RTCIceCandidate
peerConnections[peer_id]
.addIceCandidate(new RTCIceCandidate(ice_candidate))
.catch((e) => {
console.error("[Error] addIceCandidate", e);
});
});
// refresh peers name
signalingSocket.on("onCName", function (config) {
appendPeerName(config.peer_id, config.peer_name);
});
// refresh peers video - audio - hand icon status and title
signalingSocket.on("onpeerStatus", function (config) {
var peer_id = config.peer_id;
var element = config.element;
var status = config.status;
switch (element) {
case "video":
setPeerVideoStatus(peer_id, status);
break;
case "audio":
setPeerAudioStatus(peer_id, status);
break;
case "hand":
setPeerHandStatus(peer_id, status);
break;
}
});
// set my Audio off
signalingSocket.on("onmuteEveryone", function (config) {
setMyAudioOff(config.peer_name);
});
// set my Video off
signalingSocket.on("onhideEveryone", function (config) {
setMyVideoOff(config.peer_name);
});
// kick out
signalingSocket.on("onKickOut", function (config) {
kickedOut(config.peer_name);
});
// get file info
signalingSocket.on("onFileInfo", function (data) {
startDownload(data);
});
/**
* When a user leaves a channel (or is disconnected from the
* signaling server) everyone will recieve a 'removePeer' message
* telling them to trash the media channels they have open for those
* that peer. If it was this client that left a channel, they'll also
* receive the removePeers. If this client was disconnected, they
* wont receive removePeers, but rather the
* signaling_socket.on('disconnect') code will kick in and tear down
* all the peer sessions.
*/
signalingSocket.on("removePeer", function (config) {
console.log("Signaling server said to remove peer:", config);
var peer_id = config.peer_id;
if (peer_id in peerMediaElements) {
document.body.removeChild(peerMediaElements[peer_id].parentNode);
resizeVideos();
}
if (peer_id in peerConnections) {
peerConnections[peer_id].close();
}
msgerRemovePeer(peer_id);
if (useRTCDataChannel) delete chatDataChannels[peer_id];
delete fileSharingDataChannels[peer_id];
delete peerConnections[peer_id];
delete peerMediaElements[peer_id];
playSound("removePeer");
});
} // end [initPeer]
/**
* Set mirotalk theme neon - dark - ghost
* @param {*} theme
*/
function setTheme(theme) {
if (!theme) return;
mirotalkTheme = theme;
switch (mirotalkTheme) {
case "neon":
// neon theme
swalBackground = "rgba(0, 0, 0, 0.7)";
document.documentElement.style.setProperty("--body-bg", "black");
document.documentElement.style.setProperty("--msger-bg", "black");
document.documentElement.style.setProperty("--msger-private-bg", "black");
document.documentElement.style.setProperty("--left-msg-bg", "#da05f3");
document.documentElement.style.setProperty("--private-msg-bg", "#f77070");
document.documentElement.style.setProperty("--right-msg-bg", "#579ffb");
document.documentElement.style.setProperty("--wb-bg", "#ffffff");
document.documentElement.style.setProperty("--wb-btn-color", "black");
document.documentElement.style.setProperty("--btn-bg", "white");
document.documentElement.style.setProperty("--btn-color", "black");
document.documentElement.style.setProperty("--btn-opc", "1");
document.documentElement.style.setProperty("--btns-left", "20px");
document.documentElement.style.setProperty(
"--my-settings-label-color",
"limegreen"
);
document.documentElement.style.setProperty(
"--box-shadow",
"3px 3px 6px #0500ff, -3px -3px 6px #da05f3"
);
break;
case "dark":
// dark theme
swalBackground = "rgba(0, 0, 0, 0.7)";
document.documentElement.style.setProperty("--body-bg", "#16171b");
document.documentElement.style.setProperty("--msger-bg", "#16171b");
document.documentElement.style.setProperty(
"--msger-private-bg",
"#16171b"
);
document.documentElement.style.setProperty("--left-msg-bg", "#222328");
document.documentElement.style.setProperty("--private-msg-bg", "#f77070");
document.documentElement.style.setProperty("--right-msg-bg", "#0a0b0c");
document.documentElement.style.setProperty("--wb-bg", "#000000");
document.documentElement.style.setProperty("--wb-btn-color", "white");
document.documentElement.style.setProperty("--btn-bg", "white");
document.documentElement.style.setProperty("--btn-color", "black");
document.documentElement.style.setProperty("--btn-opc", "1");
document.documentElement.style.setProperty("--btns-left", "20px");
document.documentElement.style.setProperty(
"--my-settings-label-color",
"limegreen"
);
document.documentElement.style.setProperty(
"--box-shadow",
"3px 3px 6px #0a0b0c, -3px -3px 6px #222328"
);
break;
case "forest":
// forest theme
swalBackground = "rgba(0, 0, 0, 0.7)";
document.documentElement.style.setProperty("--body-bg", "black");
document.documentElement.style.setProperty("--msger-bg", "black");
document.documentElement.style.setProperty("--msger-private-bg", "black");
document.documentElement.style.setProperty("--left-msg-bg", "#2e3500");
document.documentElement.style.setProperty("--private-msg-bg", "#f77070");
document.documentElement.style.setProperty("--right-msg-bg", "#004b1c");
document.documentElement.style.setProperty("--wb-bg", "#ffffff");
document.documentElement.style.setProperty("--wb-btn-color", "black");
document.documentElement.style.setProperty("--btn-bg", "white");
document.documentElement.style.setProperty("--btn-color", "black");
document.documentElement.style.setProperty("--btn-opc", "1");
document.documentElement.style.setProperty("--btns-left", "20px");
document.documentElement.style.setProperty(
"--my-settings-label-color",
"limegreen"
);
document.documentElement.style.setProperty(
"--box-shadow",
"3px 3px 6px #27944f, -3px -3px 6px #14843d"
);
break;
case "sky":
// sky theme
swalBackground = "rgba(0, 0, 0, 0.7)";
document.documentElement.style.setProperty("--body-bg", "black");
document.documentElement.style.setProperty("--msger-bg", "black");
document.documentElement.style.setProperty("--msger-private-bg", "black");
document.documentElement.style.setProperty("--left-msg-bg", "#0c95b7");
document.documentElement.style.setProperty("--private-msg-bg", "#f77070");
document.documentElement.style.setProperty("--right-msg-bg", "#012a5f");
document.documentElement.style.setProperty("--wb-bg", "#ffffff");
document.documentElement.style.setProperty("--wb-btn-color", "black");
document.documentElement.style.setProperty("--btn-bg", "white");
document.documentElement.style.setProperty("--btn-color", "black");
document.documentElement.style.setProperty("--btn-opc", "1");
document.documentElement.style.setProperty("--btns-left", "20px");
document.documentElement.style.setProperty(
"--my-settings-label-color",
"#03a5ce"
);
document.documentElement.style.setProperty(
"--box-shadow",
"3px 3px 6px #03a5ce, -3px -3px 6px #03a5ce"
);
break;
case "ghost":
// ghost theme
swalBackground = "rgba(0, 0, 0, 0.150)";
document.documentElement.style.setProperty("--body-bg", "black");
document.documentElement.style.setProperty("--msger-bg", "transparent");
document.documentElement.style.setProperty("--msger-private-bg", "black");
document.documentElement.style.setProperty("--wb-bg", "#ffffff");
document.documentElement.style.setProperty("--wb-btn-color", "black");
document.documentElement.style.setProperty("--btn-bg", "transparent");
document.documentElement.style.setProperty("--btn-color", "white");
document.documentElement.style.setProperty("--btn-opc", "0.7");
document.documentElement.style.setProperty("--btns-left", "20px");
document.documentElement.style.setProperty("--box-shadow", "0px");
document.documentElement.style.setProperty(
"--my-settings-label-color",
"limegreen"
);
document.documentElement.style.setProperty(
"--left-msg-bg",
"rgba(0, 0, 0, 0.7)"
);
document.documentElement.style.setProperty(
"--private-msg-bg",
"rgba(252, 110, 110, 0.7)"
);
document.documentElement.style.setProperty(
"--right-msg-bg",
"rgba(0, 0, 0, 0.7)"
);
break;
// ...
default:
console.log("No theme found");
}
}
/**
* Setup local media stuff
* @param {*} callback
* @param {*} errorback
*/
function setupLocalMedia(callback, errorback) {
// if we've already been initialized do nothing
if (localMediaStream != null) {
if (callback) callback();
return;
}
getPeerGeoLocation();
/**
* Ask user for permission to use the computers microphone and/or camera,
* attach it to an <audio> or <video> tag if they give us access.
* https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia
*/
console.log("Requesting access to local audio / video inputs");
const constraints = {
audio: useAudio,
video: useVideo,
};
navigator.mediaDevices
.getUserMedia(constraints)
.then(function (stream) {
console.log("Access granted to audio/video");
// hide img bg and loading div
document.body.style.backgroundImage = "none";
getId("loadingDiv").style.display = "none";
localMediaStream = stream;
const videoWrap = document.createElement("div");
// handle my peer name video audio status
const myStatusMenu = document.createElement("div");
const myCountTime = document.createElement("p");
const myVideoParagraph = document.createElement("h4");
const myHandStatusIcon = document.createElement("button");
const myVideoStatusIcon = document.createElement("button");
const myAudioStatusIcon = document.createElement("button");
const myVideoFullScreenBtn = document.createElement("button");
const myVideoAvatarImage = document.createElement("img");
// menu Status
myStatusMenu.setAttribute("id", "myStatusMenu");
myStatusMenu.className = "statusMenu";
// session time
myCountTime.setAttribute("id", "countTime");
tippy(myCountTime, {
content: "Session Time",
});
// my peer name
myVideoParagraph.setAttribute("id", "myVideoParagraph");
myVideoParagraph.className = "videoPeerName";
tippy(myVideoParagraph, {
content: "My name",
});
// my hand status element
myHandStatusIcon.setAttribute("id", "myHandStatusIcon");
myHandStatusIcon.className = "fas fa-hand-paper pulsate";
myHandStatusIcon.style.setProperty("color", "rgb(0, 255, 0)");
tippy(myHandStatusIcon, {
content: "My hand is RAISED",
});
// my video status element
myVideoStatusIcon.setAttribute("id", "myVideoStatusIcon");
myVideoStatusIcon.className = "fas fa-video";
tippy(myVideoStatusIcon, {
content: "My video is ON",
});
// my audio status element
myAudioStatusIcon.setAttribute("id", "myAudioStatusIcon");
myAudioStatusIcon.className = "fas fa-microphone";
tippy(myAudioStatusIcon, {
content: "My audio is ON",
});
// my video full screen mode
myVideoFullScreenBtn.setAttribute("id", "myVideoFullScreenBtn");
myVideoFullScreenBtn.className = "fas fa-expand";
tippy(myVideoFullScreenBtn, {
content: "Full screen mode",
});
// my video avatar image
myVideoAvatarImage.setAttribute("id", "myVideoAvatarImage");
myVideoAvatarImage.className = "videoAvatarImage pulsate";
// add elements to myStatusMenu div
myStatusMenu.appendChild(myCountTime);
myStatusMenu.appendChild(myVideoParagraph);
myStatusMenu.appendChild(myHandStatusIcon);
myStatusMenu.appendChild(myVideoStatusIcon);
myStatusMenu.appendChild(myAudioStatusIcon);
myStatusMenu.appendChild(myVideoFullScreenBtn);
// add elements to video wrap div
videoWrap.appendChild(myStatusMenu);
videoWrap.appendChild(myVideoAvatarImage);
// hand display none on default menad is raised == false
myHandStatusIcon.style.display = "none";
const localMedia = document.createElement("video");
videoWrap.className = "video";
videoWrap.setAttribute("id", "myVideoWrap");
videoWrap.appendChild(localMedia);
localMedia.setAttribute("id", "myVideo");
localMedia.setAttribute("playsinline", true);
localMedia.className = "mirror";
localMedia.autoplay = true;
localMedia.muted = true;
localMedia.volume = 0;
localMedia.controls = false;
document.body.appendChild(videoWrap);
console.log("local-video-audio", {
video: localMediaStream.getVideoTracks()[0].label,
audio: localMediaStream.getAudioTracks()[0].label,
});
// attachMediaStream is a part of the adapter.js library
attachMediaStream(localMedia, localMediaStream);
resizeVideos();
getHtmlElementsById();
setButtonsTitle();
manageLeftButtons();
handleBodyOnMouseMove();
setupMySettings();
startCountTime();
// on click go on Full Screen mode - back
if (!isMobileDevice) {
handleVideoPlayerFs("myVideo", "myVideoFullScreenBtn");
}
if (callback) callback();
})
.catch((e) => {
// user denied access to audio/video
// https://blog.addpipe.com/common-getusermedia-errors/
console.error("Access denied for audio/video", e);
playSound("error");
window.location.href = `/permission?roomId=${roomId}&getUserMediaError=${e.message}`;
if (errorback) errorback();
});
} // end [setup_local_stream]
/**
* Resize video elements
*/
function resizeVideos() {
const numToString = ["", "one", "two", "three", "four", "five", "six"];
const videos = document.querySelectorAll(".video");
document.querySelectorAll(".video").forEach((v) => {
v.className = "video " + numToString[videos.length];
});
}
/**
* Handle peer kick out event button
* @param {*} peer_id
*/
function handlePeerKickOutBtn(peer_id) {
var peerKickOutBtn = getId(peer_id + "_kickOut");
peerKickOutBtn.addEventListener("click", (e) => {
kickOut(peer_id, peerKickOutBtn);
});
}
/**
* Refresh video - chat image avatar on name changes
* https://eu.ui-avatars.com/
*
* @param {*} videoAvatarImageId element
* @param {*} peerName
*/
function setPeerAvatarImgName(videoAvatarImageId, peerName) {
var videoAvatarImageElement = getId(videoAvatarImageId);
// default img size 64 max 512
var avatarImgSize = isMobileDevice ? 128 : 256;
videoAvatarImageElement.setAttribute(
"src",
avatarApiUrl +
"?name=" +
peerName +
"&size=" +
avatarImgSize +
"&background=random&rounded=true"
);
}
/**
* Set Chat avatar image by peer name
* @param {*} avatar left/right
* @param {*} peerName my/friends
*/
function setPeerChatAvatarImgName(avatar, peerName) {
let avatarImg =
avatarApiUrl +
"?name=" +
peerName +
"&size=32" +
"&background=random&rounded=true";
switch (avatar) {
case "left":
// console.log("Set Friend chat avatar image");
leftChatAvatar = avatarImg;
break;
case "right":
// console.log("Set My chat avatar image");
rightChatAvatar = avatarImg;
break;
}
}
/**
* On video player click, go on full screen mode ||
* On button click, go on full screen mode.
* Press Esc to exit from full screen mode, or click again.
* @param {*} videoId
* @param {*} videoFullScreenBtnId
*/
function handleVideoPlayerFs(videoId, videoFullScreenBtnId) {
var videoPlayer = getId(videoId);
var videoFullScreenBtn = getId(videoFullScreenBtnId);
// handle Chrome Firefox Opera Microsoft Edge videoPlayer ESC
videoPlayer.addEventListener("fullscreenchange", function (e) {
// if Controls enabled, or document on FS do nothing
if (videoPlayer.controls || isDocumentOnFullScreen) return;
var fullscreenElement = document.fullscreenElement;
if (!fullscreenElement) {
videoPlayer.style.pointerEvents = "auto";
isVideoOnFullScreen = false;
// console.log("Esc FS isVideoOnFullScreen", isVideoOnFullScreen);
}
});
// handle Safari videoPlayer ESC
videoPlayer.addEventListener("webkitfullscreenchange", function () {
// if Controls enabled, or document on FS do nothing
if (videoPlayer.controls || isDocumentOnFullScreen) return;
var webkitIsFullScreen = document.webkitIsFullScreen;
if (!webkitIsFullScreen) {
videoPlayer.style.pointerEvents = "auto";
isVideoOnFullScreen = false;
// console.log("Esc FS isVideoOnFullScreen", isVideoOnFullScreen);
}
});
// on button click go on FS
videoFullScreenBtn.addEventListener("click", (e) => {
handleFSVideo();
});
// on video click go on FS
videoPlayer.addEventListener("click", (e) => {
handleFSVideo();
});
function handleFSVideo() {
// if Controls enabled, or document on FS do nothing
if (videoPlayer.controls || isDocumentOnFullScreen) return;
if (!isVideoOnFullScreen) {
if (videoPlayer.requestFullscreen) {
// Chrome Firefox Opera Microsoft Edge
videoPlayer.requestFullscreen();
} else if (videoPlayer.webkitRequestFullscreen) {
// Safari request full screen mode
videoPlayer.webkitRequestFullscreen();
} else if (videoPlayer.msRequestFullscreen) {
// IE11 request full screen mode
videoPlayer.msRequestFullscreen();
}
isVideoOnFullScreen = true;
videoPlayer.style.pointerEvents = "none";
// console.log("Go on FS isVideoOnFullScreen", isVideoOnFullScreen);
} else {
if (document.exitFullscreen) {
// Chrome Firefox Opera Microsoft Edge
document.exitFullscreen();
} else if (document.webkitCancelFullScreen) {
// Safari exit full screen mode ( Not work... )
document.webkitCancelFullScreen();
} else if (document.msExitFullscreen) {
// IE11 exit full screen mode
document.msExitFullscreen();
}
isVideoOnFullScreen = false;
videoPlayer.style.pointerEvents = "auto";
// console.log("Esc FS isVideoOnFullScreen", isVideoOnFullScreen);
}
}
}
/**
* Start talk time
*/
function startCountTime() {
countTime.style.display = "inline";
callStartTime = Date.now();
setInterval(function printTime() {
callElapsedTime = Date.now() - callStartTime;
countTime.innerHTML = getTimeToString(callElapsedTime);
}, 1000);
}
/**
* Start recording time
*/
function startRecordingTime() {
recStartTime = Date.now();
var rc = setInterval(function printTime() {
if (isStreamRecording) {
recElapsedTime = Date.now() - recStartTime;
myVideoParagraph.innerHTML =
myPeerName + " 🔴 REC " + getTimeToString(recElapsedTime);
return;
}
clearInterval(rc);
}, 1000);
}
/**
* Return time to string
* @param {*} time
*/
function getTimeToString(time) {
let diffInHrs = time / 3600000;
let hh = Math.floor(diffInHrs);
let diffInMin = (diffInHrs - hh) * 60;
let mm = Math.floor(diffInMin);
let diffInSec = (diffInMin - mm) * 60;
let ss = Math.floor(diffInSec);
let formattedHH = hh.toString().padStart(2, "0");
let formattedMM = mm.toString().padStart(2, "0");
let formattedSS = ss.toString().padStart(2, "0");
return `${formattedHH}:${formattedMM}:${formattedSS}`;
}
/**
* Handle WebRTC left buttons
*/
function manageLeftButtons() {
setShareRoomBtn();
setAudioBtn();
setVideoBtn();
setSwapCameraBtn();
setScreenShareBtn();
setRecordStreamBtn();
setFullScreenBtn();
setChatRoomBtn();
setChatEmojiBtn();
setMyHandBtn();
setMyWhiteboardBtn();
setMyFileShareBtn();
setMySettingsBtn();
setAboutBtn();
setLeaveRoomBtn();
showLeftButtonsAndMenu();
}
/**
* Copy - share room url button click event
*/
function setShareRoomBtn() {
shareRoomBtn.addEventListener("click", async (e) => {
shareRoomUrl();
});
}
/**
* Audio mute - unmute button click event
*/
function setAudioBtn() {
audioBtn.addEventListener("click", (e) => {
handleAudio(e, false);
});
}
/**
* Video hide - show button click event
*/
function setVideoBtn() {
videoBtn.addEventListener("click", (e) => {
handleVideo(e, false);
});
}
/**
* Check if can swap or not cam,
* if yes show the button else hide it
*/
function setSwapCameraBtn() {
navigator.mediaDevices.enumerateDevices().then((devices) => {
const videoInput = devices.filter((device) => device.kind === "videoinput");
if (videoInput.length > 1 && isMobileDevice) {
// swap camera front - rear button click event for mobile
swapCameraBtn.addEventListener("click", (e) => {
swapCamera();
});
} else {
swapCameraBtn.style.display = "none";
}
});
}
/**
* Check if can share a screen,
* if yes show button else hide it
*/
function setScreenShareBtn() {
if (navigator.getDisplayMedia || navigator.mediaDevices.getDisplayMedia) {
// share screen on - off button click event
screenShareBtn.addEventListener("click", (e) => {
toggleScreenSharing();
});
} else {
screenShareBtn.style.display = "none";
}
}
/**
* Start - Stop Stream recording
*/
function setRecordStreamBtn() {
recordStreamBtn.addEventListener("click", (e) => {
if (isStreamRecording) {
playSound("recStop");
stopStreamRecording();
} else {
playSound("recStart");
startStreamRecording();
}
});
}
/**
* Full screen button click event
*/
function setFullScreenBtn() {
if (DetectRTC.browser.name != "Safari") {
// detect esc from full screen mode
document.addEventListener("fullscreenchange", function (e) {
var fullscreenElement = document.fullscreenElement;
if (!fullscreenElement) {
fullScreenBtn.className = "fas fa-expand-alt";
isDocumentOnFullScreen = false;
// only for desktop
if (!isMobileDevice) {
tippy(fullScreenBtn, {
content: "VIEW full screen",
placement: "right-start",
});
}
}
});
fullScreenBtn.addEventListener("click", (e) => {
toggleFullScreen();
});
} else {
fullScreenBtn.style.display = "none";
}
}
/**
* Chat room buttons click event
*/
function setChatRoomBtn() {
// adapt chat room for mobile
setChatRoomForMobile();
// open hide chat room
chatRoomBtn.addEventListener("click", (e) => {
if (!isChatRoomVisible) {
showChatRoomDraggable();
} else {
hideChatRoomAndEmojiPicker();
e.target.className = "fas fa-comment";
}
});
// ghost theme + undo
msgerTheme.addEventListener("click", (e) => {
if (mirotalkTheme == "ghost") return;
if (e.target.className == "fas fa-ghost") {
e.target.className = "fas fa-undo";
document.documentElement.style.setProperty("--msger-bg", "transparent");
document.documentElement.style.setProperty("--msger-private-bg", "black");
} else {
e.target.className = "fas fa-ghost";
mirotalkTheme == "dark"
? document.documentElement.style.setProperty("--msger-bg", "#16171b")
: document.documentElement.style.setProperty("--msger-bg", "black");
}
});
// show msger participants section
msgerCPBtn.addEventListener("click", (e) => {
if (!thereIsPeerConnections()) {
userLog("info", "No participants detected");
return;
}
msgerCP.style.display = "flex";
});
// hide msger participants section
msgerCPCloseBtn.addEventListener("click", (e) => {
msgerCP.style.display = "none";
});
// clean chat messages
msgerClean.addEventListener("click", (e) => {
cleanMessages();
});
// save chat messages to file
msgerSaveBtn.addEventListener("click", (e) => {
if (chatMessages.length != 0) {
downloadChatMsgs();
return;
}
userLog("info", "No chat messages to save");
});
// close chat room - show left button and status menu if hide
msgerClose.addEventListener("click", (e) => {
hideChatRoomAndEmojiPicker();
showLeftButtonsAndMenu();
});
// Execute a function when the user releases a key on the keyboard
msgerInput.addEventListener("keyup", function (e) {
// Number 13 is the "Enter" key on the keyboard
if (e.keyCode === 13) {
e.preventDefault();
msgerSendBtn.click();
}
});
// on input check 4emoji from map
msgerInput.oninput = function () {
for (var i in chatInputEmoji) {
var regex = new RegExp(escapeSpecialChars(i), "gim");
this.value = this.value.replace(regex, chatInputEmoji[i]);
}
};
// chat send msg
msgerSendBtn.addEventListener("click", (e) => {
// prevent refresh page
e.preventDefault();
if (!thereIsPeerConnections()) {
userLog("info", "Can't send message, no peer connection detected");
msgerInput.value = "";
return;
}
const msg = msgerInput.value;
// empity msg
if (!msg) return;
emitMsg(myPeerName, "toAll", msg, false, "");
appendMessage(myPeerName, rightChatAvatar, "right", msg, false);
msgerInput.value = "";
});
}
/**
* Emoji picker chat room button click event
*/
function setChatEmojiBtn() {
if (isMobileDevice) {
// mobile already have it
msgerEmojiBtn.style.display = "none";
} else {
// make emoji picker draggable for desktop
dragElement(msgerEmojiPicker, msgerEmojiHeader);
msgerEmojiBtn.addEventListener("click", (e) => {
// prevent refresh page
e.preventDefault();
hideShowEmojiPicker();
});
msgerCloseEmojiBtn.addEventListener("click", (e) => {
// prevent refresh page
e.preventDefault();
hideShowEmojiPicker();
});
emojiPicker.addEventListener("emoji-click", (e) => {
//console.log(e.detail);
//console.log(e.detail.emoji.unicode);
msgerInput.value += e.detail.emoji.unicode;
});
}
}
/**
* Set my hand button click event
*/
function setMyHandBtn() {
myHandBtn.addEventListener("click", async (e) => {
setMyHandStatus();
});
}
/**
* Whiteboard
* https://r8.whiteboardfox.com (good alternative)
*/
function setMyWhiteboardBtn() {
// not supported for mobile
if (isMobileDevice) {
whiteboardBtn.style.display = "none";
return;
}
setupCanvas();
whiteboardBtn.addEventListener("click", (e) => {
if (isWhiteboardVisible) {
whiteboardCont.style.display = "none";
isWhiteboardVisible = false;
} else {
whiteboardCont.style.display = "block";
isWhiteboardVisible = true;
// default web write size
drawsize = 3;
}
fitToContainer(canvas);
});
// close whiteboard
whiteboardCloseBtn.addEventListener("click", (e) => {
whiteboardCont.style.display = "none";
isWhiteboardVisible = false;
});
// erase whiteboard
whiteboardEraserBtn.addEventListener("click", (e) => {
setEraser();
});
// clean whiteboard
whiteboardCleanBtn.addEventListener("click", (e) => {
cleanBoard();
});
}
/**
* File Transfer button event click
* https://fromsmash.com for Big Data
*/
function setMyFileShareBtn() {
fileShareBtn.addEventListener("click", (e) => {
//window.open("https://fromsmash.com");
selectFileToShare();
});
sendAbortBtn.addEventListener("click", (e) => {
abortFileTransfer();
});
}
/**
* My settings button click event
*/
function setMySettingsBtn() {
mySettingsBtn.addEventListener("click", (e) => {
if (isMobileDevice) {
leftButtons.style.display = "none";
isButtonsVisible = false;
}
hideShowMySettings();
});
mySettingsCloseBtn.addEventListener("click", (e) => {
hideShowMySettings();
});
myPeerNameSetBtn.addEventListener("click", (e) => {
updateMyPeerName();
});
if (!isMobileDevice) {
// make chat room draggable for desktop
dragElement(mySettings, mySettingsHeader);
}
}
/**
* About button click event
*/
function setAboutBtn() {
aboutBtn.addEventListener("click", (e) => {
getAbout();
});
}
/**
* Leave room button click event
*/
function setLeaveRoomBtn() {
leaveRoomBtn.addEventListener("click", (e) => {
leaveRoom();
});
}
/**
* Handle left buttons - status menù show - hide on body mouse move
*/
function handleBodyOnMouseMove() {
document.body.addEventListener("mousemove", (e) => {
showLeftButtonsAndMenu();
});
}
/**
* Setup local audio - video devices - theme ...
*/
function setupMySettings() {
// audio - video select box
selectors = [audioInputSelect, audioOutputSelect, videoSelect];
audioOutputSelect.disabled = !("sinkId" in HTMLMediaElement.prototype);
navigator.mediaDevices.enumerateDevices().then(gotDevices).catch(handleError);
audioInputSelect.addEventListener("change", (e) => {
myVideoChange = false;
refreshLocalMedia();
});
audioOutputSelect.addEventListener("change", (e) => {
changeAudioDestination();
});
videoSelect.addEventListener("change", (e) => {
if (isMobileDevice) myVideoChange = true;
refreshLocalMedia();
});
themeSelect.addEventListener("change", (e) => {
setTheme(themeSelect.value);
setRecordButtonUi();
setWhiteboardBgandColors();
});
// room actions
muteEveryoneBtn.addEventListener("click", (e) => {
disableAllPeers("audio");
});
hideEveryoneBtn.addEventListener("click", (e) => {
disableAllPeers("video");
});
}
/**
* Refresh Local media audio video in - out
*/
function refreshLocalMedia() {
const audioSource = audioInputSelect.value;
const videoSource = videoSelect.value;
const constraints = {
audio: { deviceId: audioSource ? { exact: audioSource } : undefined },
video: { deviceId: videoSource ? { exact: videoSource } : undefined },
};
navigator.mediaDevices
.getUserMedia(constraints)
.then(gotStream)
.then(gotDevices)
.catch(handleError);
}
/**
* Change Audio Output
*/
function changeAudioDestination() {
const audioDestination = audioOutputSelect.value;
attachSinkId(myVideo, audioDestination);
}
/**
* Attach audio output device to video element using device/sink ID.
* @param {*} element
* @param {*} sinkId
*/
function attachSinkId(element, sinkId) {
if (typeof element.sinkId !== "undefined") {
element
.setSinkId(sinkId)
.then(() => {
console.log(`Success, audio output device attached: ${sinkId}`);
})
.catch((error) => {
let errorMessage = error;
if (error.name === "SecurityError") {
errorMessage = `You need to use HTTPS for selecting audio output device: ${error}`;
}
console.error(errorMessage);
// Jump back to first output device in the list as it's the default.
audioOutputSelect.selectedIndex = 0;
});
} else {
console.warn("Browser does not support output device selection.");
}
}
/**
* Got Stream and append to local media
* @param {*} stream
*/
function gotStream(stream) {
refreshMyStreamToPeers(stream);
refreshMyLocalStream(stream);
setMyVideoStatusTrue();
if (myVideoChange) {
myVideo.classList.toggle("mirror");
}
// Refresh button list in case labels have become available
return navigator.mediaDevices.enumerateDevices();
}
/**
* Get audio-video Devices and show it to select box
* https://github.com/webrtc/samples/tree/gh-pages/src/content/devices/input-output
* @param {*} deviceInfos
*/
function gotDevices(deviceInfos) {
// Handles being called several times to update labels. Preserve values.
const values = selectors.map((select) => select.value);
selectors.forEach((select) => {
while (select.firstChild) {
select.removeChild(select.firstChild);
}
});
// check devices
for (let i = 0; i !== deviceInfos.length; ++i) {
const deviceInfo = deviceInfos[i];
// console.log("device-info ------> ", deviceInfo);
const option = document.createElement("option");
option.value = deviceInfo.deviceId;
if (deviceInfo.kind === "audioinput") {
// audio Input
option.text =
deviceInfo.label || `microphone ${audioInputSelect.length + 1}`;
audioInputSelect.appendChild(option);
} else if (deviceInfo.kind === "audiooutput") {
// audio Output
option.text =
deviceInfo.label || `speaker ${audioOutputSelect.length + 1}`;
audioOutputSelect.appendChild(option);
} else if (deviceInfo.kind === "videoinput") {
// video Input
option.text = deviceInfo.label || `camera ${videoSelect.length + 1}`;
videoSelect.appendChild(option);
} else {
// something else
console.log("Some other kind of source/device: ", deviceInfo);
}
} // end for devices
selectors.forEach((select, selectorIndex) => {
if (
Array.prototype.slice
.call(select.childNodes)
.some((n) => n.value === values[selectorIndex])
) {
select.value = values[selectorIndex];
}
});
}
/**
* Handle getUserMedia error
* @param {*} error
*/
function handleError(error) {
console.log(
"navigator.MediaDevices.getUserMedia error: ",
error.message,
error.name
);
userLog("error", "Something wrong: " + error.message);
}
/**
* AttachMediaStream stream to element
* @param {*} element
* @param {*} stream
*/
function attachMediaStream(element, stream) {
//console.log("DEPRECATED, attachMediaStream will soon be removed.");
console.log("Success, media stream attached");
element.srcObject = stream;
}
/**
* Show left buttons & status menù for 10 seconds on body mousemove
* if mobile and chatroom open do nothing return
* if mobile and mySettings open do nothing return
*/
function showLeftButtonsAndMenu() {
if (
isButtonsVisible ||
(isMobileDevice && isChatRoomVisible) ||
(isMobileDevice && isMySettingsVisible)
) {
return;
}
toggleClassElements("statusMenu", "inline");
leftButtons.style.display = "flex";
isButtonsVisible = true;
setTimeout(function () {
toggleClassElements("statusMenu", "none");
leftButtons.style.display = "none";
isButtonsVisible = false;
}, 10000);
}
/**
* Copy room url to clipboard and share it with navigator share if supported
* https://developer.mozilla.org/en-US/docs/Web/API/Navigator/share
*/
async function shareRoomUrl() {
copyRoomURL();
// navigator share
let isSupportedNavigatorShare = false;
let errorNavigatorShare = false;
// if supported
if (navigator.share) {
isSupportedNavigatorShare = true;
try {
// not add title and description to load metadata from url
await navigator.share({ url: window.location.href });
userLog("info", "Room Shared successfully!");
} catch (error) {
errorNavigatorShare = true;
/* This feature is available only in secure contexts (HTTPS),
in some or all supporting browsers and mobile devices
console.error("navigator.share", error);
*/
}
}
// something wrong or not supported navigator.share
if (
!isSupportedNavigatorShare ||
(isSupportedNavigatorShare && errorNavigatorShare)
) {
playSound("newMessage");
Swal.fire({
allowOutsideClick: false,
background: swalBackground,
position: "center",
title: "Share the Room",
imageAlt: "mirotalk-share",
imageUrl: shareUrlImg,
html:
`
<br/>
<div id="qrRoomContainer">
<canvas id="qrRoom"></canvas>
</div>
<br/><br/>
<p style="color:white;"> Share this meeting invite others to join.</p>
<p style="color:rgb(8, 189, 89);">` +
window.location.href +
`</p>`,
showClass: {
popup: "animate__animated animate__fadeInDown",
},
hideClass: {
popup: "animate__animated animate__fadeOutUp",
},
confirmButtonText: `Copy meeting URL`,
});
makeRoomQR();
}
}
/**
* Make Room QR
* https://github.com/neocotic/qrious
*/
function makeRoomQR() {
var qr = new QRious({
element: getId("qrRoom"),
value: window.location.href,
});
qr.set({
size: 128,
});
}
/**
* Copy Room URL to clipboard
*/
function copyRoomURL() {
// save Room Url to clipboard
var roomURL = window.location.href;
var tmpInput = document.createElement("input");
document.body.appendChild(tmpInput);
tmpInput.value = roomURL;
tmpInput.select();
// for mobile devices
tmpInput.setSelectionRange(0, 99999);
document.execCommand("copy");
console.log("Copied to clipboard Join Link ", roomURL);
document.body.removeChild(tmpInput);
}
/**
* Handle Audio ON-OFF
* @param {*} e event
* @param {*} init bool true/false
*/
function handleAudio(e, init) {
// https://developer.mozilla.org/en-US/docs/Web/API/MediaStream/getAudioTracks
localMediaStream.getAudioTracks()[0].enabled =
!localMediaStream.getAudioTracks()[0].enabled;
myAudioStatus = localMediaStream.getAudioTracks()[0].enabled;
e.target.className = "fas fa-microphone" + (myAudioStatus ? "" : "-slash");
if (init) {
audioBtn.className = "fas fa-microphone" + (myAudioStatus ? "" : "-slash");
if (!isMobileDevice) {
tippy(initAudioBtn, {
content: myAudioStatus ? "Click to audio OFF" : "Click to audio ON",
placement: "top",
});
}
}
setMyAudioStatus(myAudioStatus);
}
/**
* Handle Video ON-OFF
* @param {*} e event
* @param {*} init bool true/false
*/
function handleVideo(e, init) {
// https://developer.mozilla.org/en-US/docs/Web/API/MediaStream/getVideoTracks
localMediaStream.getVideoTracks()[0].enabled =
!localMediaStream.getVideoTracks()[0].enabled;
myVideoStatus = localMediaStream.getVideoTracks()[0].enabled;
e.target.className = "fas fa-video" + (myVideoStatus ? "" : "-slash");
if (init) {
videoBtn.className = "fas fa-video" + (myVideoStatus ? "" : "-slash");
if (!isMobileDevice) {
tippy(initVideoBtn, {
content: myVideoStatus ? "Click to video OFF" : "Click to video ON",
placement: "top",
});
}
}
setMyVideoStatus(myVideoStatus);
}
/**
* SwapCamer front (user) - rear (environment)
*/
function swapCamera() {
// setup camera
camera = camera == "user" ? "environment" : "user";
if (camera == "user") useVideo = true;
else useVideo = { facingMode: { exact: camera } };
// https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia
navigator.mediaDevices
.getUserMedia({ video: useVideo })
.then((camStream) => {
refreshMyStreamToPeers(camStream);
refreshMyLocalStream(camStream);
if (useVideo) {
setMyVideoStatusTrue();
localMediaStream.getVideoTracks()[0].enabled = true;
}
myVideo.classList.toggle("mirror");
})
.catch((e) => {
console.log("[Error] to swaping camera", e);
userLog("error", "Error to swaping the camera: " + e.message);
});
}
/**
* Enable - disable screen sharing
*/
function toggleScreenSharing() {
const constraints = {
video: true,
};
let screenMediaPromise;
if (!isScreenStreaming) {
// on screen sharing start
if (navigator.getDisplayMedia) {
// https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getDisplayMedia
screenMediaPromise = navigator.getDisplayMedia(constraints);
} else if (navigator.mediaDevices.getDisplayMedia) {
screenMediaPromise = navigator.mediaDevices.getDisplayMedia(constraints);
} else {
// https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia
screenMediaPromise = navigator.mediaDevices.getUserMedia({
video: {
mediaSource: "screen",
},
});
}
} else {
// on screen sharing stop
const audioSource = audioInputSelect.value;
const videoSource = videoSelect.value;
const constraints = {
audio: { deviceId: audioSource ? { exact: audioSource } : undefined },
video: { deviceId: videoSource ? { exact: videoSource } : undefined },
};
screenMediaPromise = navigator.mediaDevices.getUserMedia(constraints);
// if screen sharing accidentally closed
if (isStreamRecording) {
stopStreamRecording();
}
}
screenMediaPromise
.then((screenStream) => {
// stop cam video track on screen share
localMediaStream.getVideoTracks()[0].stop();
isScreenStreaming = !isScreenStreaming;
refreshMyStreamToPeers(screenStream);
refreshMyLocalStream(screenStream);
myVideo.classList.toggle("mirror");
setScreenSharingStatus(isScreenStreaming);
//setMyVideoStatusTrue();
})
.catch((e) => {
console.error("[Error] Unable to share the screen", e);
userLog("error", "Unable to share the screen: " + e.message);
});
}
/**
* Set Screen Sharing Status
* @param {*} status
*/
function setScreenSharingStatus(status) {
screenShareBtn.className = status ? "fas fa-stop-circle" : "fas fa-desktop";
// only for desktop
if (!isMobileDevice) {
tippy(screenShareBtn, {
content: status ? "STOP screen sharing" : "START screen sharing",
placement: "right-start",
});
}
}
/**
* set myVideoStatus true
*/
function setMyVideoStatusTrue() {
// Put video status alredy ON
if (myVideoStatus === false) {
myVideoStatus = true;
videoBtn.className = "fas fa-video";
myVideoStatusIcon.className = "fas fa-video";
myVideoAvatarImage.style.display = "none";
emitPeerStatus("video", myVideoStatus);
// only for desktop
if (!isMobileDevice) {
tippy(videoBtn, {
content: "Click to video OFF",
placement: "right-start",
});
}
}
}
/**
* Enter - esc on full screen mode
* https://developer.mozilla.org/en-US/docs/Web/API/Fullscreen_API
*/
function toggleFullScreen() {
if (!document.fullscreenElement) {
document.documentElement.requestFullscreen();
fullScreenBtn.className = "fas fa-compress-alt";
isDocumentOnFullScreen = true;
} else {
if (document.exitFullscreen) {
document.exitFullscreen();
fullScreenBtn.className = "fas fa-expand-alt";
isDocumentOnFullScreen = false;
}
}
// only for desktop
if (!isMobileDevice) {
tippy(fullScreenBtn, {
content: isDocumentOnFullScreen ? "EXIT full screen" : "VIEW full screen",
placement: "right-start",
});
}
}
/**
* Refresh my stream changes to connected peers in the room
* @param {*} stream
*/
function refreshMyStreamToPeers(stream) {
if (thereIsPeerConnections()) {
// refresh my video stream
for (var peer_id in peerConnections) {
// https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/getSenders
var sender = peerConnections[peer_id]
.getSenders()
.find((s) => (s.track ? s.track.kind === "video" : false));
// https://developer.mozilla.org/en-US/docs/Web/API/RTCRtpSender/replaceTrack
sender.replaceTrack(stream.getVideoTracks()[0]);
}
}
}
/**
* Refresh my local stream
* @param {*} stream
*/
function refreshMyLocalStream(stream) {
stream.getVideoTracks()[0].enabled = true;
// https://developer.mozilla.org/en-US/docs/Web/API/MediaStream
const newStream = new MediaStream([
stream.getVideoTracks()[0],
localMediaStream.getAudioTracks()[0],
]);
localMediaStream = newStream;
// attachMediaStream is a part of the adapter.js library
attachMediaStream(myVideo, localMediaStream); // newstream
// on toggleScreenSharing video stop
stream.getVideoTracks()[0].onended = function () {
if (isScreenStreaming) toggleScreenSharing();
};
/** when you stop the screen sharing, on default i turn back to the webcam with video stream ON.
* if you want the webcam with video stream OFF, just disable it with the button (click to video OFF),
* before to stop the screen sharing.
*/
if (myVideoStatus === false) {
localMediaStream.getVideoTracks()[0].enabled = false;
}
}
/**
* recordind stream data
* @param {*} event
*/
function handleDataAvailable(event) {
console.log("handleDataAvailable", event);
if (event.data && event.data.size > 0) {
recordedBlobs.push(event.data);
}
}
/**
* Start Recording
* https://github.com/webrtc/samples/tree/gh-pages/src/content/getusermedia/record
*/
function startStreamRecording() {
recordedBlobs = [];
let options = { mimeType: "video/webm;codecs=vp9,opus" };
if (!MediaRecorder.isTypeSupported(options.mimeType)) {
console.error(`${options.mimeType} is not supported`);
options = { mimeType: "video/webm;codecs=vp8,opus" };
if (!MediaRecorder.isTypeSupported(options.mimeType)) {
console.error(`${options.mimeType} is not supported`);
options = { mimeType: "video/webm" };
if (!MediaRecorder.isTypeSupported(options.mimeType)) {
console.error(`${options.mimeType} is not supported`);
options = { mimeType: "" };
}
}
}
try {
// record only my local Media Stream
mediaRecorder = new MediaRecorder(localMediaStream, options);
} catch (e) {
console.error("Exception while creating MediaRecorder:", e);
userLog("error", "Can't start stream recording: " + e.message);
return;
}
console.log("Created MediaRecorder", mediaRecorder, "with options", options);
mediaRecorder.onstop = (event) => {
console.log("MediaRecorder stopped: ", event);
console.log("MediaRecorder Blobs: ", recordedBlobs);
myVideoParagraph.innerHTML = myPeerName + " (me)";
disableElements(false);
downloadRecordedStream();
// only for desktop
if (!isMobileDevice) {
tippy(recordStreamBtn, {
content: "START recording",
placement: "right-start",
});
}
};
mediaRecorder.ondataavailable = handleDataAvailable;
mediaRecorder.start();
console.log("MediaRecorder started", mediaRecorder);
isStreamRecording = true;
recordStreamBtn.style.setProperty("background-color", "red");
startRecordingTime();
disableElements(true);
// only for desktop
if (!isMobileDevice) {
tippy(recordStreamBtn, {
content: "STOP recording",
placement: "right-start",
});
}
}
/**
* Stop recording
*/
function stopStreamRecording() {
mediaRecorder.stop();
isStreamRecording = false;
setRecordButtonUi();
}
/**
* Set Record Button UI on change theme
*/
function setRecordButtonUi() {
if (mirotalkTheme == "ghost") {
recordStreamBtn.style.setProperty("background-color", "transparent");
} else {
recordStreamBtn.style.setProperty("background-color", "white");
}
}
/**
* Download recorded stream
*/
function downloadRecordedStream() {
const recFileName = getDataTimeString() + "-REC.webm";
const currentDevice = isMobileDevice ? "MOBILE" : "PC";
userLog(
"success",
`Recording file name: ${recFileName}, please wait to be processed, then will be downloaded to your ${currentDevice} device.`
);
const blob = new Blob(recordedBlobs, { type: "video/webm" });
const url = window.URL.createObjectURL(blob);
const a = document.createElement("a");
a.style.display = "none";
a.href = url;
a.download = recFileName;
document.body.appendChild(a);
a.click();
setTimeout(() => {
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
}, 100);
}
/**
* Data Formated DD-MM-YYYY-H_M_S
* https://convertio.co/it/
* @returns data string
*/
function getDataTimeString() {
const d = new Date();
const date = d.toISOString().split("T")[0];
const time = d.toTimeString().split(" ")[0];
return `${date}-${time}`;
}
/**
* Disable - enable some elements on Recording
* I can Record One Media Stream at time
* @param {*} b boolean true/false
*/
function disableElements(b) {
swapCameraBtn.disabled = b;
screenShareBtn.disabled = b;
audioSource.disabled = b;
videoSource.disabled = b;
}
/**
* Set the chat room on full screen mode for mobile
*/
function setChatRoomForMobile() {
if (isMobileDevice) {
document.documentElement.style.setProperty("--msger-height", "99%");
document.documentElement.style.setProperty("--msger-width", "99%");
} else {
// make chat room draggable for desktop
dragElement(msgerDraggable, msgerHeader);
}
}
/**
* Show msger draggable on center screen position
*/
function showChatRoomDraggable() {
playSound("newMessage");
if (isMobileDevice) {
leftButtons.style.display = "none";
isButtonsVisible = false;
}
chatRoomBtn.className = "fas fa-comment-slash";
msgerDraggable.style.top = "50%";
msgerDraggable.style.left = "50%";
msgerDraggable.style.display = "flex";
isChatRoomVisible = true;
// only for desktop
if (!isMobileDevice) {
tippy(chatRoomBtn, {
content: "CLOSE the chat",
placement: "right-start",
});
}
}
/**
* Clean chat messages
* https://sweetalert2.github.io
*/
function cleanMessages() {
Swal.fire({
background: swalBackground,
position: "center",
title: "Clean up chat Messages?",
icon: "warning",
showDenyButton: true,
confirmButtonText: `Yes`,
denyButtonText: `No`,
showClass: {
popup: "animate__animated animate__fadeInDown",
},
hideClass: {
popup: "animate__animated animate__fadeOutUp",
},
}).then((result) => {
// clean chat messages
if (result.isConfirmed) {
var msgs = msgerChat.firstChild;
while (msgs) {
msgerChat.removeChild(msgs);
msgs = msgerChat.firstChild;
}
// clean object
chatMessages = [];
}
});
}
/**
* Hide chat room and emoji picker
*/
function hideChatRoomAndEmojiPicker() {
msgerDraggable.style.display = "none";
msgerEmojiPicker.style.display = "none";
chatRoomBtn.className = "fas fa-comment";
isChatRoomVisible = false;
isChatEmojiVisible = false;
// only for desktop
if (!isMobileDevice) {
tippy(chatRoomBtn, {
content: "OPEN the chat",
placement: "right-start",
});
}
}
/**
* Create Chat Room Data Channel
* @param {*} peer_id
*/
function createChatDataChannel(peer_id) {
chatDataChannels[peer_id] = peerConnections[peer_id].createDataChannel(
"mirotalk_chat_channel"
);
}
/**
* handle Incoming Data Channel Chat Messages
* @param {*} dataMessages
*/
function handleDataChannelChat(dataMessages) {
switch (dataMessages.type) {
case "chat":
// private message but not for me return
if (dataMessages.privateMsg && dataMessages.toName != myPeerName) return;
// log incoming dataMessages json
console.log("handleDataChannelChat", dataMessages);
// chat message for me also
if (!isChatRoomVisible) {
showChatRoomDraggable();
chatRoomBtn.className = "fas fa-comment-slash";
}
playSound("newMessage");
setPeerChatAvatarImgName("left", dataMessages.name);
appendMessage(
dataMessages.name,
leftChatAvatar,
"left",
dataMessages.msg,
dataMessages.privateMsg
);
break;
// .........
default:
break;
}
}
/**
* Escape Special Chars
* @param {*} regex
*/
function escapeSpecialChars(regex) {
return regex.replace(/([()[{*+.$^\\|?])/g, "\\$1");
}
/**
* Append Message to msger chat room
* @param {*} name
* @param {*} img
* @param {*} side
* @param {*} text
* @param {*} privateMsg
*/
function appendMessage(name, img, side, text, privateMsg) {
let time = getFormatDate(new Date());
// collect chat msges to save it later
chatMessages.push({
time: time,
name: name,
text: text,
private_msg: privateMsg,
});
// check if i receive a private message
let msgBubble = privateMsg ? "private-msg-bubble" : "msg-bubble";
// console.log("chatMessages", chatMessages);
let ctext = detectUrl(text);
const msgHTML = `
<div class="msg ${side}-msg">
<div class="msg-img" style="background-image: url('${img}')"></div>
<div class=${msgBubble}>
<div class="msg-info">
<div class="msg-info-name">${name}</div>
<div class="msg-info-time">${time}</div>
</div>
<div class="msg-text">${ctext}</div>
</div>
</div>
`;
msgerChat.insertAdjacentHTML("beforeend", msgHTML);
msgerChat.scrollTop += 500;
}
/**
* Add participants in the chat room lists
* @param {*} peers
*/
function msgerAddPeers(peers) {
// console.log("peers", peers);
// add all current Participants
for (var peer_id in peers) {
var peer_name = peers[peer_id]["peer_name"];
// bypass insert to myself in the list :)
if (peer_name != myPeerName) {
var exsistMsgerPrivateDiv = getId(peer_id + "_pMsgDiv");
// if there isn't add it....
if (!exsistMsgerPrivateDiv) {
var msgerPrivateDiv = `
<div id="${peer_id}_pMsgDiv" class="msger-inputarea">
<input
id="${peer_id}_pMsgInput"
class="msger-input"
type="text"
placeholder="Enter your message..."
/>
<button id="${peer_id}_pMsgBtn" class="fas fa-paper-plane" value="${peer_name}">&nbsp;${peer_name}</button>
</div>
`;
msgerCPList.insertAdjacentHTML("beforeend", msgerPrivateDiv);
msgerCPList.scrollTop += 500;
var msgerPrivateMsgInput = getId(peer_id + "_pMsgInput");
var msgerPrivateBtn = getId(peer_id + "_pMsgBtn");
addMsgerPrivateBtn(msgerPrivateBtn, msgerPrivateMsgInput, peer_id);
}
}
}
}
/**
* Remove participant from chat room lists
* @param {*} peer_id
*/
function msgerRemovePeer(peer_id) {
var msgerPrivateDiv = getId(peer_id + "_pMsgDiv");
if (msgerPrivateDiv) {
var peerToRemove = msgerPrivateDiv.firstChild;
while (peerToRemove) {
msgerPrivateDiv.removeChild(peerToRemove);
peerToRemove = msgerPrivateDiv.firstChild;
}
msgerPrivateDiv.remove();
}
}
/**
* Setup msger buttons to send private messages
* @param {*} msgerPrivateBtn
* @param {*} msgerPrivateMsgInput
* @param {*} peer_id
*/
function addMsgerPrivateBtn(msgerPrivateBtn, msgerPrivateMsgInput, peer_id) {
// add button to send private messages
msgerPrivateBtn.addEventListener("click", (e) => {
e.preventDefault();
var pMsg = msgerPrivateMsgInput.value;
if (!pMsg) return;
var toPeerName = msgerPrivateBtn.value;
// userLog("info", toPeerName + ":" + peer_id);
emitMsg(myPeerName, toPeerName, pMsg, true, peer_id);
appendMessage(
myPeerName,
rightChatAvatar,
"right",
pMsg + "<br/><hr>Private message to " + toPeerName,
true
);
msgerPrivateMsgInput.value = "";
msgerCP.style.display = "none";
});
}
/**
* Detect url from text and make it clickable
* Detect also if url is a img to create preview of it
* @param {*} text
* @returns html
*/
function detectUrl(text) {
var urlRegex = /(https?:\/\/[^\s]+)/g;
return text.replace(urlRegex, function (url) {
if (isImageURL(text)) {
return (
'<p><img src="' + url + '" alt="img" width="200" height="auto"/></p>'
);
}
return (
'<a id="chat-msg-a" href="' + url + '" target="_blank">' + url + "</a>"
);
});
}
/**
* Check if url passed is a image
* @param {*} url
* @returns true/false
*/
function isImageURL(url) {
return url.match(/\.(jpeg|jpg|gif|png|tiff|bmp)$/) != null;
}
/**
* Format data h:m:s
* @param {*} date
*/
function getFormatDate(date) {
const time = date.toTimeString().split(" ")[0];
return `${time}`;
}
/**
* Send message over Secure dataChannels if use useRTCDataChannel(true)
* otherwise over signaling server
* @param {*} name
* @param {*} toName
* @param {*} msg
* @param {*} privateMsg private message true/false
* @param {*} peer_id to sent private message
*/
function emitMsg(name, toName, msg, privateMsg, peer_id) {
if (msg) {
if (useRTCDataChannel) {
const chatMessage = {
type: "chat",
name: name,
toName: toName,
msg: msg,
privateMsg: privateMsg,
};
// peer to peer over DataChannels
Object.keys(chatDataChannels).map((peerId) =>
chatDataChannels[peerId].send(JSON.stringify(chatMessage))
);
} else {
// client over signaling server
signalingSocket.emit("msg", {
peerConnections: peerConnections,
room_id: roomId,
privateMsg: privateMsg,
peer_id: peer_id,
name: name,
msg: msg,
});
}
console.log("Send msg", {
room_id: roomId,
privateMsg: privateMsg,
peer_id: peer_id,
name: name,
msg: msg,
});
}
}
/**
* Hide - Show emoji picker div
*/
function hideShowEmojiPicker() {
if (!isChatEmojiVisible) {
playSound("newMessage");
msgerEmojiPicker.style.display = "block";
isChatEmojiVisible = true;
return;
}
msgerEmojiPicker.style.display = "none";
isChatEmojiVisible = false;
}
/**
* Download Chat messages in json format
* https://developer.mozilla.org/it/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify
*/
function downloadChatMsgs() {
var a = document.createElement("a");
a.href =
"data:text/json;charset=utf-8," +
encodeURIComponent(JSON.stringify(chatMessages, null, 1));
a.download = getDataTimeString() + "-CHAT.txt";
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
}
/**
* Make chat room - devices draggable
* https://www.w3schools.com/howto/howto_js_draggable.asp
* @param {*} elmnt
* @param {*} dragObj
*/
function dragElement(elmnt, dragObj) {
var pos1 = 0,
pos2 = 0,
pos3 = 0,
pos4 = 0;
if (dragObj) {
// if present, the header is where you move the DIV from:
dragObj.onmousedown = dragMouseDown;
} else {
// otherwise, move the DIV from anywhere inside the DIV:
elmnt.onmousedown = dragMouseDown;
}
function dragMouseDown(e) {
e = e || window.event;
e.preventDefault();
// get the mouse cursor position at startup:
pos3 = e.clientX;
pos4 = e.clientY;
document.onmouseup = closeDragElement;
// call a function whenever the cursor moves:
document.onmousemove = elementDrag;
}
function elementDrag(e) {
e = e || window.event;
e.preventDefault();
// calculate the new cursor position:
pos1 = pos3 - e.clientX;
pos2 = pos4 - e.clientY;
pos3 = e.clientX;
pos4 = e.clientY;
// set the element's new position:
elmnt.style.top = elmnt.offsetTop - pos2 + "px";
elmnt.style.left = elmnt.offsetLeft - pos1 + "px";
}
function closeDragElement() {
// stop moving when mouse button is released:
document.onmouseup = null;
document.onmousemove = null;
}
}
/**
* Hide - show my settings
*/
function hideShowMySettings() {
if (!isMySettingsVisible) {
playSound("newMessage");
// adapt it for mobile
if (isMobileDevice) {
mySettings.style.setProperty("width", "90%");
document.documentElement.style.setProperty(
"--mySettings-select-w",
"99%"
);
}
// my current peer name
myPeerNameSet.placeholder = myPeerName;
// center screen on show
mySettings.style.top = "50%";
mySettings.style.left = "50%";
mySettings.style.display = "block";
isMySettingsVisible = true;
return;
}
mySettings.style.display = "none";
isMySettingsVisible = false;
}
/**
* Update myPeerName to other peers in the room
*/
function updateMyPeerName() {
var myNewPeerName = myPeerNameSet.value;
var myOldPeerName = myPeerName;
// myNewPeerName empty
if (!myNewPeerName) return;
myPeerName = myNewPeerName;
myVideoParagraph.innerHTML = myPeerName + " (me)";
signalingSocket.emit("cName", {
peerConnections: peerConnections,
room_id: roomId,
peer_name_old: myOldPeerName,
peer_name_new: myPeerName,
});
myPeerNameSet.value = "";
myPeerNameSet.placeholder = myPeerName;
setPeerAvatarImgName("myVideoAvatarImage", myPeerName);
setPeerChatAvatarImgName("right", myPeerName);
}
/**
* Append updated peer name to video player
* @param {*} id
* @param {*} name
*/
function appendPeerName(id, name) {
var videoName = getId(id + "_name");
if (videoName) {
videoName.innerHTML = name;
}
// change also btn value - name on chat lists....
var msgerPeerName = getId(id + "_pMsgBtn");
if (msgerPeerName) {
msgerPeerName.innerHTML = `&nbsp;${name}`;
msgerPeerName.value = name;
}
// refresh also peer video avatar name
setPeerAvatarImgName(id + "_avatar", name);
}
/**
* Send my Video-Audio-Hand... status
* @param {*} element
* @param {*} status
*/
function emitPeerStatus(element, status) {
signalingSocket.emit("peerStatus", {
peerConnections: peerConnections,
room_id: roomId,
peer_name: myPeerName,
element: element,
status: status,
});
}
/**
* Set my Hand Status and Icon
*/
function setMyHandStatus() {
if (myHandStatus) {
// Raise hand
myHandStatus = false;
if (!isMobileDevice) {
tippy(myHandBtn, {
content: "RAISE your hand",
placement: "right-start",
});
}
} else {
// Lower hand
myHandStatus = true;
if (!isMobileDevice) {
tippy(myHandBtn, {
content: "LOWER your hand",
placement: "right-start",
});
}
playSound("rHand");
}
myHandStatusIcon.style.display = myHandStatus ? "inline" : "none";
emitPeerStatus("hand", myHandStatus);
}
/**
* Set My Audio Status Icon and Title
* @param {*} status
*/
function setMyAudioStatus(status) {
myAudioStatusIcon.className = "fas fa-microphone" + (status ? "" : "-slash");
// send my audio status to all peers in the room
emitPeerStatus("audio", status);
tippy(myAudioStatusIcon, {
content: status ? "My audio is ON" : "My audio is OFF",
});
// only for desktop
if (!isMobileDevice) {
tippy(audioBtn, {
content: status ? "Click to audio OFF" : "Click to audio ON",
placement: "right-start",
});
}
}
/**
* Set My Video Status Icon and Title
* https://atomiks.github.io/tippyjs/
* @param {*} status
*/
function setMyVideoStatus(status) {
// on vdeo OFF display my video avatar name
myVideoAvatarImage.style.display = status ? "none" : "block";
myVideoStatusIcon.className = "fas fa-video" + (status ? "" : "-slash");
// send my video status to all peers in the room
emitPeerStatus("video", status);
tippy(myVideoStatusIcon, {
content: status ? "My video is ON" : "My video is OFF",
});
// only for desktop
if (!isMobileDevice) {
tippy(videoBtn, {
content: status ? "Click to video OFF" : "Click to video ON",
placement: "right-start",
});
}
}
/**
* Set Participant Hand Status Icon and Title
* @param {*} peer_id
* @param {*} status
*/
function setPeerHandStatus(peer_id, status) {
let peerHandStatus = getId(peer_id + "_handStatus");
peerHandStatus.style.display = status ? "block" : "none";
if (status) playSound("rHand");
}
/**
* Set Participant Audio Status Icon and Title
* https://atomiks.github.io/tippyjs/
* @param {*} peer_id
* @param {*} status
*/
function setPeerAudioStatus(peer_id, status) {
let peerAudioStatus = getId(peer_id + "_audioStatus");
peerAudioStatus.className = "fas fa-microphone" + (status ? "" : "-slash");
tippy(peerAudioStatus, {
content: status ? "Participant audio is ON" : "Participant audio is OFF",
});
}
/**
* Set Participant Video Status Icon and Title
* https://atomiks.github.io/tippyjs/
* @param {*} peer_id
* @param {*} status
*/
function setPeerVideoStatus(peer_id, status) {
let peerVideoAvatarImage = getId(peer_id + "_avatar");
let peerVideoStatus = getId(peer_id + "_videoStatus");
peerVideoStatus.className = "fas fa-video" + (status ? "" : "-slash");
peerVideoAvatarImage.style.display = status ? "none" : "block";
tippy(peerVideoStatus, {
content: status ? "Participant video is ON" : "Participant video is OFF",
});
}
/**
* Set whiteboard color
* @param {*} newcolor
*/
function setColor(newcolor) {
color = newcolor;
drawsize = 3;
whiteboardColorPicker.value = color;
}
/**
* Whiteboard eraser
*/
function setEraser() {
mirotalkTheme == "dark" ? (color = "#000000") : (color = "#ffffff");
drawsize = 10;
whiteboardColorPicker.value = color;
}
/**
* Clean whiteboard content
*/
function cleanBoard() {
playSound("newMessage");
Swal.fire({
background: swalBackground,
position: "center",
title: "Clean the board",
text: "Are you sure you want to clean the board?",
showDenyButton: true,
confirmButtonText: `Yes`,
denyButtonText: `No`,
showClass: {
popup: "animate__animated animate__fadeInDown",
},
hideClass: {
popup: "animate__animated animate__fadeOutUp",
},
}).then((result) => {
if (result.isConfirmed) {
ctx.clearRect(0, 0, canvas.width, canvas.height);
}
});
}
/**
* Draw on whiteboard
* @param {*} newx
* @param {*} newy
* @param {*} oldx
* @param {*} oldy
*/
function draw(newx, newy, oldx, oldy) {
ctx.strokeStyle = color;
ctx.lineWidth = drawsize;
ctx.beginPath();
ctx.moveTo(oldx, oldy);
ctx.lineTo(newx, newy);
ctx.stroke();
ctx.closePath();
}
/**
* Resize canvas
* @param {*} canvas
*/
function fitToContainer(canvas) {
canvas.style.width = "100%";
canvas.style.height = "100%";
canvas.width = canvas.offsetWidth;
canvas.height = canvas.offsetHeight;
}
/**
* Handle whiteboard on windows resize
* here i lose drawing, Todo fix it
*/
function reportWindowSize() {
fitToContainer(canvas);
}
/**
* Whiteboard setup
*/
function setupCanvas() {
fitToContainer(canvas);
canvas.addEventListener("mousedown", (e) => {
x = e.offsetX;
y = e.offsetY;
isDrawing = 1;
});
canvas.addEventListener("mousemove", (e) => {
if (isDrawing) {
draw(e.offsetX, e.offsetY, x, y);
x = e.offsetX;
y = e.offsetY;
}
});
window.addEventListener("mouseup", (e) => {
if (isDrawing) {
isDrawing = 0;
}
});
window.onresize = reportWindowSize;
}
/**
* Whiteboard bg and colors on theme changes
*/
function setWhiteboardBgandColors() {
if (mirotalkTheme == "dark") {
whiteboardWhiteColor.style.display = "flex";
whiteboardBlackColor.style.display = "none";
setColor("#ffffff");
} else {
whiteboardWhiteColor.style.display = "none";
whiteboardBlackColor.style.display = "flex";
setColor("#000000");
}
}
/**
* WEBRCT FILE TRANSFER
* https://webrtc.github.io/samples/src/content/datachannel/filetransfer/
* https://github.com/webrtc/samples/blob/gh-pages/src/content/datachannel/filetransfer/js/main.js
*/
/**
* Create File Sharing Data Channel
* @param {*} peer_id
*/
function createFileSharingDataChannel(peer_id) {
fileSharingDataChannels[peer_id] = peerConnections[peer_id].createDataChannel(
"mirotalk_file_sharing_channel"
);
fileSharingDataChannels[peer_id].binaryType = "arraybuffer";
fileSharingDataChannels[peer_id].addEventListener(
"open",
onFSChannelStateChange
);
fileSharingDataChannels[peer_id].addEventListener(
"close",
onFSChannelStateChange
);
fileSharingDataChannels[peer_id].addEventListener("error", onFsError);
}
/**
* Handle File Sharing
* @param {*} data
*/
function handleDataChannelFileSharing(data) {
receiveBuffer.push(data);
receivedSize += data.byteLength;
// let getPercentage = ((receivedSize / incomingFileInfo.fileSize) * 100).toFixed(2);
// console.log("Received progress: " + getPercentage + "%");
if (receivedSize === incomingFileInfo.fileSize) {
incomingFileData = receiveBuffer;
receiveBuffer = [];
endDownload();
}
}
/**
* Handle File Sharing data channel state
* @param {*} event
*/
function onFSChannelStateChange(event) {
console.log("onFSChannelStateChange", event.type);
if (event.type === "close") {
if (sendInProgress) {
userLog("error", "File Sharing channel closed");
sendFileDiv.style.display = "none";
sendInProgress = false;
}
fsDataChannelOpen = false;
return;
}
fsDataChannelOpen = true;
}
/**
* Handle File sharing data channel error
* @param {*} event
*/
function onFsError(event) {
// cleanup
receiveBuffer = [];
incomingFileData = [];
receivedSize = 0;
sendFileDiv.style.display = "none";
// Popup what wrong
if (sendInProgress) {
console.error("onFsError", event);
userLog("error", "File Sharing " + event.error);
sendInProgress = false;
}
}
/**
* Send File Data trought datachannel
*/
function sendFileData() {
console.log(
"Send file " +
fileToSend.name +
" size " +
bytesToSize(fileToSend.size) +
" type " +
fileToSend.type
);
sendInProgress = true;
sendFileInfo.innerHTML =
"File name: " +
fileToSend.name +
"<br>" +
"File type: " +
fileToSend.type +
"<br>" +
"File size: " +
bytesToSize(fileToSend.size) +
"<br>";
sendFileDiv.style.display = "inline";
sendProgress.max = fileToSend.size;
fileReader = new FileReader();
let offset = 0;
fileReader.addEventListener("error", (error) =>
console.error("fileReader error", error)
);
fileReader.addEventListener("abort", (event) =>
console.log("fileReader aborted", event)
);
fileReader.addEventListener("load", (e) => {
if (!sendInProgress || !fsDataChannelOpen) return;
// peer to peer over DataChannels
sendFSData(e.target.result);
offset += e.target.result.byteLength;
sendProgress.value = offset;
sendFilePercentage.innerHTML =
"Send progress: " + ((offset / fileToSend.size) * 100).toFixed(2) + "%";
// send file completed
if (offset === fileToSend.size) {
sendInProgress = false;
sendFileDiv.style.display = "none";
userLog(
"success",
"The file " + fileToSend.name + " was sent successfully."
);
}
if (offset < fileToSend.size) {
readSlice(offset);
}
});
const readSlice = (o) => {
const slice = fileToSend.slice(offset, o + chunkSize);
fileReader.readAsArrayBuffer(slice);
};
readSlice(0);
}
/**
* Send Data if channel open
* @param {*} data fileReader e.target.result
*/
function sendFSData(data) {
for (var peer_id in fileSharingDataChannels) {
if (fileSharingDataChannels[peer_id].readyState === "open") {
fileSharingDataChannels[peer_id].send(data);
}
}
}
/**
* Abort the file transfer
*/
function abortFileTransfer() {
if (fileReader && fileReader.readyState === 1) {
fileReader.abort();
sendFileDiv.style.display = "none";
}
}
/**
* Select the File to Share
*/
function selectFileToShare() {
playSound("newMessage");
Swal.fire({
allowOutsideClick: false,
background: swalBackground,
imageAlt: "mirotalk-file-sharing",
imageUrl: fileSharingImg,
position: "center",
title: "Share the file",
input: "file",
inputAttributes: {
accept: fileSharingInput,
"aria-label": "Select the file",
},
showDenyButton: true,
confirmButtonText: `Send`,
denyButtonText: `Cancel`,
showClass: {
popup: "animate__animated animate__fadeInDown",
},
hideClass: {
popup: "animate__animated animate__fadeOutUp",
},
}).then((result) => {
if (result.isConfirmed) {
fileToSend = result.value;
if (fileToSend && fileToSend.size > 0) {
// no peers in the room
if (!thereIsPeerConnections()) {
userLog("info", "No participants detected");
return;
}
// something wrong channel not open
if (!fsDataChannelOpen) {
userLog(
"error",
"Unable to Sharing the file, DataChannel seems closed."
);
return;
}
// send some metadata about our file to peers in the room
signalingSocket.emit("fileInfo", {
peerConnections: peerConnections,
peer_name: myPeerName,
room_id: roomId,
file: {
fileName: fileToSend.name,
fileSize: fileToSend.size,
fileType: fileToSend.type,
},
});
// send the File
setTimeout(() => {
sendFileData();
}, 1000);
} else {
userLog("error", "File not selected or empty.");
}
}
});
}
/**
* Start to Download the File
* @param {*} data file
*/
function startDownload(data) {
incomingFileInfo = data;
incomingFileData = [];
receiveBuffer = [];
receivedSize = 0;
console.log(
"incoming file: " +
incomingFileInfo.fileName +
" size: " +
bytesToSize(incomingFileInfo.fileSize) +
" type " +
incomingFileInfo.fileType
);
}
/**
* The file will be saved in the blob
* You will be asked to confirm if you want to save it on your PC / Mobile device.
* https://developer.mozilla.org/en-US/docs/Web/API/Blob
*/
function endDownload() {
playSound("download");
// save received file into Blob
const blob = new Blob(incomingFileData);
incomingFileData = [];
// if file is image, show the preview
if (isImageURL(incomingFileInfo.fileName)) {
const reader = new FileReader();
reader.onload = (e) => {
Swal.fire({
allowOutsideClick: false,
background: swalBackground,
position: "center",
title: "Received file",
text:
incomingFileInfo.fileName +
" size " +
bytesToSize(incomingFileInfo.fileSize),
imageUrl: e.target.result,
imageAlt: "mirotalk-file-img-download",
showDenyButton: true,
confirmButtonText: `Save`,
denyButtonText: `Cancel`,
showClass: {
popup: "animate__animated animate__fadeInDown",
},
hideClass: {
popup: "animate__animated animate__fadeOutUp",
},
}).then((result) => {
if (result.isConfirmed) {
saveFileFromBlob();
}
});
};
// blob where is stored downloaded file
reader.readAsDataURL(blob);
} else {
// not img file
Swal.fire({
allowOutsideClick: false,
background: swalBackground,
imageAlt: "mirotalk-file-download",
imageUrl: fileSharingImg,
position: "center",
title: "Received file",
text:
incomingFileInfo.fileName +
" size " +
bytesToSize(incomingFileInfo.fileSize),
showDenyButton: true,
confirmButtonText: `Save`,
denyButtonText: `Cancel`,
showClass: {
popup: "animate__animated animate__fadeInDown",
},
hideClass: {
popup: "animate__animated animate__fadeOutUp",
},
}).then((result) => {
if (result.isConfirmed) {
saveFileFromBlob();
}
});
}
// save to PC / Mobile devices
function saveFileFromBlob() {
const url = window.URL.createObjectURL(blob);
const a = document.createElement("a");
a.style.display = "none";
a.href = url;
a.download = incomingFileInfo.fileName;
document.body.appendChild(a);
a.click();
setTimeout(() => {
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
}, 100);
}
}
/**
* Convert bytes to KB-MB-GB-TB
* @param {*} bytes
* @returns size
*/
function bytesToSize(bytes) {
var sizes = ["Bytes", "KB", "MB", "GB", "TB"];
if (bytes == 0) return "0 Byte";
var i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024)));
return Math.round(bytes / Math.pow(1024, i), 2) + " " + sizes[i];
}
/**
* Mute everyone except yourself
* Once muted, you won't be able to unmute them, but they can unmute themselves at any time
*/
function muteEveryone() {
signalingSocket.emit("muteEveryone", {
peerConnections: peerConnections,
room_id: roomId,
peer_name: myPeerName,
});
}
/**
* Hide everyone except yourself
* Once hided, you won't be able to unhide them, but they can unhide themselves at any time
*/
function hideEveryone() {
signalingSocket.emit("hideEveryone", {
peerConnections: peerConnections,
room_id: roomId,
peer_name: myPeerName,
});
}
/**
* Popup the peer_name that do this actions
* @param {*} peer_name
*/
function setMyAudioOff(peer_name) {
if (myAudioStatus === false) return;
localMediaStream.getAudioTracks()[0].enabled = false;
myAudioStatus = localMediaStream.getAudioTracks()[0].enabled;
audioBtn.className = "fas fa-microphone-slash";
setMyAudioStatus(myAudioStatus);
userLog("info", peer_name + " has disabled your audio");
}
/**
* Popup the peer_name that do this actions
* @param {*} peer_name
*/
function setMyVideoOff(peer_name) {
if (myVideoStatus === false) return;
localMediaStream.getVideoTracks()[0].enabled = false;
myVideoStatus = localMediaStream.getVideoTracks()[0].enabled;
videoBtn.className = "fas fa-video-slash";
setMyVideoStatus(myVideoStatus);
userLog("info", peer_name + " has disabled your video");
}
/**
* Mute or Hide everyone except yourself
* @param {*} element audio/video
*/
function disableAllPeers(element) {
if (!thereIsPeerConnections()) {
userLog("info", "No participants detected");
return;
}
Swal.fire({
background: swalBackground,
position: "center",
imageAlt: "mirotalk-disable-" + element,
imageUrl: confirmImg,
title:
element == "audio"
? "Mute everyone except yourself?"
: "Hide everyone except yourself?",
text:
element == "audio"
? "Once muted, you won't be able to unmute them, but they can unmute themselves at any time."
: "Once hided, you won't be able to unhide them, but they can unhide themselves at any time.",
showDenyButton: true,
confirmButtonText: element == "audio" ? `Mute` : `Hide`,
denyButtonText: `Cancel`,
showClass: {
popup: "animate__animated animate__fadeInDown",
},
hideClass: {
popup: "animate__animated animate__fadeOutUp",
},
}).then((result) => {
if (result.isConfirmed) {
switch (element) {
case "audio":
muteEveryone();
break;
case "video":
hideEveryone();
break;
}
}
});
}
/**
* Kick out confirm
* @param {*} peer_id
* @param {*} peerKickOutBtn
*/
function kickOut(peer_id, peerKickOutBtn) {
let pName = getId(peer_id + "_name").innerHTML;
Swal.fire({
background: swalBackground,
position: "center",
imageUrl: confirmImg,
title: "Kick out " + pName,
text: "Are you sure you want to kick out this participant?",
showDenyButton: true,
confirmButtonText: `Yes`,
denyButtonText: `No`,
showClass: {
popup: "animate__animated animate__fadeInDown",
},
hideClass: {
popup: "animate__animated animate__fadeOutUp",
},
}).then((result) => {
if (result.isConfirmed) {
// send peer to kick out from room
signalingSocket.emit("kickOut", {
room_id: roomId,
peer_id: peer_id,
peer_name: myPeerName,
});
peerKickOutBtn.style.display = "none";
}
});
}
/**
* Who Kick out you msg popup
* @param {*} peer_name
*/
function kickedOut(peer_name) {
playSound("kickedOut");
let timerInterval;
Swal.fire({
allowOutsideClick: false,
background: swalBackground,
position: "center",
icon: "warning",
title: "You will be kicked out!",
html:
`<h2 style="color: red;">` +
peer_name +
`</h2> will kick out you after <b style="color: red;"></b> milliseconds.`,
timer: 10000,
timerProgressBar: true,
didOpen: () => {
Swal.showLoading();
timerInterval = setInterval(() => {
const content = Swal.getHtmlContainer();
if (content) {
const b = content.querySelector("b");
if (b) {
b.textContent = Swal.getTimerLeft();
}
}
}, 100);
},
willClose: () => {
clearInterval(timerInterval);
},
showClass: {
popup: "animate__animated animate__fadeInDown",
},
hideClass: {
popup: "animate__animated animate__fadeOutUp",
},
}).then(() => {
window.location.href = "/newcall";
});
}
/**
* About info
* https://sweetalert2.github.io
*/
function getAbout() {
playSound("newMessage");
Swal.fire({
background: swalBackground,
position: "center",
title: "<strong>WebRTC Made with ❤️</strong>",
imageAlt: "mirotalk-about",
imageUrl: aboutImg,
html: `
<br/>
<div id="about"><b>open source</b> project on<a href="https://github.com/miroslavpejic85/mirotalk" target="_blank"><h1><strong> GitHub </strong></h1></a></div>
<div id="author"><a href="https://www.linkedin.com/in/miroslav-pejic-976a07101/" target="_blank">Author: Miroslav Pejic</a></div><br>
<button id="sponsorBtn" class="far fa-heart pulsate" onclick="window.open('https://github.com/sponsors/miroslavpejic85?o=esb')"> Sponsor</button>
`,
showClass: {
popup: "animate__animated animate__fadeInDown",
},
hideClass: {
popup: "animate__animated animate__fadeOutUp",
},
});
}
/**
* Leave the Room and create a new one
* https://sweetalert2.github.io
*/
function leaveRoom() {
playSound("newMessage");
Swal.fire({
background: swalBackground,
position: "center",
imageAlt: "mirotalk-leave",
imageUrl: leaveRoomImg,
title: "Leave this room?",
showDenyButton: true,
confirmButtonText: `Yes`,
denyButtonText: `No`,
showClass: {
popup: "animate__animated animate__fadeInDown",
},
hideClass: {
popup: "animate__animated animate__fadeOutUp",
},
}).then((result) => {
if (result.isConfirmed) {
window.location.href = "/newcall";
}
});
}
/**
* Basic user logging
* https://sweetalert2.github.io
* @param {*} type
* @param {*} message
*/
function userLog(type, message) {
switch (type) {
case "error":
Swal.fire({
background: swalBackground,
position: "center",
icon: "error",
title: "Oops...",
text: message,
showClass: {
popup: "animate__animated animate__fadeInDown",
},
hideClass: {
popup: "animate__animated animate__fadeOutUp",
},
});
playSound("error");
break;
case "info":
Swal.fire({
background: swalBackground,
position: "center",
icon: "info",
title: "Info",
text: message,
showClass: {
popup: "animate__animated animate__fadeInDown",
},
hideClass: {
popup: "animate__animated animate__fadeOutUp",
},
});
break;
case "success":
Swal.fire({
background: swalBackground,
position: "center",
icon: "success",
title: "Success",
text: message,
showClass: {
popup: "animate__animated animate__fadeInDown",
},
hideClass: {
popup: "animate__animated animate__fadeOutUp",
},
});
break;
// ......
default:
alert(message);
}
}
/**
* Sound notifications
* https://notificationsounds.com/notification-sounds
* @param {*} state
*/
async function playSound(state) {
if (!notifyBySound) return;
let file_audio = "";
switch (state) {
case "addPeer":
file_audio = notifyAddPeer;
break;
case "download":
file_audio = notifyDownload;
break;
case "kickedOut":
file_audio = notifyKickedOut;
break;
case "removePeer":
file_audio = notifyRemovePeer;
break;
case "newMessage":
file_audio = notifyNewMessage;
break;
case "recStart":
file_audio = notifyRecStart;
break;
case "recStop":
file_audio = notifyRecStop;
break;
case "rHand":
file_audio = notifyRaiseHand;
break;
case "error":
file_audio = notifyError;
break;
// ...
default:
console.log("no file audio");
}
if (file_audio != "") {
let audioToPlay = new Audio(file_audio);
try {
await audioToPlay.play();
} catch (e) {
// console.error("Cannot play sound", e);
// Automatic playback failed. (safari)
return;
}
}
}
/**
* Show-Hide all elements grp by class name
* @param {*} className
* @param {*} displayState
*/
function toggleClassElements(className, displayState) {
var elements = getEcN(className);
for (var i = 0; i < elements.length; i++) {
elements[i].style.display = displayState;
}
}
/**
* Get Html element by Id
* @param {*} id
*/
function getId(id) {
return document.getElementById(id);
}
/**
* Get Html element by selector
* @param {*} selector
*/
function getSl(selector) {
return document.querySelector(selector);
}
/**
* Get Html element by class name
* @param {*} className
*/
function getEcN(className) {
return document.getElementsByClassName(className);
}