Merge branch 'master' of https://github.com/bigbluebutton/bigbluebutton into chat-improvements

Conflicts:
	bigbluebutton-html5/app/client/views/whiteboard/whiteboard.coffee
This commit is contained in:
Oleksandr Zhurbenko 2015-12-13 20:20:10 -08:00
commit da7242737b
29 changed files with 642 additions and 134 deletions

View File

@ -390,7 +390,7 @@ trait UsersApp {
* and is reconnecting. Make the user as joined only in the voice conference. If we get a
* user left voice conference message, then we will remove the user from the users list.
*/
handleUserJoinedVoiceConfMessage((new UserJoinedVoiceConfMessage(mProps.voiceBridge,
switchUserToPhoneUser((new UserJoinedVoiceConfMessage(mProps.voiceBridge,
vu.userId, u.userID, u.externUserID, vu.callerName,
vu.callerNum, vu.muted, vu.talking, u.listenOnly)));
}
@ -461,6 +461,34 @@ trait UsersApp {
}
}
def switchUserToPhoneUser(msg: UserJoinedVoiceConfMessage) = {
log.info("User has been disconnected but still in voice conf. Switching to phone user. meetingId="
+ mProps.meetingID + " callername=" + msg.callerIdName
+ " userId=" + msg.userId + " extUserId=" + msg.externUserId)
usersModel.getUser(msg.userId) match {
case Some(user) => {
val vu = new VoiceUser(msg.voiceUserId, msg.userId, msg.callerIdName,
msg.callerIdNum, joined = true, locked = false,
msg.muted, msg.talking, msg.listenOnly)
val nu = user.copy(voiceUser = vu, listenOnly = msg.listenOnly)
usersModel.addUser(nu)
log.info("User joined voice. meetingId=" + mProps.meetingID + " userId=" + user.userID + " user=" + nu)
outGW.send(new UserJoinedVoice(mProps.meetingID, mProps.recorded, mProps.voiceBridge, nu))
if (meetingModel.isMeetingMuted()) {
outGW.send(new MuteVoiceUser(mProps.meetingID, mProps.recorded,
nu.userID, nu.userID, mProps.voiceBridge,
nu.voiceUser.userId, meetingModel.isMeetingMuted()))
}
}
case None => {
handleUserJoinedVoiceFromPhone(msg)
}
}
}
def handleUserJoinedVoiceConfMessage(msg: UserJoinedVoiceConfMessage) = {
log.info("Received user joined voice. meetingId=" + mProps.meetingID + " callername=" + msg.callerIdName
+ " userId=" + msg.userId + " extUserId=" + msg.externUserId)

View File

@ -43,6 +43,8 @@ Author: Fred Dixon <ffdixon@bigbluebutton.org>
<%@ include file="bbb_api.jsp"%>
<%@ page import="java.util.regex.*"%>
<%@ page import="org.apache.commons.lang.StringEscapeUtils"%>
<br>
@ -121,7 +123,7 @@ $(document).ready(function(){
<tbody>
<tr>
<td width="50%">
<center><strong> <%=username%>'s meeting</strong> has been
<center><strong> <%=StringEscapeUtils.escapeXml(username)%>'s meeting</strong> has been
created.</center>
</td>
@ -213,7 +215,7 @@ function mycallback() {
<tr>
<td width="50%">
<p>Hi <%=username%>,</p>
<p>Hi <%=StringEscapeUtils.escapeXml(username)%>,</p>
<p>Now waiting for the moderator to start <strong><%=meetingID%></strong>.</p>
<br />
<p>(Your browser will automatically refresh and join the meeting

View File

@ -26,9 +26,6 @@
color = colourToHex(color)
color
@getCurrentSlideDoc = -> # returns only one document
BBB.getCurrentSlide()
@getInSession = (k) -> SessionAmplify.get k
@getTime = -> # returns epoch in ms
@ -60,7 +57,9 @@ Handlebars.registerHelper "getCurrentMeeting", ->
Meteor.Meetings.findOne()
Handlebars.registerHelper "getCurrentSlide", ->
getCurrentSlideDoc()
result = BBB.getCurrentSlide("helper getCurrentSlide")
# console.log "result=#{JSON.stringify result}"
result
# Allow access through all templates
Handlebars.registerHelper "getInSession", (k) -> SessionAmplify.get k
@ -69,7 +68,7 @@ Handlebars.registerHelper "getMeetingName", ->
BBB.getMeetingName()
Handlebars.registerHelper "getShapesForSlide", ->
currentSlide = getCurrentSlideDoc()
currentSlide = BBB.getCurrentSlide("helper getShapesForSlide")
# try to reuse the lines above
Meteor.Shapes.find({whiteboardId: currentSlide?.slide?.id})
@ -112,6 +111,9 @@ Handlebars.registerHelper "isCurrentUserTalking", ->
Handlebars.registerHelper "isCurrentUserPresenter", ->
BBB.isUserPresenter(getInSession('userId'))
Handlebars.registerHelper "isCurrentUserModerator", ->
BBB.getMyRole() is "MODERATOR"
Handlebars.registerHelper "isDisconnected", ->
return !Meteor.status().connected
@ -149,7 +151,7 @@ Handlebars.registerHelper "pointerLocation", ->
currentPresentation = Meteor.Presentations.findOne({"presentation.current": true})
presentationId = currentPresentation?.presentation?.id
currentSlideDoc = Meteor.Slides.findOne({"presentationId": presentationId, "slide.current": true})
pointer = currentPresentation?.pointer
pointer = Meteor.Cursor.findOne()
pointer.x = (- currentSlideDoc.slide.x_offset * 2 + currentSlideDoc.slide.width_ratio * pointer.x) / 100
pointer.y = (- currentSlideDoc.slide.y_offset * 2 + currentSlideDoc.slide.height_ratio * pointer.y) / 100
pointer
@ -445,6 +447,12 @@ Handlebars.registerHelper "getPollQuestions", ->
console.log "logging out"
clearSessionVar(document.location = getInSession 'logoutURL') # navigate to logout
@kickUser = (meetingId, toKickUserId, requesterUserId, authToken) ->
Meteor.call("kickUser", meetingId, toKickUserId, requesterUserId, authToken)
@setUserPresenter = (meetingId, newPresenterId, requesterSetPresenter, newPresenterName, authToken) ->
Meteor.call("setUserPresenter", meetingId, newPresenterId, requesterSetPresenter, newPresenterName, authToken)
# Clear the local user session
@clearSessionVar = (callback) ->
amplify.store('authToken', null)

View File

@ -123,10 +123,12 @@ https://github.com/bigbluebutton/bigbluebutton/blob/master/bigbluebutton-client/
return lockedMicForRoom and BBB.amILocked()
BBB.getCurrentSlide = ->
BBB.getCurrentSlide = (callingLocaton)->
currentPresentation = Meteor.Presentations.findOne({"presentation.current": true})
presentationId = currentPresentation?.presentation?.id
currentSlide = Meteor.Slides.findOne({"presentationId": presentationId, "slide.current": true})
# console.log "trigger:#{callingLocaton} currentSlideId=#{currentSlide?._id}"
currentSlide
BBB.getMeetingName = ->
Meteor.Meetings.findOne()?.meetingName or null

View File

@ -42,11 +42,11 @@
// 120px is size of the icon list with all icons enabled (lock, cam, mic/listen)
@media @landscape {
height: 27px;
min-height: 30px;
font-size: 4.5mm;
}
@media @desktop-portrait {
height: 27px;
min-height: 30px;
font-size: 4.5mm;
}
@media @phone-portrait, @phone-portrait-with-keyboard {
@ -104,9 +104,30 @@
max-height: 100% !important;
height: 100%;
#content{
.setPresenter{
color: #A0A0A0;
}
@media @desktop-landscape, @desktop-portrait {
.kickUser, .setPresenter{
opacity:0;
color: #fff;
cursor: pointer;
display: none;
-webkit-animation: fadeInAnimation .5s;
animation: fadeInAnimation .5s;
}
}
}
#content:hover {
@media @landscape, @desktop-portrait {
background-color: #2C4155;
.kickUser, .setPresenter{
display: inline;
opacity:1;
}
}
}
#content {
@ -195,3 +216,12 @@
padding-left: 5px;
}
}
@-webkit-keyframes fadeInAnimation {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}

View File

@ -9,6 +9,9 @@ Template.displayUserIcons.events
# the userId of the person who is lowering the hand
BBB.lowerHand(getInSession("meetingId"), @userId, getInSession("userId"), getInSession("authToken"))
'click .kickUser': (event) ->
kickUser BBB.getMeetingId(), @.userId, getInSession("userId"), getInSession("authToken")
Template.displayUserIcons.helpers
userLockedIconApplicable: (userId) ->
# the lock settings affect the user (and requiire a lock icon) if
@ -54,6 +57,9 @@ Template.usernameEntry.events
break
setInSession 'chats', chats
'click .setPresenter': (event) ->
setUserPresenter BBB.getMeetingId(), @.userId, getInSession('userId'), @.user.name, getInSession('authToken')
Template.usernameEntry.helpers
hasGotUnreadMailClass: (userId) ->
chats = getInSession('chats')

View File

@ -43,6 +43,14 @@
{{/if}}
{{/if}}
{{#unless isCurrentUser userId}}
{{#if isCurrentUserModerator}}
<span class="kickUser" rel="tooltip" data-placement="bottom" title="Kick {{user.name}}">
<i class="icon fi-x-circle usericon"></i>
</span>
{{/if}}
{{/unless}}
{{#if isUserSharingVideo userId}}
<span rel="tooltip" data-placement="bottom" title="{{user.name}} is sharing their webcam">
<i class="icon fi-video usericon"></i>
@ -58,6 +66,13 @@
<template name="usernameEntry">
<div class="status">
{{#if isCurrentUserModerator}}
{{#unless user.presenter}}
<span class="setPresenter" rel="tooltip" data-placement="bottom" title="set {{user.name}} as presenter">
<i class="icon fi-projection-screen statusIcon"></i>
</span>
{{/unless}}
{{/if}}
{{#if equals user.emoji_status "happy"}}
{{> icon name="happy-face" size="25"}}
{{else}}

View File

@ -1,10 +1,13 @@
Template.slide.rendered = ->
currentSlide = getCurrentSlideDoc()
reactOnSlideChange(@)
@reactOnSlideChange = =>
currentSlide = BBB.getCurrentSlide("slide.rendered")
pic = new Image()
pic.onload = ->
setInSession 'slideOriginalWidth', this.width
setInSession 'slideOriginalHeight', this.height
setInSession 'slideOriginalWidth', @width
setInSession 'slideOriginalHeight', @height
$(window).resize( ->
# redraw the whiteboard to adapt to the resized window
if !$('.panel-footer').hasClass('ui-resizable-resizing') # not in the middle of resizing the message input
@ -14,13 +17,15 @@ Template.slide.rendered = ->
createWhiteboardPaper (wpm) ->
displaySlide wpm
pic.src = currentSlide?.slide?.img_uri
return ""
@createWhiteboardPaper = (callback) =>
# console.log "CREATING WPM"
@whiteboardPaperModel = new Meteor.WhiteboardPaperModel('whiteboard-paper')
callback(@whiteboardPaperModel)
@displaySlide = (wpm) ->
currentSlide = getCurrentSlideDoc()
currentSlide = BBB.getCurrentSlide("displaySlide")
wpm.create()
adjustedDimensions = scaleSlide(getInSession('slideOriginalWidth'), getInSession('slideOriginalHeight'))
@ -32,7 +37,7 @@ Template.slide.rendered = ->
return if Meteor.WhiteboardCleanStatus.findOne({in_progress: true})?
currentSlide = getCurrentSlideDoc()
currentSlide = BBB.getCurrentSlide("manuallyDisplayShapes")
wpm = @whiteboardPaperModel
shapes = Meteor.Shapes.find({whiteboardId: currentSlide?.slide?.id}).fetch()
for s in shapes
@ -90,9 +95,7 @@ Template.slide.rendered = ->
Template.slide.helpers
updatePointerLocation: (pointer) ->
if whiteboardPaperModel?
wpm = whiteboardPaperModel
wpm?.moveCursor(pointer.x, pointer.y)
whiteboardPaperModel?.moveCursor(pointer.x, pointer.y)
#### SHAPE ####
Template.shape.rendered = ->

View File

@ -1,12 +1,12 @@
# scale the whiteboard to adapt to the resized window
@scaleWhiteboard = (callback) ->
adjustedDimensions = scaleSlide(getInSession('slideOriginalWidth'), getInSession('slideOriginalHeight'))
wpm = whiteboardPaperModel
wpm.scale(adjustedDimensions.width, adjustedDimensions.height)
if whiteboardPaperModel?
whiteboardPaperModel.scale(adjustedDimensions.width, adjustedDimensions.height)
if callback
callback()
Template.whiteboard.helpers
isPollStarted: ->
if BBB.isPollGoing(getInSession('userId'))
return true
@ -16,6 +16,17 @@ Template.whiteboard.helpers
hasNoPresentation: ->
Meteor.Presentations.findOne({'presentation.current':true})
forceSlideShow: ->
reactOnSlideChange()
clearSlide: ->
#clear the slide
whiteboardPaperModel?.removeAllImagesFromPaper()
# hide the cursor
whiteboardPaperModel?.cursor?.remove()
Template.whiteboard.events
'click .whiteboardFullscreenButton': (event, template) ->
enterWhiteboardFullscreen()
@ -76,16 +87,13 @@ Template.whiteboard.events
Template.whiteboardControls.helpers
presentationProgress: ->
console.log "test"
currentPresentation = Meteor.Presentations.findOne({'presentation.current':true})
currentSlideNum = Meteor.Slides.findOne({'presentationId': currentPresentation?.presentation.id, 'slide.current':true})?.slide.num
totalSlideNum = Meteor.Slides.find({'presentationId': currentPresentation?.presentation.id})?.count()
console.log('slide', currentSlideNum)
if currentSlideNum isnt undefined
console.log currentSlideNum
return "#{currentSlideNum}/#{totalSlideNum}"
else
console.log currentSlideNum
return ''
Template.whiteboardControls.events

View File

@ -1,8 +1,12 @@
<template name="whiteboard">
<div id="{{id}}" {{visibility name}} class="component">
{{#if getCurrentSlide}}
{{> slide}}
{{>slide}}
{{forceSlideShow}}
{{else}}
{{clearSlide}}
{{/if}}
<div id="whiteboard-container" class="{{whiteboardSize}}">
<div id="whiteboard-paper">
</div>

View File

@ -33,6 +33,6 @@ Meteor.methods
# applies zooming to the stroke thickness
@zoomStroke = (thickness) ->
currentSlide = @getCurrentSlideDoc()
currentSlide = BBB.getCurrentSlide("zoomStroke")
ratio = (currentSlide?.slide.width_ratio + currentSlide?.slide.height_ratio) / 2
thickness * 100 / ratio

View File

@ -240,9 +240,9 @@ class Meteor.WhiteboardPaperModel
@containerOffsetTop = $container.offset()?.top
_updateZoomRatios: ->
currentSlideDoc = getCurrentSlideDoc()
@widthRatio = currentSlideDoc.slide.width_ratio
@heightRatio = currentSlideDoc.slide.height_ratio
currentSlideDoc = BBB.getCurrentSlide("_updateZoomRatios")
@widthRatio = currentSlideDoc?.slide.width_ratio
@heightRatio = currentSlideDoc?.slide.height_ratio
# Retrieves an image element from the paper.
# The url must be in the slides array.
@ -312,7 +312,7 @@ class Meteor.WhiteboardPaperModel
boardWidth = @containerWidth
boardHeight = @containerHeight
currentSlide = getCurrentSlideDoc()
currentSlide = BBB.getCurrentSlide("_displayPage")
currentPresentation = Meteor.Presentations.findOne({"presentation.current": true})
presentationId = currentPresentation?.presentation?.id
currentSlideCursor = Meteor.Slides.find({"presentationId": presentationId, "slide.current": true})
@ -355,5 +355,6 @@ class Meteor.WhiteboardPaperModel
@addImageToPaper(data, boardWidth, @adjustedHeight)
@adjustedWidth = boardWidth
@zoomAndPan(currentSlide.slide.width_ratio, currentSlide.slide.height_ratio,
currentSlide.slide.x_offset, currentSlide.slide.y_offset)
if currentSlide?
@zoomAndPan(currentSlide.slide.width_ratio, currentSlide.slide.height_ratio,
currentSlide.slide.x_offset, currentSlide.slide.y_offset)

View File

@ -2,8 +2,9 @@ Meteor.Users = new Meteor.Collection("bbb_users")
Meteor.Chat = new Meteor.Collection("bbb_chat")
Meteor.Meetings = new Meteor.Collection("meetings")
Meteor.Presentations = new Meteor.Collection("presentations")
Meteor.Cursor = new Meteor.Collection("bbb_cursor")
Meteor.Shapes = new Meteor.Collection("shapes")
Meteor.Slides = new Meteor.Collection("slides")
Meteor.Polls = new Meteor.Collection("bbb_poll")
Meteor.WhiteboardCleanStatus = new Meteor.Collection("whiteboard-clean-status")
Meteor.WhiteboardCleanStatus = new Meteor.Collection("whiteboard-clean-status")

View File

@ -61,30 +61,31 @@
Meteor.subscribe 'users', meetingId, userId, authToken, onError: onErrorFunction, onReady: =>
Meteor.subscribe 'whiteboard-clean-status', meetingId, onReady: =>
Meteor.subscribe 'bbb_poll', meetingId, userId, authToken, onReady: =>
# done subscribing, start rendering the client and set default settings
@render('main')
onLoadComplete()
Meteor.subscribe 'bbb_cursor', meetingId, onReady: =>
# done subscribing, start rendering the client and set default settings
@render('main')
onLoadComplete()
handleLogourUrlError = () ->
alert "Error: could not find the logoutURL"
setInSession("logoutURL", document.location.hostname)
return
# obtain the logoutURL
a = $.ajax({dataType: 'json', url: '/bigbluebutton/api/enter'})
a.done (data) ->
if data.response.logoutURL? # for a meeting with 0 users
setInSession("logoutURL", data.response.logoutURL)
return
else
if data.response.logoutUrl? # for a running meeting
setInSession("logoutURL", data.response.logoutUrl)
handleLogourUrlError = () ->
alert "Error: could not find the logoutURL"
setInSession("logoutURL", document.location.hostname)
return
else
handleLogourUrlError()
a.fail (data, textStatus, errorThrown) ->
handleLogourUrlError()
# obtain the logoutURL
a = $.ajax({dataType: 'json', url: '/bigbluebutton/api/enter'})
a.done (data) ->
if data.response.logoutURL? # for a meeting with 0 users
setInSession("logoutURL", data.response.logoutURL)
return
else
if data.response.logoutUrl? # for a running meeting
setInSession("logoutURL", data.response.logoutUrl)
return
else
handleLogourUrlError()
a.fail (data, textStatus, errorThrown) ->
handleLogourUrlError()
@render('loading')

View File

@ -0,0 +1,39 @@
# --------------------------------------------------------------------------------------------
# Private methods on server
# --------------------------------------------------------------------------------------------
@initializeCursor = (meetingId) ->
Meteor.Cursor.upsert({meetingId:meetingId}, {
meetingId:meetingId
x:0
y:0
}, (err, numChanged) ->
if err
Meteor.log.error "err upserting cursor for #{meetingId}"
else
# Meteor.log.info "ok upserting cursor for #{meetingId}"
)
@updateCursorLocation = (meetingId, cursorObject) ->
Meteor.Cursor.update({meetingId:meetingId}, {$set:{
x:cursorObject.x
y:cursorObject.y
}}, (err, numChanged) ->
if err?
Meteor.log.error "_unsucc update of cursor for #{meetingId} #{JSON.stringify cursorObject}
err=#{JSON.stringify err}"
else
# Meteor.log.info "updated cursor for #{meetingId} #{JSON.stringify cursorObject}"
)
# called on server start and meeting end
@clearCursorCollection = (meetingId) ->
if meetingId?
Meteor.Cursor.remove {meetingId: meetingId}, ->
Meteor.log.info "cleared Cursor Collection (meetingId: #{meetingId})!"
else
Meteor.Cursor.remove {}, ->
Meteor.log.info "cleared Cursor Collection (all meetings)!"
# --------------------------------------------------------------------------------------------
# end Private methods on server
# --------------------------------------------------------------------------------------------

View File

@ -5,7 +5,7 @@
@addMeetingToCollection = (meetingId, name, intendedForRecording, voiceConf, duration, callback) ->
#check if the meeting is already in the collection
obj = Meteor.Meetings.upsert({meetingId:meetingId}, {$set: {
Meteor.Meetings.upsert({meetingId:meetingId}, {$set: {
meetingName:name
intendedForRecording: intendedForRecording
currentlyBeingRecorded: false # default value
@ -31,6 +31,9 @@
callback()
)
# initialize the cursor in the meeting
initializeCursor(meetingId)
@clearMeetingsCollection = (meetingId) ->
@ -63,6 +66,9 @@
# delete the meeting
clearMeetingsCollection(meetingId)
# delete the cursor for the meeting
clearCursorCollection(meetingId)
callback()
else
funct = (localCallback) ->

View File

@ -61,10 +61,6 @@ Meteor.methods
name: presentationObject.name
current: presentationObject.current
pointer: #initially we have no data about the cursor
x: 0.0
y: 0.0
id = Meteor.Presentations.insert(entry)
#Meteor.log.info "presentation added id =[#{id}]:#{presentationObject.id} in #{meetingId}. Presentations.size is now #{Meteor.Presentations.find({meetingId: meetingId}).count()}"

View File

@ -131,6 +131,41 @@ Meteor.methods
Meteor.log.info "a user is logging out from #{meetingId}:" + userId
requestUserLeaving meetingId, userId
#meetingId: the meeting where the user is
#toKickUserId: the userid of the user to kick
#requesterUserId: the userid of the user that wants to kick
#authToken: the authToken of the user that wants to kick
kickUser: (meetingId, toKickUserId, requesterUserId, authToken) ->
if isAllowedTo('kickUser', meetingId, requesterUserId, authToken)
message =
"payload":
"userid": toKickUserId
"ejected_by": requesterUserId
"meeting_id": meetingId
"header":
"name": "eject_user_from_meeting_request_message"
publish Meteor.config.redis.channels.toBBBApps.users, message
#meetingId: the meeting where the user is
#newPresenterId: the userid of the new presenter
#requesterSetPresenter: the userid of the user that wants to change the presenter
#newPresenterName: user name of the new presenter
#authToken: the authToken of the user that wants to kick
setUserPresenter: (meetingId, newPresenterId, requesterSetPresenter, newPresenterName, authToken) ->
if isAllowedTo('setPresenter', meetingId, requesterSetPresenter, authToken)
message =
"payload":
"new_presenter_id": newPresenterId
"new_presenter_name": newPresenterName
"meeting_id": meetingId
"assigned_by": requesterSetPresenter
"header":
"name": "assign_presenter_request_message"
publish Meteor.config.redis.channels.toBBBApps.users, message
# --------------------------------------------------------------------------------------------
# Private methods on server
# --------------------------------------------------------------------------------------------

View File

@ -44,7 +44,7 @@ Meteor.publish 'users', (meetingId, userid, authToken) ->
Meteor.publish 'chat', (meetingId, userid, authToken) ->
if isAllowedTo('subscribeChat', meetingId, userid, authToken)
Meteor.log.info "publishing chat for #{meetingId} #{userid} #{authToken}"
return Meteor.Chat.find({$or: [
{'message.chat_type': 'PUBLIC_CHAT', 'meetingId': meetingId},
@ -86,6 +86,10 @@ Meteor.publish 'presentations', (meetingId) ->
Meteor.log.info "publishing presentations for #{meetingId}"
Meteor.Presentations.find({meetingId: meetingId})
Meteor.publish 'bbb_cursor', (meetingId) ->
Meteor.log.info "publishing cursor for #{meetingId}"
Meteor.Cursor.find({meetingId: meetingId})
Meteor.publish 'whiteboard-clean-status', (meetingId) ->
Meteor.log.info "whiteboard clean status #{meetingId}"
Meteor.WhiteboardCleanStatus.find({meetingId: meetingId})

View File

@ -10,16 +10,17 @@ Meteor.startup ->
clearSlidesCollection()
clearPresentationsCollection()
clearPollCollection()
clearCursorCollection()
# create create a PubSub connection, start listening
Meteor.redisPubSub = new Meteor.RedisPubSub(->
Meteor.log.info "created pubsub")
Meteor.myQueue = new PowerQueue({
# autoStart:true
# isPaused: true
})
Meteor.myQueue.taskHandler = (data, next, failures) ->
eventName = JSON.parse(data.jsonMsg)?.header.name
if failures > 0
@ -61,15 +62,15 @@ Meteor.startup ->
"presentation_page_resized_message"
"presentation_cursor_updated_message"
"get_presentation_info_reply"
# "get_users_reply"
#"get_users_reply"
"get_chat_history_reply"
# "get_all_meetings_reply"
#"get_all_meetings_reply"
"get_whiteboard_shapes_reply"
"presentation_shared_message"
"presentation_conversion_done_message"
"presentation_conversion_progress_message"
"presentation_page_generated_message"
# "presentation_page_changed_message"
#"presentation_page_changed_message"
"BbbPubSubPongMessage"
"bbb_apps_is_alive_message"
"user_voice_talking_message"
@ -382,10 +383,12 @@ Meteor.startup ->
# for now not handling this serially #TODO
else if eventName is "presentation_cursor_updated_message"
x = message.payload.x_percent
y = message.payload.y_percent
Meteor.Presentations.update({"presentation.current": true, meetingId: meetingId},
{$set: {"pointer.x": x, "pointer.y": y}})
cursor =
x: message.payload.x_percent
y: message.payload.y_percent
# update the location of the cursor on the whiteboard
updateCursorLocation(meetingId, cursor)
callback()
# for now not handling this serially #TODO

View File

@ -42,6 +42,10 @@ moderator =
setEmojiStatus: true
clearEmojiStatus: true
#user control
kickUser: true
setPresenter: true
# holds the values for whether the viewer user is allowed to perform an action (true)
# or false if not allowed. Some actions have dynamic values depending on the current lock settings
viewer = (meetingId, userId) ->

View File

@ -43,18 +43,19 @@ repositories {
dependencies {
//redis
compile "redis.clients:jedis:2.7.2"
compile 'org.apache.commons:commons-pool2:2.3'
compile 'redis.clients:jedis:2.7.2'
compile 'org.apache.commons:commons-pool2:2.3'
compile 'commons-lang:commons-lang:2.5'
compile 'commons-io:commons-io:2.4'
compile 'com.google.code.gson:gson:1.7.1'
compile 'commons-httpclient:commons-httpclient:3.1'
compile 'com.zaxxer:nuprocess:1.0.4'
compile 'org.bigbluebutton:bbb-common-message:0.0.13'
// Logging
// Commenting out as it results in build failure (ralam - may 11, 2014)
// Commenting out as it results in build failure (ralam - may 11, 2014)
//compile 'ch.qos.logback:logback-core:1.0.9@jar'
//compile 'ch.qos.logback:logback-classic:1.0.9@jar'
//compile 'org.slf4j:log4j-over-slf4j:1.7.2@jar'
@ -64,7 +65,7 @@ dependencies {
//junit
compile 'junit:junit:4.8.2'
// Logging
/**** UNCOMMENT WHEN you want to run gradle test
compile 'ch.qos.logback:logback-core:1.0.13@jar'

View File

@ -68,6 +68,16 @@ maxNumPages=200
# Maximum swf file size for load to the client (default 500000).
MAX_SWF_FILE_SIZE=500000
#----------------------------------------------------
# Maximum allowed number of place object tags in the converted SWF, if exceeded the conversion will fallback to full BMP (default 8000)
placementsThreshold=8000
# Maximum allowed number of bitmap images in the converted SWF, if exceeded the conversion will fallback to full BMP (default 8000)
imageTagThreshold=8000
# Maximum allowed number of define text tags in the converted SWF, if exceeded the conversion will fallback to full BMP (default 2500)
defineTextThreshold=2500
#----------------------------------------------------
# Additional conversion of the presentation slides to SVG
# to be used in the HTML5 client

View File

@ -62,6 +62,9 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
<bean id="pdf2SwfPageConverter" class="org.bigbluebutton.presentation.imp.Pdf2SwfPageConverter">
<property name="swfToolsDir" value="${swfToolsDir}"/>
<property name="fontsDir" value="${fontsDir}"/>
<property name="placementsThreshold" value="${placementsThreshold}"/>
<property name="defineTextThreshold" value="${defineTextThreshold}"/>
<property name="imageTagThreshold" value="${imageTagThreshold}"/>
</bean>
<bean id="imageConvSvc" class="org.bigbluebutton.presentation.imp.PdfPageToImageConversionService">
@ -79,9 +82,9 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
<property name="imageMagickDir" value="${imageMagickDir}"/>
</bean>
<bean id="svgImageCreator" class="org.bigbluebutton.presentation.imp.SvgImageCreatorImp">
<property name="imageMagickDir" value="${imageMagickDir}"/>
</bean>
<bean id="svgImageCreator" class="org.bigbluebutton.presentation.imp.SvgImageCreatorImp">
<property name="imageMagickDir" value="${imageMagickDir}"/>
</bean>
<bean id="generatedSlidesInfoHelper" class="org.bigbluebutton.presentation.GeneratedSlidesInfoHelperImp"/>

View File

@ -0,0 +1,32 @@
/**
* BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
*
* Copyright (c) 2015 BigBlueButton Inc. and by respective authors (see below).
*
* This program is free software; you can redistribute it and/or modify it under the
* terms of the GNU Lesser General Public License as published by the Free Software
* Foundation; either version 3.0 of the License, or (at your option) any later
* version.
*
* BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
* PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License along
* with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
*
*/
package org.bigbluebutton.presentation;
import java.io.File;
public interface PageAnalyser {
/**
*
* @param output
* a {@link File} to analyse
* @return true if the file has been parsed without any error
*/
public boolean analyse(File output);
}

View File

@ -0,0 +1,90 @@
/**
* BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
*
* Copyright (c) 2015 BigBlueButton Inc. and by respective authors (see below).
*
* This program is free software; you can redistribute it and/or modify it under the
* terms of the GNU Lesser General Public License as published by the Free Software
* Foundation; either version 3.0 of the License, or (at your option) any later
* version.
*
* BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
* PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License along
* with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
*
*/
package org.bigbluebutton.presentation.handlers;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.StandardCharsets;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.zaxxer.nuprocess.NuAbstractProcessHandler;
import com.zaxxer.nuprocess.NuProcess;
public abstract class AbstractPageConverterHandler extends
NuAbstractProcessHandler {
private static Logger log = LoggerFactory
.getLogger(AbstractPageConverterHandler.class);
protected NuProcess nuProcess;
protected int exitCode;
final protected StringBuilder stdoutBuilder = new StringBuilder();
final protected StringBuilder stderrBuilder = new StringBuilder();
@Override
public void onPreStart(NuProcess nuProcess) {
this.nuProcess = nuProcess;
}
@Override
public void onStart(NuProcess nuProcess) {
super.onStart(nuProcess);
}
@Override
public void onStdout(ByteBuffer buffer, boolean closed) {
if (buffer != null) {
CharBuffer charBuffer = StandardCharsets.UTF_8.decode(buffer);
stdoutBuilder.append(charBuffer);
}
}
@Override
public void onStderr(ByteBuffer buffer, boolean closed) {
if (buffer != null) {
CharBuffer charBuffer = StandardCharsets.UTF_8.decode(buffer);
stderrBuilder.append(charBuffer);
}
}
@Override
public void onExit(int statusCode) {
exitCode = statusCode;
}
/**
*
* @return true if the exit code of the process is different from 0
*/
public Boolean exitedWithError() {
return exitCode != 0;
}
protected Boolean stdoutContains(String value) {
return stdoutBuilder.indexOf(value) > -1;
}
protected Boolean stderrContains(String value) {
return stderrBuilder.indexOf(value) > -1;
}
public abstract Boolean isConversionSuccessfull();
}

View File

@ -0,0 +1,109 @@
/**
* BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
*
* Copyright (c) 2015 BigBlueButton Inc. and by respective authors (see below).
*
* This program is free software; you can redistribute it and/or modify it under the
* terms of the GNU Lesser General Public License as published by the Free Software
* Foundation; either version 3.0 of the License, or (at your option) any later
* version.
*
* BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
* PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License along
* with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
*
*/
package org.bigbluebutton.presentation.handlers;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
*
* The default command output the anlayse looks like the following: </br> 20
* DEBUG Using</br> 60 VERBOSE Updating font</br> 80 VERBOSE Drawing
*
*/
public class Pdf2SwfPageConverterHandler extends AbstractPageConverterHandler {
private static Logger log = LoggerFactory
.getLogger(Pdf2SwfPageConverterHandler.class);
private static String PLACEMENT_OUTPUT = "DEBUG Using";
private static String TEXT_TAG_OUTPUT = "VERBOSE Updating";
private static String IMAGE_TAG_OUTPUT = "VERBOSE Drawing";
private static String PLACEMENT_PATTERN = "\\d+\\s" + PLACEMENT_OUTPUT;
private static String TEXT_TAG_PATTERN = "\\d+\\s" + TEXT_TAG_OUTPUT;
private static String IMAGE_TAG_PATTERN = "\\d+\\s" + IMAGE_TAG_OUTPUT;
@Override
public Boolean isConversionSuccessfull() {
return !exitedWithError();
}
/**
*
* @return The number of PlaceObject2 tags in the generated SWF
*/
public long numberOfPlacements() {
if (stdoutContains(PLACEMENT_OUTPUT)) {
try {
String out = stdoutBuilder.toString();
Pattern r = Pattern.compile(PLACEMENT_PATTERN);
Matcher m = r.matcher(out);
m.find();
return Integer
.parseInt(m.group(0).replace(PLACEMENT_OUTPUT, "").trim());
} catch (Exception e) {
return 0;
}
}
return 0;
}
/**
*
* @return The number of text tags in the generated SWF.
*/
public long numberOfTextTags() {
if (stdoutContains(TEXT_TAG_OUTPUT)) {
try {
String out = stdoutBuilder.toString();
Pattern r = Pattern.compile(TEXT_TAG_PATTERN);
Matcher m = r.matcher(out);
m.find();
return Integer.parseInt(m.group(0).replace(TEXT_TAG_OUTPUT, "").trim());
} catch (Exception e) {
return 0;
}
}
return 0;
}
/**
*
* @return The number of image tags in the generated SWF.
*/
public long numberOfImageTags() {
if (stdoutContains(IMAGE_TAG_OUTPUT)) {
try {
String out = stdoutBuilder.toString();
Pattern r = Pattern.compile(IMAGE_TAG_PATTERN);
Matcher m = r.matcher(out);
m.find();
return Integer
.parseInt(m.group(0).replace(IMAGE_TAG_OUTPUT, "").trim());
} catch (Exception e) {
return 0;
}
}
return 0;
}
}

View File

@ -1,65 +1,132 @@
/**
* BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
*
* Copyright (c) 2012 BigBlueButton Inc. and by respective authors (see below).
*
* This program is free software; you can redistribute it and/or modify it under the
* terms of the GNU Lesser General Public License as published by the Free Software
* Foundation; either version 3.0 of the License, or (at your option) any later
* version.
*
* BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
* PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License along
* with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
*
*/
* BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
*
* Copyright (c) 2012 BigBlueButton Inc. and by respective authors (see below).
*
* This program is free software; you can redistribute it and/or modify it under the
* terms of the GNU Lesser General Public License as published by the Free Software
* Foundation; either version 3.0 of the License, or (at your option) any later
* version.
*
* BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
* PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License along
* with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
*
*/
package org.bigbluebutton.presentation.imp;
import java.io.File;
import java.util.Arrays;
import java.util.concurrent.TimeUnit;
import org.bigbluebutton.presentation.PageConverter;
import org.bigbluebutton.presentation.handlers.Pdf2SwfPageConverterHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.zaxxer.nuprocess.NuProcess;
import com.zaxxer.nuprocess.NuProcessBuilder;
public class Pdf2SwfPageConverter implements PageConverter {
private static Logger log = LoggerFactory.getLogger(Pdf2SwfPageConverter.class);
private String SWFTOOLS_DIR;
private String fontsDir;
public boolean convert(File presentation, File output, int page) {
String source = presentation.getAbsolutePath();
String dest = output.getAbsolutePath();
String AVM2SWF = "-T9";
String COMMAND = SWFTOOLS_DIR + File.separator + "pdf2swf " + AVM2SWF + " -F " + fontsDir + " -p " + page + " " + source + " -o " + dest;
private static Logger log = LoggerFactory
.getLogger(Pdf2SwfPageConverter.class);
boolean done = new ExternalProcessExecutor().exec(COMMAND, 60000);
File destFile = new File(dest);
if (done && destFile.exists()) {
return true;
} else {
COMMAND = SWFTOOLS_DIR + File.separator + "pdf2swf " + AVM2SWF + " -s poly2bitmap -F " + fontsDir + " -p " + page + " " + source + " -o " + dest;
done = new ExternalProcessExecutor().exec(COMMAND, 60000);
if (done && destFile.exists()){
return true;
} else {
log.warn("Failed to convert: " + dest + " does not exist.");
return false;
}
}
}
private String SWFTOOLS_DIR;
private String fontsDir;
private long placementsThreshold;
private long defineTextThreshold;
private long imageTagThreshold;
public boolean convert(File presentation, File output, int page) {
String source = presentation.getAbsolutePath();
String dest = output.getAbsolutePath();
String AVM2SWF = "-T9";
// Building the command line wrapped in shell to be able to use shell
// feature like the pipe
NuProcessBuilder pb = new NuProcessBuilder(
Arrays.asList(
"/bin/sh",
"-c",
SWFTOOLS_DIR
+ File.separator
+ "pdf2swf"
+ " -vv "
+ AVM2SWF
+ " -F "
+ fontsDir
+ " -p "
+ String.valueOf(page)
+ " "
+ source
+ " -o "
+ dest
+ " | egrep 'shape id|Updating font|Drawing' | sed 's/ / /g' | cut -d' ' -f 1-3 | sort | uniq -cw 15"));
Pdf2SwfPageConverterHandler pHandler = new Pdf2SwfPageConverterHandler();
pb.setProcessListener(pHandler);
NuProcess process = pb.start();
try {
process.waitFor(60, TimeUnit.SECONDS);
} catch (InterruptedException e) {
log.error(e.getMessage());
}
File destFile = new File(dest);
if (pHandler.isConversionSuccessfull() && destFile.exists()
&& pHandler.numberOfPlacements() < placementsThreshold
&& pHandler.numberOfTextTags() < defineTextThreshold
&& pHandler.numberOfImageTags() < imageTagThreshold) {
return true;
} else {
log.debug(
"Previous conversion generated {} PlaceObject tags, {} DefineText tags and {} Images. Falling back to 'poly2bitmap' option for pdf2swf.",
pHandler.numberOfPlacements(), pHandler.numberOfTextTags(),
pHandler.numberOfImageTags());
NuProcessBuilder pbBmp = new NuProcessBuilder(Arrays.asList(SWFTOOLS_DIR
+ File.separator + "pdf2swf", AVM2SWF, "-s", "poly2bitmap", "-F",
fontsDir, "-p", String.valueOf(page), source, "-o", dest));
Pdf2SwfPageConverterHandler pBmpHandler = new Pdf2SwfPageConverterHandler();
pbBmp.setProcessListener(pBmpHandler);
NuProcess processBmp = pbBmp.start();
try {
processBmp.waitFor(60, TimeUnit.SECONDS);
} catch (InterruptedException e) {
log.error(e.getMessage());
}
boolean doneBmp = pBmpHandler.isConversionSuccessfull();
if (doneBmp && destFile.exists()) {
return true;
} else {
log.warn("Failed to convert: " + dest + " does not exist.");
return false;
}
}
}
public void setSwfToolsDir(String dir) {
SWFTOOLS_DIR = dir;
}
public void setFontsDir(String dir) {
fontsDir = dir;
}
public void setPlacementsThreshold(long threshold) {
placementsThreshold = threshold;
}
public void setDefineTextThreshold(long threshold) {
defineTextThreshold = threshold;
}
public void setImageTagThreshold(long threshold) {
imageTagThreshold = threshold;
}
public void setSwfToolsDir(String dir) {
SWFTOOLS_DIR = dir;
}
public void setFontsDir(String dir) {
fontsDir = dir;
}
}

View File

@ -33,13 +33,13 @@ import java.util.concurrent.TimeUnit;
import org.bigbluebutton.presentation.ConversionMessageConstants;
import org.bigbluebutton.presentation.ConversionUpdateMessage;
import org.bigbluebutton.presentation.ConversionUpdateMessage.MessageBuilder;
import org.bigbluebutton.presentation.PageConverter;
import org.bigbluebutton.presentation.PdfToSwfSlide;
import org.bigbluebutton.presentation.SvgImageCreator;
import org.bigbluebutton.presentation.TextFileCreator;
import org.bigbluebutton.presentation.ThumbnailCreator;
import org.bigbluebutton.presentation.UploadedPresentation;
import org.bigbluebutton.presentation.ConversionUpdateMessage.MessageBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -152,12 +152,12 @@ public class PdfToSwfSlidesGenerationService {
slidesCompleted++;
notifier.sendConversionUpdateMessage(slidesCompleted, pres);
} else {
log.warn("Timedout waiting for page to finish conversion. meetingId=" + pres.getMeetingId() + " presId=" + pres.getId() + " presName=" + pres.getName() );
log.warn("Timedout waiting for page to finish conversion. meetingId=" + pres.getMeetingId() + " presId=" + pres.getId() + " presName=[" + pres.getName() + "]");
}
} catch (InterruptedException e) {
log.error("InterruptedException while creating slide. meetingId=" + pres.getMeetingId() + " presId=" + pres.getId() + " name=[" + pres.getName());
log.error("InterruptedException while creating slide. meetingId=" + pres.getMeetingId() + " presId=" + pres.getId() + " presName=[" + pres.getName() + "]");
} catch (ExecutionException e) {
log.error("ExecutionException while creating slide. meetingId=" + pres.getMeetingId() + " presId=" + pres.getId() + " name=[" + pres.getName());
log.error("ExecutionException while creating slide. meetingId=" + pres.getMeetingId() + " presId=" + pres.getId() + " presName=[" + pres.getName() + "]");
}
}