diff --git a/css/style.css b/css/style.css index 838ca7a4aa8..670801e0ec7 100644 --- a/css/style.css +++ b/css/style.css @@ -139,6 +139,9 @@ background-position-y: 8px !important; } +/** + * Video styles + */ #videos { position: absolute; @@ -248,6 +251,14 @@ video { display: block !important; } +.participants-1 #video-fullscreen { + display: none; +} + +.participants-1 #toggleScreensharing { + display: none; +} + /* big speaker video */ .participants-1 .videoContainer, .participants-2 .videoContainer, @@ -297,10 +308,23 @@ video { #app-content.participants-7, #app-content.participants-8, #app-content.participants-9, -#app-content.participants-10 { +#app-content.participants-10, +#app-content.screensharing { background-color: #000; } +#app-content.screensharing .videoContainer video { + max-height: 200px; + background-color: transparent; + box-shadow: 0; +} + +#app-content.screensharing #localScreenContainer { + height: calc(100% - 200px); + overflow: scroll; + background-color: transparent; +} + .nameIndicator { position: absolute; bottom: 0; @@ -311,7 +335,7 @@ video { text-align: center; font-size: 20px; white-space: nowrap; - overflow: hidden; + overflow: visible; text-overflow: ellipsis; } .videoView .nameIndicator { @@ -323,6 +347,17 @@ video { padding: 12px 35%; } +#video-fullscreen { + position: absolute; + right: 0px; + z-index: 90; +} + +#video-fullscreen.public { + top: 45px; +} + +#video-fullscreen, .nameIndicator button { background-color: transparent; border: none; @@ -330,8 +365,10 @@ video { height: 44px; background-size: 25px; } + .nameIndicator button.audio-disabled, -.nameIndicator button.video-disabled { +.nameIndicator button.video-disabled, +.nameIndicator button.screensharing-disabled { opacity: .7; } diff --git a/js/app.js b/js/app.js index 9a5d5efe4da..87eaab8ab9c 100644 --- a/js/app.js +++ b/js/app.js @@ -155,6 +155,11 @@ }); }); + // Initialize button tooltips + $('[data-toggle="tooltip"]').tooltip({trigger: 'hover'}).click(function() { + $(this).tooltip('hide'); + }); + $('#hideVideo').click(function() { if(!OCA.SpreedMe.app.videoWasEnabledAtLeastOnce) { // don't allow clicking the video toggle @@ -173,6 +178,7 @@ localStorage.setItem("videoDisabled", true); } }); + $('#mute').click(function() { if (OCA.SpreedMe.webrtc.webrtc.isAudioEnabled()) { OCA.SpreedMe.app.disableAudio(); @@ -197,6 +203,7 @@ } else if (fullscreenElem.msRequestFullscreen) { fullscreenElem.msRequestFullscreen(); } + $(this).attr('data-original-title', 'Exit fullscreen'); } else { if (document.exitFullscreen) { document.exitFullscreen(); @@ -207,6 +214,60 @@ } else if (document.msExitFullscreen) { document.msExitFullscreen(); } + $(this).attr('data-original-title', 'Fullscreen'); + } + }); + + var screensharingStopped = function() { + console.log("Screensharing now stopped"); + $('#toggleScreensharing').attr('data-original-title', 'Enable screensharing') + .addClass('screensharing-disabled icon-screen-off-white') + .removeClass('icon-screen-white'); + }; + + OCA.SpreedMe.webrtc.on('localScreenStopped', function() { + screensharingStopped(); + }); + + $('#toggleScreensharing').click(function() { + var webrtc = OCA.SpreedMe.webrtc; + if (!webrtc.capabilities.supportScreenSharing) { + OC.Notification.showTemporary(t('spreed', 'Screensharing is not supported by your browser.')); + return; + } + + if (webrtc.getLocalScreen()) { + webrtc.stopScreenShare(); + screensharingStopped(); + } else { + webrtc.shareScreen(function(err) { + if (!err) { + OC.Notification.showTemporary(t('spreed', 'Screensharing is about to start…')); + $('#toggleScreensharing').attr('data-original-title', 'Stop screensharing') + .removeClass('screensharing-disabled icon-screen-off-white') + .addClass('icon-screen-white'); + return; + } + + switch (err.name) { + case "HTTPS_REQUIRED": + OC.Notification.showTemporary(t('spreed', 'Screensharing requires the page to be loaded through HTTPS.')); + break; + case "PERMISSION_DENIED": + case "NotAllowedError": + case "CEF_GETSCREENMEDIA_CANCELED": // Experimental, may go away in the future. + OC.Notification.showTemporary(t('spreed', 'The screensharing request has been cancelled.')); + break; + case "EXTENSION_UNAVAILABLE": + // TODO(fancycode): Show popup with links to Chrome/Firefox extensions. + OC.Notification.showTemporary(t('spreed', 'An extension is required to start screensharing.')); + break; + default: + OC.Notification.showTemporary(t('spreed', 'An error occurred while starting screensharing.')); + console.log("Could not start screensharing", err); + break; + } + }); } }); @@ -417,7 +478,7 @@ }, enableAudio: function() { OCA.SpreedMe.webrtc.unmute(); - $('#mute').data('title', 'Mute audio') + $('#mute').attr('data-original-title', 'Mute audio') .removeClass('audio-disabled icon-audio-off-white') .addClass('icon-audio-white'); @@ -425,7 +486,7 @@ }, disableAudio: function() { OCA.SpreedMe.webrtc.mute(); - $('#mute').data('title', 'Enable audio') + $('#mute').attr('data-original-title', 'Enable audio') .addClass('audio-disabled icon-audio-off-white') .removeClass('icon-audio-white'); @@ -437,9 +498,10 @@ var localVideo = $hideVideoButton.closest('.videoView').find('#localVideo'); OCA.SpreedMe.webrtc.resumeVideo(); - $hideVideoButton.data('title', 'Disable video') + $hideVideoButton.attr('data-original-title', 'Disable video') .removeClass('video-disabled icon-video-off-white') .addClass('icon-video-white'); + avatarContainer.hide(); localVideo.show(); @@ -450,7 +512,7 @@ var avatarContainer = $hideVideoButton.closest('.videoView').find('.avatar-container'); var localVideo = $hideVideoButton.closest('.videoView').find('#localVideo'); - $hideVideoButton.data('title', 'Enable video') + $hideVideoButton.attr('data-original-title', 'Enable video') .addClass('video-disabled icon-video-off-white') .removeClass('icon-video-white'); diff --git a/js/simplewebrtc.js b/js/simplewebrtc.js index 690d9c72eed..4389df65562 100644 --- a/js/simplewebrtc.js +++ b/js/simplewebrtc.js @@ -3971,28 +3971,20 @@ var ffver = parseInt(window.navigator.userAgent.match(/Firefox\/(.*)/)[1], 10); if (ffver >= 33) { constraints = (hasConstraints && constraints) || { - video: { - mozMediaSource: 'window', - mediaSource: 'window' - } - }; - getUserMedia(constraints, function (err, stream) { - callback(err, stream); - // workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=1045810 - if (!err) { - var lastTime = stream.currentTime; - var polly = window.setInterval(function () { - if (!stream) window.clearInterval(polly); - if (stream.currentTime == lastTime) { - window.clearInterval(polly); - if (stream.onended) { - stream.onended(); - } - } - lastTime = stream.currentTime; - }, 500); + video: { + mozMediaSource: 'window', + mediaSource: 'window' } - }); + }; + // Notify extension to add domain to whitelist and defer actual + // getUserMedia call until extension finished adding the domain. + var pending = window.setTimeout(function () { + error = new Error('NavigatorUserMediaError'); + error.name = 'EXTENSION_UNAVAILABLE'; + return callback(error); + }, 1000); + cache[pending] = [callback, constraints]; + window.postMessage({ type: 'webrtcStartScreensharing', id: pending }, '*'); } else { error = new Error('NavigatorUserMediaError'); error.name = 'EXTENSION_UNAVAILABLE'; // does not make much sense but... @@ -4001,7 +3993,7 @@ }; typeof window !== 'undefined' && window.addEventListener('message', function (event) { - if (event.origin != window.location.origin) { + if (event.origin != window.location.origin && !event.isTrusted) { return; } if (event.data.type == 'gotScreen' && cache[event.data.id]) { @@ -4032,6 +4024,33 @@ } } else if (event.data.type == 'getScreenPending') { window.clearTimeout(event.data.id); + } else if (event.data.type == 'webrtcScreensharingWhitelisted' && cache[event.data.id]) { + var data = cache[event.data.id]; + window.clearTimeout(event.data.id); + var constraints = data[1]; + var callback = data[0]; + delete cache[event.data.id]; + + getUserMedia(constraints, function (err, stream) { + // Notify extension to remove domain from whitelist. + window.postMessage({ type: 'webrtcStopScreensharing' }, '*'); + callback(err, stream); + if (err) { + return; + } + // workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=1045810 + var lastTime = stream.currentTime; + var polly = window.setInterval(function () { + if (!stream) window.clearInterval(polly); + if (stream.currentTime == lastTime) { + window.clearInterval(polly); + if (stream.onended) { + stream.onended(); + } + } + lastTime = stream.currentTime; + }, 500); + }); } }); @@ -17739,6 +17758,9 @@ mLine.iceTransport.addRemoteCandidate({}); } }); + } else if (message.type === 'unshareScreen') { + this.parent.emit('unshareScreen', {id: message.from}); + this.end(); } }; @@ -18317,7 +18339,11 @@ if (this.getLocalScreen()) { this.webrtc.stopScreenShare(); } + // Notify peers were sending to. this.webrtc.peers.forEach(function (peer) { + if (peer.type === 'screen' && peer.sharemyscreen) { + peer.send('unshareScreen'); + } if (peer.broadcaster) { peer.end(); } @@ -18478,6 +18504,17 @@ } }); + this.on('unshareScreen', function(message) { + // End peers we were receiving the screensharing stream from. + var peers = self.getPeers(message.from, 'screen'); + peers.forEach(function(peer) { + if (!peer.sharemyscreen) { + peer.end(); + } + }); + }); + + // log events in debug mode if (this.config.debug) { this.on('*', function (event, val1, val2) { diff --git a/js/webrtc.js b/js/webrtc.js index 52f78dad0be..6f4a6b1562e 100644 --- a/js/webrtc.js +++ b/js/webrtc.js @@ -35,10 +35,34 @@ var spreedMappingTable = []; var appContentElement = $('#app-content'), participantsClass = 'participants-' + currentUsersNo; - if (!appContentElement.hasClass(participantsClass)) { + if (!appContentElement.hasClass(participantsClass) && !appContentElement.hasClass('screensharing')) { appContentElement.attr('class', '').addClass(participantsClass); } + //Send shared screen to new participants + var webrtc = OCA.SpreedMe.webrtc; + if (webrtc.getLocalScreen()) { + var newUsers = currentUsersInRoom.diff(previousUsersInRoom); + var currentUser = webrtc.connection.getSessionid(); + newUsers.forEach(function(user) { + if (user !== currentUser) { + var peer = webrtc.webrtc.createPeer({ + id: user, + type: 'screen', + sharemyscreen: true, + enableDataChannels: false, + receiveMedia: { + offerToReceiveAudio: 0, + offerToReceiveVideo: 0 + }, + broadcaster: currentUser, + }); + webrtc.emit('createdPeer', peer); + peer.start(); + } + }); + } + var disconnectedUsers = previousUsersInRoom.diff(currentUsersInRoom); disconnectedUsers.forEach(function(user) { console.log('XXX Remove peer', user); @@ -128,6 +152,17 @@ var spreedMappingTable = []; var spreedListofSpeakers = {}; var latestSpeakerId = null; + var screenSharingActive = false; + + window.addEventListener('resize', function() { + if (screenSharingActive) { + $('#localScreenContainer').children('video').each(function() { + $(this).width('100%'); + $(this).height($('#localScreenContainer').height()); + }); + } + }); + OCA.SpreedMe.speakers = { showStatus: function() { var data = []; @@ -153,6 +188,10 @@ var spreedMappingTable = []; return '#container_' + sanitizedId + '_type_incoming'; }, switchVideoToId: function(id) { + if (screenSharingActive) { + return; + } + var newContainer = $(OCA.SpreedMe.speakers.getContainerId(id)); if(newContainer.find('video').length === 0) { console.warn('promote: no video found for ID', id); @@ -177,6 +216,14 @@ var spreedMappingTable = []; latestSpeakerId = id; }, + unpromoteLatestSpeaker: function() { + if (latestSpeakerId) { + var oldContainer = $(OCA.SpreedMe.speakers.getContainerId(latestSpeakerId)); + oldContainer.removeClass('promoted'); + latestSpeakerId = null; + $('.videoContainer-dummy').remove(); + } + }, updateVideoContainerDummy: function(id) { var newContainer = $(OCA.SpreedMe.speakers.getContainerId(id)); @@ -350,6 +397,11 @@ var spreedMappingTable = []; OCA.SpreedMe.webrtc.on('videoAdded', function(video, peer) { console.log('video added', peer); + if (peer.type === 'screen') { + OCA.SpreedMe.webrtc.emit('localScreenAdded', video); + return; + } + var remotes = document.getElementById('videos'); if (remotes) { // Indicator for username @@ -468,6 +520,14 @@ var spreedMappingTable = []; // a peer was removed OCA.SpreedMe.webrtc.on('videoRemoved', function(video, peer) { + if (video.dataset.screensharing) { + // SimpleWebRTC notifies about stopped screensharing through + // the generic "videoRemoved" API, but the stream must be + // handled differently. + OCA.SpreedMe.webrtc.emit('localScreenRemoved', video); + return; + } + // a removed peer can't speak anymore ;) OCA.SpreedMe.speakers.remove(peer, true); @@ -492,6 +552,32 @@ var spreedMappingTable = []; OCA.SpreedMe.webrtc.sendDirectlyToAll('videoOff'); }); + // Local screen added. + OCA.SpreedMe.webrtc.on('localScreenAdded', function (video) { + var initialHeight = $('#app-content').height() - 200; + + video.style.width = '100%'; + video.style.height = initialHeight + 'px'; + + OCA.SpreedMe.speakers.unpromoteLatestSpeaker(); + + screenSharingActive = true; + $('#app-content').attr('class', '').addClass('screensharing'); + + video.dataset.screensharing = true; + document.getElementById('localScreenContainer').appendChild(video); + }); + // Local screen removed. + OCA.SpreedMe.webrtc.on('localScreenRemoved', function (video) { + document.getElementById('localScreenContainer').removeChild(video); + OCA.SpreedMe.webrtc.emit('localScreenStopped'); + + if (!document.getElementById('localScreenContainer').hasChildNodes()) { + screenSharingActive = false; + $('#app-content').removeClass('screensharing'); + } + }); + // Peer changed nick OCA.SpreedMe.webrtc.on('nick', function(data) { var el = document.getElementById('container_' + OCA.SpreedMe.webrtc.getDomId({ diff --git a/templates/index-public.php b/templates/index-public.php index 04503b66f1a..5abc235646b 100644 --- a/templates/index-public.php +++ b/templates/index-public.php @@ -24,7 +24,7 @@ ); ?> -