Merge pull request #227 from nextcloud/screensharing

Add screensharing support.
This commit is contained in:
Ivan Sein 2017-03-14 20:33:37 +01:00 коммит произвёл GitHub
Родитель c29495af9d ae9d60dbc1
Коммит d88480378f
6 изменённых файлов: 266 добавлений и 38 удалений

Просмотреть файл

@ -146,6 +146,9 @@
background-position-y: 8px !important; background-position-y: 8px !important;
} }
/**
* Video styles
*/
#videos { #videos {
position: absolute; position: absolute;
@ -264,6 +267,14 @@ video {
display: block !important; display: block !important;
} }
.participants-1 #video-fullscreen {
display: none;
}
.participants-1 #toggleScreensharing {
display: none;
}
/* big speaker video */ /* big speaker video */
.participants-1 .videoContainer, .participants-1 .videoContainer,
.participants-2 .videoContainer, .participants-2 .videoContainer,
@ -313,10 +324,23 @@ video {
#app-content.participants-7, #app-content.participants-7,
#app-content.participants-8, #app-content.participants-8,
#app-content.participants-9, #app-content.participants-9,
#app-content.participants-10 { #app-content.participants-10,
#app-content.screensharing {
background-color: #000; 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 { .nameIndicator {
position: absolute; position: absolute;
bottom: 0; bottom: 0;
@ -327,7 +351,7 @@ video {
text-align: center; text-align: center;
font-size: 20px; font-size: 20px;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: visible;
text-overflow: ellipsis; text-overflow: ellipsis;
} }
.videoView .nameIndicator { .videoView .nameIndicator {
@ -348,6 +372,17 @@ video {
padding: 12px 35%; padding: 12px 35%;
} }
#video-fullscreen {
position: absolute;
right: 0px;
z-index: 90;
}
#video-fullscreen.public {
top: 45px;
}
#video-fullscreen,
.nameIndicator button { .nameIndicator button {
background-color: transparent; background-color: transparent;
border: none; border: none;
@ -355,8 +390,10 @@ video {
height: 44px; height: 44px;
background-size: 25px; background-size: 25px;
} }
.nameIndicator button.audio-disabled, .nameIndicator button.audio-disabled,
.nameIndicator button.video-disabled { .nameIndicator button.video-disabled,
.nameIndicator button.screensharing-disabled {
opacity: .7; opacity: .7;
} }

Просмотреть файл

@ -155,6 +155,11 @@
}); });
}); });
// Initialize button tooltips
$('[data-toggle="tooltip"]').tooltip({trigger: 'hover'}).click(function() {
$(this).tooltip('hide');
});
$('#hideVideo').click(function() { $('#hideVideo').click(function() {
if(!OCA.SpreedMe.app.videoWasEnabledAtLeastOnce) { if(!OCA.SpreedMe.app.videoWasEnabledAtLeastOnce) {
// don't allow clicking the video toggle // don't allow clicking the video toggle
@ -173,6 +178,7 @@
localStorage.setItem("videoDisabled", true); localStorage.setItem("videoDisabled", true);
} }
}); });
$('#mute').click(function() { $('#mute').click(function() {
if (OCA.SpreedMe.webrtc.webrtc.isAudioEnabled()) { if (OCA.SpreedMe.webrtc.webrtc.isAudioEnabled()) {
OCA.SpreedMe.app.disableAudio(); OCA.SpreedMe.app.disableAudio();
@ -197,6 +203,7 @@
} else if (fullscreenElem.msRequestFullscreen) { } else if (fullscreenElem.msRequestFullscreen) {
fullscreenElem.msRequestFullscreen(); fullscreenElem.msRequestFullscreen();
} }
$(this).attr('data-original-title', 'Exit fullscreen');
} else { } else {
if (document.exitFullscreen) { if (document.exitFullscreen) {
document.exitFullscreen(); document.exitFullscreen();
@ -207,6 +214,60 @@
} else if (document.msExitFullscreen) { } else if (document.msExitFullscreen) {
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;
}
});
} }
}); });
@ -446,7 +507,7 @@
}, },
enableAudio: function() { enableAudio: function() {
OCA.SpreedMe.webrtc.unmute(); OCA.SpreedMe.webrtc.unmute();
$('#mute').data('title', 'Mute audio') $('#mute').attr('data-original-title', 'Mute audio')
.removeClass('audio-disabled icon-audio-off-white') .removeClass('audio-disabled icon-audio-off-white')
.addClass('icon-audio-white'); .addClass('icon-audio-white');
@ -454,7 +515,7 @@
}, },
disableAudio: function() { disableAudio: function() {
OCA.SpreedMe.webrtc.mute(); OCA.SpreedMe.webrtc.mute();
$('#mute').data('title', 'Enable audio') $('#mute').attr('data-original-title', 'Enable audio')
.addClass('audio-disabled icon-audio-off-white') .addClass('audio-disabled icon-audio-off-white')
.removeClass('icon-audio-white'); .removeClass('icon-audio-white');
@ -466,9 +527,10 @@
var localVideo = $hideVideoButton.closest('.videoView').find('#localVideo'); var localVideo = $hideVideoButton.closest('.videoView').find('#localVideo');
OCA.SpreedMe.webrtc.resumeVideo(); OCA.SpreedMe.webrtc.resumeVideo();
$hideVideoButton.data('title', 'Disable video') $hideVideoButton.attr('data-original-title', 'Disable video')
.removeClass('video-disabled icon-video-off-white') .removeClass('video-disabled icon-video-off-white')
.addClass('icon-video-white'); .addClass('icon-video-white');
avatarContainer.hide(); avatarContainer.hide();
localVideo.show(); localVideo.show();
@ -479,7 +541,7 @@
var avatarContainer = $hideVideoButton.closest('.videoView').find('.avatar-container'); var avatarContainer = $hideVideoButton.closest('.videoView').find('.avatar-container');
var localVideo = $hideVideoButton.closest('.videoView').find('#localVideo'); 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') .addClass('video-disabled icon-video-off-white')
.removeClass('icon-video-white'); .removeClass('icon-video-white');

Просмотреть файл

@ -3971,28 +3971,20 @@
var ffver = parseInt(window.navigator.userAgent.match(/Firefox\/(.*)/)[1], 10); var ffver = parseInt(window.navigator.userAgent.match(/Firefox\/(.*)/)[1], 10);
if (ffver >= 33) { if (ffver >= 33) {
constraints = (hasConstraints && constraints) || { constraints = (hasConstraints && constraints) || {
video: { video: {
mozMediaSource: 'window', mozMediaSource: 'window',
mediaSource: '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);
} }
}); };
// 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 { } else {
error = new Error('NavigatorUserMediaError'); error = new Error('NavigatorUserMediaError');
error.name = 'EXTENSION_UNAVAILABLE'; // does not make much sense but... error.name = 'EXTENSION_UNAVAILABLE'; // does not make much sense but...
@ -4001,7 +3993,7 @@
}; };
typeof window !== 'undefined' && window.addEventListener('message', function (event) { typeof window !== 'undefined' && window.addEventListener('message', function (event) {
if (event.origin != window.location.origin) { if (event.origin != window.location.origin && !event.isTrusted) {
return; return;
} }
if (event.data.type == 'gotScreen' && cache[event.data.id]) { if (event.data.type == 'gotScreen' && cache[event.data.id]) {
@ -4032,6 +4024,33 @@
} }
} else if (event.data.type == 'getScreenPending') { } else if (event.data.type == 'getScreenPending') {
window.clearTimeout(event.data.id); 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({}); mLine.iceTransport.addRemoteCandidate({});
} }
}); });
} else if (message.type === 'unshareScreen') {
this.parent.emit('unshareScreen', {id: message.from});
this.end();
} }
}; };
@ -18317,7 +18339,11 @@
if (this.getLocalScreen()) { if (this.getLocalScreen()) {
this.webrtc.stopScreenShare(); this.webrtc.stopScreenShare();
} }
// Notify peers were sending to.
this.webrtc.peers.forEach(function (peer) { this.webrtc.peers.forEach(function (peer) {
if (peer.type === 'screen' && peer.sharemyscreen) {
peer.send('unshareScreen');
}
if (peer.broadcaster) { if (peer.broadcaster) {
peer.end(); 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 // log events in debug mode
if (this.config.debug) { if (this.config.debug) {
this.on('*', function (event, val1, val2) { this.on('*', function (event, val1, val2) {

Просмотреть файл

@ -37,10 +37,34 @@ var spreedMappingTable = [];
var appContentElement = $('#app-content'), var appContentElement = $('#app-content'),
participantsClass = 'participants-' + currentUsersNo; participantsClass = 'participants-' + currentUsersNo;
if (!appContentElement.hasClass(participantsClass)) { if (!appContentElement.hasClass(participantsClass) && !appContentElement.hasClass('screensharing')) {
appContentElement.attr('class', '').addClass(participantsClass); 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); var disconnectedUsers = previousUsersInRoom.diff(currentUsersInRoom);
disconnectedUsers.forEach(function(user) { disconnectedUsers.forEach(function(user) {
console.log('XXX Remove peer', user); console.log('XXX Remove peer', user);
@ -130,6 +154,17 @@ var spreedMappingTable = [];
var spreedListofSpeakers = {}; var spreedListofSpeakers = {};
var latestSpeakerId = null; 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 = { OCA.SpreedMe.speakers = {
showStatus: function() { showStatus: function() {
var data = []; var data = [];
@ -155,6 +190,10 @@ var spreedMappingTable = [];
return '#container_' + sanitizedId + '_video_incoming'; return '#container_' + sanitizedId + '_video_incoming';
}, },
switchVideoToId: function(id) { switchVideoToId: function(id) {
if (screenSharingActive) {
return;
}
var newContainer = $(OCA.SpreedMe.speakers.getContainerId(id)); var newContainer = $(OCA.SpreedMe.speakers.getContainerId(id));
if(newContainer.find('video').length === 0) { if(newContainer.find('video').length === 0) {
console.warn('promote: no video found for ID', id); console.warn('promote: no video found for ID', id);
@ -179,6 +218,14 @@ var spreedMappingTable = [];
latestSpeakerId = id; latestSpeakerId = id;
}, },
unpromoteLatestSpeaker: function() {
if (latestSpeakerId) {
var oldContainer = $(OCA.SpreedMe.speakers.getContainerId(latestSpeakerId));
oldContainer.removeClass('promoted');
latestSpeakerId = null;
$('.videoContainer-dummy').remove();
}
},
updateVideoContainerDummy: function(id) { updateVideoContainerDummy: function(id) {
var newContainer = $(OCA.SpreedMe.speakers.getContainerId(id)); var newContainer = $(OCA.SpreedMe.speakers.getContainerId(id));
@ -347,6 +394,11 @@ var spreedMappingTable = [];
OCA.SpreedMe.webrtc.on('videoAdded', function(video, peer) { OCA.SpreedMe.webrtc.on('videoAdded', function(video, peer) {
console.log('video added', peer); console.log('video added', peer);
if (peer.type === 'screen') {
OCA.SpreedMe.webrtc.emit('localScreenAdded', video);
return;
}
var remotes = document.getElementById('videos'); var remotes = document.getElementById('videos');
if (remotes) { if (remotes) {
// Indicator for username // Indicator for username
@ -465,6 +517,14 @@ var spreedMappingTable = [];
// a peer was removed // a peer was removed
OCA.SpreedMe.webrtc.on('videoRemoved', function(video, peer) { 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 ;) // a removed peer can't speak anymore ;)
OCA.SpreedMe.speakers.remove(peer, true); OCA.SpreedMe.speakers.remove(peer, true);
@ -489,6 +549,32 @@ var spreedMappingTable = [];
OCA.SpreedMe.webrtc.sendDirectlyToAll('videoOff'); 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 // Peer changed nick
OCA.SpreedMe.webrtc.on('nick', function(data) { OCA.SpreedMe.webrtc.on('nick', function(data) {
var el = document.getElementById('container_' + OCA.SpreedMe.webrtc.getDomId({ var el = document.getElementById('container_' + OCA.SpreedMe.webrtc.getDomId({

Просмотреть файл

@ -24,7 +24,7 @@ script(
); );
?> ?>
<div id="app" data-roomId="<?php p($_['roomId']) ?>"> <div id="app" class="nc-enable-screensharing-extension" data-roomId="<?php p($_['roomId']) ?>">
<div id="app-content" class="participants-1"> <div id="app-content" class="participants-1">
<header> <header>
@ -49,6 +49,8 @@ script(
</div> </div>
</header> </header>
<button id="video-fullscreen" class="icon-fullscreen-white public" data-placement="bottom" data-toggle="tooltip" data-original-title="<?php p($l->t('Fullscreen')) ?>"></button>
<div id="video-speaking"> <div id="video-speaking">
</div> </div>
@ -59,13 +61,14 @@ script(
<div class="avatar"></div> <div class="avatar"></div>
</div> </div>
<div class="nameIndicator"> <div class="nameIndicator">
<button id="mute" class="icon-audio-white" data-title="<?php p($l->t('Mute audio')) ?>"></button> <button id="mute" class="icon-audio-white" data-placement="top" data-toggle="tooltip" data-original-title="<?php p($l->t('Mute audio')) ?>"></button>
<button id="hideVideo" class="icon-video-white" data-title="<?php p($l->t('Pause video')) ?>"></button> <button id="hideVideo" class="icon-video-white" data-placement="top" data-toggle="tooltip" data-original-title="<?php p($l->t('Disable video')) ?>"></button>
<button id="video-fullscreen" class="icon-fullscreen-white" data-title="<?php p($l->t('Fullscreen')) ?>"></button> <button id="toggleScreensharing" class="icon-screen-off-white screensharing-disabled" data-placement="top" data-toggle="tooltip" data-original-title="<?php p($l->t('Share screen')) ?>"></button>
</div> </div>
</div> </div>
</div> </div>
<div id="localScreenContainer"></div>
<div id="emptycontent"> <div id="emptycontent">
<div id="emptycontent-icon" class="icon-video"></div> <div id="emptycontent-icon" class="icon-video"></div>

Просмотреть файл

@ -24,7 +24,7 @@ script(
); );
?> ?>
<div id="app" data-roomId="<?php p($_['roomId']) ?>"> <div id="app" class="nc-enable-screensharing-extension" data-roomId="<?php p($_['roomId']) ?>">
<div id="app-navigation" class="icon-loading"> <div id="app-navigation" class="icon-loading">
<form id="oca-spreedme-add-room"> <form id="oca-spreedme-add-room">
<input id="edit-roomname" type="text" placeholder="<?php p($l->t('Choose person …')) ?>"/> <input id="edit-roomname" type="text" placeholder="<?php p($l->t('Choose person …')) ?>"/>
@ -35,6 +35,8 @@ script(
<div id="app-content" class="participants-1"> <div id="app-content" class="participants-1">
<button id="video-fullscreen" class="icon-fullscreen-white" data-placement="bottom" data-toggle="tooltip" data-original-title="<?php p($l->t('Fullscreen')) ?>"></button>
<div id="video-speaking"> <div id="video-speaking">
</div> </div>
@ -45,13 +47,14 @@ script(
<div class="avatar"></div> <div class="avatar"></div>
</div> </div>
<div class="nameIndicator"> <div class="nameIndicator">
<button id="mute" class="icon-audio-white" data-title="<?php p($l->t('Mute audio')) ?>"></button> <button id="mute" class="icon-audio-white" data-placement="top" data-toggle="tooltip" data-original-title="<?php p($l->t('Mute audio')) ?>"></button>
<button id="hideVideo" class="icon-video-white" data-title="<?php p($l->t('Pause video')) ?>"></button> <button id="hideVideo" class="icon-video-white" data-placement="top" data-toggle="tooltip" data-original-title="<?php p($l->t('Disable video')) ?>"></button>
<button id="video-fullscreen" class="icon-fullscreen-white" data-title="<?php p($l->t('Fullscreen')) ?>"></button> <button id="toggleScreensharing" class="icon-screen-off-white screensharing-disabled" data-placement="top" data-toggle="tooltip" data-original-title="<?php p($l->t('Share screen')) ?>"></button>
</div> </div>
</div> </div>
</div> </div>
<div id="localScreenContainer"></div>
<div id="emptycontent"> <div id="emptycontent">
<div id="emptycontent-icon" class="icon-video"></div> <div id="emptycontent-icon" class="icon-video"></div>