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

142
app.js
View File

@ -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"} );
} else {
$('#desktopsharing').css( {display:"none"} );
}
}

View File

@ -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
};

View File

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

View File

@ -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} <tt>true</tt> 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'],
obtainDesktopStream(
function(stream) {
// We now use screen stream
isUsingScreenStream = true;
gotScreenStream(stream);
},
getSwitchStreamFailed
// Hook 'ended' event to restore camera when screen stream stops
stream.addEventListener('ended',
function(e) {
if(!switchInProgress) {
toggleScreenSharing();
}
}
);
newStreamCreated(stream);
},
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");
}
}
);
}

View File

@ -14,9 +14,9 @@
<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="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="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="etherpad.js?v=5"></script><!-- etherpad plugin -->
<script src="smileys.js?v=1"></script><!-- smiley images -->
@ -85,7 +85,7 @@
<div id="remoteVideos">
<span id="localVideoContainer" class="videocontainer">
<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>
<span class="focusindicator"></span>
</span>

View File

@ -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

View File

@ -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;