Adds desktop streaming using Chrome extension. Does not flip local screen preview.

This commit is contained in:
paweldomas 2014-03-17 10:02:40 +01:00
parent 0da0f865a1
commit 452704d6b3
7 changed files with 221 additions and 76 deletions

146
app.js
View File

@ -9,6 +9,7 @@ var sharedKey = '';
var roomUrl = null; var roomUrl = null;
var ssrc2jid = {}; var ssrc2jid = {};
var localVideoSrc = null; var localVideoSrc = null;
var flipXLocalVideo = true;
var preziPlayer = null; var preziPlayer = null;
/* window.onbeforeunload = closePageWarning; */ /* window.onbeforeunload = closePageWarning; */
@ -71,7 +72,7 @@ function audioStreamReady(stream) {
function videoStreamReady(stream) { function videoStreamReady(stream) {
change_local_video(stream); change_local_video(stream, true);
doJoin(); doJoin();
} }
@ -134,27 +135,37 @@ function change_local_audio(stream) {
document.getElementById('localAudio').volume = 0; document.getElementById('localAudio').volume = 0;
} }
function change_local_video(stream) { function change_local_video(stream, flipX) {
connection.jingle.localVideo = stream; connection.jingle.localVideo = stream;
RTC.attachMediaStream($('#localVideo'), stream);
document.getElementById('localVideo').autoplay = true;
document.getElementById('localVideo').volume = 0;
localVideoSrc = document.getElementById('localVideo').src; var localVideo = document.createElement('video');
updateLargeVideo(localVideoSrc, true, 0); localVideo.id = 'localVideo_'+stream.id;
localVideo.autoplay = true;
localVideo.volume = 0; // is it required if audio is separated ?
localVideo.oncontextmenu = function () { return false; };
$('#localVideo').click(function () { var localVideoContainer = document.getElementById('localVideoContainer');
$(document).trigger("video.selected", [false]); localVideoContainer.appendChild(localVideo);
updateLargeVideo($(this).attr('src'), true, 0);
$('video').each(function (idx, el) { var localVideoSelector = $('#' + localVideo.id);
if (el.id.indexOf('mixedmslabel') !== -1) { // Add click handler
el.volume = 0; localVideoSelector.click(function () { handleVideoThumbClicked(localVideo.src); } );
el.volume = 1; // Add stream ended handler
} stream.onended = function () {
}); localVideoContainer.removeChild(localVideo);
}); checkChangeLargeVideo(localVideo.src);
};
// Flip video x axis if needed
flipXLocalVideo = flipX;
if(flipX) {
localVideoSelector.addClass("flipVideoX");
}
// Attach WebRTC stream
RTC.attachMediaStream(localVideoSelector, stream);
localVideoSrc = localVideo.src;
updateLargeVideo(localVideoSrc, 0);
} }
$(document).bind('remotestreamadded.jingle', function (event, data, sid) { $(document).bind('remotestreamadded.jingle', function (event, data, sid) {
@ -243,42 +254,27 @@ $(document).bind('remotestreamadded.jingle', function (event, data, sid) {
data.stream.onended = function () { data.stream.onended = function () {
console.log('stream ended', this.id); console.log('stream ended', this.id);
if (sel.attr('src') === $('#largeVideo').attr('src')) {
// this is currently displayed as large
// pick the last visible video in the row
// if nobody else is left, this picks the local video
var pick = $('#remoteVideos>span[id!="mixedstream"]:visible:last>video').get(0);
// mute if localvideo
var isLocalVideo = false;
if (pick) {
if (pick.src === localVideoSrc)
isLocalVideo = true;
updateLargeVideo(pick.src, isLocalVideo, pick.volume);
}
}
// Mark video as removed to cancel waiting loop(if video is removed before has started) // Mark video as removed to cancel waiting loop(if video is removed before has started)
sel.removed = true; sel.removed = true;
sel.remove();
var userContainer = sel.parent(); var audioCount = $('#'+container.id+'>audio').length;
if(userContainer.children().length === 0) { var videoCount = $('#'+container.id+'>video').length;
if(!audioCount && !videoCount) {
console.log("Remove whole user"); console.log("Remove whole user");
// Remove whole container // Remove whole container
userContainer.remove(); container.remove();
Util.playSoundNotification('userLeft'); Util.playSoundNotification('userLeft');
resizeThumbnails(); resizeThumbnails();
} else {
// Remove only stream holder
sel.remove();
console.log("Remove stream only", sel);
} }
checkChangeLargeVideo(vid.src);
}; };
sel.click(
function () { // Add click handler
$(document).trigger("video.selected", [false]); sel.click(function () { handleVideoThumbClicked(vid.src); });
updateLargeVideo($(this).attr('src'), false, 1);
}
);
// an attempt to work around https://github.com/jitsi/jitmeet/issues/32 // an attempt to work around https://github.com/jitsi/jitmeet/issues/32
if (isVideo if (isVideo
&& data.peerjid && sess.peerjid === data.peerjid && && data.peerjid && sess.peerjid === data.peerjid &&
@ -290,6 +286,46 @@ $(document).bind('remotestreamadded.jingle', function (event, data, sid) {
} }
}); });
function handleVideoThumbClicked(videoSrc) {
$(document).trigger("video.selected", [false]);
updateLargeVideo(videoSrc, 1);
$('audio').each(function (idx, el) {
// We no longer mix so we check for local audio now
if(el.id != 'localAudio') {
el.volume = 0;
el.volume = 1;
}
});
}
/**
* Checks if removed video is currently displayed and tries to display another one instead.
* @param removedVideoSrc src stream identifier of the video.
*/
function checkChangeLargeVideo(removedVideoSrc){
if (removedVideoSrc === $('#largeVideo').attr('src')) {
// this is currently displayed as large
// pick the last visible video in the row
// if nobody else is left, this picks the local video
var pick = $('#remoteVideos>span[id!="mixedstream"]:visible:last>video').get(0);
if(!pick) {
console.info("Last visible video no longer exists");
pick = $('#remoteVideos>span[id!="mixedstream"]>video').get(0);
}
// mute if localvideo
if (pick) {
updateLargeVideo(pick.src, pick.volume);
} else {
console.warn("Failed to elect large video");
}
}
}
// an attempt to work around https://github.com/jitsi/jitmeet/issues/32 // an attempt to work around https://github.com/jitsi/jitmeet/issues/32
function sendKeyframe(pc) { function sendKeyframe(pc) {
console.log('sendkeyframe', pc.iceConnectionState); console.log('sendkeyframe', pc.iceConnectionState);
@ -404,7 +440,7 @@ $(document).bind('callactive.jingle', function (event, videoelem, sid) {
videoelem.show(); videoelem.show();
resizeThumbnails(); resizeThumbnails();
updateLargeVideo(videoelem.attr('src'), false, 1); updateLargeVideo(videoelem.attr('src'), 1);
showFocusIndicator(); showFocusIndicator();
} }
@ -566,6 +602,8 @@ $(document).bind('presence.muc', function (event, jid, info, pres) {
break; break;
case 'recvonly': case 'recvonly':
el.hide(); el.hide();
// FIXME: Check if we have to change large video
//checkChangeLargeVideo(el);
break; break;
} }
} }
@ -751,23 +789,27 @@ function isPresentationVisible() {
/** /**
* Updates the large video with the given new video source. * Updates the large video with the given new video source.
*/ */
function updateLargeVideo(newSrc, localVideo, vol) { function updateLargeVideo(newSrc, vol) {
console.log('hover in', newSrc); console.log('hover in', newSrc);
setPresentationVisible(false); setPresentationVisible(false);
if ($('#largeVideo').attr('src') !== newSrc) { if ($('#largeVideo').attr('src') !== newSrc) {
document.getElementById('largeVideo').volume = vol; // FIXME: is it still required ? audio is separated
//document.getElementById('largeVideo').volume = vol;
$('#largeVideo').fadeOut(300, function () { $('#largeVideo').fadeOut(300, function () {
$(this).attr('src', newSrc); $(this).attr('src', newSrc);
// Screen stream is already rotated
var flipX = (newSrc === localVideoSrc) && flipXLocalVideo;
var videoTransform = document.getElementById('largeVideo').style.webkitTransform; var videoTransform = document.getElementById('largeVideo').style.webkitTransform;
if (localVideo && videoTransform !== 'scaleX(-1)') { if (flipX && videoTransform !== 'scaleX(-1)') {
document.getElementById('largeVideo').style.webkitTransform = "scaleX(-1)"; document.getElementById('largeVideo').style.webkitTransform = "scaleX(-1)";
} }
else if (!localVideo && videoTransform === 'scaleX(-1)') { else if (!flipX && videoTransform === 'scaleX(-1)') {
document.getElementById('largeVideo').style.webkitTransform = "none"; document.getElementById('largeVideo').style.webkitTransform = "none";
} }
@ -1203,8 +1245,14 @@ function showToolbar() {
// TODO: Enable settings functionality. Need to uncomment the settings button in index.html. // TODO: Enable settings functionality. Need to uncomment the settings button in index.html.
// $('#settingsButton').css({visibility:"visible"}); // $('#settingsButton').css({visibility:"visible"});
} }
showDesktopSharingButton();
}
function showDesktopSharingButton() {
if(isDesktopSharingEnabled()) { if(isDesktopSharingEnabled()) {
$('#desktopsharing').css({display:"inline"}); $('#desktopsharing').css( {display:"inline"} );
} else {
$('#desktopsharing').css( {display:"none"} );
} }
} }

View File

@ -9,5 +9,6 @@ var config = {
// useIPv6: true, // ipv6 support. use at your own risk // useIPv6: true, // ipv6 support. use at your own risk
useNicks: false, useNicks: false,
bosh: '//lambada.jitsi.net/http-bind', // FIXME: use xep-0156 for that bosh: '//lambada.jitsi.net/http-bind', // FIXME: use xep-0156 for that
chromeDesktopSharing: false // Desktop sharing is disabled by default desktopSharing: false, // Desktop sharing is disabled by default(call setDesktopSharing in the console to enable)
chromeExtensionId: 'nhkhigmiepmkogopmkfipjlfkeablnch' // Id of Jitsi Desktop Streamer chrome extension
}; };

View File

@ -49,7 +49,7 @@ html, body{
font-size: 10pt; font-size: 10pt;
} }
#localVideo { .flipVideoX {
-moz-transform: scaleX(-1); -moz-transform: scaleX(-1);
-webkit-transform: scaleX(-1); -webkit-transform: scaleX(-1);
-o-transform: scaleX(-1); -o-transform: scaleX(-1);

View File

@ -9,13 +9,43 @@ var isUsingScreenStream = false;
*/ */
var switchInProgress = false; var switchInProgress = false;
/**
* Method used to get screen sharing stream.
*
* @type {function(stream_callback, failure_callback}
*/
var obtainDesktopStream = obtainScreenFromExtension;
/**
* Desktop sharing must be enabled in config and works on chrome only.
*/
var desktopSharingEnabled = config.desktopSharing;
/** /**
* @returns {boolean} <tt>true</tt> if desktop sharing feature is available and enabled. * @returns {boolean} <tt>true</tt> if desktop sharing feature is available and enabled.
*/ */
function isDesktopSharingEnabled() { function isDesktopSharingEnabled() {
// Desktop sharing must be enabled in config and works on chrome only. return desktopSharingEnabled;
// Flag 'chrome://flags/#enable-usermedia-screen-capture' must be enabled. }
return config.chromeDesktopSharing && RTC.browser == 'chrome';
/**
* Call this method to toggle desktop sharing feature.
* @param method pass "ext" to use chrome extension for desktop capture(chrome extension required),
* pass "webrtc" to use WebRTC "screen" desktop source('chrome://flags/#enable-usermedia-screen-capture'
* must be enabled), pass any other string or nothing in order to disable this feature completely.
*/
function setDesktopSharing(method) {
if(method == "ext") {
obtainDesktopStream = obtainScreenFromExtension;
desktopSharingEnabled = true;
} else if(method == "webrtc") {
obtainDesktopStream = obtainWebRTCScreen;
desktopSharingEnabled = true;
} else {
obtainDesktopStream = null;
desktopSharingEnabled = false;
}
showDesktopSharingButton();
} }
/* /*
@ -25,7 +55,8 @@ function toggleScreenSharing() {
if (!(connection && connection.connected if (!(connection && connection.connected
&& !switchInProgress && !switchInProgress
&& getConferenceHandler().peerconnection.signalingState == 'stable' && getConferenceHandler().peerconnection.signalingState == 'stable'
&& getConferenceHandler().peerconnection.iceConnectionState == 'connected')) { && getConferenceHandler().peerconnection.iceConnectionState == 'connected'
&& obtainDesktopStream )) {
return; return;
} }
switchInProgress = true; switchInProgress = true;
@ -33,22 +64,29 @@ function toggleScreenSharing() {
// Only the focus is able to set a shared key. // Only the focus is able to set a shared key.
if(!isUsingScreenStream) if(!isUsingScreenStream)
{ {
// Enable screen stream obtainDesktopStream(
getUserMediaWithConstraints( function(stream) {
['screen'], // We now use screen stream
function(stream){
isUsingScreenStream = true; isUsingScreenStream = true;
gotScreenStream(stream); // Hook 'ended' event to restore camera when screen stream stops
stream.addEventListener('ended',
function(e) {
if(!switchInProgress) {
toggleScreenSharing();
}
}
);
newStreamCreated(stream);
}, },
getSwitchStreamFailed getSwitchStreamFailed );
);
} else { } else {
// Disable screen stream // Disable screen stream
getUserMediaWithConstraints( getUserMediaWithConstraints(
['video'], ['video'],
function(stream) { function(stream) {
// We are now using camera stream
isUsingScreenStream = false; isUsingScreenStream = false;
gotScreenStream(stream); newStreamCreated(stream);
}, },
getSwitchStreamFailed, config.resolution || '360' getSwitchStreamFailed, config.resolution || '360'
); );
@ -60,18 +98,64 @@ function getSwitchStreamFailed(error) {
switchInProgress = false; switchInProgress = false;
} }
function gotScreenStream(stream) { function newStreamCreated(stream) {
var oldStream = connection.jingle.localVideo; var oldStream = connection.jingle.localVideo;
change_local_video(stream); change_local_video(stream, !isUsingScreenStream);
// FIXME: will block switchInProgress on true value in case of exception // FIXME: will block switchInProgress on true value in case of exception
getConferenceHandler().switchStreams(stream, oldStream, onDesktopStreamEnabled); getConferenceHandler().switchStreams(
stream, oldStream,
function() {
// Switch operation has finished
switchInProgress = false;
});
} }
function onDesktopStreamEnabled() { /**
// Wait a moment before enabling the button * Method obtains desktop stream from WebRTC 'screen' source.
window.setTimeout(function() { * Flag 'chrome://flags/#enable-usermedia-screen-capture' must be enabled.
switchInProgress = false; */
}, 3000); function obtainWebRTCScreen(streamCallback, failCallback) {
getUserMediaWithConstraints(
['screen'],
streamCallback,
failCallback
);
}
/**
* Asks Chrome extension to call chooseDesktopMedia and gets chrome 'desktop' stream for returned stream token.
*/
function obtainScreenFromExtension(streamCallback, failCallback) {
// Check for extension API
if(!chrome || !chrome.runtime) {
failCallback("Failed to communicate with extension - no API available");
return;
}
// Sends 'getStream' msg to the extension. Extension id must be defined in the config.
chrome.runtime.sendMessage(
config.chromeExtensionId,
{ getStream: true},
function(response) {
if(!response) {
failCallback(chrome.runtime.lastError);
return;
}
console.log("Response from extension: "+response);
if(response.streamId) {
getUserMediaWithConstraints(
['desktop'],
function(stream) {
streamCallback(stream);
},
failCallback,
null, null, null,
response.streamId);
} else {
failCallback("Extension failed to get the stream");
}
}
);
} }

View File

@ -14,9 +14,9 @@
<script src="//code.jquery.com/ui/1.10.4/jquery-ui.js"></script> <script src="//code.jquery.com/ui/1.10.4/jquery-ui.js"></script>
<script src="muc.js?v=9"></script><!-- simple MUC library --> <script src="muc.js?v=9"></script><!-- simple MUC library -->
<script src="estos_log.js?v=2"></script><!-- simple stanza logger --> <script src="estos_log.js?v=2"></script><!-- simple stanza logger -->
<script src="desktopsharing.js?v=1"></script><!-- desktop sharing -->
<script src="app.js?v=23"></script><!-- application logic --> <script src="app.js?v=23"></script><!-- application logic -->
<script src="chat.js?v=3"></script><!-- chat logic --> <script src="chat.js?v=3"></script><!-- chat logic -->
<script src="desktopsharing.js?v=1"></script><!-- desktop sharing logic -->
<script src="util.js?v=2"></script><!-- utility functions --> <script src="util.js?v=2"></script><!-- utility functions -->
<script src="etherpad.js?v=5"></script><!-- etherpad plugin --> <script src="etherpad.js?v=5"></script><!-- etherpad plugin -->
<script src="smileys.js?v=1"></script><!-- smiley images --> <script src="smileys.js?v=1"></script><!-- smiley images -->
@ -85,7 +85,7 @@
<div id="remoteVideos"> <div id="remoteVideos">
<span id="localVideoContainer" class="videocontainer"> <span id="localVideoContainer" class="videocontainer">
<span id="localNick"></span> <span id="localNick"></span>
<video id="localVideo" autoplay oncontextmenu="return false;" muted></video> <!--<video id="localVideo" autoplay oncontextmenu="return false;" muted></video> - is now per stream generated -->
<audio id="localAudio" autoplay oncontextmenu="return false;" muted></audio> <audio id="localAudio" autoplay oncontextmenu="return false;" muted></audio>
<span class="focusindicator"></span> <span class="focusindicator"></span>
</span> </span>

View File

@ -337,11 +337,9 @@ TraceablePeerConnection.prototype.modifySources = function(successCallback) {
switch(self.pendingop) { switch(self.pendingop) {
case 'mute': case 'mute':
sdp.media[1] = sdp.media[1].replace('a=sendrecv', 'a=recvonly'); sdp.media[1] = sdp.media[1].replace('a=sendrecv', 'a=recvonly');
console.error("MUTE");
break; break;
case 'unmute': case 'unmute':
sdp.media[1] = sdp.media[1].replace('a=recvonly', 'a=sendrecv'); sdp.media[1] = sdp.media[1].replace('a=recvonly', 'a=sendrecv');
console.error("UNMUTE");
break; break;
} }
sdp.raw = sdp.session + sdp.media.join(''); sdp.raw = sdp.session + sdp.media.join('');
@ -502,7 +500,7 @@ function setupRTC() {
return RTC; return RTC;
} }
function getUserMediaWithConstraints(um, success_callback, failure_callback, resolution, bandwidth, fps) { function getUserMediaWithConstraints(um, success_callback, failure_callback, resolution, bandwidth, fps, desktopStream) {
var constraints = {audio: false, video: false}; var constraints = {audio: false, video: false};
if (um.indexOf('video') >= 0) { if (um.indexOf('video') >= 0) {
@ -521,6 +519,17 @@ function getUserMediaWithConstraints(um, success_callback, failure_callback, res
} }
}; };
} }
if (um.indexOf('desktop') >= 0) {
constraints.video = {
mandatory: {
chromeMediaSource: "desktop",
chromeMediaSourceId: desktopStream,
maxWidth: window.screen.width,
maxHeight: window.screen.height,
maxFrameRate: 3
}
}
}
if (resolution && !constraints.video) { if (resolution && !constraints.video) {
constraints.video = {mandatory: {}};// same behaviour as true constraints.video = {mandatory: {}};// same behaviour as true

View File

@ -51,6 +51,9 @@ SessionBase.prototype.switchStreams = function (new_stream, oldStream, success_c
// Remember SDP to figure out added/removed SSRCs // Remember SDP to figure out added/removed SSRCs
var oldSdp = new SDP(self.peerconnection.localDescription.sdp); var oldSdp = new SDP(self.peerconnection.localDescription.sdp);
// Stop the stream to trigger onended event for old stream
oldStream.stop();
self.peerconnection.removeStream(oldStream); self.peerconnection.removeStream(oldStream);
self.connection.jingle.localVideo = new_stream; self.connection.jingle.localVideo = new_stream;