diff --git a/app.js b/app.js index a7584d099..678126688 100644 --- a/app.js +++ b/app.js @@ -9,6 +9,7 @@ var sharedKey = ''; var roomUrl = null; var ssrc2jid = {}; var localVideoSrc = null; +var flipXLocalVideo = true; var preziPlayer = null; /* window.onbeforeunload = closePageWarning; */ @@ -71,7 +72,7 @@ function audioStreamReady(stream) { function videoStreamReady(stream) { - change_local_video(stream); + change_local_video(stream, true); doJoin(); } @@ -134,27 +135,37 @@ function change_local_audio(stream) { document.getElementById('localAudio').volume = 0; } -function change_local_video(stream) { +function change_local_video(stream, flipX) { connection.jingle.localVideo = stream; - RTC.attachMediaStream($('#localVideo'), stream); - document.getElementById('localVideo').autoplay = true; - document.getElementById('localVideo').volume = 0; - localVideoSrc = document.getElementById('localVideo').src; - updateLargeVideo(localVideoSrc, true, 0); + var localVideo = document.createElement('video'); + 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 () { - $(document).trigger("video.selected", [false]); - updateLargeVideo($(this).attr('src'), true, 0); + var localVideoContainer = document.getElementById('localVideoContainer'); + localVideoContainer.appendChild(localVideo); - $('video').each(function (idx, el) { - if (el.id.indexOf('mixedmslabel') !== -1) { - el.volume = 0; - el.volume = 1; - } - }); - }); + var localVideoSelector = $('#' + localVideo.id); + // Add click handler + localVideoSelector.click(function () { handleVideoThumbClicked(localVideo.src); } ); + // 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) { @@ -243,42 +254,27 @@ $(document).bind('remotestreamadded.jingle', function (event, data, sid) { data.stream.onended = function () { 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) sel.removed = true; + sel.remove(); - var userContainer = sel.parent(); - if(userContainer.children().length === 0) { + var audioCount = $('#'+container.id+'>audio').length; + var videoCount = $('#'+container.id+'>video').length; + if(!audioCount && !videoCount) { console.log("Remove whole user"); // Remove whole container - userContainer.remove(); + container.remove(); Util.playSoundNotification('userLeft'); resizeThumbnails(); - } else { - // Remove only stream holder - sel.remove(); - console.log("Remove stream only", sel); } + + checkChangeLargeVideo(vid.src); }; - sel.click( - function () { - $(document).trigger("video.selected", [false]); - updateLargeVideo($(this).attr('src'), false, 1); - } - ); + + // Add click handler + sel.click(function () { handleVideoThumbClicked(vid.src); }); + // an attempt to work around https://github.com/jitsi/jitmeet/issues/32 if (isVideo && 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 function sendKeyframe(pc) { console.log('sendkeyframe', pc.iceConnectionState); @@ -404,7 +440,7 @@ $(document).bind('callactive.jingle', function (event, videoelem, sid) { videoelem.show(); resizeThumbnails(); - updateLargeVideo(videoelem.attr('src'), false, 1); + updateLargeVideo(videoelem.attr('src'), 1); showFocusIndicator(); } @@ -566,6 +602,8 @@ $(document).bind('presence.muc', function (event, jid, info, pres) { break; case 'recvonly': el.hide(); + // FIXME: Check if we have to change large video + //checkChangeLargeVideo(el); break; } } @@ -751,23 +789,27 @@ function isPresentationVisible() { /** * Updates the large video with the given new video source. */ -function updateLargeVideo(newSrc, localVideo, vol) { +function updateLargeVideo(newSrc, vol) { console.log('hover in', newSrc); setPresentationVisible(false); 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 () { $(this).attr('src', newSrc); + // Screen stream is already rotated + var flipX = (newSrc === localVideoSrc) && flipXLocalVideo; + var videoTransform = document.getElementById('largeVideo').style.webkitTransform; - if (localVideo && videoTransform !== 'scaleX(-1)') { + if (flipX && videoTransform !== '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"; } @@ -1203,8 +1245,14 @@ function showToolbar() { // TODO: Enable settings functionality. Need to uncomment the settings button in index.html. // $('#settingsButton').css({visibility:"visible"}); } + showDesktopSharingButton(); +} + +function showDesktopSharingButton() { if(isDesktopSharingEnabled()) { - $('#desktopsharing').css({display:"inline"}); + $('#desktopsharing').css( {display:"inline"} ); + } else { + $('#desktopsharing').css( {display:"none"} ); } } diff --git a/config.js b/config.js index 344cd45ea..9b53c2872 100644 --- a/config.js +++ b/config.js @@ -9,5 +9,6 @@ var config = { // useIPv6: true, // ipv6 support. use at your own risk useNicks: false, 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 }; diff --git a/css/main.css b/css/main.css index 0b3bda683..e702cc948 100644 --- a/css/main.css +++ b/css/main.css @@ -49,7 +49,7 @@ html, body{ font-size: 10pt; } -#localVideo { +.flipVideoX { -moz-transform: scaleX(-1); -webkit-transform: scaleX(-1); -o-transform: scaleX(-1); diff --git a/desktopsharing.js b/desktopsharing.js index e0ac06787..1918a6426 100644 --- a/desktopsharing.js +++ b/desktopsharing.js @@ -9,13 +9,43 @@ var isUsingScreenStream = 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} true if desktop sharing feature is available and enabled. */ function isDesktopSharingEnabled() { - // Desktop sharing must be enabled in config and works on chrome only. - // Flag 'chrome://flags/#enable-usermedia-screen-capture' must be enabled. - return config.chromeDesktopSharing && RTC.browser == 'chrome'; + return desktopSharingEnabled; +} + +/** + * 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 && !switchInProgress && getConferenceHandler().peerconnection.signalingState == 'stable' - && getConferenceHandler().peerconnection.iceConnectionState == 'connected')) { + && getConferenceHandler().peerconnection.iceConnectionState == 'connected' + && obtainDesktopStream )) { return; } switchInProgress = true; @@ -33,22 +64,29 @@ function toggleScreenSharing() { // Only the focus is able to set a shared key. if(!isUsingScreenStream) { - // Enable screen stream - getUserMediaWithConstraints( - ['screen'], - function(stream){ + obtainDesktopStream( + function(stream) { + // We now use screen stream 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 { // Disable screen stream getUserMediaWithConstraints( ['video'], function(stream) { + // We are now using camera stream isUsingScreenStream = false; - gotScreenStream(stream); + newStreamCreated(stream); }, getSwitchStreamFailed, config.resolution || '360' ); @@ -60,18 +98,64 @@ function getSwitchStreamFailed(error) { switchInProgress = false; } -function gotScreenStream(stream) { +function newStreamCreated(stream) { + 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 - 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 - window.setTimeout(function() { - switchInProgress = false; - }, 3000); +/** + * Method obtains desktop stream from WebRTC 'screen' source. + * Flag 'chrome://flags/#enable-usermedia-screen-capture' must be enabled. + */ +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"); + } + } + ); } diff --git a/index.html b/index.html index 392fbc08e..784859488 100644 --- a/index.html +++ b/index.html @@ -14,9 +14,9 @@ + - @@ -85,7 +85,7 @@
- + diff --git a/libs/strophe/strophe.jingle.adapter.js b/libs/strophe/strophe.jingle.adapter.js index b24f82dad..a9e1ac55a 100644 --- a/libs/strophe/strophe.jingle.adapter.js +++ b/libs/strophe/strophe.jingle.adapter.js @@ -337,11 +337,9 @@ TraceablePeerConnection.prototype.modifySources = function(successCallback) { switch(self.pendingop) { case 'mute': sdp.media[1] = sdp.media[1].replace('a=sendrecv', 'a=recvonly'); - console.error("MUTE"); break; case 'unmute': sdp.media[1] = sdp.media[1].replace('a=recvonly', 'a=sendrecv'); - console.error("UNMUTE"); break; } sdp.raw = sdp.session + sdp.media.join(''); @@ -502,7 +500,7 @@ function setupRTC() { 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}; 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) { constraints.video = {mandatory: {}};// same behaviour as true diff --git a/libs/strophe/strophe.jingle.sessionbase.js b/libs/strophe/strophe.jingle.sessionbase.js index ac389d2c6..7bdf88785 100644 --- a/libs/strophe/strophe.jingle.sessionbase.js +++ b/libs/strophe/strophe.jingle.sessionbase.js @@ -51,6 +51,9 @@ SessionBase.prototype.switchStreams = function (new_stream, oldStream, success_c // Remember SDP to figure out added/removed SSRCs 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.connection.jingle.localVideo = new_stream;