// TODO(fancycode): Should load through AMD if possible. /* global SimpleWebRTC, OC, OCA: false */ var webrtc; var guestNamesTable = {}; var spreedMappingTable = {}; var spreedPeerConnectionTable = []; (function(OCA, OC) { 'use strict'; OCA.SpreedMe = OCA.SpreedMe || {}; var previousUsersInRoom = []; var usersInCallMapping = {}; var ownPeer = null; var ownScreenPeer = null; var hasLocalMedia = false; function updateParticipantsUI(currentUsersNo) { 'use strict'; if (!currentUsersNo) { currentUsersNo = 1; } var $appContentElement = $('#app-content'), participantsClass = 'participants-' + currentUsersNo, hadSidebar = $appContentElement.hasClass('with-app-sidebar'); if (!$appContentElement.hasClass(participantsClass) && !$appContentElement.hasClass('screensharing')) { $appContentElement.attr('class', '').addClass(participantsClass); if (currentUsersNo > 1) { $appContentElement.addClass('incall'); } else { $appContentElement.removeClass('incall'); } if (hadSidebar) { $appContentElement.addClass('with-app-sidebar'); } } } function createScreensharingPeer(signaling, sessionId) { var currentSessionId = signaling.getSessionid(); var useMcu = signaling.hasFeature("mcu"); if (useMcu && !webrtc.webrtc.getPeers(currentSessionId, 'screen').length) { if (ownScreenPeer) { ownScreenPeer.end(); } // Create own publishing stream. ownScreenPeer = webrtc.webrtc.createPeer({ id: currentSessionId, type: 'screen', sharemyscreen: true, enableDataChannels: false, receiveMedia: { offerToReceiveAudio: 0, offerToReceiveVideo: 0 }, broadcaster: currentSessionId, }); webrtc.emit('createdPeer', ownScreenPeer); ownScreenPeer.start(); } if (sessionId === currentSessionId) { return; } if (!webrtc.webrtc.getPeers(sessionId, 'screen').length) { if (useMcu) { // TODO(jojo): Already create peer object to avoid duplicate offers. // TODO(jojo): We should use "requestOffer" as with regular // audio/video peers. Not possible right now as there is no way // for clients to know that screensharing is active and an offer // from the MCU should be requested. webrtc.connection.sendOffer(sessionId, "screen"); } else { var peer = webrtc.webrtc.createPeer({ id: sessionId, type: 'screen', sharemyscreen: true, enableDataChannels: false, receiveMedia: { offerToReceiveAudio: 0, offerToReceiveVideo: 0 }, broadcaster: currentSessionId, }); webrtc.emit('createdPeer', peer); peer.start(); } } } function checkStartPublishOwnPeer(signaling) { 'use strict'; var currentSessionId = signaling.getSessionid(); if (!hasLocalMedia || webrtc.webrtc.getPeers(currentSessionId, 'video').length) { // No media yet or already publishing. return; } if (ownPeer) { OCA.SpreedMe.webrtc.removePeers(ownPeer.id); OCA.SpreedMe.speakers.remove(ownPeer.id, true); OCA.SpreedMe.videos.remove(ownPeer.id); delete spreedMappingTable[ownPeer.id]; ownPeer.end(); } // Create own publishing stream. ownPeer = webrtc.webrtc.createPeer({ id: currentSessionId, type: "video", enableDataChannels: true, receiveMedia: { offerToReceiveAudio: 0, offerToReceiveVideo: 0 } }); webrtc.emit('createdPeer', ownPeer); ownPeer.start(); } function usersChanged(signaling, newUsers, disconnectedSessionIds) { 'use strict'; var currentSessionId = signaling.getSessionid(); var useMcu = signaling.hasFeature("mcu"); if (useMcu && newUsers.length) { checkStartPublishOwnPeer(signaling); } newUsers.forEach(function(user) { if (!user.inCall) { return; } // TODO(fancycode): Adjust property name of internal PHP backend to be all lowercase. var sessionId = user.sessionId || user.sessionid; if (!sessionId || sessionId === currentSessionId || previousUsersInRoom.indexOf(sessionId) !== -1) { return; } previousUsersInRoom.push(sessionId); // TODO(fancycode): Adjust property name of internal PHP backend to be all lowercase. spreedMappingTable[sessionId] = user.userId || user.userid; var videoContainer = $(OCA.SpreedMe.videos.getContainerId(sessionId)); if (videoContainer.length === 0) { OCA.SpreedMe.videos.add(sessionId); } var peer; if (!webrtc.webrtc.getPeers(sessionId, 'video').length) { if (useMcu) { // TODO(jojo): Already create peer object to avoid duplicate offers. webrtc.connection.requestOffer(user, "video"); } else if (sessionId < currentSessionId) { // To avoid overloading the user joining a room (who previously called // all the other participants), we decide who calls who by comparing // the session ids of the users: "larger" ids call "smaller" ones. console.log("Starting call with", user); peer = webrtc.webrtc.createPeer({ id: sessionId, type: "video", enableDataChannels: true, receiveMedia: { offerToReceiveAudio: 1, offerToReceiveVideo: 1 } }); webrtc.emit('createdPeer', peer); peer.start(); } } //Send shared screen to new participants if (webrtc.getLocalScreen()) { createScreensharingPeer(signaling, sessionId); } }); disconnectedSessionIds.forEach(function(sessionId) { console.log('XXX Remove peer', sessionId); OCA.SpreedMe.webrtc.removePeers(sessionId); OCA.SpreedMe.speakers.remove(sessionId, true); OCA.SpreedMe.videos.remove(sessionId); delete spreedMappingTable[sessionId]; delete guestNamesTable[sessionId]; }); previousUsersInRoom = previousUsersInRoom.diff(disconnectedSessionIds); updateParticipantsUI(previousUsersInRoom.length + 1); } function usersInCallChanged(signaling, users) { // The passed list are the users that are currently in the room, // i.e. that are in the call and should call each other. var currentSessionId = signaling.getSessionid(); var currentUsersInRoom = []; var userMapping = {}; var selfInCall = false; var sessionId; for (sessionId in users) { if (!users.hasOwnProperty(sessionId)) { continue; } var user = users[sessionId]; if (!user.inCall) { continue; } if (sessionId === currentSessionId) { selfInCall = true; continue; } currentUsersInRoom.push(sessionId); userMapping[sessionId] = user; } if (!selfInCall) { // Own session is no longer in the call, disconnect from all others. usersChanged(signaling, [], previousUsersInRoom); return; } var newSessionIds = currentUsersInRoom.diff(previousUsersInRoom); var disconnectedSessionIds = previousUsersInRoom.diff(currentUsersInRoom); var newUsers = []; newSessionIds.forEach(function(sessionId) { newUsers.push(userMapping[sessionId]); }); if (newUsers.length || disconnectedSessionIds.length) { usersChanged(signaling, newUsers, disconnectedSessionIds); } } /** * @param {OCA.Talk.Application} app */ function initWebRTC(app) { Array.prototype.diff = function(a) { return this.filter(function(i) { return a.indexOf(i) < 0; }); }; var signaling = app.signaling; signaling.on('usersLeft', function(users) { users.forEach(function(user) { delete usersInCallMapping[user]; }); usersChanged(signaling, [], users); }); signaling.on('usersChanged', function(users) { users.forEach(function(user) { var sessionId = user.sessionId || user.sessionid; usersInCallMapping[sessionId] = user; }); usersInCallChanged(signaling, usersInCallMapping); }); signaling.on('usersInRoom', function(users) { usersInCallMapping = {}; users.forEach(function(user) { var sessionId = user.sessionId || user.sessionid; usersInCallMapping[sessionId] = user; }); usersInCallChanged(signaling, usersInCallMapping); }); signaling.on('leaveCall', function () { webrtc.leaveCall(); }); webrtc = new SimpleWebRTC({ localVideoEl: 'localVideo', remoteVideosEl: '', autoRequestMedia: true, debug: false, media: { audio: true, video: { width: { max: 1280 }, height: { max: 720 } } }, autoAdjustMic: false, audioFallback: true, detectSpeakingEvents: true, connection: signaling, enableDataChannels: true, nick: OC.getCurrentUser().displayName }); OCA.SpreedMe.webrtc = webrtc; OCA.SpreedMe.webrtc.startMedia = function (token) { app.setEmptyContentMessage( 'icon-video-off', t('spreed', 'Waiting for camera and microphone permissions'), t('spreed', 'Please, give your browser access to use your camera and microphone in order to use this app.') ); webrtc.joinCall(token); }; var spreedListofSpeakers = {}; var spreedListofSharedScreens = {}; var latestSpeakerId = null; var unpromotedSpeakerId = null; var latestScreenId = null; var screenSharingActive = false; window.addEventListener('resize', function() { if (screenSharingActive) { $('#screens').children('video').each(function() { $(this).width('100%'); $(this).height($('#screens').height()); }); } }); var sendDataChannelToAll = function(channel, message, payload) { // If running with MCU, the message must be sent through the // publishing peer and will be distributed by the MCU to subscribers. var conn = OCA.SpreedMe.webrtc.connection; if (ownPeer && conn.hasFeature && conn.hasFeature('mcu')) { ownPeer.sendDirectly(channel, message, payload); return; } OCA.SpreedMe.webrtc.sendDirectlyToAll(channel, message, payload); }; OCA.SpreedMe.videos = { getContainerId: function(id) { var sanitizedId = id.replace(/[!"#$%&'()*+,.\/:;<=>?@[\\\]^`{|}~]/g, "\\$&"); return '#container_' + sanitizedId + '_video_incoming'; }, add: function(id) { if (!(typeof id === 'string' || id instanceof String)) { return; } // Indicator for username var userIndicator = document.createElement('div'); userIndicator.className = 'nameIndicator'; // Avatar for username var avatar = document.createElement('div'); avatar.className = 'avatar icon-loading'; var userId = spreedMappingTable[id]; if (userId && userId.length) { $(avatar).avatar(userId, 128); } else { $(avatar).imageplaceholder('?', undefined, 128); $(avatar).css('background-color', '#b9b9b9'); } $(avatar).css('opacity', '0.5'); var avatarContainer = document.createElement('div'); avatarContainer.className = 'avatar-container'; avatarContainer.appendChild(avatar); // Media indicators var mediaIndicator = document.createElement('div'); mediaIndicator.className = 'mediaIndicator'; var muteIndicator = document.createElement('button'); muteIndicator.className = 'muteIndicator icon-white icon-shadow icon-audio-off audio-on'; muteIndicator.disabled = true; var hideRemoteVideoButton = document.createElement('button'); hideRemoteVideoButton.className = 'hideRemoteVideo icon-white icon-shadow icon-video'; hideRemoteVideoButton.setAttribute('style', 'display: none;'); hideRemoteVideoButton.setAttribute('data-original-title', t('spreed', 'Disable video')); hideRemoteVideoButton.onclick = function() { OCA.SpreedMe.videos._toggleRemoteVideo(id); }; var screenSharingIndicator = document.createElement('button'); screenSharingIndicator.className = 'screensharingIndicator icon-white icon-shadow icon-screen screen-off'; screenSharingIndicator.setAttribute('data-original-title', t('spreed', 'Show screen')); var iceFailedIndicator = document.createElement('button'); iceFailedIndicator.className = 'iceFailedIndicator icon-white icon-shadow icon-error not-failed'; iceFailedIndicator.disabled = true; $(hideRemoteVideoButton).tooltip({ placement: 'top', trigger: 'hover' }); $(screenSharingIndicator).tooltip({ placement: 'top', trigger: 'hover' }); mediaIndicator.appendChild(muteIndicator); mediaIndicator.appendChild(hideRemoteVideoButton); mediaIndicator.appendChild(screenSharingIndicator); mediaIndicator.appendChild(iceFailedIndicator); // Generic container var container = document.createElement('div'); container.className = 'videoContainer'; container.id = 'container_' + id + '_video_incoming'; container.appendChild(avatarContainer); container.appendChild(userIndicator); container.appendChild(mediaIndicator); $(container).prependTo($('#videos')); return container; }, muteRemoteVideo: function(id) { if (!(typeof id === 'string' || id instanceof String)) { return; } var $container = $(OCA.SpreedMe.videos.getContainerId(id)); $container.find('.avatar-container').show(); $container.find('video').hide(); $container.find('.hideRemoteVideo').hide(); }, unmuteRemoteVideo: function(id) { if (!(typeof id === 'string' || id instanceof String)) { return; } var $container = $(OCA.SpreedMe.videos.getContainerId(id)); var $hideRemoteVideoButton = $container.find('.hideRemoteVideo'); $hideRemoteVideoButton.show(); if ($hideRemoteVideoButton.hasClass('icon-video')) { $container.find('.avatar-container').hide(); $container.find('video').show(); } }, _toggleRemoteVideo: function(id) { if (!(typeof id === 'string' || id instanceof String)) { return; } var $container = $(OCA.SpreedMe.videos.getContainerId(id)); var $hideRemoteVideoButton = $container.find('.hideRemoteVideo'); if ($hideRemoteVideoButton.hasClass('icon-video')) { $container.find('.avatar-container').show(); $container.find('video').hide(); $hideRemoteVideoButton.attr('data-original-title', t('spreed', 'Enable video')) .removeClass('icon-video') .addClass('icon-video-off'); } else { $container.find('.avatar-container').hide(); $container.find('video').show(); $hideRemoteVideoButton.attr('data-original-title', t('spreed', 'Disable video')) .removeClass('icon-video-off') .addClass('icon-video'); } if (latestSpeakerId === id) { OCA.SpreedMe.speakers.updateVideoContainerDummy(id); } }, remove: function(id) { if (!(typeof id === 'string' || id instanceof String)) { return; } $(OCA.SpreedMe.videos.getContainerId(id)).remove(); }, addPeer: function(peer) { var signaling = OCA.SpreedMe.app.signaling; if (peer.id === webrtc.connection.getSessionid()) { console.log("Not adding video for own peer", peer); OCA.SpreedMe.videos.startSendingNick(peer); return; } var newContainer = $(OCA.SpreedMe.videos.getContainerId(peer.id)); if (newContainer.length === 0) { newContainer = $(OCA.SpreedMe.videos.add(peer.id)); } // Initialize ice restart counter for peer spreedPeerConnectionTable[peer.id] = 0; peer.pc.on('iceConnectionStateChange', function () { var userId = spreedMappingTable[peer.id]; var avatar = $(newContainer).find('.avatar'); var nameIndicator = $(newContainer).find('.nameIndicator'); var mediaIndicator = $(newContainer).find('.mediaIndicator'); avatar.removeClass('icon-loading'); mediaIndicator.find('.iceFailedIndicator').addClass('not-failed'); switch (peer.pc.iceConnectionState) { case 'checking': avatar.addClass('icon-loading'); console.log('Connecting to peer...'); break; case 'connected': case 'completed': // on caller side console.log('Connection established.'); avatar.css('opacity', '1'); // Ensure that the peer name is shown, as the name // indicator for registered users without microphone // nor camera will not be updated later. if (userId && userId.length) { nameIndicator.text(peer.nick); } // Send the current information about the video and microphone state if (!OCA.SpreedMe.webrtc.webrtc.isVideoEnabled()) { OCA.SpreedMe.webrtc.emit('videoOff'); } else { OCA.SpreedMe.webrtc.emit('videoOn'); } if (!OCA.SpreedMe.webrtc.webrtc.isAudioEnabled()) { OCA.SpreedMe.webrtc.emit('audioOff'); } else { OCA.SpreedMe.webrtc.emit('audioOn'); } if (!OC.getCurrentUser()['uid']) { var currentGuestNick = localStorage.getItem("nick"); sendDataChannelToAll('status', 'nickChanged', currentGuestNick); } // Reset ice restart counter for peer if (spreedPeerConnectionTable[peer.id] > 0) { spreedPeerConnectionTable[peer.id] = 0; } break; case 'disconnected': console.log('Disconnected.'); if (!signaling.hasFeature("mcu")) { // ICE failures will be handled in "iceFailed" // below for MCU installations. setTimeout(function() { // If the peer is still disconnected after 5 seconds we try ICE restart. if(peer.pc.iceConnectionState === 'disconnected') { avatar.addClass('icon-loading'); if (spreedPeerConnectionTable[peer.id] < 5) { if (peer.pc.pc.peerconnection.localDescription.type === 'offer' && peer.pc.pc.peerconnection.signalingState === 'stable') { spreedPeerConnectionTable[peer.id] ++; console.log('ICE restart.'); peer.icerestart(); } } } }, 5000); } break; case 'failed': console.log('Connection failed.'); if (!signaling.hasFeature("mcu")) { // ICE failures will be handled in "iceFailed" // below for MCU installations. if (spreedPeerConnectionTable[peer.id] < 5) { avatar.addClass('icon-loading'); if (peer.pc.pc.peerconnection.localDescription.type === 'offer' && peer.pc.pc.peerconnection.signalingState === 'stable') { spreedPeerConnectionTable[peer.id] ++; console.log('ICE restart.'); peer.icerestart(); } } else { console.log('ICE failed after 5 tries.'); mediaIndicator.children().hide(); mediaIndicator.find('.iceFailedIndicator').removeClass('not-failed').show(); } } break; case 'closed': console.log('Connection closed.'); break; } if (latestSpeakerId === peer.id) { OCA.SpreedMe.speakers.updateVideoContainerDummy(peer.id); } }); peer.pc.on('PeerConnectionTrace', function (event) { console.log('trace', event); }); }, // The nick name below the avatar is distributed through the // DataChannel of the PeerConnection and only sent once during // establishment. For the MCU case, the sending PeerConnection // is created once and then never changed when more participants // join. For this, we periodically send the nick to all other // participants through the sending PeerConnection. // // TODO: The name for the avatar should come from the participant // list which already has all information and get rid of using the // DataChannel for this. startSendingNick: function(peer) { if (!signaling.hasFeature("mcu")) { return; } OCA.SpreedMe.videos.stopSendingNick(peer); peer.nickInterval = setInterval(function() { var payload; var user = OC.getCurrentUser(); if (!user.uid) { payload = localStorage.getItem("nick"); } else { payload = { "name": user.displayName, "userid": user.uid }; } peer.sendDirectly('status', "nickChanged", payload); }, 1000); }, stopSendingNick: function(peer) { if (!peer.nickInterval) { return; } clearInterval(peer.nickInterval); peer.nickInterval = null; } }; OCA.SpreedMe.speakers = { switchVideoToId: function(id) { if (screenSharingActive || latestSpeakerId === id) { return; } var newContainer = $(OCA.SpreedMe.videos.getContainerId(id)); if(newContainer.find('video').length === 0) { console.warn('promote: no video found for ID', id); return; } if (latestSpeakerId !== null) { // move old video to new location var oldContainer = $(OCA.SpreedMe.videos.getContainerId(latestSpeakerId)); oldContainer.removeClass('promoted'); } newContainer.addClass('promoted'); OCA.SpreedMe.speakers.updateVideoContainerDummy(id); latestSpeakerId = id; }, unpromoteLatestSpeaker: function() { if (latestSpeakerId) { var oldContainer = $(OCA.SpreedMe.videos.getContainerId(latestSpeakerId)); oldContainer.removeClass('promoted'); unpromotedSpeakerId = latestSpeakerId; latestSpeakerId = null; $('.videoContainer-dummy').remove(); } }, updateVideoContainerDummy: function(id) { var newContainer = $(OCA.SpreedMe.videos.getContainerId(id)); $('.videoContainer-dummy').remove(); newContainer.after( $('
') .addClass('videoContainer videoContainer-dummy') .append(newContainer.find('.nameIndicator').clone()) .append(newContainer.find('.mediaIndicator').clone()) .append(newContainer.find('.speakingIndicator').clone()) ); // Cloning does not copy event handlers by default; it could be // forced with a parameter, but the tooltip would have to be // explicitly set on the new element anyway. Due to this the // click handler is explicitly copied too. $('.videoContainer-dummy').find('.hideRemoteVideo').get(0).onclick = newContainer.find('.hideRemoteVideo').get(0).onclick; $('.videoContainer-dummy').find('.hideRemoteVideo').tooltip({ placement: 'top', trigger: 'hover' }); }, add: function(id, notPromote) { if (!(typeof id === 'string' || id instanceof String)) { return; } if (notPromote) { spreedListofSpeakers[id] = 1; return; } spreedListofSpeakers[id] = (new Date()).getTime(); // set speaking class $(OCA.SpreedMe.videos.getContainerId(id)).addClass('speaking'); if (latestSpeakerId === id) { return; } OCA.SpreedMe.speakers.switchVideoToId(id); }, remove: function(id, enforce) { if (!(typeof id === 'string' || id instanceof String)) { return; } if (enforce) { delete spreedListofSpeakers[id]; } // remove speaking class $(OCA.SpreedMe.videos.getContainerId(id)).removeClass('speaking'); if (latestSpeakerId !== id) { return; } var mostRecentTime = 0, mostRecentId = null; for (var currentId in spreedListofSpeakers) { // skip loop if the property is from prototype if (!spreedListofSpeakers.hasOwnProperty(currentId)) { continue; } // skip non-string ids if (!(typeof currentId === 'string' || currentId instanceof String)) { continue; } var currentTime = spreedListofSpeakers[currentId]; if (currentTime > mostRecentTime && $(OCA.SpreedMe.videos.getContainerId(currentId)).length > 0) { mostRecentTime = currentTime; mostRecentId = currentId; } } if (mostRecentId !== null) { OCA.SpreedMe.speakers.switchVideoToId(mostRecentId); } else if (enforce === true) { // if there is no mostRecentId available, there is no user left in call // remove the remaining dummy container then too OCA.SpreedMe.speakers.unpromoteLatestSpeaker(); $('.videoContainer-dummy').remove(); } } }; OCA.SpreedMe.sharedScreens = { getContainerId: function(id) { var currentUser = OCA.SpreedMe.webrtc.connection.getSessionid(); if (currentUser === id) { return '#localScreenContainer'; } else { var sanitizedId = id.replace(/[!"#$%&'()*+,.\/:;<=>?@[\\\]^`{|}~]/g, "\\$&"); return '#container_' + sanitizedId + '_screen_incoming'; } }, switchScreenToId: function(id) { var selectedScreen = $(OCA.SpreedMe.sharedScreens.getContainerId(id)); if(selectedScreen.find('video').length === 0) { console.warn('promote: no screen video found for ID', id); return; } if(latestScreenId === id) { return; } var screenContainerId = null; for (var currentId in spreedListofSharedScreens) { // skip loop if the property is from prototype if (!spreedListofSharedScreens.hasOwnProperty(currentId)) { continue; } // skip non-string ids if (!(typeof currentId === 'string' || currentId instanceof String)) { continue; } screenContainerId = OCA.SpreedMe.sharedScreens.getContainerId(currentId); if (currentId === id) { $(screenContainerId).removeClass('hidden'); } else { $(screenContainerId).addClass('hidden'); } } // Add screen visible icon to video container $('#videos').find('.screensharingIndicator').removeClass('screen-visible'); $(OCA.SpreedMe.videos.getContainerId(id)).find('.screensharingIndicator').addClass('screen-visible'); latestScreenId = id; }, add: function(id) { if (!(typeof id === 'string' || id instanceof String)) { return; } spreedListofSharedScreens[id] = (new Date()).getTime(); var currentUser = OCA.SpreedMe.webrtc.connection.getSessionid(); if (currentUser !== id) { var screensharingIndicator = $(OCA.SpreedMe.videos.getContainerId(id)).find('.screensharingIndicator'); screensharingIndicator.removeClass('screen-off'); screensharingIndicator.addClass('screen-on'); screensharingIndicator.click(function() { if (!this.classList.contains('screen-visible')) { OCA.SpreedMe.sharedScreens.switchScreenToId(id); } $(this).tooltip('hide'); }); } OCA.SpreedMe.sharedScreens.switchScreenToId(id); }, remove: function(id) { if (!(typeof id === 'string' || id instanceof String)) { return; } delete spreedListofSharedScreens[id]; var screensharingIndicator = $(OCA.SpreedMe.videos.getContainerId(id)).find('.screensharingIndicator'); screensharingIndicator.addClass('screen-off'); screensharingIndicator.removeClass('screen-on'); var mostRecentTime = 0, mostRecentId = null; for (var currentId in spreedListofSharedScreens) { // skip loop if the property is from prototype if (!spreedListofSharedScreens.hasOwnProperty(currentId)) { continue; } // skip non-string ids if (!(typeof currentId === 'string' || currentId instanceof String)) { continue; } var currentTime = spreedListofSharedScreens[currentId]; if (currentTime > mostRecentTime) { mostRecentTime = currentTime; mostRecentId = currentId; } } if (mostRecentId !== null) { OCA.SpreedMe.sharedScreens.switchScreenToId(mostRecentId); } } }; OCA.SpreedMe.webrtc.on('createdPeer', function (peer) { console.log('PEER CREATED', peer); if (peer.type === 'video') { OCA.SpreedMe.videos.addPeer(peer); // Make sure required data channels exist for all peers. This // is required for peers that get created by SimpleWebRTC from // received "Offer" messages. Otherwise the "channelMessage" // will not be called. peer.getDataChannel('status'); } }); function checkPeerMedia(peer, track, mediaType) { var defer = $.Deferred(); peer.pc.pc.getStats(track, function(stats) { var result = false; Object.keys(stats).forEach(function(key) { var value = stats[key]; if (!result && !value || value.mediaType !== mediaType || !value.hasOwnProperty('bytesReceived')) { return; } if (value.bytesReceived > 0) { OCA.SpreedMe.webrtc.emit('unmute', { id: peer.id, name: mediaType }); result = true; } }); if (result) { defer.resolve(); } else { defer.reject(); } }); return defer; } function stopPeerCheckMedia(peer) { if (peer.check_audio_interval) { clearInterval(peer.check_audio_interval); peer.check_audio_interval = null; } if (peer.check_video_interval) { clearInterval(peer.check_video_interval); peer.check_video_interval = null; } OCA.SpreedMe.videos.stopSendingNick(peer); } function startPeerCheckMedia(peer, stream) { stopPeerCheckMedia(peer); peer.check_video_interval = setInterval(function() { stream.getVideoTracks().forEach(function(video) { checkPeerMedia(peer, video, 'video').then(function() { clearInterval(peer.check_video_interval); peer.check_video_interval = null; }); }); }, 1000); peer.check_audio_interval = setInterval(function() { stream.getAudioTracks().forEach(function(audio) { checkPeerMedia(peer, audio, 'audio').then(function() { clearInterval(peer.check_audio_interval); peer.check_audio_interval = null; }); }); }, 1000); } OCA.SpreedMe.webrtc.on('peerStreamAdded', function (peer) { // With the MCU, a newly subscribed stream might not get the // "audioOn"/"videoOn" messages as they are only sent when // a user starts publishing. Instead wait for initial data // and trigger events locally. if (!OCA.SpreedMe.app.signaling.hasFeature("mcu")) { return; } startPeerCheckMedia(peer, peer.stream); }); OCA.SpreedMe.webrtc.on('peerStreamRemoved', function (peer) { stopPeerCheckMedia(peer); }); OCA.SpreedMe.webrtc.on('localScreenStopped', function() { app.disableScreensharingButton(); }); OCA.SpreedMe.webrtc.webrtc.on('iceFailed', function (/* peer */) { var signaling = OCA.SpreedMe.app.signaling; if (!signaling.hasFeature("mcu")) { // ICE restarts will be handled by "iceConnectionStateChange" // above. return; } // For now assume the connection to the MCU is interrupted on ICE // failures and force a reconnection of all streams. if (ownPeer) { OCA.SpreedMe.webrtc.removePeers(ownPeer.id); OCA.SpreedMe.speakers.remove(ownPeer.id, true); OCA.SpreedMe.videos.remove(ownPeer.id); delete spreedMappingTable[ownPeer.id]; ownPeer.end(); ownPeer = null; } usersChanged(signaling, [], previousUsersInRoom); usersInCallMapping = {}; previousUsersInRoom = []; // Reconnects with a new session id will trigger "usersChanged" // with the users in the room and that will re-establish the // peerconnection streams. signaling.forceReconnect(true); }); OCA.SpreedMe.webrtc.on('localMediaStarted', function (configuration) { console.log('localMediaStarted'); app.startLocalMedia(configuration); hasLocalMedia = true; var signaling = OCA.SpreedMe.app.signaling; if (signaling.hasFeature("mcu")) { checkStartPublishOwnPeer(signaling); } }); OCA.SpreedMe.webrtc.on('localMediaError', function(error) { console.log('Access to microphone & camera failed', error); hasLocalMedia = false; var message; if (error.name === "NotAllowedError") { if (error.message && error.message.indexOf("Only secure origins") !== -1) { message = t('spreed', 'Access to microphone & camera is only possible with HTTPS'); message += ': ' + t('spreed', 'Please move your setup to HTTPS'); } else { message = t('spreed', 'Access to microphone & camera was denied'); } } else if(!OCA.SpreedMe.webrtc.capabilities.support) { console.log('WebRTC not supported'); message = t('spreed', 'WebRTC is not supported in your browser'); message += ': ' + t('spreed', 'Please use a different browser like Firefox or Chrome'); } else { message = t('spreed', 'Error while accessing microphone & camera'); console.log('Error while accessing microphone & camera: ', error.message || error.name); } app.startWithoutLocalMedia(webrtc.webrtc.isAudioEnabled(), webrtc.webrtc.isVideoEnabled()); OC.Notification.show(message, { type: 'error', timeout: 15, }); app.restoreEmptyContent(); }); if(!OCA.SpreedMe.webrtc.capabilities.support) { console.log('WebRTC not supported'); OCA.SpreedMe.app.setEmptyContentMessage( 'icon-video-off', t('spreed', 'WebRTC is not supported in your browser :-/'), t('spreed', 'Please use a different browser like Firefox or Chrome') ); } OCA.SpreedMe.webrtc.on('channelOpen', function(channel) { console.log('%s datachannel is open', channel.label); }); OCA.SpreedMe.webrtc.on('channelMessage', function (peer, label, data) { if (label === 'status') { if(data.type === 'speaking') { OCA.SpreedMe.speakers.add(peer.id); } else if(data.type === 'stoppedSpeaking') { OCA.SpreedMe.speakers.remove(peer.id); } else if(data.type === 'audioOn') { OCA.SpreedMe.webrtc.emit('unmute', {id: peer.id, name:'audio'}); } else if(data.type === 'audioOff') { OCA.SpreedMe.webrtc.emit('mute', {id: peer.id, name:'audio'}); } else if(data.type === 'videoOn') { OCA.SpreedMe.webrtc.emit('unmute', {id: peer.id, name:'video'}); } else if(data.type === 'videoOff') { OCA.SpreedMe.webrtc.emit('mute', {id: peer.id, name:'video'}); } else if (data.type === 'nickChanged') { var payload = data.payload || ''; if (typeof(payload) === 'string') { OCA.SpreedMe.webrtc.emit('nick', {id: peer.id, name:data.payload}); app._messageCollection.updateGuestName(new Hashes.SHA1().hex(peer.id), data.payload); } else { OCA.SpreedMe.webrtc.emit('nick', {id: peer.id, name: payload.name, userid: payload.userid}); } } } else if (label === 'hark') { // Ignore messages from hark datachannel } else { console.log('Uknown message from %s datachannel', label, data); } }); OCA.SpreedMe.webrtc.on('videoAdded', function(video, peer) { console.log('VIDEO ADDED', peer); if (peer.type === 'screen') { OCA.SpreedMe.webrtc.emit('screenAdded', video, peer); return; } var videoContainer = $(OCA.SpreedMe.videos.getContainerId(peer.id)); if (videoContainer.length) { var userId = spreedMappingTable[peer.id]; var guestName = guestNamesTable[peer.id]; var nameIndicator = videoContainer.find('.nameIndicator'); var avatar = videoContainer.find('.avatar'); if (userId && userId.length) { avatar.avatar(userId, 128); nameIndicator.text(peer.nick); } else { avatar.imageplaceholder('?', peer.nick || guestName, 128); avatar.css('background-color', '#b9b9b9'); nameIndicator.text(peer.nick || guestName || t('spreed', 'Guest')); } $(videoContainer).prepend(video); video.oncontextmenu = function() { return false; }; } var otherSpeakerPromoted = false; for (var key in spreedListofSpeakers) { if (spreedListofSpeakers.hasOwnProperty(key) && spreedListofSpeakers[key] > 1) { otherSpeakerPromoted = true; break; } } if (!otherSpeakerPromoted) { OCA.SpreedMe.speakers.add(peer.id); } else { OCA.SpreedMe.speakers.add(peer.id, true); } }); OCA.SpreedMe.webrtc.on('speaking', function(){ sendDataChannelToAll('status', 'speaking'); $('#localVideoContainer').addClass('speaking'); }); OCA.SpreedMe.webrtc.on('stoppedSpeaking', function(){ sendDataChannelToAll('status', 'stoppedSpeaking'); $('#localVideoContainer').removeClass('speaking'); }); // a peer was removed OCA.SpreedMe.webrtc.on('videoRemoved', function(video, peer) { if (peer) { if (peer.type === 'video') { // a removed peer can't speak anymore ;) OCA.SpreedMe.speakers.remove(peer.id, true); var videoContainer = document.getElementById('container_' + OCA.SpreedMe.webrtc.getDomId(peer)); var el = document.getElementById(OCA.SpreedMe.webrtc.getDomId(peer)); if (videoContainer && el) { videoContainer.removeChild(el); } } else if (peer.type === 'screen') { var remotes = document.getElementById('screens'); var screenContainer = document.getElementById('container_' + OCA.SpreedMe.webrtc.getDomId(peer)); if (remotes && screenContainer) { remotes.removeChild(screenContainer); } OCA.SpreedMe.sharedScreens.remove(peer.id); } } else if (video.id === 'localScreen') { // SimpleWebRTC notifies about stopped screensharing through // the generic "videoRemoved" API, but the stream must be // handled differently. OCA.SpreedMe.webrtc.emit('localScreenStopped'); var screens = document.getElementById('screens'); var localScreenContainer = document.getElementById('localScreenContainer'); if (screens && localScreenContainer) { screens.removeChild(localScreenContainer); } OCA.SpreedMe.sharedScreens.remove(OCA.SpreedMe.webrtc.connection.getSessionid()); } // Check if there are still some screens if (!document.getElementById('screens').hasChildNodes()) { screenSharingActive = false; $('#app-content').removeClass('screensharing'); if (unpromotedSpeakerId) { OCA.SpreedMe.speakers.switchVideoToId(unpromotedSpeakerId); unpromotedSpeakerId = null; } } }); // Send the audio on and off events via data channel OCA.SpreedMe.webrtc.on('audioOn', function() { sendDataChannelToAll('status', 'audioOn'); }); OCA.SpreedMe.webrtc.on('audioOff', function() { sendDataChannelToAll('status', 'audioOff'); }); OCA.SpreedMe.webrtc.on('videoOn', function() { sendDataChannelToAll('status', 'videoOn'); }); OCA.SpreedMe.webrtc.on('videoOff', function() { sendDataChannelToAll('status', 'videoOff'); }); OCA.SpreedMe.webrtc.on('screenAdded', function(video, peer) { OCA.SpreedMe.speakers.unpromoteLatestSpeaker(); screenSharingActive = true; $('#app-content').addClass('screensharing'); var screens = document.getElementById('screens'); if (screens) { // Indicator for username var userIndicator = document.createElement('div'); userIndicator.className = 'nameIndicator'; if (peer) { var guestName = guestNamesTable[peer.id]; if (peer.nick) { userIndicator.textContent = t('spreed', "{participantName}'s screen", {participantName: peer.nick}); } else if (guestName && guestName.length > 0) { userIndicator.textContent = t('spreed', "{participantName}'s screen", {participantName: guestName}); } else { userIndicator.textContent = t('spreed', "Guest's screen"); } } else { userIndicator.textContent = t('spreed', 'Your screen'); } // Generic container var container = document.createElement('div'); container.className = 'screenContainer'; container.id = peer ? 'container_' + OCA.SpreedMe.webrtc.getDomId(peer) : 'localScreenContainer'; container.appendChild(video); container.appendChild(userIndicator); video.oncontextmenu = function() { return false; }; $(container).prependTo($('#screens')); if (peer) { OCA.SpreedMe.sharedScreens.add(peer.id); } else { OCA.SpreedMe.sharedScreens.add(OCA.SpreedMe.webrtc.connection.getSessionid()); } } }); // Local screen added. OCA.SpreedMe.webrtc.on('localScreenAdded', function(video) { OCA.SpreedMe.webrtc.emit('screenAdded', video, null); var signaling = OCA.SpreedMe.app.signaling; var currentSessionId = signaling.getSessionid(); for (var sessionId in usersInCallMapping) { if (!usersInCallMapping.hasOwnProperty(sessionId)) { continue; } else if (sessionId === currentSessionId) { // Running with MCU, no need to create screensharing // subscriber for client itself. continue; } createScreensharingPeer(signaling, sessionId); } }); OCA.SpreedMe.webrtc.on('localScreenStopped', function() { var signaling = OCA.SpreedMe.app.signaling; if (!signaling.hasFeature('mcu')) { // Only need to notify clients here if running with MCU. // Otherwise SimpleWebRTC will notify each client on its own. return; } var currentSessionId = signaling.getSessionid(); OCA.SpreedMe.webrtc.getPeers().forEach(function(existingPeer) { if (ownScreenPeer && existingPeer.type === 'screen' && existingPeer.id === currentSessionId) { ownScreenPeer = null; existingPeer.end(); signaling.sendRoomMessage({ roomType: 'screen', type: 'unshareScreen' }); } }); }); // Peer changed nick OCA.SpreedMe.webrtc.on('nick', function(data) { // Video var video = document.getElementById('container_' + OCA.SpreedMe.webrtc.getDomId({ id: data.id, type: 'video', broadcaster: false })); var videoNameIndicator = $(video).find('.nameIndicator'); var videoAvatar = $(video).find('.avatar'); //Screen var screen = document.getElementById('container_' + OCA.SpreedMe.webrtc.getDomId({ id: data.id, type: 'screen', broadcaster: false })); var screenNameIndicator = $(screen).find('.nameIndicator'); if (!data.name) { screenNameIndicator.text(t('spreed', "Guest's screen")); } else { screenNameIndicator.text(t('spreed', "{participantName}'s screen", {participantName: data.name})); if (!data.userid) { guestNamesTable[data.id] = data.name; } } videoNameIndicator.text(data.name || t('spreed', 'Guest')); if (data.userid) { videoAvatar.avatar(data.userid, 128); } else { videoAvatar.imageplaceholder('?', data.name, 128); videoAvatar.css('background-color', '#b9b9b9'); } if (latestSpeakerId === data.id) { OCA.SpreedMe.speakers.updateVideoContainerDummy(data.id); } }); // Peer is muted OCA.SpreedMe.webrtc.on('mute', function(data) { var el = document.getElementById('container_' + OCA.SpreedMe.webrtc.getDomId({ id: data.id, type: 'video', broadcaster: false })); var $el = $(el); if (data.name === 'video') { OCA.SpreedMe.videos.muteRemoteVideo(data.id); } else { var muteIndicator = $el.find('.muteIndicator'); muteIndicator.removeClass('audio-on'); muteIndicator.addClass('audio-off'); $el.removeClass('speaking'); } if (latestSpeakerId === data.id) { OCA.SpreedMe.speakers.updateVideoContainerDummy(data.id); } }); // Peer is umuted OCA.SpreedMe.webrtc.on('unmute', function(data) { var el = document.getElementById('container_' + OCA.SpreedMe.webrtc.getDomId({ id: data.id, type: 'video', broadcaster: false })); var $el = $(el); if (data.name === 'video') { OCA.SpreedMe.videos.unmuteRemoteVideo(data.id); } else { var muteIndicator = $el.find('.muteIndicator'); muteIndicator.removeClass('audio-off'); muteIndicator.addClass('audio-on'); } if (latestSpeakerId === data.id) { OCA.SpreedMe.speakers.updateVideoContainerDummy(data.id); } }); OCA.SpreedMe.webrtc.on('localStream', function() { console.log('localStream'); if (!app.videoWasEnabledAtLeastOnce) { app.videoWasEnabledAtLeastOnce = true; } //Reset audio and video control panel app.hasAudio(); app.hasVideo(); if (!app.videoDisabled) { app.enableVideo(); } if (!OCA.SpreedMe.webrtc.webrtc.isAudioEnabled()) { app.disableAudio(); app.hasNoAudio(); } if (!OCA.SpreedMe.webrtc.webrtc.isVideoEnabled()) { app.disableVideo(); app.hasNoVideo(); } }); } OCA.SpreedMe.initWebRTC = initWebRTC; })(OCA, OC);