diff --git a/INSTALL.md b/INSTALL.md index 9bfa0cf63..99b3b91cf 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -230,3 +230,26 @@ org.jitsi.videobridge.NAT_HARVESTER_PUBLIC_ADDRESS= # Hold your first conference You are now all set and ready to have your first meet by going to http://jitsi.example.com + + +## Enabling recording +Currently recording is only supported for linux-64 and macos. To enable it, add +the following properties to sip-communicator.properties: +``` +org.jitsi.videobridge.ENABLE_MEDIA_RECORDING=true +org.jitsi.videobridge.MEDIA_RECORDING_PATH=/path/to/recordings/dir +org.jitsi.videobridge.MEDIA_RECORDING_TOKEN=secret +``` + +where /path/to/recordings/dir is the path to a pre-existing directory where recordings +will be stored (needs to be writeable by the user running jitsi-videobridge), +and "secret" is a string which will be used for authentication. + +Then, edit the Jitsi-Meet config.js file and set: +``` +enableRecording: true +``` + +Restart jitsi-videobridge and start a new conference (making sure that the page +is reloaded with the new config.js) -- the organizer of the conference should +now have a "recoriding" button in the floating menu, near the "mute" button. diff --git a/app.js b/app.js index c7e7a0424..afd786b38 100644 --- a/app.js +++ b/app.js @@ -1,19 +1,30 @@ /* jshint -W117 */ /* application specific logic */ var connection = null; +var authenticatedUser = false; var focus = null; var activecall = null; var RTC = null; var nickname = null; var sharedKey = ''; +var recordingToken =''; var roomUrl = null; +var roomName = null; var ssrc2jid = {}; +var mediaStreams = []; + /** * The stats collector that process stats data and triggers updates to app.js. * @type {StatsCollector} */ var statsCollector = null; +/** + * The stats collector for the local stream. + * @type {LocalStatsCollector} + */ +var localStatsCollector = null; + /** * Indicates whether ssrc is camera video or desktop stream. * FIXME: remove those maps @@ -58,6 +69,11 @@ function init() { return; } + var jid = document.getElementById('jid').value || config.hosts.anonymousdomain || config.hosts.domain || window.location.hostname; + connect(jid); +} + +function connect(jid, password) { connection = new Strophe.Connection(document.getElementById('boshURL').value || config.bosh || '/http-bind'); if (nickname) { @@ -74,9 +90,11 @@ function init() { connection.jingle.pc_constraints.optional.push({googIPv6: true}); } - var jid = document.getElementById('jid').value || config.hosts.domain || window.location.hostname; + if(!password) + password = document.getElementById('password').value; - connection.connect(jid, document.getElementById('password').value, function (status) { + var anonymousConnectionFailed = false; + connection.connect(jid, password, function (status, msg) { if (status === Strophe.Status.CONNECTED) { console.log('connected'); if (config.useStunTurn) { @@ -90,6 +108,23 @@ function init() { }); document.getElementById('connect').disabled = true; + + if(password) + authenticatedUser = true; + } else if (status === Strophe.Status.CONNFAIL) { + if(msg === 'x-strophe-bad-non-anon-jid') { + anonymousConnectionFailed = true; + } + console.log('status', status); + } else if (status === Strophe.Status.DISCONNECTED) { + if(anonymousConnectionFailed) { + // prompt user for username and password + $(document).trigger('passwordrequired.main'); + } + } else if (status === Strophe.Status.AUTHFAIL) { + // wrong password or username, prompt user + $(document).trigger('passwordrequired.main'); + } else { console.log('status', status); } @@ -123,6 +158,8 @@ function audioStreamReady(stream) { VideoLayout.changeLocalAudio(stream); + startLocalRtpStatsCollector(stream); + if (RTC.browser !== 'firefox') { getUserMediaWithConstraints(['video'], videoStreamReady, @@ -173,7 +210,9 @@ function doJoin() { } } - roomjid = roomnode + '@' + config.hosts.muc; + roomName = roomnode + '@' + config.hosts.muc; + + roomjid = roomName; if (config.useNicks) { var nick = window.prompt('Your nickname (optional)'); @@ -183,45 +222,52 @@ function doJoin() { roomjid += '/' + Strophe.getNodeFromJid(connection.jid); } } else { - roomjid += '/' + Strophe.getNodeFromJid(connection.jid).substr(0, 8); + + var tmpJid = Strophe.getNodeFromJid(connection.jid); + + if(!authenticatedUser) + tmpJid = tmpJid.substr(0, 8); + + roomjid += '/' + tmpJid; } connection.emuc.doJoin(roomjid); } -$(document).bind('remotestreamadded.jingle', function (event, data, sid) { - function waitForRemoteVideo(selector, sid, ssrc) { - if (selector.removed) { - console.warn("media removed before had started", selector); - return; - } - var sess = connection.jingle.sessions[sid]; - if (data.stream.id === 'mixedmslabel') return; - var videoTracks = data.stream.getVideoTracks(); -// console.log("waiting..", videoTracks, selector[0]); - - if (videoTracks.length === 0 || selector[0].currentTime > 0) { - RTC.attachMediaStream(selector, data.stream); // FIXME: why do i have to do this for FF? - - // FIXME: add a class that will associate peer Jid, video.src, it's ssrc and video type - // in order to get rid of too many maps - if (ssrc) { - videoSrcToSsrc[sel.attr('src')] = ssrc; - } else { - console.warn("No ssrc given for video", sel); - } - - $(document).trigger('callactive.jingle', [selector, sid]); - console.log('waitForremotevideo', sess.peerconnection.iceConnectionState, sess.peerconnection.signalingState); - } else { - setTimeout(function () { waitForRemoteVideo(selector, sid, ssrc); }, 250); - } +function waitForRemoteVideo(selector, ssrc, stream) { + if (selector.removed || !selector.parent().is(":visible")) { + console.warn("Media removed before had started", selector); + return; } + + if (stream.id === 'mixedmslabel') return; + + if (selector[0].currentTime > 0) { + RTC.attachMediaStream(selector, stream); // FIXME: why do i have to do this for FF? + + // FIXME: add a class that will associate peer Jid, video.src, it's ssrc and video type + // in order to get rid of too many maps + if (ssrc && selector.attr('src')) { + videoSrcToSsrc[selector.attr('src')] = ssrc; + } else { + console.warn("No ssrc given for video", selector); + } + + $(document).trigger('videoactive.jingle', [selector]); + } else { + setTimeout(function () { + waitForRemoteVideo(selector, ssrc, stream); + }, 250); + } +} + +$(document).bind('remotestreamadded.jingle', function (event, data, sid) { var sess = connection.jingle.sessions[sid]; var thessrc; // look up an associated JID for a stream id if (data.stream.id.indexOf('mixedmslabel') === -1) { - var ssrclines = SDPUtil.find_lines(sess.peerconnection.remoteDescription.sdp, 'a=ssrc'); + var ssrclines + = SDPUtil.find_lines(sess.peerconnection.remoteDescription.sdp, 'a=ssrc'); ssrclines = ssrclines.filter(function (line) { return line.indexOf('mslabel:' + data.stream.label) !== -1; }); @@ -235,11 +281,14 @@ $(document).bind('remotestreamadded.jingle', function (event, data, sid) { } } + mediaStreams.push(new MediaStream(data, sid, thessrc)); + var container; var remotes = document.getElementById('remoteVideos'); if (data.peerjid) { VideoLayout.ensurePeerContainerExists(data.peerjid); + container = document.getElementById( 'participant_' + Strophe.getResourceFromJid(data.peerjid)); } else { @@ -252,78 +301,22 @@ $(document).bind('remotestreamadded.jingle', function (event, data, sid) { } // FIXME: for the mixed ms we dont need a video -- currently container = document.createElement('span'); + container.id = 'mixedstream'; container.className = 'videocontainer'; remotes.appendChild(container); Util.playSoundNotification('userJoined'); } var isVideo = data.stream.getVideoTracks().length > 0; - var vid = isVideo ? document.createElement('video') : document.createElement('audio'); - var id = (isVideo ? 'remoteVideo_' : 'remoteAudio_') + sid + '_' + data.stream.id; - vid.id = id; - vid.autoplay = true; - vid.oncontextmenu = function () { return false; }; - - container.appendChild(vid); - - // TODO: make mixedstream display:none via css? - if (id.indexOf('mixedmslabel') !== -1) { - container.id = 'mixedstream'; - $(container).hide(); + if (container) { + VideoLayout.addRemoteStreamElement( container, + sid, + data.stream, + data.peerjid, + thessrc); } - var sel = $('#' + id); - sel.hide(); - RTC.attachMediaStream(sel, data.stream); - - if (isVideo) { - waitForRemoteVideo(sel, sid, thessrc); - } - - data.stream.onended = function () { - console.log('stream ended', this.id); - - // Mark video as removed to cancel waiting loop(if video is removed before has started) - sel.removed = true; - sel.remove(); - - var audioCount = $('#' + container.id + '>audio').length; - var videoCount = $('#' + container.id + '>video').length; - if (!audioCount && !videoCount) { - console.log("Remove whole user", container.id); - // Remove whole container - container.remove(); - Util.playSoundNotification('userLeft'); - VideoLayout.resizeThumbnails(); - } - - VideoLayout.checkChangeLargeVideo(vid.src); - }; - - // Add click handler - sel.click(function () { - VideoLayout.handleVideoThumbClicked(vid.src); - }); - // Add hover handler - $(container).hover( - function() { - VideoLayout.showDisplayName(container.id, true); - }, - function() { - var videoSrc = null; - if ($('#' + container.id + '>video') - && $('#' + container.id + '>video').length > 0) { - videoSrc = $('#' + container.id + '>video').get(0).src; - } - - // If the video has been "pinned" by the user we want to keep the - // display name on place. - if (focusedVideoSrc !== videoSrc) - VideoLayout.showDisplayName(container.id, false); - } - ); - // an attempt to work around https://github.com/jitsi/jitmeet/issues/32 if (isVideo && data.peerjid && sess.peerjid === data.peerjid && @@ -423,21 +416,25 @@ function muteVideo(pc, unmute) { } /** - * Callback called by {@link StatsCollector} in intervals supplied to it's - * constructor. - * @param statsCollector {@link StatsCollector} source of the event. + * Callback for audio levels changed. + * @param jid JID of the user + * @param audioLevel the audio level value */ -function statsUpdated(statsCollector) +function audioLevelUpdated(jid, audioLevel) { - Object.keys(statsCollector.jid2stats).forEach(function (jid) + var resourceJid; + if(jid === LocalStatsCollector.LOCAL_JID) { - var peerStats = statsCollector.jid2stats[jid]; - Object.keys(peerStats.ssrc2AudioLevel).forEach(function (ssrc) - { - console.info(jid + " audio level: " + - peerStats.ssrc2AudioLevel[ssrc] + " of ssrc: " + ssrc); - }); - }); + resourceJid = AudioLevels.LOCAL_LEVEL; + if(isAudioMuted()) + return; + } + else + { + resourceJid = Strophe.getResourceFromJid(jid); + } + + AudioLevels.updateAudioLevel(resourceJid, audioLevel); } /** @@ -445,15 +442,52 @@ function statsUpdated(statsCollector) */ function startRtpStatsCollector() { + stopRTPStatsCollector(); if (config.enableRtpStats) { statsCollector = new StatsCollector( - getConferenceHandler().peerconnection, 200, statsUpdated); - + getConferenceHandler().peerconnection, 200, audioLevelUpdated); statsCollector.start(); } } +/** + * Stops the {@link StatsCollector}. + */ +function stopRTPStatsCollector() +{ + if (statsCollector) + { + statsCollector.stop(); + statsCollector = null; + } +} + +/** + * Starts the {@link LocalStatsCollector} if the feature is enabled in config.js + * @param stream the stream that will be used for collecting statistics. + */ +function startLocalRtpStatsCollector(stream) +{ + if(config.enableRtpStats) + { + localStatsCollector = new LocalStatsCollector(stream, 100, audioLevelUpdated); + localStatsCollector.start(); + } +} + +/** + * Stops the {@link LocalStatsCollector}. + */ +function stopLocalRtpStatsCollector() +{ + if(localStatsCollector) + { + localStatsCollector.stop(); + localStatsCollector = null; + } +} + $(document).bind('callincoming.jingle', function (event, sid) { var sess = connection.jingle.sessions[sid]; @@ -490,19 +524,6 @@ $(document).bind('conferenceCreated.jingle', function (event, focus) } }); -$(document).bind('callactive.jingle', function (event, videoelem, sid) { - if (videoelem.attr('id').indexOf('mixedmslabel') === -1) { - // ignore mixedmslabela0 and v0 - videoelem.show(); - VideoLayout.resizeThumbnails(); - - if (!focusedVideoSrc) - VideoLayout.updateLargeVideo(videoelem.attr('src'), 1); - - VideoLayout.showFocusIndicator(); - } -}); - $(document).bind('callterminated.jingle', function (event, sid, jid, reason) { // Leave the room if my call has been remotely terminated. if (connection.emuc.joined && focus == null && reason === 'kick') { @@ -530,8 +551,8 @@ $(document).bind('setLocalDescription.jingle', function (event, sid) { directions[type] = ( SDPUtil.find_line(media, 'a=sendrecv') || SDPUtil.find_line(media, 'a=recvonly') || - SDPUtil.find_line('a=sendonly') || - SDPUtil.find_line('a=inactive') || + SDPUtil.find_line(media, 'a=sendonly') || + SDPUtil.find_line(media, 'a=inactive') || 'a=sendrecv').substr(2); } }); @@ -562,6 +583,17 @@ $(document).bind('joined.muc', function (event, jid, info) { if (Object.keys(connection.emuc.members).length < 1) { focus = new ColibriFocus(connection, config.hosts.bridge); + if (nickname !== null) { + focus.setEndpointDisplayName(connection.emuc.myroomjid, + nickname); + } + Toolbar.showSipCallButton(true); + Toolbar.showRecordingButton(false); + } + + if (!focus) + { + Toolbar.showSipCallButton(false); } if (focus && config.etherpad_base) { @@ -570,18 +602,25 @@ $(document).bind('joined.muc', function (event, jid, info) { VideoLayout.showFocusIndicator(); + // Add myself to the contact list. + ContactList.addContact(jid); + // Once we've joined the muc show the toolbar Toolbar.showToolbar(); var displayName = ''; if (info.displayName) displayName = info.displayName + ' (me)'; + else + displayName = "Me"; - VideoLayout.setDisplayName('localVideoContainer', displayName); + $(document).trigger('displaynamechanged', + ['localVideoContainer', displayName]); }); $(document).bind('entered.muc', function (event, jid, info, pres) { console.log('entered', jid, info); + console.log('is focus?' + focus ? 'true' : 'false'); // Add Peer's container @@ -592,6 +631,7 @@ $(document).bind('entered.muc', function (event, jid, info, pres) { if (focus.confid === null) { console.log('make new conference with', jid); focus.makeConference(Object.keys(connection.emuc.members)); + Toolbar.showRecordingButton(true); } else { console.log('invite', jid, 'into conference'); focus.addNewParticipant(jid); @@ -637,17 +677,31 @@ $(document).bind('left.muc', function (event, jid) { && !sessionTerminated) { console.log('welcome to our new focus... myself'); focus = new ColibriFocus(connection, config.hosts.bridge); + if (nickname !== null) { + focus.setEndpointDisplayName(connection.emuc.myroomjid, + nickname); + } + + Toolbar.showSipCallButton(true); + if (Object.keys(connection.emuc.members).length > 0) { focus.makeConference(Object.keys(connection.emuc.members)); + Toolbar.showRecordingButton(true); } $(document).trigger('focusechanged.muc', [focus]); } else if (focus && Object.keys(connection.emuc.members).length === 0) { console.log('everyone left'); // FIXME: closing the connection is a hack to avoid some - // problemswith reinit + // problems with reinit disposeConference(); focus = new ColibriFocus(connection, config.hosts.bridge); + if (nickname !== null) { + focus.setEndpointDisplayName(connection.emuc.myroomjid, + nickname); + } + Toolbar.showSipCallButton(true); + Toolbar.showRecordingButton(false); } if (connection.emuc.getPrezi(jid)) { $(document).trigger('presentationremoved.muc', @@ -662,7 +716,7 @@ $(document).bind('presence.muc', function (event, jid, info, pres) { if (ssrc2jid[ssrc] == jid) { delete ssrc2jid[ssrc]; } - if (ssrc2videoType == jid) { + if (ssrc2videoType[ssrc] == jid) { delete ssrc2videoType[ssrc]; } }); @@ -685,25 +739,28 @@ $(document).bind('presence.muc', function (event, jid, info, pres) { case 'recvonly': el.hide(); // FIXME: Check if we have to change large video - //VideoLayout.checkChangeLargeVideo(el); + //VideoLayout.updateLargeVideo(el); break; } } }); - if (info.displayName) { - if (jid === connection.emuc.myroomjid) { - VideoLayout.setDisplayName('localVideoContainer', - info.displayName + ' (me)'); - } else { - VideoLayout.ensurePeerContainerExists(jid); - VideoLayout.setDisplayName( - 'participant_' + Strophe.getResourceFromJid(jid), - info.displayName); - } + if (info.displayName && info.displayName.length > 0) + $(document).trigger('displaynamechanged', + [jid, info.displayName]); + + if (focus !== null && info.displayName !== null) { + focus.setEndpointDisplayName(jid, info.displayName); } }); +$(document).bind('presence.status.muc', function (event, jid, info, pres) { + + VideoLayout.setPresenceStatus( + 'participant_' + Strophe.getResourceFromJid(jid), info.status); + +}); + $(document).bind('passwordrequired.muc', function (event, jid) { console.log('on password required', jid); @@ -728,6 +785,31 @@ $(document).bind('passwordrequired.muc', function (event, jid) { }); }); +$(document).bind('passwordrequired.main', function (event) { + console.log('password is required'); + + $.prompt('

