/* global Metamaps, $ */ /* * Metamaps.Realtime.js * * Dependencies: * - Metamaps.Backbone * - Metamaps.Erb * - Metamaps.Mappers * - Metamaps.Mappings * - Metamaps.Messages * - Metamaps.Synapses * - Metamaps.Topics */ import _ from 'lodash' import SimpleWebRTC from 'simplewebrtc' import SocketIoConnection from 'simplewebrtc/socketioconnection' import Active from '../Active' import GlobalUI from '../GlobalUI' import JIT from '../JIT' import Synapse from '../Synapse' import Topic from '../Topic' import Util from '../Util' import Views from '../Views' import Visualize from '../Visualize' import { JUNTO_UPDATED, INVITED_TO_CALL, INVITED_TO_JOIN, CALL_ACCEPTED, CALL_DENIED, INVITE_DENIED, CALL_IN_PROGRESS, CALL_STARTED, MAPPER_LIST_UPDATED, MAPPER_JOINED_CALL, MAPPER_LEFT_CALL, NEW_MAPPER, LOST_MAPPER, MESSAGE_CREATED, TOPIC_DRAGGED, TOPIC_CREATED, TOPIC_UPDATED, TOPIC_REMOVED, TOPIC_DELETED, SYNAPSE_CREATED, SYNAPSE_UPDATED, SYNAPSE_REMOVED, SYNAPSE_DELETED, PEER_COORDS_UPDATED, MAP_UPDATED } from './events' import { juntoUpdated, invitedToCall, invitedToJoin, callAccepted, callDenied, inviteDenied, callInProgress, callStarted, mapperListUpdated, mapperJoinedCall, mapperLeftCall, peerCoordsUpdated, newMapper, lostMapper, messageCreated, topicDragged, topicCreated, topicUpdated, topicRemoved, topicDeleted, synapseCreated, synapseUpdated, synapseRemoved, synapseDeleted, mapUpdated, } from './receivable' import { joinMap, leaveMap, checkForCall, acceptCall, denyCall, denyInvite, inviteToJoin, inviteACall, joinCall, leaveCall, sendCoords, sendMapperInfo, createMessage, dragTopic, createTopic, updateTopic, removeTopic, deleteTopic, createSynapse, updateSynapse, removeSynapse, deleteSynapse, updateMap } from './sendable' let Realtime = { juntoState: { connectedPeople: {}, liveMaps: {} }, videoId: 'video-wrapper', socket: null, webrtc: null, readyToCall: false, mappersOnMap: {}, disconnected: false, chatOpen: false, soundId: null, broadcastingStatus: false, inConversation: false, localVideo: null, init: function () { var self = Realtime self.addJuntoListeners() self.socket = new SocketIoConnection({ url: Metamaps.Erb['REALTIME_SERVER']}) self.socket.on('connect', function () { console.log('connected') subscribeToEvents(self, self.socket) if (!self.disconnected) { self.startActiveMap() } else self.disconnected = false }) self.socket.on('disconnect', function () { self.disconnected = true }) if (Active.Mapper) { self.webrtc = new SimpleWebRTC({ connection: self.socket, localVideoEl: self.videoId, remoteVideosEl: '', debug: true, detectSpeakingEvents: false, //true, autoAdjustMic: false, // true, autoRequestMedia: false, localVideo: { autoplay: true, mirror: true, muted: true }, media: { video: true, audio: true }, nick: Active.Mapper.id }) self.webrtc.webrtc.on('iceFailed', function (peer) { console.log('local ice failure', peer) // local ice failure }) self.webrtc.webrtc.on('connectivityError', function (peer) { console.log('remote ice failure', peer) // remote ice failure }) var $video = $('').attr('id', self.videoId) self.localVideo = { $video: $video, view: new Views.VideoView($video[0], $('body'), 'me', true, { DOUBLE_CLICK_TOLERANCE: 200, avatar: Active.Mapper ? Active.Mapper.get('image') : '' }) } self.room = new Views.Room({ webrtc: self.webrtc, socket: self.socket, username: Active.Mapper ? Active.Mapper.get('name') : '', image: Active.Mapper ? Active.Mapper.get('image') : '', room: 'global', $video: self.localVideo.$video, myVideoView: self.localVideo.view, config: { DOUBLE_CLICK_TOLERANCE: 200 } }) self.room.videoAdded(self.handleVideoAdded) if (!Active.Map) { self.room.chat.$container.hide() } $('body').prepend(self.room.chat.$container) } // if Active.Mapper }, addJuntoListeners: function () { var self = Realtime $(document).on(Views.ChatView.events.openTray, function () { $('.main').addClass('compressed') self.chatOpen = true self.positionPeerIcons() }) $(document).on(Views.ChatView.events.closeTray, function () { $('.main').removeClass('compressed') self.chatOpen = false self.positionPeerIcons() }) $(document).on(Views.ChatView.events.videosOn, function () { $('#wrapper').removeClass('hideVideos') }) $(document).on(Views.ChatView.events.videosOff, function () { $('#wrapper').addClass('hideVideos') }) $(document).on(Views.ChatView.events.cursorsOn, function () { $('#wrapper').removeClass('hideCursors') }) $(document).on(Views.ChatView.events.cursorsOff, function () { $('#wrapper').addClass('hideCursors') }) }, startActiveMap: function () { var self = Realtime if (Active.Map && Active.Mapper) { if (Active.Map.authorizeToEdit(Active.Mapper)) { self.turnOn() self.setupSocket() self.setupLocalSendables() } self.room.addMessages(new Metamaps.Backbone.MessageCollection(Metamaps.Messages), true) } }, endActiveMap: function () { var self = Realtime $(document).off('.map') // leave the appropriate rooms to leave if (self.inConversation) self.leaveCall() self.leaveMap() $('.collabCompass').remove() if (self.room) { self.room.leave() self.room.chat.$container.hide() self.room.chat.close() } }, turnOn: function (notify) { var self = Realtime $('.collabCompass').show() self.room.chat.$container.show() self.room.room = 'map-' + Active.Map.id self.activeMapper = { id: Active.Mapper.id, name: Active.Mapper.get('name'), username: Active.Mapper.get('name'), image: Active.Mapper.get('image'), color: Util.getPastelColor(), self: true } self.localVideo.view.$container.find('.video-cutoff').css({ border: '4px solid ' + self.activeMapper.color }) self.room.chat.addParticipant(self.activeMapper) }, setupSocket: function () { var self = Realtime // subscribe to rooms on the websocket? self.checkForCall() self.joinMap() }, setupLocalSendables: function () { var self = Realtime // local event listeners that trigger events var sendCoords = function (event) { var pixels = { x: event.pageX, y: event.pageY } var coords = Util.pixelsToCoords(pixels) self.sendCoords(coords) } $(document).on('mousemove.map', sendCoords) var zoom = function (event, e) { if (e) { var pixels = { x: e.pageX, y: e.pageY } var coords = Util.pixelsToCoords(pixels) self.sendCoords(coords) } self.positionPeerIcons() } $(document).on(JIT.events.zoom + '.map', zoom) $(document).on(JIT.events.pan + '.map', self.positionPeerIcons) var dragTopic = function (event, positions) { self.dragTopic(positions) } $(document).on(JIT.events.topicDrag + '.map', dragTopic) var createTopic = function (event, data) { self.createTopic(data) } $(document).on(JIT.events.newTopic + '.map', createTopic) var deleteTopic = function (event, data) { self.deleteTopic(data) } $(document).on(JIT.events.deleteTopic + '.map', deleteTopic) var removeTopic = function (event, data) { self.removeTopic(data) } $(document).on(JIT.events.removeTopic + '.map', removeTopic) var createSynapse = function (event, data) { self.createSynapse(data) } $(document).on(JIT.events.newSynapse + '.map', createSynapse) var deleteSynapse = function (event, data) { self.deleteSynapse(data) } $(document).on(JIT.events.deleteSynapse + '.map', deleteSynapse) var removeSynapse = function (event, data) { self.removeSynapse(data) } $(document).on(JIT.events.removeSynapse + '.map', removeSynapse) var createMessage = function (event, data) { self.createMessage(data) } $(document).on(Views.Room.events.newMessage + '.map', createMessage) }, countOthersInConversation: function () { var self = Realtime var count = 0 for (var key in self.mappersOnMap) { if (self.mappersOnMap[key].inConversation) count++ } return count }, handleVideoAdded: function (v, id) { var self = Realtime self.positionVideos() v.setParent($('#wrapper')) v.$container.find('.video-cutoff').css({ border: '4px solid ' + self.mappersOnMap[id].color }) $('#wrapper').append(v.$container) }, positionVideos: function () { var self = Realtime var videoIds = Object.keys(self.room.videos) var numOfVideos = videoIds.length var numOfVideosToPosition = _.filter(videoIds, function (id) { return !self.room.videos[id].manuallyPositioned }).length var screenHeight = $(document).height() var screenWidth = $(document).width() var topExtraPadding = 20 var topPadding = 30 var leftPadding = 30 var videoHeight = 150 var videoWidth = 180 var column = 0 var row = 0 var yFormula = function () { var y = topExtraPadding + (topPadding + videoHeight) * row + topPadding if (y + videoHeight > screenHeight) { row = 0 column += 1 y = yFormula() } row++ return y } var xFormula = function () { var x = (leftPadding + videoWidth) * column + leftPadding return x } // do self first var myVideo = Realtime.localVideo.view if (!myVideo.manuallyPositioned) { myVideo.$container.css({ top: yFormula() + 'px', left: xFormula() + 'px' }) } videoIds.forEach(function (id) { var video = self.room.videos[id] if (!video.manuallyPositioned) { video.$container.css({ top: yFormula() + 'px', left: xFormula() + 'px' }) } }) }, callEnded: function () { var self = Realtime self.room.conversationEnding() self.room.leaveVideoOnly() self.inConversation = false self.localVideo.view.$container.hide().css({ top: '72px', left: '30px' }) self.localVideo.view.audioOn() self.localVideo.view.videoOn() }, createCompass: function (name, id, image, color) { var str = '

' + name + '

' str += '
' $('#compass' + id).remove() $('
', { id: 'compass' + id, class: 'collabCompass' }).html(str).appendTo('#wrapper') $('#compass' + id + ' img').css({ 'border': '2px solid ' + color }) $('#compass' + id + ' p').css({ 'background-color': color }) }, positionPeerIcons: function () { var self = Realtime for (var key in self.mappersOnMap) { self.positionPeerIcon(key) } }, positionPeerIcon: function (id) { var self = Realtime var boundary = self.chatOpen ? '#wrapper' : document var mapper = self.mappersOnMap[id] var xMax = $(boundary).width() var yMax = $(boundary).height() var compassDiameter = 56 var compassArrowSize = 24 var origPixels = Util.coordsToPixels(mapper.coords) var pixels = self.limitPixelsToScreen(origPixels) $('#compass' + id).css({ left: pixels.x + 'px', top: pixels.y + 'px' }) /* showing the arrow if the collaborator is off of the viewport screen */ if (origPixels.x !== pixels.x || origPixels.y !== pixels.y) { var dy = origPixels.y - pixels.y // opposite var dx = origPixels.x - pixels.x // adjacent var ratio = dy / dx var angle = Math.atan2(dy, dx) $('#compassArrow' + id).show().css({ transform: 'rotate(' + angle + 'rad)', '-webkit-transform': 'rotate(' + angle + 'rad)', }) if (dx > 0) { $('#compass' + id).addClass('labelLeft') } } else { $('#compassArrow' + id).hide() $('#compass' + id).removeClass('labelLeft') } }, limitPixelsToScreen: function (pixels) { var self = Realtime var boundary = self.chatOpen ? '#wrapper' : document var xLimit, yLimit var xMax = $(boundary).width() var yMax = $(boundary).height() var compassDiameter = 56 var compassArrowSize = 24 xLimit = Math.max(0 + compassArrowSize, pixels.x) xLimit = Math.min(xLimit, xMax - compassDiameter) yLimit = Math.max(0 + compassArrowSize, pixels.y) yLimit = Math.min(yLimit, yMax - compassDiameter) return {x: xLimit,y: yLimit} } } const sendables = [ ['joinMap',joinMap], ['leaveMap',leaveMap], ['checkForCall',checkForCall], ['acceptCall',acceptCall], ['denyCall',denyCall], ['denyInvite',denyInvite], ['inviteToJoin',inviteToJoin], ['inviteACall',inviteACall], ['joinCall',joinCall], ['leaveCall',leaveCall], ['sendMapperInfo',sendMapperInfo], ['sendCoords',sendCoords], ['createMessage',createMessage], ['dragTopic',dragTopic], ['createTopic',createTopic], ['updateTopic',updateTopic], ['removeTopic',removeTopic], ['deleteTopic',deleteTopic], ['createSynapse',createSynapse], ['updateSynapse',updateSynapse], ['removeSynapse',removeSynapse], ['deleteSynapse',deleteSynapse], ['updateMap',updateMap] ] sendables.forEach(sendable => { Realtime[sendable[0]] = sendable[1](Realtime) }) const subscribeToEvents = (Realtime, socket) => { socket.on(JUNTO_UPDATED, juntoUpdated(Realtime)) socket.on(INVITED_TO_CALL, invitedToCall(Realtime)) socket.on(INVITED_TO_JOIN, invitedToJoin(Realtime)) socket.on(CALL_ACCEPTED, callAccepted(Realtime)) socket.on(CALL_DENIED, callDenied(Realtime)) socket.on(INVITE_DENIED, inviteDenied(Realtime)) socket.on(CALL_IN_PROGRESS, callInProgress(Realtime)) socket.on(CALL_STARTED, callStarted(Realtime)) socket.on(MAPPER_LIST_UPDATED, mapperListUpdated(Realtime)) socket.on(MAPPER_JOINED_CALL, mapperJoinedCall(Realtime)) socket.on(MAPPER_LEFT_CALL, mapperLeftCall(Realtime)) socket.on(PEER_COORDS_UPDATED, peerCoordsUpdated(Realtime)) socket.on(NEW_MAPPER, newMapper(Realtime)) socket.on(LOST_MAPPER, lostMapper(Realtime)) socket.on(MESSAGE_CREATED, messageCreated(Realtime)) socket.on(TOPIC_DRAGGED, topicDragged(Realtime)) socket.on(TOPIC_CREATED, topicCreated(Realtime)) socket.on(TOPIC_UPDATED, topicUpdated(Realtime)) socket.on(TOPIC_REMOVED, topicRemoved(Realtime)) socket.on(TOPIC_DELETED, topicDeleted(Realtime)) socket.on(SYNAPSE_CREATED, synapseCreated(Realtime)) socket.on(SYNAPSE_UPDATED, synapseUpdated(Realtime)) socket.on(SYNAPSE_REMOVED, synapseRemoved(Realtime)) socket.on(SYNAPSE_DELETED, synapseDeleted(Realtime)) socket.on(MAP_UPDATED, mapUpdated(Realtime)) } export default Realtime