Password required

' + + '' + + '', { + persistent: true, + buttons: { "Ok": true, "Cancel": false}, + defaultButton: 1, + loaded: function (event) { + document.getElementById('passwordrequired.username').focus(); + }, + submit: function (e, v, m, f) { + if (v) { + var username = document.getElementById('passwordrequired.username'); + var password = document.getElementById('passwordrequired.password'); + + if (username.value !== null && password.value != null) { + connect(username.value, password.value); + } + } + } + }); +}); + /** * Checks if video identified by given src is desktop stream. * @param videoSrc eg. @@ -818,6 +900,71 @@ function toggleAudio() { buttonClick("#mute", "icon-microphone icon-mic-disabled"); } +/** + * Checks whether the audio is muted or not. + * @returns {boolean} true if audio is muted and false if not. + */ +function isAudioMuted() +{ + var localAudio = connection.jingle.localAudio; + for (var idx = 0; idx < localAudio.getAudioTracks().length; idx++) { + if(localAudio.getAudioTracks()[idx].enabled === true) + return false; + } + return true; +} + +// Starts or stops the recording for the conference. +function toggleRecording() { + if (focus === null || focus.confid === null) { + console.log('non-focus, or conference not yet organized: not enabling recording'); + return; + } + + if (!recordingToken) + { + $.prompt('

Enter recording token

' + + '', + { + persistent: false, + buttons: { "Save": true, "Cancel": false}, + defaultButton: 1, + loaded: function (event) { + document.getElementById('recordingToken').focus(); + }, + submit: function (e, v, m, f) { + if (v) { + var token = document.getElementById('recordingToken'); + + if (token.value) { + setRecordingToken(Util.escapeHtml(token.value)); + toggleRecording(); + } + } + } + } + ); + + return; + } + + var oldState = focus.recordingEnabled; + Toolbar.toggleRecordingButtonState(); + focus.setRecording(!oldState, + recordingToken, + function (state) { + console.log("New recording state: ", state); + if (state == oldState) //failed to change, reset the token because it might have been wrong + { + Toolbar.toggleRecordingButtonState(); + setRecordingToken(null); + } + } + ); + + +} + /** * Returns an array of the video horizontal and vertical indents, * so that if fits its parent. @@ -897,6 +1044,33 @@ function getCameraVideoSize(videoWidth, } $(document).ready(function () { + + if(config.enableWelcomePage && window.location.pathname == "/") + { + $("#videoconference_page").hide(); + $("#enter_room_button").click(function() + { + var val = Util.escapeHtml($("#enter_room_field").val()); + window.location.pathname = "/" + val; + }); + + $("#enter_room_field").keydown(function (event) { + if (event.keyCode === 13) { + var val = Util.escapeHtml(this.value); + window.location.pathname = "/" + val; + } + }); + + if(!config.isBrand) + { + $("#brand_logo").hide(); + $("#brand_header").hide(); + $("#header_text").hide(); + } + return; + } + + $("#welcome_page").hide(); Chat.init(); $('body').popover({ selector: '[data-toggle=popover]', @@ -960,10 +1134,10 @@ $(window).bind('beforeunload', function () { } }); } - disposeConference(); + disposeConference(true); }); -function disposeConference() { +function disposeConference(onUnload) { var handler = getConferenceHandler(); if (handler && handler.peerconnection) { // FIXME: probably removing streams is not required and close() should be enough @@ -975,10 +1149,9 @@ function disposeConference() { } handler.peerconnection.close(); } - if (statsCollector) - { - statsCollector.stop(); - statsCollector = null; + stopRTPStatsCollector(); + if(onUnload) { + stopLocalRtpStatsCollector(); } focus = null; activecall = null; @@ -1055,6 +1228,10 @@ function setSharedKey(sKey) { sharedKey = sKey; } +function setRecordingToken(token) { + recordingToken = token; +} + /** * Updates the room invite url. */ @@ -1115,6 +1292,28 @@ function setView(viewName) { // } } +function hangUp() { + if (connection && connection.connected) { + // ensure signout + $.ajax({ + type: 'POST', + url: config.bosh, + async: false, + cache: false, + contentType: 'application/xml', + data: "", + success: function (data) { + console.log('signed out'); + console.log(data); + }, + error: function (XMLHttpRequest, textStatus, errorThrown) { + console.log('signout error', textStatus + ' (' + errorThrown + ')'); + } + }); + } + disposeConference(true); +} + $(document).bind('fatalError.jingle', function (event, session, error) { @@ -1124,3 +1323,31 @@ $(document).bind('fatalError.jingle', "Your browser version is too old. Please update and try again..."); } ); + +function callSipButtonClicked() +{ + $.prompt('

Enter SIP number

' + + '', + { + persistent: false, + buttons: { "Dial": true, "Cancel": false}, + defaultButton: 2, + loaded: function (event) + { + document.getElementById('sipNumber').focus(); + }, + submit: function (e, v, m, f) + { + if (v) + { + var numberInput = document.getElementById('sipNumber'); + if (numberInput.value && numberInput.value.length) + { + connection.rayo.dial( + numberInput.value, 'fromnumber', roomName); + } + } + } + } + ); +} diff --git a/audio_levels.js b/audio_levels.js new file mode 100644 index 000000000..3a94fff22 --- /dev/null +++ b/audio_levels.js @@ -0,0 +1,217 @@ +/** + * The audio Levels plugin. + */ +var AudioLevels = (function(my) { + var CANVAS_EXTRA = 104; + var CANVAS_RADIUS = 7; + var SHADOW_COLOR = '#00ccff'; + var audioLevelCanvasCache = {}; + + my.LOCAL_LEVEL = 'local'; + + /** + * Updates the audio level canvas for the given peerJid. If the canvas + * didn't exist we create it. + */ + my.updateAudioLevelCanvas = function (peerJid) { + var resourceJid = null; + var videoSpanId = null; + if (!peerJid) + videoSpanId = 'localVideoContainer'; + else { + resourceJid = Strophe.getResourceFromJid(peerJid); + + videoSpanId = 'participant_' + resourceJid; + } + + videoSpan = document.getElementById(videoSpanId); + + if (!videoSpan) { + if (resourceJid) + console.error("No video element for jid", resourceJid); + else + console.error("No video element for local video."); + + return; + } + + var audioLevelCanvas = $('#' + videoSpanId + '>canvas'); + + var videoSpaceWidth = $('#remoteVideos').width(); + var thumbnailSize + = VideoLayout.calculateThumbnailSize(videoSpaceWidth); + var thumbnailWidth = thumbnailSize[0]; + var thumbnailHeight = thumbnailSize[1]; + + if (!audioLevelCanvas || audioLevelCanvas.length === 0) { + + audioLevelCanvas = document.createElement('canvas'); + audioLevelCanvas.className = "audiolevel"; + audioLevelCanvas.style.bottom = "-" + CANVAS_EXTRA/2 + "px"; + audioLevelCanvas.style.left = "-" + CANVAS_EXTRA/2 + "px"; + resizeAudioLevelCanvas( audioLevelCanvas, + thumbnailWidth, + thumbnailHeight); + + videoSpan.appendChild(audioLevelCanvas); + } else { + audioLevelCanvas = audioLevelCanvas.get(0); + + resizeAudioLevelCanvas( audioLevelCanvas, + thumbnailWidth, + thumbnailHeight); + } + }; + + /** + * Updates the audio level UI for the given resourceJid. + * + * @param resourceJid the resource jid indicating the video element for + * which we draw the audio level + * @param audioLevel the newAudio level to render + */ + my.updateAudioLevel = function (resourceJid, audioLevel) { + drawAudioLevelCanvas(resourceJid, audioLevel); + + var videoSpanId = getVideoSpanId(resourceJid); + + var audioLevelCanvas = $('#' + videoSpanId + '>canvas').get(0); + + if (!audioLevelCanvas) + return; + + var drawContext = audioLevelCanvas.getContext('2d'); + + var canvasCache = audioLevelCanvasCache[resourceJid]; + + drawContext.clearRect (0, 0, + audioLevelCanvas.width, audioLevelCanvas.height); + drawContext.drawImage(canvasCache, 0, 0); + }; + + /** + * Resizes the given audio level canvas to match the given thumbnail size. + */ + function resizeAudioLevelCanvas(audioLevelCanvas, + thumbnailWidth, + thumbnailHeight) { + audioLevelCanvas.width = thumbnailWidth + CANVAS_EXTRA; + audioLevelCanvas.height = thumbnailHeight + CANVAS_EXTRA; + }; + + /** + * Draws the audio level canvas into the cached canvas object. + * + * @param resourceJid the resource jid indicating the video element for + * which we draw the audio level + * @param audioLevel the newAudio level to render + */ + function drawAudioLevelCanvas(resourceJid, audioLevel) { + if (!audioLevelCanvasCache[resourceJid]) { + + var videoSpanId = getVideoSpanId(resourceJid); + + var audioLevelCanvasOrig = $('#' + videoSpanId + '>canvas').get(0); + + /* + * FIXME Testing has shown that audioLevelCanvasOrig may not exist. + * In such a case, the method CanvasUtil.cloneCanvas may throw an + * error. Since audio levels are frequently updated, the errors have + * been observed to pile into the console, strain the CPU. + */ + if (audioLevelCanvasOrig) + { + audioLevelCanvasCache[resourceJid] + = CanvasUtil.cloneCanvas(audioLevelCanvasOrig); + } + } + + var canvas = audioLevelCanvasCache[resourceJid]; + + if (!canvas) + return; + + var drawContext = canvas.getContext('2d'); + + drawContext.clearRect(0, 0, canvas.width, canvas.height); + + var shadowLevel = getShadowLevel(audioLevel); + + if (shadowLevel > 0) + // drawContext, x, y, w, h, r, shadowColor, shadowLevel + CanvasUtil.drawRoundRectGlow( drawContext, + CANVAS_EXTRA/2, CANVAS_EXTRA/2, + canvas.width - CANVAS_EXTRA, + canvas.height - CANVAS_EXTRA, + CANVAS_RADIUS, + SHADOW_COLOR, + shadowLevel); + }; + + /** + * Returns the shadow/glow level for the given audio level. + * + * @param audioLevel the audio level from which we determine the shadow + * level + */ + function getShadowLevel (audioLevel) { + var shadowLevel = 0; + + if (audioLevel <= 0.3) { + shadowLevel = Math.round(CANVAS_EXTRA/2*(audioLevel/0.3)); + } + else if (audioLevel <= 0.6) { + shadowLevel = Math.round(CANVAS_EXTRA/2*((audioLevel - 0.3) / 0.3)); + } + else { + shadowLevel = Math.round(CANVAS_EXTRA/2*((audioLevel - 0.6) / 0.4)); + } + return shadowLevel; + }; + + /** + * Returns the video span id corresponding to the given resourceJid or local + * user. + */ + function getVideoSpanId(resourceJid) { + var videoSpanId = null; + if (resourceJid === AudioLevels.LOCAL_LEVEL + || (connection.emuc.myroomjid && resourceJid + === Strophe.getResourceFromJid(connection.emuc.myroomjid))) + videoSpanId = 'localVideoContainer'; + else + videoSpanId = 'participant_' + resourceJid; + + return videoSpanId; + }; + + /** + * Indicates that the remote video has been resized. + */ + $(document).bind('remotevideo.resized', function (event, width, height) { + var resized = false; + $('#remoteVideos>span>canvas').each(function() { + var canvas = $(this).get(0); + if (canvas.width !== width + CANVAS_EXTRA) { + canvas.width = width + CANVAS_EXTRA; + resized = true; + } + + if (canvas.heigh !== height + CANVAS_EXTRA) { + canvas.height = height + CANVAS_EXTRA; + resized = true; + } + }); + + if (resized) + Object.keys(audioLevelCanvasCache).forEach(function (resourceJid) { + audioLevelCanvasCache[resourceJid].width + = width + CANVAS_EXTRA; + audioLevelCanvasCache[resourceJid].height + = height + CANVAS_EXTRA; + }); + }); + + return my; + +})(AudioLevels || {}); diff --git a/bottom_toolbar.js b/bottom_toolbar.js new file mode 100644 index 000000000..f8bfd4642 --- /dev/null +++ b/bottom_toolbar.js @@ -0,0 +1,32 @@ +var BottomToolbar = (function (my) { + my.toggleChat = function() { + if (ContactList.isVisible()) { + buttonClick("#contactListButton", "active"); + ContactList.toggleContactList(); + } + + buttonClick("#chatBottomButton", "active"); + + Chat.toggleChat(); + }; + + my.toggleContactList = function() { + if (Chat.isVisible()) { + buttonClick("#chatBottomButton", "active"); + Chat.toggleChat(); + } + + buttonClick("#contactListButton", "active"); + + ContactList.toggleContactList(); + }; + + + $(document).bind("remotevideo.resized", function (event, width, height) { + var bottom = (height - $('#bottomToolbar').outerHeight())/2 + 18; + + $('#bottomToolbar').css({bottom: bottom + 'px'}); + }); + + return my; +}(BottomToolbar || {})); diff --git a/canvas_util.js b/canvas_util.js new file mode 100644 index 000000000..b8a1b0e9e --- /dev/null +++ b/canvas_util.js @@ -0,0 +1,109 @@ +/** + * Utility class for drawing canvas shapes. + */ +var CanvasUtil = (function(my) { + + /** + * Draws a round rectangle with a glow. The glowWidth indicates the depth + * of the glow. + * + * @param drawContext the context of the canvas to draw to + * @param x the x coordinate of the round rectangle + * @param y the y coordinate of the round rectangle + * @param w the width of the round rectangle + * @param h the height of the round rectangle + * @param glowColor the color of the glow + * @param glowWidth the width of the glow + */ + my.drawRoundRectGlow + = function(drawContext, x, y, w, h, r, glowColor, glowWidth) { + + // Save the previous state of the context. + drawContext.save(); + + if (w < 2 * r) r = w / 2; + if (h < 2 * r) r = h / 2; + + // Draw a round rectangle. + drawContext.beginPath(); + drawContext.moveTo(x+r, y); + drawContext.arcTo(x+w, y, x+w, y+h, r); + drawContext.arcTo(x+w, y+h, x, y+h, r); + drawContext.arcTo(x, y+h, x, y, r); + drawContext.arcTo(x, y, x+w, y, r); + drawContext.closePath(); + + // Add a shadow around the rectangle + drawContext.shadowColor = glowColor; + drawContext.shadowBlur = glowWidth; + drawContext.shadowOffsetX = 0; + drawContext.shadowOffsetY = 0; + + // Fill the shape. + drawContext.fill(); + + drawContext.save(); + + drawContext.restore(); + +// 1) Uncomment this line to use Composite Operation, which is doing the +// same as the clip function below and is also antialiasing the round +// border, but is said to be less fast performance wise. + +// drawContext.globalCompositeOperation='destination-out'; + + drawContext.beginPath(); + drawContext.moveTo(x+r, y); + drawContext.arcTo(x+w, y, x+w, y+h, r); + drawContext.arcTo(x+w, y+h, x, y+h, r); + drawContext.arcTo(x, y+h, x, y, r); + drawContext.arcTo(x, y, x+w, y, r); + drawContext.closePath(); + +// 2) Uncomment this line to use Composite Operation, which is doing the +// same as the clip function below and is also antialiasing the round +// border, but is said to be less fast performance wise. + +// drawContext.fill(); + + // Comment these two lines if choosing to do the same with composite + // operation above 1 and 2. + drawContext.clip(); + drawContext.clearRect(0, 0, 277, 200); + + // Restore the previous context state. + drawContext.restore(); + }; + + /** + * Clones the given canvas. + * + * @return the new cloned canvas. + */ + my.cloneCanvas = function (oldCanvas) { + /* + * FIXME Testing has shown that oldCanvas may not exist. In such a case, + * the method CanvasUtil.cloneCanvas may throw an error. Since audio + * levels are frequently updated, the errors have been observed to pile + * into the console, strain the CPU. + */ + if (!oldCanvas) + return oldCanvas; + + //create a new canvas + var newCanvas = document.createElement('canvas'); + var context = newCanvas.getContext('2d'); + + //set dimensions + newCanvas.width = oldCanvas.width; + newCanvas.height = oldCanvas.height; + + //apply the old canvas to the new one + context.drawImage(oldCanvas, 0, 0); + + //return the new canvas + return newCanvas; + }; + + return my; +})(CanvasUtil || {}); diff --git a/chat.js b/chat.js index b7255a346..505c8a47f 100644 --- a/chat.js +++ b/chat.js @@ -80,7 +80,7 @@ var Chat = (function (my) { else { divClassName = "remoteuser"; - if (!$('#chatspace').is(":visible")) { + if (!Chat.isVisible()) { unreadMessages++; Util.playSoundNotification('chatNotification'); setVisualNotification(true); @@ -115,8 +115,7 @@ var Chat = (function (my) { + ''); $('#chatconversation').animate( { scrollTop: $('#chatconversation')[0].scrollHeight}, 1000); - - } + }; /** * Sets the subject to the UI @@ -135,8 +134,7 @@ var Chat = (function (my) { { $("#subject").css({display: "block"}); } - } - + }; /** * Opens / closes the chat area. @@ -159,6 +157,10 @@ var Chat = (function (my) { var horizontalIndent = videoPosition[0]; var verticalIndent = videoPosition[1]; + var thumbnailSize = VideoLayout.calculateThumbnailSize(videospaceWidth); + var thumbnailsWidth = thumbnailSize[0]; + var thumbnailsHeight = thumbnailSize[1]; + if (chatspace.is(":visible")) { videospace.animate({right: chatSize[0], width: videospaceWidth, @@ -166,6 +168,21 @@ var Chat = (function (my) { {queue: false, duration: 500}); + $('#remoteVideos').animate({height: thumbnailsHeight}, + {queue: false, + duration: 500}); + + $('#remoteVideos>span').animate({height: thumbnailsHeight, + width: thumbnailsWidth}, + {queue: false, + duration: 500, + complete: function() { + $(document).trigger( + "remotevideo.resized", + [thumbnailsWidth, + thumbnailsHeight]); + }}); + $('#largeVideoContainer').animate({ width: videospaceWidth, height: videospaceHeight}, {queue: false, @@ -187,6 +204,11 @@ var Chat = (function (my) { duration: 500}); } else { + // Undock the toolbar when the chat is shown and if we're in a + // video mode. + if (VideoLayout.isLargeVideoVisible()) + Toolbar.dockToolbar(false); + videospace.animate({right: chatSize[0], width: videospaceWidth, height: videospaceHeight}, @@ -198,6 +220,20 @@ var Chat = (function (my) { } }); + $('#remoteVideos').animate({height: thumbnailsHeight}, + {queue: false, + duration: 500}); + + $('#remoteVideos>span').animate({height: thumbnailsHeight, + width: thumbnailsWidth}, + {queue: false, + duration: 500, + complete: function() { + $(document).trigger( + "remotevideo.resized", + [thumbnailsWidth, thumbnailsHeight]); + }}); + $('#largeVideoContainer').animate({ width: videospaceWidth, height: videospaceHeight}, {queue: false, @@ -265,6 +301,13 @@ var Chat = (function (my) { return [chatWidth, availableHeight]; }; + /** + * Indicates if the chat is currently visible. + */ + my.isVisible = function () { + return $('#chatspace').is(":visible"); + }; + /** * Resizes the chat conversation. */ @@ -290,7 +333,7 @@ var Chat = (function (my) { if (unreadMessages) { unreadMsgElement.innerHTML = unreadMessages.toString(); - Toolbar.showToolbar(); + Toolbar.dockToolbar(true); var chatButtonElement = document.getElementById('chatButton').parentNode; diff --git a/config.js b/config.js index 22d93589a..920e056aa 100644 --- a/config.js +++ b/config.js @@ -1,17 +1,25 @@ var config = { hosts: { - domain: 'guest.jit.si', - muc: 'meet.jit.si', // FIXME: use XEP-0030 - bridge: 'jitsi-videobridge.lambada.jitsi.net' // FIXME: use XEP-0030 + domain: 'jitsi-meet.example.com', + //anonymousdomain: 'guest.example.com', + muc: 'conference.jitsi-meet.example.com', // FIXME: use XEP-0030 + bridge: 'jitsi-videobridge.jitsi-meet.example.com', // FIXME: use XEP-0030 + //call_control: 'callcontrol.jitsi-meet.example.com' }, // getroomnode: function (path) { return 'someprefixpossiblybasedonpath'; }, // useStunTurn: true, // use XEP-0215 to fetch STUN and TURN server // useIPv6: true, // ipv6 support. use at your own risk useNicks: false, - bosh: '//lambada.jitsi.net/http-bind', // FIXME: use xep-0156 for that + bosh: '//jitsi-meet.example.com/http-bind', // FIXME: use xep-0156 for that desktopSharing: 'ext', // Desktop sharing method. Can be set to 'ext', 'webrtc' or false to disable. chromeExtensionId: 'diibjkoicjeejcmhdnailmkgecihlobk', // Id of desktop streamer Chrome extension minChromeExtVersion: '0.1', // Required version of Chrome extension - enableRtpStats: false, // Enables RTP stats processing - openSctp: true //Toggle to enable/disable SCTP channels -}; \ No newline at end of file + enableRtpStats: true, // Enables RTP stats processing + openSctp: true, // Toggle to enable/disable SCTP channels + channelLastN: -1, // The default value of the channel attribute last-n. +// useRtcpMux: true, +// useBundle: true, + enableRecording: false, + enableWelcomePage: false, + isBrand: false +}; diff --git a/contact_list.js b/contact_list.js new file mode 100644 index 000000000..cdbdc3e47 --- /dev/null +++ b/contact_list.js @@ -0,0 +1,235 @@ +/** + * Contact list. + */ +var ContactList = (function (my) { + /** + * Indicates if the chat is currently visible. + * + * @return true if the chat is currently visible, false - + * otherwise + */ + my.isVisible = function () { + return $('#contactlist').is(":visible"); + }; + + /** + * Adds a contact for the given peerJid if such doesn't yet exist. + * + * @param peerJid the peerJid corresponding to the contact + */ + my.ensureAddContact = function(peerJid) { + var resourceJid = Strophe.getResourceFromJid(peerJid); + + var contact = $('#contactlist>ul>li[id="' + resourceJid + '"]'); + + if (!contact || contact.length <= 0) + ContactList.addContact(peerJid); + }; + + /** + * Adds a contact for the given peer jid. + * + * @param peerJid the jid of the contact to add + */ + my.addContact = function(peerJid) { + var resourceJid = Strophe.getResourceFromJid(peerJid); + + var contactlist = $('#contactlist>ul'); + + var newContact = document.createElement('li'); + newContact.id = resourceJid; + + newContact.appendChild(createAvatar()); + newContact.appendChild(createDisplayNameParagraph("Participant")); + + var clElement = contactlist.get(0); + + if (resourceJid === Strophe.getResourceFromJid(connection.emuc.myroomjid) + && $('#contactlist>ul .title')[0].nextSibling.nextSibling) + { + clElement.insertBefore(newContact, + $('#contactlist>ul .title')[0].nextSibling.nextSibling); + } + else { + clElement.appendChild(newContact); + } + }; + + /** + * Removes a contact for the given peer jid. + * + * @param peerJid the peerJid corresponding to the contact to remove + */ + my.removeContact = function(peerJid) { + var resourceJid = Strophe.getResourceFromJid(peerJid); + + var contact = $('#contactlist>ul>li[id="' + resourceJid + '"]'); + + if (contact && contact.length > 0) { + var contactlist = $('#contactlist>ul'); + + contactlist.get(0).removeChild(contact.get(0)); + } + }; + + /** + * Opens / closes the contact list area. + */ + my.toggleContactList = function () { + var contactlist = $('#contactlist'); + var videospace = $('#videospace'); + + var chatSize = (ContactList.isVisible()) ? [0, 0] : Chat.getChatSize(); + var videospaceWidth = window.innerWidth - chatSize[0]; + var videospaceHeight = window.innerHeight; + var videoSize + = getVideoSize(null, null, videospaceWidth, videospaceHeight); + var videoWidth = videoSize[0]; + var videoHeight = videoSize[1]; + var videoPosition = getVideoPosition(videoWidth, + videoHeight, + videospaceWidth, + videospaceHeight); + var horizontalIndent = videoPosition[0]; + var verticalIndent = videoPosition[1]; + + var thumbnailSize = VideoLayout.calculateThumbnailSize(videospaceWidth); + var thumbnailsWidth = thumbnailSize[0]; + var thumbnailsHeight = thumbnailSize[1]; + + if (ContactList.isVisible()) { + videospace.animate({right: chatSize[0], + width: videospaceWidth, + height: videospaceHeight}, + {queue: false, + duration: 500}); + + $('#remoteVideos').animate({height: thumbnailsHeight}, + {queue: false, + duration: 500}); + + $('#remoteVideos>span').animate({height: thumbnailsHeight, + width: thumbnailsWidth}, + {queue: false, + duration: 500, + complete: function() { + $(document).trigger( + "remotevideo.resized", + [thumbnailsWidth, + thumbnailsHeight]); + }}); + + $('#largeVideoContainer').animate({ width: videospaceWidth, + height: videospaceHeight}, + {queue: false, + duration: 500 + }); + + $('#largeVideo').animate({ width: videoWidth, + height: videoHeight, + top: verticalIndent, + bottom: verticalIndent, + left: horizontalIndent, + right: horizontalIndent}, + { queue: false, + duration: 500 + }); + + $('#contactlist').hide("slide", { direction: "right", + queue: false, + duration: 500}); + } + else { + // Undock the toolbar when the chat is shown and if we're in a + // video mode. + if (VideoLayout.isLargeVideoVisible()) + Toolbar.dockToolbar(false); + + videospace.animate({right: chatSize[0], + width: videospaceWidth, + height: videospaceHeight}, + {queue: false, + duration: 500, + complete: function () { + contactlist.trigger('shown'); + } + }); + + $('#remoteVideos').animate({height: thumbnailsHeight}, + {queue: false, + duration: 500}); + + $('#remoteVideos>span').animate({height: thumbnailsHeight, + width: thumbnailsWidth}, + {queue: false, + duration: 500, + complete: function() { + $(document).trigger( + "remotevideo.resized", + [thumbnailsWidth, thumbnailsHeight]); + }}); + + $('#largeVideoContainer').animate({ width: videospaceWidth, + height: videospaceHeight}, + {queue: false, + duration: 500 + }); + + $('#largeVideo').animate({ width: videoWidth, + height: videoHeight, + top: verticalIndent, + bottom: verticalIndent, + left: horizontalIndent, + right: horizontalIndent}, + {queue: false, + duration: 500 + }); + + $('#contactlist').show("slide", { direction: "right", + queue: false, + duration: 500}); + } + }; + + /** + * Creates the avatar element. + * + * @return the newly created avatar element + */ + function createAvatar() { + var avatar = document.createElement('i'); + avatar.className = "icon-avatar avatar"; + + return avatar; + }; + + /** + * Creates the display name paragraph. + * + * @param displayName the display name to set + */ + function createDisplayNameParagraph(displayName) { + var p = document.createElement('p'); + p.innerHTML = displayName; + + return p; + }; + + /** + * Indicates that the display name has changed. + */ + $(document).bind( 'displaynamechanged', + function (event, peerJid, displayName) { + if (peerJid === 'localVideoContainer') + peerJid = connection.emuc.myroomjid; + + var resourceJid = Strophe.getResourceFromJid(peerJid); + + var contactName = $('#contactlist #' + resourceJid + '>p'); + + if (contactName && displayName && displayName.length > 0) + contactName.html(displayName); + }); + + return my; +}(ContactList || {})); diff --git a/css/contact_list.css b/css/contact_list.css new file mode 100644 index 000000000..bb7823ee1 --- /dev/null +++ b/css/contact_list.css @@ -0,0 +1,35 @@ +#contactlist { + background-color:rgba(0,0,0,.65); +} + +#contactlist>ul { + margin: 0px; + padding: 0px; +} + +#contactlist>ul>li { + list-style-type: none; + text-align: left; + color: #FFF; + font-size: 10pt; + padding: 8px 10px; +} + +#contactlist>ul>li>p { + display: inline-block; + vertical-align: middle; + margin: 0px; +} + +#contactlist>ul>li.title { + color: #00ccff; + font-size: 11pt; + border-bottom: 1px solid #676767; +} + +.avatar { + padding: 0px; + margin-right: 10px; + vertical-align: middle; + font-size: 22pt; +} \ No newline at end of file diff --git a/css/font.css b/css/font.css index b0caacf70..89587c18f 100755 --- a/css/font.css +++ b/css/font.css @@ -23,7 +23,24 @@ -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } - +.icon-contactList:before { + content: "\e615"; +} +.icon-avatar:before { + content: "\e616"; +} +.icon-callRetro:before { + content: "\e611"; +} +.icon-callModern:before { + content: "\e612"; +} +.icon-recDisable:before { + content: "\e613"; +} +.icon-recEnable:before { + content: "\e614"; +} .icon-kick1:before { content: "\e60f"; } @@ -60,6 +77,9 @@ .icon-share-doc:before { content: "\e605"; } +.icon-telephone:before { + content: "\e611"; +} .icon-security-locked:before { content: "\e607"; } diff --git a/css/main.css b/css/main.css index eb9cc650d..d70f989e4 100644 --- a/css/main.css +++ b/css/main.css @@ -8,7 +8,8 @@ html, body{ overflow-x: hidden; } -#chatspace { +#chatspace, +#contactlist { display:none; position:absolute; float: right; @@ -18,12 +19,14 @@ html, body{ width: 20%; max-width: 200px; overflow: hidden; - /* background-color:#dfebf1;*/ - background-color:#FFFFFF; - border-left:1px solid #424242; z-index: 5; } +#chatspace { + background-color:#FFF; + border-left:1px solid #424242; +} + #chatconversation { visibility: hidden; position: relative; @@ -131,18 +134,49 @@ html, body{ } #chatButton { - -webkit-transition: all .5s ease-in-out;; - -moz-transition: all .5s ease-in-out;; - transition: all .5s ease-in-out;; + -webkit-transition: all .5s ease-in-out; + -moz-transition: all .5s ease-in-out; + transition: all .5s ease-in-out; } - +/*#ffde00*/ #chatButton.active { - -webkit-text-shadow: 0 0 10px #ffffff; - -moz-text-shadow: 0 0 10px #ffffff; - text-shadow: 0 0 10px #ffffff; + -webkit-text-shadow: -1px 0 10px #00ccff, + 0 1px 10px #00ccff, + 1px 0 10px #00ccff, + 0 -1px 10px #00ccff; + -moz-text-shadow: 1px 0 10px #00ccff, + 0 1px 10px #00ccff, + 1px 0 10px #00ccff, + 0 -1px 10px #00ccff; + text-shadow: -1px 0 10px #00ccff, + 0 1px 10px #00ccff, + 1px 0 10px #00ccff, + 0 -1px 10px #00ccff; } -a.button:hover { +#recordButton { + -webkit-transition: all .5s ease-in-out; + -moz-transition: all .5s ease-in-out; + transition: all .5s ease-in-out; +} +/*#ffde00*/ +#recordButton.active { + -webkit-text-shadow: -1px 0 10px #00ccff, + 0 1px 10px #00ccff, + 1px 0 10px #00ccff, + 0 -1px 10px #00ccff; + -moz-text-shadow: 1px 0 10px #00ccff, + 0 1px 10px #00ccff, + 1px 0 10px #00ccff, + 0 -1px 10px #00ccff; + text-shadow: -1px 0 10px #00ccff, + 0 1px 10px #00ccff, + 1px 0 10px #00ccff, + 0 -1px 10px #00ccff; +} + +a.button:hover, +a.bottomToolbarButton:hover { top: 0; cursor: pointer; background: rgba(0, 0, 0, 0.3); @@ -165,7 +199,7 @@ a.button:hover { background: #676767; } -input[type='text'], textarea { +input[type='text'], input[type='password'], textarea { display: inline-block; font-size: 14px; padding: 5px; @@ -181,7 +215,7 @@ input[type='text'], textarea { resize: none; /* prevents the user-resizing, adjust to taste */ } -input[type='text'], textarea:focus { +input[type='text'], input[type='password'], textarea:focus { box-shadow: inset 0 0 3px 2px #ACD8F0; /* provides a more style-able replacement to the outline */ } @@ -229,3 +263,195 @@ form { overflow: visible; z-index: 100; } + +#enter_room_field { + border-radius: 10px; + font-size: 16px; + padding: 15px 55px 10px 30px; + border: none; + -moz-border-radius: 10px; + -webkit-border-radius: 10px; + -webkit-appearance: none; + width: 318px; + height: 55px; + position:absolute; + font-weight: 500; + font-family: Helvetica; + box-shadow: none; + z-index: 2; + } + +#enter_room_button { + width: 73px; + height: 45px; + background-color: #16a8fe; + moz-border-radius: 15px; + -webkit-border-radius: 15px; + color: #ffffff; + font-weight: 600; + border: none; + position:absolute; + margin-left: 240px; + margin-top: 5px; + font-size: 19px; + font-family: Helvetica; + padding-top: 6px; + z-index: 2; + outline: none; +} + +#enter_room { + margin: 70px auto 0px auto; + width:318px; +} + +#welcome_page_header +{ + background-image: url(../images/welcome_page/pattern-header.png); + height: 290px; + width: 100%; + position: absolute; +} + +#welcome_page_main +{ + background-image:url(../images/welcome_page/pattern-body.png); + width: 100%; + position: absolute; + margin-top: 290px; +} + +#jitsi_logo +{ + background-image:url(../images/welcome_page/jitsi-logo.png); + width: 186px; + height: 74px; + position: absolute; + top: 15px; + left: 30px; +} + +#brand_logo +{ + background-image:url(../images/welcome_page/brand-logo.png); + width: 215px; + height: 55px; + position: absolute; + top: 15px; + right: 30px; + +} + +#brand_header +{ + background-image:url(../images/welcome_page/header-big.png); + position:absolute; + width: 583px; + height: 274px; + left: 340px; + top:15px; +} + +#header_text +{ + position: absolute; + left: 200px; + top: 150px; + width: 885px; + height: 100px; + color: #ffffff; + font-family: Helvetica; + font-size: 24px; + text-align: center; +} + +#features +{ + margin-top: 30px; +} + +.feature_row +{ + width: 100%; + left: 115px; + position: relative; + float: left; + margin-bottom: 30px; +} + +.feature_holder +{ + float:left; + width: 169px; + padding-left: 75px; +} + +.feature_icon +{ + background-image:url(../images/welcome_page/bubble.png); + background-repeat: no-repeat; + width: 169px; + height: 169px; + font-family: Helvetica; + color: #ffffff; + font-size: 22px; + /*font-weight: bold;*/ + text-align: center; + display: table-cell; + padding: 50px 29px 0px 17px; +} + +.feature_description +{ + width: 169px; + font-family: Helvetica; + color: #ffffff; + font-size: 16px; + padding-top: 30px; + line-height: 22px; + font-weight: 200; +} + +#bottomToolbar { + display:block; + position: absolute; + right: -1; + bottom: 40px; + width: 29px; + border-top-left-radius: 10px; + border-bottom-left-radius: 10px; + color: #FFF; + border: 1px solid #000; + background: rgba(50,50,50,.65); + padding-top: 5px; + padding-bottom: 5px; + z-index: 6; /*+1 from #remoteVideos*/ +} + +.bottomToolbarButton { + display: inline-block; + position: relative; + color: #FFFFFF; + top: 0; + padding-top: 3px; + width: 29px; + height: 20px; + cursor: pointer; + font-size: 10pt; + text-align: center; + text-shadow: 0px 1px 0px rgba(255,255,255,.3), 0px -1px 0px rgba(0,0,0,.7); + z-index: 1; +} + +.active { + color: #00ccff; +} + +.bottomToolbar_span>span { + display: inline-block; + position: absolute; + font-size: 7pt; + color: #ffffff; + text-align:center; + cursor: pointer; +} diff --git a/css/modaldialog.css b/css/modaldialog.css index 97afbeb57..b8f17b8bc 100644 --- a/css/modaldialog.css +++ b/css/modaldialog.css @@ -15,7 +15,7 @@ margin: 10px 0; } -.jqistates input[type="text"] { +.jqistates input[type='text'], input[type='password'] { width: 100%; } diff --git a/css/videolayout_default.css b/css/videolayout_default.css index 2e8195272..9067c0ef2 100644 --- a/css/videolayout_default.css +++ b/css/videolayout_default.css @@ -4,6 +4,7 @@ top: 0px; left: 0px; right: 0px; + overflow: hidden; } #remoteVideos { @@ -14,7 +15,7 @@ padding: 18px; bottom: 0; left: 0; - right: 0; + right: 20px; width:auto; border:1px solid transparent; z-index: 5; @@ -32,6 +33,7 @@ background-size: contain; border-radius:8px; border: 2px solid #212425; + margin-right: 3px; } #remoteVideos .videocontainer:hover, @@ -48,9 +50,21 @@ -webkit-animation-name: greyPulse; -webkit-animation-duration: 2s; -webkit-animation-iteration-count: 1; - -webkit-box-shadow: 0 0 18px #388396; - border: 2px solid #388396; - z-index: 3; +} + +#remoteVideos .videocontainer:hover { + -webkit-box-shadow: inset 0 0 10px #FFFFFF, 0 0 10px #FFFFFF; + border: 2px solid #FFFFFF; +} + +#remoteVideos .videocontainer.videoContainerFocused { + -webkit-box-shadow: inset 0 0 28px #006d91; + border: 2px solid #006d91; +} + +#remoteVideos .videocontainer.videoContainerFocused:hover { + -webkit-box-shadow: inset 0 0 5px #FFFFFF, 0 0 10px #FFFFFF, inset 0 0 60px #006d91; + border: 2px solid #FFFFFF; } #localVideoWrapper { @@ -94,9 +108,8 @@ height: 100%; } -.activespeaker { - -webkit-filter: grayscale(1); - filter: grayscale(1); +.dominantspeaker { + background: #000 !important; } #etherpad, @@ -158,10 +171,31 @@ border-radius:20px; } +.videocontainer>span.status { + display: inline-block; + position: absolute; + color: #FFFFFF; + background: rgba(0,0,0,.7); + text-align: center; + text-overflow: ellipsis; + width: 70%; + height: 15%; + left: 15%; + bottom: 2%; + padding: 5px; + font-size: 10pt; + overflow: hidden; + white-space: nowrap; + z-index: 2; + border-radius:20px; +} + +#localVideoContainer>span.status:hover, #localVideoContainer>span.displayname:hover { cursor: text; } +.videocontainer>span.status, .videocontainer>span.displayname { pointer-events: none; } @@ -174,6 +208,7 @@ pointer-events: auto !important; } +.videocontainer>a.status, .videocontainer>a.displayname { display: inline-block; position: absolute; @@ -292,3 +327,14 @@ background-image:url(../images/rightwatermark.png); background-position: center right; } + +.audiolevel { + display: inline-block; + position: absolute; + z-index: 0; + border-radius:10px; +} + +#mixedstream { + display:none !important; +} diff --git a/data_channels.js b/data_channels.js index aba083a46..17cc334d4 100644 --- a/data_channels.js +++ b/data_channels.js @@ -1,16 +1,22 @@ /* global connection, Strophe, updateLargeVideo, focusedVideoSrc*/ + +// cache datachannels to avoid garbage collection +// https://code.google.com/p/chromium/issues/detail?id=405545 +var _dataChannels = []; + /** * Callback triggered by PeerConnection when new data channel is opened * on the bridge. * @param event the event info object. */ + function onDataChannel(event) { var dataChannel = event.channel; dataChannel.onopen = function () { - console.info("Data channel opened by the bridge !!!", dataChannel); + console.info("Data channel opened by the Videobridge!", dataChannel); // Code sample for sending string and/or binary data // Sends String message to the bridge @@ -26,30 +32,61 @@ function onDataChannel(event) dataChannel.onmessage = function (event) { - var msgData = event.data; - console.info("Got Data Channel Message:", msgData, dataChannel); + var data = event.data; + // JSON + var obj; - // Active speaker event - if (msgData.indexOf('activeSpeaker') === 0 && !focusedVideoSrc) + try { - // Endpoint ID from the bridge - var endpointId = msgData.split(":")[1]; - console.info("New active speaker: " + endpointId); + obj = JSON.parse(data); + } + catch (e) + { + console.error( + "Failed to parse data channel message as JSON: ", + data, + dataChannel); + } + if (('undefined' !== typeof(obj)) && (null !== obj)) + { + var colibriClass = obj.colibriClass; - var container = document.getElementById( - 'participant_' + endpointId); - - // Local video will not have container found, but that's ok - // since we don't want to switch to local video - - if (container) + if ("DominantSpeakerEndpointChangeEvent" === colibriClass) { - var video = container.getElementsByTagName("video"); - if (video.length) - { - VideoLayout.updateLargeVideo(video[0].src); - VideoLayout.enableActiveSpeaker(endpointId, true); - } + // Endpoint ID from the Videobridge. + var dominantSpeakerEndpoint = obj.dominantSpeakerEndpoint; + + console.info( + "Data channel new dominant speaker event: ", + dominantSpeakerEndpoint); + $(document).trigger( + 'dominantspeakerchanged', + [dominantSpeakerEndpoint]); + } + else if ("LastNEndpointsChangeEvent" === colibriClass) + { + // The new/latest list of last-n endpoint IDs. + var lastNEndpoints = obj.lastNEndpoints; + /* + * The list of endpoint IDs which are entering the list of + * last-n at this time i.e. were not in the old list of last-n + * endpoint IDs. + */ + var endpointsEnteringLastN = obj.endpointsEnteringLastN; + + var stream = obj.stream; + + console.log( + "Data channel new last-n event: ", + lastNEndpoints, endpointsEnteringLastN, obj); + + $(document).trigger( + 'lastnchanged', + [lastNEndpoints, endpointsEnteringLastN, stream]); + } + else + { + console.debug("Data channel JSON-formatted message: ", obj); } } }; @@ -57,7 +94,11 @@ function onDataChannel(event) dataChannel.onclose = function () { console.info("The Data Channel closed", dataChannel); + var idx = _dataChannels.indexOf(dataChannel); + if (idx > -1) + _dataChannels = _dataChannels.splice(idx, 1); }; + _dataChannels.push(dataChannel); } /** @@ -90,4 +131,5 @@ function bindDataChannelListener(peerConnection) var msgData = event.data; console.info("Got My Data Channel Message:", msgData, dataChannel); };*/ -} \ No newline at end of file +} + diff --git a/debian/changelog b/debian/changelog new file mode 100644 index 000000000..256b1fe7e --- /dev/null +++ b/debian/changelog @@ -0,0 +1,6 @@ +jitsi-meet (1.0.1-1) unstable; urgency=low + + * Initial release + * Jitsi Meet github snapshot from 2014-07-01 + + -- Yasen Pramatarov Tue, 01 Jul 2014 16:31:41 +0300 diff --git a/debian/compat b/debian/compat new file mode 100644 index 000000000..45a4fb75d --- /dev/null +++ b/debian/compat @@ -0,0 +1 @@ +8 diff --git a/debian/control b/debian/control new file mode 100644 index 000000000..f34afcd98 --- /dev/null +++ b/debian/control @@ -0,0 +1,33 @@ +Source: jitsi-meet +Section: net +Priority: extra +Maintainer: Jitsi Team +Uploaders: Emil Ivov , Damian Minkov +Build-Depends: debhelper (>= 8.0.0) +Standards-Version: 3.9.3 +Homepage: https://jitsi.org/meet + +Package: jitsi-meet +Architecture: all +Pre-Depends: adduser, openssl, jitsi-videobridge +Depends: ${misc:Depends}, nginx, jitsi-meet-prosody +Description: WebRTC JavaScript video conferences + Jitsi Meet is a WebRTC JavaScript application that uses Jitsi + Videobridge to provide high quality, scalable video conferences. + . + It is a web interface to Jitsi Videobridge for audio and video + forwarding and relaying, configured to work with nginx + +Package: jitsi-meet-prosody +Architecture: all +Pre-Depends: adduser, openssl, prosody-trunk, jitsi-videobridge +Depends: ${misc:Depends}, nginx, prosody-modules-otalk, lua-sec +Description: Prosody configuration for Jitsi Meet + Jitsi Meet is a WebRTC JavaScript application that uses Jitsi + Videobridge to provide high quality, scalable video conferences. + . + It is a web interface to Jitsi Videobridge for audio and video + forwarding and relaying, configured to work with nginx + . + This package contains configuration for Prosody to be used with + Jitsi Meet. diff --git a/debian/jitsi-meet-prosody.README.Debian b/debian/jitsi-meet-prosody.README.Debian new file mode 100644 index 000000000..c0127d625 --- /dev/null +++ b/debian/jitsi-meet-prosody.README.Debian @@ -0,0 +1,7 @@ +Prosody configuration for Jitsi Meet for Debian +---------------------------- + +Jitsi Meet is a WebRTC video conferencing application. This package contains +configuration of prosody which are needed for Jitsi Meet to work. + + -- Yasen Pramatarov Mon, 30 Jun 2014 23:05:18 +0100 diff --git a/debian/jitsi-meet-prosody.copyright b/debian/jitsi-meet-prosody.copyright new file mode 100644 index 000000000..06376c2a9 --- /dev/null +++ b/debian/jitsi-meet-prosody.copyright @@ -0,0 +1,31 @@ +Format: http://dep.debian.net/deps/dep5 +Upstream-Name: Jitsi Meet +Upstream-Contact: Emil Ivov +Source: https://github.com/jitsi/jitsi-meet + +Files: * +Copyright: 2013-2014 Jitsi +License: MIT + +License: MIT + The MIT License (MIT) + . + Copyright (c) 2013 ESTOS GmbH + Copyright (c) 2013 BlueJimp SARL + . + Permission is hereby granted, free of charge, to any person obtaining a copy of + this software and associated documentation files (the "Software"), to deal in + the Software without restriction, including without limitation the rights to + use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + the Software, and to permit persons to whom the Software is furnished to do so, + subject to the following conditions: + . + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + . + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/debian/jitsi-meet-prosody.docs b/debian/jitsi-meet-prosody.docs new file mode 100644 index 000000000..f640d7fa4 --- /dev/null +++ b/debian/jitsi-meet-prosody.docs @@ -0,0 +1 @@ +debian/usr/share/doc/jitsi-meet-prosody/prosody.cfg.lua-jvb.example.gz diff --git a/debian/jitsi-meet-prosody.install b/debian/jitsi-meet-prosody.install new file mode 100644 index 000000000..c579d36da --- /dev/null +++ b/debian/jitsi-meet-prosody.install @@ -0,0 +1 @@ +debian/usr/share/* usr/share/ diff --git a/debian/jitsi-meet-prosody.postinst b/debian/jitsi-meet-prosody.postinst new file mode 100644 index 000000000..27337f532 --- /dev/null +++ b/debian/jitsi-meet-prosody.postinst @@ -0,0 +1,59 @@ +#!/bin/sh +# postinst script for jitsi-meet-prosody +# +# see: dh_installdeb(1) + +set -e + +# summary of how this script can be called: +# * `configure' +# * `abort-upgrade' +# * `abort-remove' `in-favour' +# +# * `abort-remove' +# * `abort-deconfigure' `in-favour' +# `removing' +# +# for details, see http://www.debian.org/doc/debian-policy/ or +# the debian-policy package + + +case "$1" in + configure) + + . /etc/default/jitsi-videobridge + + if [ -x /etc/prosody/prosody.cfg.lua ]; then + mv /etc/prosody/prosody.cfg.lua /etc/prosody/prosody.cfg.lua.orig + fi + gunzip -c /usr/share/doc/jitsi-meet-prosody/prosody.cfg.lua-jvb.example.gz > /etc/prosody/prosody.cfg.lua + sed -i "s/jitmeet.example.com/$JVB_HOSTNAME/g" /etc/prosody/prosody.cfg.lua + sed -i "s/jitmeetSecret/$JVB_SECRET/g" /etc/prosody/prosody.cfg.lua + if [ ! -f /var/lib/prosody/$JVB_HOSTNAME.crt ]; then + HOST="$( (hostname -s; echo localhost) | head -n 1)" + DOMAIN="$( (hostname -d; echo localdomain) | head -n 1)" + openssl req -new -newkey rsa:4096 -days 365 -nodes -x509 -subj \ + "/O=$DOMAIN/OU=$HOST/CN=$JVB_HOSTNAME/emailAddress=webmaster@$HOST.$DOMAIN" \ + -keyout /var/lib/prosody/$JVB_HOSTNAME.key \ + -out /var/lib/prosody/$JVB_HOSTNAME.crt + fi + ln -sf /var/lib/prosody/$JVB_HOSTNAME.key /etc/prosody/certs/$JVB_HOSTNAME.key + ln -sf /var/lib/prosody/$JVB_HOSTNAME.crt /etc/prosody/certs/$JVB_HOSTNAME.crt + invoke-rc.d prosody restart + ;; + + abort-upgrade|abort-remove|abort-deconfigure) + ;; + + *) + echo "postinst called with unknown argument \`$1'" >&2 + exit 1 + ;; +esac + +# dh_installdeb will replace this with shell code automatically +# generated by other debhelper scripts. + +#DEBHELPER# + +exit 0 diff --git a/debian/jitsi-meet-prosody.postrm b/debian/jitsi-meet-prosody.postrm new file mode 100644 index 000000000..330d19fcf --- /dev/null +++ b/debian/jitsi-meet-prosody.postrm @@ -0,0 +1,48 @@ +#!/bin/sh +# postrm script for jitsi-meet-prosody +# +# see: dh_installdeb(1) + +set -e + +# summary of how this script can be called: +# * `remove' +# * `purge' +# * `upgrade' +# * `failed-upgrade' +# * `abort-install' +# * `abort-install' +# * `abort-upgrade' +# * `disappear' +# +# for details, see http://www.debian.org/doc/debian-policy/ or +# the debian-policy package + +# Load debconf +. /usr/share/debconf/confmodule + + +case "$1" in + purge|remove) + if [ -x "/etc/init.d/prosody" ]; then + invoke-rc.d nginx reload + fi + ;; + + upgrade|failed-upgrade|abort-install|abort-upgrade|disappear) + ;; + + *) + echo "postrm called with unknown argument \`$1'" >&2 + exit 1 + ;; +esac + +# dh_installdeb will replace this with shell code automatically +# generated by other debhelper scripts. + +#DEBHELPER# + +db_stop + +exit 0 diff --git a/debian/jitsi-meet-prosody.preinst b/debian/jitsi-meet-prosody.preinst new file mode 100644 index 000000000..015564ee3 --- /dev/null +++ b/debian/jitsi-meet-prosody.preinst @@ -0,0 +1,35 @@ +#!/bin/sh +# preinst script for jitsi-meet-prosody +# +# see: dh_installdeb(1) + +set -e + +# summary of how this script can be called: +# * `install' +# * `install' +# * `upgrade' +# * `abort-upgrade' +# for details, see http://www.debian.org/doc/debian-policy/ or +# the debian-policy package + + +case "$1" in + install|upgrade) + ;; + + abort-upgrade) + ;; + + *) + echo "preinst called with unknown argument \`$1'" >&2 + exit 1 + ;; +esac + +# dh_installdeb will replace this with shell code automatically +# generated by other debhelper scripts. + +#DEBHELPER# + +exit 0 diff --git a/debian/jitsi-meet-prosody.prerm b/debian/jitsi-meet-prosody.prerm new file mode 100644 index 000000000..c447b17ad --- /dev/null +++ b/debian/jitsi-meet-prosody.prerm @@ -0,0 +1,36 @@ +#!/bin/sh +# prerm script for jitsi-meet-prosody +# +# see: dh_installdeb(1) + +set -e + +# summary of how this script can be called: +# * `remove' +# * `upgrade' +# * `failed-upgrade' +# * `remove' `in-favour' +# * `deconfigure' `in-favour' +# `removing' +# +# for details, see http://www.debian.org/doc/debian-policy/ or +# the debian-policy package + + +case "$1" in + remove|purge) + ;; + + upgrade|deconfigure) + ;; + + failed-upgrade) + ;; + + *) + echo "prerm called with unknown argument \`$1'" >&2 + exit 1 + ;; +esac + +exit 0 diff --git a/debian/jitsi-meet-prosody.substvars b/debian/jitsi-meet-prosody.substvars new file mode 100644 index 000000000..abd3ebebc --- /dev/null +++ b/debian/jitsi-meet-prosody.substvars @@ -0,0 +1 @@ +misc:Depends= diff --git a/debian/jitsi-meet.README.Debian b/debian/jitsi-meet.README.Debian new file mode 100644 index 000000000..e79c133d2 --- /dev/null +++ b/debian/jitsi-meet.README.Debian @@ -0,0 +1,8 @@ +Jitsi Meet for Debian +---------------------------- + +This is a WebRTC frontend of the video conferencing tool Jitsi Meet. It depends on the +jitsi-videobridge package, which is a SFU (Selective Forwarding Unit) and both packages +are designed to work together. + + -- Yasen Pramatarov Mon, 30 Jun 2014 23:05:18 +0100 diff --git a/debian/jitsi-meet.README.source b/debian/jitsi-meet.README.source new file mode 100644 index 000000000..45d7a1eb7 --- /dev/null +++ b/debian/jitsi-meet.README.source @@ -0,0 +1,6 @@ +jitsi-meet for Debian +--------------------- + +The jitsi-meet package is built from the sources of Jitsi Meet. + +Jitsi Meet is downloaded from https://github.com/jitsi/jitsi-meet and the git files are removed. you can recreate the source with 'git clone https://github.com/jitsi/jitsi-meet.git'. diff --git a/debian/jitsi-meet.copyright b/debian/jitsi-meet.copyright new file mode 100644 index 000000000..06376c2a9 --- /dev/null +++ b/debian/jitsi-meet.copyright @@ -0,0 +1,31 @@ +Format: http://dep.debian.net/deps/dep5 +Upstream-Name: Jitsi Meet +Upstream-Contact: Emil Ivov +Source: https://github.com/jitsi/jitsi-meet + +Files: * +Copyright: 2013-2014 Jitsi +License: MIT + +License: MIT + The MIT License (MIT) + . + Copyright (c) 2013 ESTOS GmbH + Copyright (c) 2013 BlueJimp SARL + . + Permission is hereby granted, free of charge, to any person obtaining a copy of + this software and associated documentation files (the "Software"), to deal in + the Software without restriction, including without limitation the rights to + use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + the Software, and to permit persons to whom the Software is furnished to do so, + subject to the following conditions: + . + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + . + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/debian/jitsi-meet.docs b/debian/jitsi-meet.docs new file mode 100644 index 000000000..380f29e42 --- /dev/null +++ b/debian/jitsi-meet.docs @@ -0,0 +1,3 @@ +README.md +debian/usr/share/doc/jitsi-meet/README +debian/usr/share/doc/jitsi-meet/jitsi-meet.example diff --git a/debian/jitsi-meet.install b/debian/jitsi-meet.install new file mode 100644 index 000000000..b49f84bd6 --- /dev/null +++ b/debian/jitsi-meet.install @@ -0,0 +1,2 @@ +* usr/share/jitsi-meet/ +debian/usr/share/* usr/share/ diff --git a/debian/jitsi-meet.postinst b/debian/jitsi-meet.postinst new file mode 100644 index 000000000..eab06e568 --- /dev/null +++ b/debian/jitsi-meet.postinst @@ -0,0 +1,64 @@ +#!/bin/sh +# postinst script for jitsi-meet +# +# see: dh_installdeb(1) + +set -e + +# summary of how this script can be called: +# * `configure' +# * `abort-upgrade' +# * `abort-remove' `in-favour' +# +# * `abort-remove' +# * `abort-deconfigure' `in-favour' +# `removing' +# +# for details, see http://www.debian.org/doc/debian-policy/ or +# the debian-policy package + + +case "$1" in + configure) + + # nginx conf + . /etc/default/jitsi-videobridge + cp /usr/share/doc/jitsi-meet/jitsi-meet.example /etc/nginx/sites-available/$JVB_HOSTNAME.conf + if [ ! -f /etc/nginx/sites-enabled/$JVB_HOSTNAME.conf ]; then + ln -s /etc/nginx/sites-available/$JVB_HOSTNAME.conf /etc/nginx/sites-enabled/$JVB_HOSTNAME.conf + fi + sed -i "s/jitsi-meet.example.com/$JVB_HOSTNAME/g" /etc/nginx/sites-available/$JVB_HOSTNAME.conf + # FIXME do we need the default? + if [ ! -f /etc/nginx/sites-enabled/default ]; then + ln -s /etc/nginx/sites-available/default /etc/nginx/sites-enabled/default + fi + if grep "# server_names_hash_bucket_size 64" /etc/nginx/nginx.conf > /dev/null; then + sed -i "s/#\ server_names_hash_bucket_size\ 64/\ server_names_hash_bucket_size\ 64/" /etc/nginx/nginx.conf + fi + + # jitsi meet + chown -R www-data:www-data /usr/share/jitsi-meet/ + sed -i "s/jitsi-meet.example.com/$JVB_HOSTNAME/g" /usr/share/jitsi-meet/config.js + # enable turn + if grep "// useStunTurn: true," /usr/share/jitsi-meet/config.js > /dev/null; then + sed -i "s/\/\/\ \ useStunTurn:\ true,/\ \ \ \ useStunTurn:\ true,/" /usr/share/jitsi-meet/config.js + fi + invoke-rc.d nginx restart + + ;; + + abort-upgrade|abort-remove|abort-deconfigure) + ;; + + *) + echo "postinst called with unknown argument \`$1'" >&2 + exit 1 + ;; +esac + +# dh_installdeb will replace this with shell code automatically +# generated by other debhelper scripts. + +#DEBHELPER# + +exit 0 diff --git a/debian/jitsi-meet.postrm b/debian/jitsi-meet.postrm new file mode 100644 index 000000000..89619fa24 --- /dev/null +++ b/debian/jitsi-meet.postrm @@ -0,0 +1,48 @@ +#!/bin/sh +# postrm script for jitsi-meet +# +# see: dh_installdeb(1) + +set -e + +# summary of how this script can be called: +# * `remove' +# * `purge' +# * `upgrade' +# * `failed-upgrade' +# * `abort-install' +# * `abort-install' +# * `abort-upgrade' +# * `disappear' +# +# for details, see http://www.debian.org/doc/debian-policy/ or +# the debian-policy package + +# Load debconf +. /usr/share/debconf/confmodule + + +case "$1" in + purge|remove) + if [ -x "/etc/init.d/nginx" ]; then + invoke-rc.d nginx reload + fi + ;; + + upgrade|failed-upgrade|abort-install|abort-upgrade|disappear) + ;; + + *) + echo "postrm called with unknown argument \`$1'" >&2 + exit 1 + ;; +esac + +# dh_installdeb will replace this with shell code automatically +# generated by other debhelper scripts. + +#DEBHELPER# + +db_stop + +exit 0 diff --git a/debian/jitsi-meet.preinst b/debian/jitsi-meet.preinst new file mode 100644 index 000000000..794394d6d --- /dev/null +++ b/debian/jitsi-meet.preinst @@ -0,0 +1,35 @@ +#!/bin/sh +# preinst script for jitsi-meet +# +# see: dh_installdeb(1) + +set -e + +# summary of how this script can be called: +# * `install' +# * `install' +# * `upgrade' +# * `abort-upgrade' +# for details, see http://www.debian.org/doc/debian-policy/ or +# the debian-policy package + + +case "$1" in + install|upgrade) + ;; + + abort-upgrade) + ;; + + *) + echo "preinst called with unknown argument \`$1'" >&2 + exit 1 + ;; +esac + +# dh_installdeb will replace this with shell code automatically +# generated by other debhelper scripts. + +#DEBHELPER# + +exit 0 diff --git a/debian/jitsi-meet.prerm b/debian/jitsi-meet.prerm new file mode 100644 index 000000000..10f8454d5 --- /dev/null +++ b/debian/jitsi-meet.prerm @@ -0,0 +1,36 @@ +#!/bin/sh +# prerm script for jitsi-meet +# +# see: dh_installdeb(1) + +set -e + +# summary of how this script can be called: +# * `remove' +# * `upgrade' +# * `failed-upgrade' +# * `remove' `in-favour' +# * `deconfigure' `in-favour' +# `removing' +# +# for details, see http://www.debian.org/doc/debian-policy/ or +# the debian-policy package + + +case "$1" in + remove|purge) + ;; + + upgrade|deconfigure) + ;; + + failed-upgrade) + ;; + + *) + echo "prerm called with unknown argument \`$1'" >&2 + exit 1 + ;; +esac + +exit 0 diff --git a/debian/rules b/debian/rules new file mode 100755 index 000000000..b50198c09 --- /dev/null +++ b/debian/rules @@ -0,0 +1,16 @@ +#!/usr/bin/make -f +# -*- makefile -*- +# Sample debian/rules that uses debhelper. +# This file was originally written by Joey Hess and Craig Small. +# As a special exception, when this file is copied by dh-make into a +# dh-make output file, you may use that output file without restriction. +# This special exception was added by Craig Small in version 0.37 of dh-make. + +# Uncomment this to turn on verbose mode. +#export DH_VERBOSE=1 + +%: + dh $@ + +override_dh_install-indep: + dh_install -Xdebian -Xdoc -XINSTALL.md -XLICENSE -XREADME.md usr/share/jitsi-meet/ diff --git a/debian/source/format b/debian/source/format new file mode 100644 index 000000000..163aaf8d8 --- /dev/null +++ b/debian/source/format @@ -0,0 +1 @@ +3.0 (quilt) diff --git a/debian/source/include-binaries b/debian/source/include-binaries new file mode 100644 index 000000000..ee3f013b5 --- /dev/null +++ b/debian/source/include-binaries @@ -0,0 +1,19 @@ +debian/usr/share/jitsi-meet/favicon.ico +debian/usr/share/jitsi-meet/fonts/jitsi.eot +debian/usr/share/jitsi-meet/fonts/jitsi.woff +debian/usr/share/jitsi-meet/fonts/jitsi.ttf +debian/usr/share/jitsi-meet/sounds/left.wav +debian/usr/share/jitsi-meet/sounds/incomingMessage.wav +debian/usr/share/jitsi-meet/sounds/joined.wav +debian/usr/share/jitsi-meet/images/estoslogo.png +debian/usr/share/jitsi-meet/images/chromelogo.png +debian/usr/share/jitsi-meet/images/jitsilogo.png +debian/usr/share/jitsi-meet/images/watermark.png +debian/usr/share/jitsi-meet/images/avatarprezi.png +debian/usr/share/jitsi-meet/images/chromepointer.png +debian/usr/share/jitsi-meet/images/avatar1.png +debian/usr/share/jitsi-meet/images/popupPointer.png +debian/usr/share/jitsi-meet/images/favicon.ico +debian/usr/share/doc/jitsi-meet/changelog.Debian.gz +debian/usr/share/doc/jitsi-meet-prosody/changelog.Debian.gz +debian/usr/share/doc/jitsi-meet-prosody/prosody.cfg.lua-jvb.example.gz diff --git a/debian/usr/share/doc/jitsi-meet-prosody/README b/debian/usr/share/doc/jitsi-meet-prosody/README new file mode 100644 index 000000000..d141f6b71 --- /dev/null +++ b/debian/usr/share/doc/jitsi-meet-prosody/README @@ -0,0 +1 @@ +Prosody configuration for Jitsi Meet diff --git a/debian/usr/share/doc/jitsi-meet-prosody/changelog.Debian.gz b/debian/usr/share/doc/jitsi-meet-prosody/changelog.Debian.gz new file mode 100644 index 000000000..8159f41b6 Binary files /dev/null and b/debian/usr/share/doc/jitsi-meet-prosody/changelog.Debian.gz differ diff --git a/debian/usr/share/doc/jitsi-meet-prosody/copyright b/debian/usr/share/doc/jitsi-meet-prosody/copyright new file mode 100644 index 000000000..06376c2a9 --- /dev/null +++ b/debian/usr/share/doc/jitsi-meet-prosody/copyright @@ -0,0 +1,31 @@ +Format: http://dep.debian.net/deps/dep5 +Upstream-Name: Jitsi Meet +Upstream-Contact: Emil Ivov +Source: https://github.com/jitsi/jitsi-meet + +Files: * +Copyright: 2013-2014 Jitsi +License: MIT + +License: MIT + The MIT License (MIT) + . + Copyright (c) 2013 ESTOS GmbH + Copyright (c) 2013 BlueJimp SARL + . + Permission is hereby granted, free of charge, to any person obtaining a copy of + this software and associated documentation files (the "Software"), to deal in + the Software without restriction, including without limitation the rights to + use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + the Software, and to permit persons to whom the Software is furnished to do so, + subject to the following conditions: + . + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + . + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/debian/usr/share/doc/jitsi-meet-prosody/prosody.cfg.lua-jvb.example.gz b/debian/usr/share/doc/jitsi-meet-prosody/prosody.cfg.lua-jvb.example.gz new file mode 100644 index 000000000..5ccfeebc7 Binary files /dev/null and b/debian/usr/share/doc/jitsi-meet-prosody/prosody.cfg.lua-jvb.example.gz differ diff --git a/debian/usr/share/doc/jitsi-meet/README b/debian/usr/share/doc/jitsi-meet/README new file mode 100644 index 000000000..a198dcae3 --- /dev/null +++ b/debian/usr/share/doc/jitsi-meet/README @@ -0,0 +1,13 @@ +Jitsi Meet + +==== + +A WebRTC-powered multi-user videochat. For a live demo, check out either +https://meet.estos.de/ or https://meet.jit.si/. + +Built using colibri.js[0] and strophe.jingle[1], powered by the jitsi-videobridge[2] + + +[0] https://github.com/ESTOS/colibri.js +[1] https://github.com/ESTOS/strophe.jingle +[3] https://github.com/jitsi/jitsi-videobridge diff --git a/debian/usr/share/doc/jitsi-meet/changelog.Debian.gz b/debian/usr/share/doc/jitsi-meet/changelog.Debian.gz new file mode 100644 index 000000000..91191c766 Binary files /dev/null and b/debian/usr/share/doc/jitsi-meet/changelog.Debian.gz differ diff --git a/debian/usr/share/doc/jitsi-meet/copyright b/debian/usr/share/doc/jitsi-meet/copyright new file mode 100644 index 000000000..06376c2a9 --- /dev/null +++ b/debian/usr/share/doc/jitsi-meet/copyright @@ -0,0 +1,31 @@ +Format: http://dep.debian.net/deps/dep5 +Upstream-Name: Jitsi Meet +Upstream-Contact: Emil Ivov +Source: https://github.com/jitsi/jitsi-meet + +Files: * +Copyright: 2013-2014 Jitsi +License: MIT + +License: MIT + The MIT License (MIT) + . + Copyright (c) 2013 ESTOS GmbH + Copyright (c) 2013 BlueJimp SARL + . + Permission is hereby granted, free of charge, to any person obtaining a copy of + this software and associated documentation files (the "Software"), to deal in + the Software without restriction, including without limitation the rights to + use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + the Software, and to permit persons to whom the Software is furnished to do so, + subject to the following conditions: + . + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + . + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/debian/usr/share/doc/jitsi-meet/jitsi-meet.example b/debian/usr/share/doc/jitsi-meet/jitsi-meet.example new file mode 100644 index 000000000..dbba423aa --- /dev/null +++ b/debian/usr/share/doc/jitsi-meet/jitsi-meet.example @@ -0,0 +1,36 @@ +server { + listen 80; + server_name jitsi-meet.example.com; + return 301 https://$host$request_uri; +} +server { + listen 443 ssl; + server_name jitsi-meet.example.com; + + ssl_certificate /var/lib/prosody/jitsi-meet.example.com.crt; + ssl_certificate_key /var/lib/prosody/jitsi-meet.example.com.key; + + root /usr/share/jitsi-meet; + index index.html index.htm; + + location ~ ^/([a-zA-Z0-9]+)$ { + rewrite ^/(.*)$ / break; + } + + # BOSH + location /http-bind { + proxy_pass http://localhost:5280/http-bind; + proxy_set_header X-Forwarded-For $remote_addr; + proxy_set_header Host $http_host; + } + + # xmpp websockets + location /xmpp-websocket { + proxy_pass http://localhost:5280; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + tcp_nodelay on; + } +} diff --git a/doc/quick-install.md b/doc/quick-install.md new file mode 100644 index 000000000..8b3869b9a --- /dev/null +++ b/doc/quick-install.md @@ -0,0 +1,103 @@ +# Jitsi Meet quick install + +This documents decribes the needed steps for quick Jitsi Meet installation on a Debian based GNU/Linux system. + +N.B.: All commands are supposed to be run by root. If you are logged in as a regular user with sudo rights, please prepend ___sudo___ to each of the commands. + +## Basic Jitsi Meet install + +### Add the repository + +```sh +add-apt-repository 'deb http://download.jitsi.org/nightly/deb unstable/' +wget -qO - https://download.jitsi.org/nightly/deb/unstable/archive.key | apt-key add - +``` + +add-apt-repository is in the default Ubuntu install and is available for both Ubuntu and Debian, but if it's not present, either install it with + +```sh +apt-get -y install software-properties-common +add-apt-repository 'deb http://download.jitsi.org/nightly/deb unstable/' +wget -qO - https://download.jitsi.org/nightly/deb/unstable/archive.key | apt-key add - +``` + +or add the repository by hand with + +```sh +echo 'deb http://download.jitsi.org/nightly/deb unstable/' >> /etc/apt/sources.list +wget -qO - https://download.jitsi.org/nightly/deb/unstable/archive.key | apt-key add - +``` + +### Update the package lists + +```sh +apt-get update +``` + +### Install Jitsi Meet + +```sh +apt-get -y install jitsi-meet +``` + +During the installation you'll be asked to enter the hostname of the Jitsi Meet instance. If you have a FQDN hostname for the instance already set ip in DNS, enter it there. If you don't have a resolvable hostname, you can enter the IP address of the machine (if it is static or doesn't change). + +This hostname (or IP address) will be used for virtualhost configuration inside the Jitsi Meet and also you and your correspondents will be using it to access the web conferences. + +### Open a conference + +Launch a web broswer (Chrome, Chromium or latest Opera) and enter in the URL bar the hostname (or IP address) you used in the previous step. + +Confirm that you trust the self-signed certificate of the newly installed Jitsi Meet. + +Enjoy! + +## Adding sip-gateway to Jitsi Meet + +### Install Jigasi + +```sh +apt-get -o Dpkg::Options::="--force-overwrite" -y install jigasi +``` + +or + +```sh +wget https://download.jitsi.org/jigasi_1.0-1_amd64.deb +dpkg -i --force-overwrite jigasi_1.0-1_amd64.deb +``` + +You need to pass "--force-overwrite" option to dpkg, because the jigasi package patches some of the files in the jitsi-meet package in order to enable the SIP support in Jitsi Meet. + +During the installation you'll be asked to enter your SIP account and password. This account will be used to invite the other SIP participants. + +### Reload Jitsi Meet + +Launch again a browser with the Jitsi Meet URL and you'll see a telephone icon on the right end of the toolbar. Use it to invite SIP accounts to join the current conference. + +Enjoy! + +## Troubleshoot + +If the SIP gateway doesn't work on first try, restart it. + +```sh +/etc/init.d/jigasi restart +``` + +## Deinstall + +```sh +apt-get purge jigasi jitsi-meet jitsi-videobridge +``` + +Somethimes the following packages will fail to uninstall properly: + +- jigasi +- jitsi-videobridge + +When this happens, just run the deinstall command a second time and it should be ok. + +The reason for failure is that not allways the daemons are stopped right away, there is a timeout before the actual stop. And if the unistall script goes on before the services' stop, there is an error. + +The second run of the deinstall command fixes this, as by then the jigasi or jvb daemons are already stopped. diff --git a/favicon.ico b/favicon.ico new file mode 100644 index 000000000..bb34caf1f Binary files /dev/null and b/favicon.ico differ diff --git a/fonts/jitsi.eot b/fonts/jitsi.eot index 0679560af..de08ba656 100755 Binary files a/fonts/jitsi.eot and b/fonts/jitsi.eot differ diff --git a/fonts/jitsi.svg b/fonts/jitsi.svg index b2289b675..0276e562c 100755 --- a/fonts/jitsi.svg +++ b/fonts/jitsi.svg @@ -15,13 +15,19 @@ - + - - + + + + + + + + \ No newline at end of file diff --git a/fonts/jitsi.ttf b/fonts/jitsi.ttf index dfe9bade0..1959f3756 100755 Binary files a/fonts/jitsi.ttf and b/fonts/jitsi.ttf differ diff --git a/fonts/jitsi.woff b/fonts/jitsi.woff index 23574d131..9c81e8ec1 100755 Binary files a/fonts/jitsi.woff and b/fonts/jitsi.woff differ diff --git a/images/avatar2.png b/images/avatar2.png new file mode 100644 index 000000000..f59231c87 Binary files /dev/null and b/images/avatar2.png differ diff --git a/images/welcome_page/bubble.png b/images/welcome_page/bubble.png new file mode 100644 index 000000000..9a3edc6cc Binary files /dev/null and b/images/welcome_page/bubble.png differ diff --git a/images/welcome_page/jitsi-logo.png b/images/welcome_page/jitsi-logo.png new file mode 100644 index 000000000..67421994e Binary files /dev/null and b/images/welcome_page/jitsi-logo.png differ diff --git a/images/welcome_page/pattern-body.png b/images/welcome_page/pattern-body.png new file mode 100644 index 000000000..122dbca32 Binary files /dev/null and b/images/welcome_page/pattern-body.png differ diff --git a/images/welcome_page/pattern-header.png b/images/welcome_page/pattern-header.png new file mode 100644 index 000000000..83ff8eec3 Binary files /dev/null and b/images/welcome_page/pattern-header.png differ diff --git a/index.html b/index.html index 69bc395f4..6dd1737bb 100644 --- a/index.html +++ b/index.html @@ -1,4 +1,4 @@ - + Jitsi Videobridge meets WebRTC @@ -9,7 +9,7 @@ - + @@ -19,18 +19,20 @@ - + + - - + + - - + + - - + + + @@ -38,16 +40,22 @@ - - + + + + + + + - - - + + + + @@ -57,84 +65,201 @@ -
-