fix merge conflicts

This commit is contained in:
KDSBrowne 2017-03-24 07:25:25 -07:00
commit eb3ac219e9
47 changed files with 1111 additions and 914 deletions

View File

@ -15,6 +15,14 @@ ApplicationControlBar {
dropShadowColor: #000000;
}
.defaultControlBarStyle {
color : #0B333C;
}
.darkControlBarStyle {
color : #ffffff;
}
Panel {
borderColor: #dfdfdf;
borderAlpha: 1;
@ -755,6 +763,18 @@ MDIWindow { /*None of the following properties are overridden by the MDIWindow c
borderThicknessRight: 3;
}
.videoDockStyleFocusChatLayout {
borderStyle : none;
borderColor: #42444c;
backgroundColor: #42444c;
}
.videoDockStyleNoFocusChatLayout {
borderStyle : none;
borderColor: #42444c;
backgroundColor: #42444c;
}
.presentationSlideViewStyle {
backgroundColor: #b9babc;
}
@ -865,6 +885,14 @@ MDIWindow { /*None of the following properties are overridden by the MDIWindow c
borderThicknessRight: 3;
}
.defaultShellStyle {
backgroundColor: #fefeff;
}
.darkShellStyle {
backgroundColor: #42444c;
}
.mdiWindowTitle {
color: #3f3f41;
fontFamily: Arial;

View File

@ -23,8 +23,6 @@ package org.bigbluebutton.common
import flexlib.mdi.containers.MDIWindow;
import flexlib.mdi.managers.MDIManager;
import mx.utils.ObjectUtil;
/**
* This class exists so we can properly handle context menus on MDIWindow
* instances. Also, we'll be able in the future to properly handle shortcuts

View File

@ -38,6 +38,8 @@ package org.bigbluebutton.main.model.users {
public var users:ArrayCollection;
public var invitedRecently : Boolean;
// Can be one of three following values self, none, other
public var listenStatus:String = NONE;

View File

@ -30,7 +30,6 @@ package org.bigbluebutton.main.model.users {
import org.as3commons.logging.api.getClassLogger;
import org.bigbluebutton.common.Role;
import org.bigbluebutton.core.BBB;
import org.bigbluebutton.core.managers.UserManager;
import org.bigbluebutton.core.model.Config;
import org.bigbluebutton.core.model.MeetingModel;
import org.bigbluebutton.core.vo.CameraSettingsVO;
@ -568,7 +567,20 @@ package org.bigbluebutton.main.model.users {
}
breakoutRooms.addItem(newRoom);
sortBreakoutRooms();
}
}
public function setLastBreakoutRoomInvitation(sequence:int):void {
var aRoom:BreakoutRoom;
for (var i:int = 0; i < breakoutRooms.length; i++) {
aRoom = breakoutRooms.getItemAt(i) as BreakoutRoom;
if (aRoom.sequence != sequence) {
aRoom.invitedRecently = false;
} else {
aRoom.invitedRecently = true;
}
}
sortBreakoutRooms();
}
private function sortBreakoutRooms() : void {
var sort:Sort = new Sort();

View File

@ -69,6 +69,8 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
<mate:Listener type="{BreakoutRoomEvent.OPEN_BREAKOUT_ROOMS_PANEL}" method="openBreakoutRoomsWindow" />
<mate:Listener type="{InvalidAuthTokenEvent.INVALID_AUTH_TOKEN}" method="handleInvalidAuthToken" />
<mate:Listener type="{SwitchedLayoutEvent.SWITCHED_LAYOUT_EVENT}" method="onLayoutChanged" />
<mx:Script>
<![CDATA[
import com.asfusion.mate.events.Dispatcher;
@ -101,6 +103,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
import org.bigbluebutton.core.PopUpUtil;
import org.bigbluebutton.core.UsersUtil;
import org.bigbluebutton.core.events.LockControlEvent;
import org.bigbluebutton.core.events.SwitchedLayoutEvent;
import org.bigbluebutton.core.managers.UserManager;
import org.bigbluebutton.core.vo.LockSettingsVO;
import org.bigbluebutton.main.events.AppVersionEvent;
@ -680,6 +683,17 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
ExternalInterface.call("chatLinkClicked", e.text);
}
}
private function onLayoutChanged(e:SwitchedLayoutEvent):void {
if (e.layoutID != "bbb.layout.name.videochat") {
this.styleName = "defaultShellStyle";
controlBar.styleName = "defaultControlBarStyle";
} else {
this.styleName = "darkShellStyle";
controlBar.styleName = "darkControlBarStyle";
}
}
]]>
</mx:Script>

View File

@ -1,17 +0,0 @@
package org.bigbluebutton.modules.layout.events
{
import flash.events.Event;
public class LayoutChangedEvent extends Event
{
public static const LAYOUT_CHANGED:String = "layout changed event";
public var layoutName:String;
public function LayoutChangedEvent(layoutName:String)
{
super(LAYOUT_CHANGED, true, false);
this.layoutName = layoutName;
}
}
}

View File

@ -219,18 +219,18 @@ package org.bigbluebutton.modules.layout.managers
var e:SyncLayoutEvent = new SyncLayoutEvent(layout);
_globalDispatcher.dispatchEvent(e);
}
}
}
private function applyLayout(layout:LayoutDefinition):void {
_detectContainerChange = false;
if (layout != null) {
layout.applyToCanvas(_canvas);
dispatchSwitchedLayoutEvent(layout.name);
}
//trace(LOG + " applyLayout layout [" + layout.name + "]");
updateCurrentLayout(layout);
_detectContainerChange = true;
}
private function applyLayout(layout:LayoutDefinition):void {
_detectContainerChange = false;
if (layout != null) {
layout.applyToCanvas(_canvas);
dispatchSwitchedLayoutEvent(layout.name);
}
//trace(LOG + " applyLayout layout [" + layout.name + "]");
updateCurrentLayout(layout);
_detectContainerChange = true;
}
public function handleLockLayoutEvent(e: LockLayoutEvent):void {
@ -283,46 +283,47 @@ package org.bigbluebutton.modules.layout.managers
}
}
private function onContainerResized(e:ResizeEvent):void {
//trace(LOG + "Canvas is changing as user is resizing browser");
/*
* the main canvas has been resized
* while the user is resizing the window, this event is dispatched
* multiple times, so we use a timer to re-apply the current layout
* only once, when the user finished his action
*/
_applyCurrentLayoutTimer.reset();
_applyCurrentLayoutTimer.start();
}
private function onContainerResized(e:ResizeEvent):void {
//trace(LOG + "Canvas is changing as user is resizing browser");
/*
* the main canvas has been resized
* while the user is resizing the window, this event is dispatched
* multiple times, so we use a timer to re-apply the current layout
* only once, when the user finished his action
*/
_applyCurrentLayoutTimer.reset();
_applyCurrentLayoutTimer.start();
}
private function onActionOverWindowFinished(e:MDIManagerEvent):void {
if (LayoutDefinition.ignoreWindow(e.window))
return;
private function onActionOverWindowFinished(e:MDIManagerEvent):void {
if (LayoutDefinition.ignoreWindow(e.window))
return;
checkPermissionsOverWindow(e.window);
//trace(LOG + "Window is being resized. Event=[" + e.type + "]");
//updateCurrentLayout(null);
/*
* All events must be delayed because the window doesn't actually
* change size until after the animation has finished.
*/
_sendCurrentLayoutUpdateTimer.reset();
_sendCurrentLayoutUpdateTimer.start();
}
checkPermissionsOverWindow(e.window);
//trace(LOG + "Window is being resized. Event=[" + e.type + "]");
//updateCurrentLayout(null);
/*
* All events must be delayed because the window doesn't actually
* change size until after the animation has finished.
*/
_sendCurrentLayoutUpdateTimer.reset();
_sendCurrentLayoutUpdateTimer.start();
}
private function updateCurrentLayout(layout:LayoutDefinition):LayoutDefinition {
//trace(LOG + "updateCurrentLayout");
if (layout != null) {
if (_currentLayout) _currentLayout.currentLayout = false;
_currentLayout = layout;
//trace(LOG + "updateCurrentLayout - currentLayout = [" + layout.name + "]");
layout.currentLayout = true;
} else {
_currentLayout = LayoutDefinition.getLayout(_canvas, ResourceUtil.getInstance().getString('bbb.layout.combo.customName'));
//trace(LOG + "updateCurrentLayout - layout is NULL! Setting currentLayout = [" + _currentLayout.name + "]");
}
private function updateCurrentLayout(layout:LayoutDefinition):LayoutDefinition {
//trace(LOG + "updateCurrentLayout");
if (layout != null) {
if (_currentLayout)
_currentLayout.currentLayout = false;
_currentLayout = layout;
//trace(LOG + "updateCurrentLayout - currentLayout = [" + layout.name + "]");
layout.currentLayout = true;
} else {
_currentLayout = LayoutDefinition.getLayout(_canvas, ResourceUtil.getInstance().getString('bbb.layout.combo.customName'));
//trace(LOG + "updateCurrentLayout - layout is NULL! Setting currentLayout = [" + _currentLayout.name + "]");
}
return _currentLayout;
}
return _currentLayout;
}
}
}

View File

@ -20,10 +20,11 @@ package org.bigbluebutton.modules.users.services
{
import com.asfusion.mate.events.Dispatcher;
import flash.utils.setTimeout;
import org.as3commons.lang.StringUtils;
import org.as3commons.logging.api.ILogger;
import org.as3commons.logging.api.getClassLogger;
import org.bigbluebutton.common.Role;
import org.bigbluebutton.core.BBB;
import org.bigbluebutton.core.EventConstants;
import org.bigbluebutton.core.UsersUtil;
@ -649,11 +650,17 @@ package org.bigbluebutton.modules.users.services
private function handleBreakoutRoomJoinURL(msg:Object):void{
var map:Object = JSON.parse(msg.msg);
var externalMeetingId : String = StringUtils.substringBetween(map.redirectJoinURL, "meetingID=", "&");
var breakoutRoom : BreakoutRoom = UserManager.getInstance().getConference().getBreakoutRoomByExternalId(externalMeetingId);
var sequence : int = breakoutRoom.sequence;
var event : BreakoutRoomEvent = new BreakoutRoomEvent(BreakoutRoomEvent.BREAKOUT_JOIN_URL);
event.joinURL = map.redirectJoinURL;
var externalMeetingId : String = StringUtils.substringBetween(event.joinURL, "meetingID=", "&");
event.breakoutMeetingSequence = UserManager.getInstance().getConference().getBreakoutRoomByExternalId(externalMeetingId).sequence;
event.breakoutMeetingSequence = sequence;
dispatcher.dispatchEvent(event);
// We delay assigning last room invitation sequence to be sure it is handle in time by the item renderer
setTimeout(function() : void {UserManager.getInstance().getConference().setLastBreakoutRoomInvitation(sequence)}, 1000);
}
private function handleUpdateBreakoutUsers(msg:Object):void{

View File

@ -155,7 +155,7 @@
var ls:LockSettingsVO = UserManager.getInstance().getConference().getLockSettings();
if (data != null) {
kickUserBtn.visible = !data.me && rolledOver && options.allowKickUser;
kickUserBtn.visible = !data.me && rolledOver && options.allowKickUser && !UserManager.getInstance().getConference().isBreakout;
if (!data.voiceJoined) {
if (data.listenOnly) {

View File

@ -21,7 +21,12 @@
[Bindable]
private var images:Images = new Images();
[Bindable]
private var moderator:Boolean = false;
protected function onCreationCompleteHandler(event:FlexEvent):void {
moderator = UserManager.getInstance().getConference().amIModerator();
this.addEventListener(FlexEvent.DATA_CHANGE, dataChangeHandler);
}
@ -51,13 +56,19 @@
]]>
</mx:Script>
<mx:Button id="joinBtn" width="20" height="20"
includeInLayout="{UserManager.getInstance().getConference().breakoutRoomsReady}" visible="{joinBtn.includeInLayout}"
icon="{images.join}" toolTip="{ResourceUtil.getInstance().getString('bbb.users.roomsGrid.join')}"
click="requestBreakoutJoinUrl(event)"/>
<mx:Button id="listenBtn" toggle="true"
width="20" height="20"
visible="{data.listenStatus != BreakoutRoom.OTHER &amp;&amp; UserManager.getInstance().getConference().voiceJoined || data.listenStatus == BreakoutRoom.SELF}" includeInLayout="{listenBtn.visible}"
icon="{images.transfer}" toolTip="{ResourceUtil.getInstance().getString('bbb.users.roomsGrid.transfer')}"
click="listenToBreakoutRoom(event)"/>
<mx:Button id="joinBtn"
width="20"
height="20"
icon="{images.join}"
visible="{(UserManager.getInstance().getConference().breakoutRoomsReady &amp;&amp; moderator) || (!moderator &amp;&amp; data.invitedRecently)}"
toolTip="{ResourceUtil.getInstance().getString('bbb.users.roomsGrid.join')}"
click="requestBreakoutJoinUrl(event)" />
<mx:Button id="listenBtn"
toggle="true"
width="20"
height="20"
visible="{moderator &amp;&amp; data.listenStatus != BreakoutRoom.OTHER &amp;&amp; UserManager.getInstance().getConference().voiceJoined || data.listenStatus == BreakoutRoom.SELF}"
icon="{images.transfer}"
toolTip="{ResourceUtil.getInstance().getString('bbb.users.roomsGrid.transfer')}"
click="listenToBreakoutRoom(event)" />
</mx:HBox>

View File

@ -442,7 +442,9 @@
keyCombos = new Object(); // always start with a fresh array bbb.shortcutkey.users.muteall
keyCombos[modifier + (ResourceUtil.getInstance().getString('bbb.shortcutkey.users.focusUsers') as String)] = FOCUS_USERS_LIST;
keyCombos[modifier + (ResourceUtil.getInstance().getString('bbb.shortcutkey.users.makePresenter') as String)] = MAKE_PRESENTER;
keyCombos[modifier + (ResourceUtil.getInstance().getString('bbb.shortcutkey.users.kick') as String)] = KICK_USER;
if (!UserManager.getInstance().getConference().isBreakout) {
keyCombos[modifier + (ResourceUtil.getInstance().getString('bbb.shortcutkey.users.kick') as String)] = KICK_USER;
}
keyCombos[modifier + (ResourceUtil.getInstance().getString('bbb.shortcutkey.users.mute') as String)] = MUTE_USER;
keyCombos[modifier + (ResourceUtil.getInstance().getString('bbb.shortcutkey.users.muteall') as String)] = MUTE_ALL_USER;
keyCombos[modifier + (ResourceUtil.getInstance().getString('bbb.shortcutkey.users.focusBreakoutRooms') as String)] = FOCUS_BREAKOUT_ROOMS_LIST;
@ -536,7 +538,7 @@
}
public function remoteKickUser():void {
if (amIModerator && usersGrid.selectedIndex != -1 && partOptions.allowKickUser) {
if (amIModerator && usersGrid.selectedIndex != -1 && partOptions.allowKickUser && !UserManager.getInstance().getConference().isBreakout) {
var selData:Object = usersGrid.selectedItem;
if (!selData.me)
@ -636,8 +638,8 @@
</views:BBBDataGrid>
<mx:VBox id="roomsBox" styleName="breakoutRoomsBox"
visible="{breakoutRoomsList.length > 0 &amp;&amp; amIModerator}"
includeInLayout="{breakoutRoomsList.length > 0 &amp;&amp; amIModerator}"
visible="{breakoutRoomsList.length > 0}"
includeInLayout="{breakoutRoomsList.length > 0}"
horizontalScrollPolicy="off"
width="100%" height="180">
<mx:HBox width="100%">
@ -658,7 +660,6 @@
showDataTips="true"
headerText="{ResourceUtil.getInstance().getString('bbb.users.roomsGrid.users')}"/>
<mx:DataGridColumn dataField="meetingId"
visible="{amIModerator}"
headerText="{ResourceUtil.getInstance().getString('bbb.users.roomsGrid.action')}"
itemRenderer="org.bigbluebutton.modules.users.views.RoomActionsRenderer"/>
</views:columns>

View File

@ -1,260 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
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/>.
-->
<pubVid:VideoWindowItf
xmlns:mx="http://www.adobe.com/2006/mxml"
xmlns:pubVid="org.bigbluebutton.modules.videoconf.business.*"
implements="org.bigbluebutton.common.IBbbModuleWindow"
styleNameFocus="videoAvatarStyleFocus"
styleNameNoFocus="videoAvatarStyleNoFocus"
creationComplete="onCreationComplete()"
width="{defaultWidth + 6}" height="{defaultHeight + 6}"
xmlns:mate="http://mate.asfusion.com/"
resize="onResize()"
horizontalScrollPolicy="off"
verticalScrollPolicy="off"
layout="absolute">
<mate:Listener type="{BBBEvent.USER_VOICE_MUTED}" method="handleUserVoiceMutedEvent" />
<mate:Listener type="{EventConstants.USER_TALKING}" method="handleUserTalkingEvent" />
<mate:Listener type="{SwitchedPresenterEvent.SWITCHED_PRESENTER}" method="handleSwitchedPresenterEvent" />
<mate:Listener type="{MadePresenterEvent.SWITCH_TO_PRESENTER_MODE}" method="handleMadePresenterEvent" />
<mate:Listener type="{BBBEvent.USER_VOICE_JOINED}" method="handleNewRoleEvent" />
<mate:Listener type="{BBBEvent.USER_VOICE_LEFT}" method="handleNewRoleEvent" />
<mate:Listener type="{CloseAllWindowsEvent.CLOSE_ALL_WINDOWS}" method="closeWindow" />
<mx:Script>
<![CDATA[
import org.as3commons.logging.api.getClassLogger;
import org.as3commons.logging.api.ILogger;
import org.bigbluebutton.modules.users.views.UsersWindow;
import flexlib.mdi.events.MDIWindowEvent;
import mx.core.UIComponent;
import mx.events.ResizeEvent;
import org.bigbluebutton.common.Images;
import org.bigbluebutton.common.Role;
import org.bigbluebutton.common.events.CloseWindowEvent;
import org.bigbluebutton.common.events.LocaleChangeEvent;
import org.bigbluebutton.core.EventConstants;
import org.bigbluebutton.core.UsersUtil;
import org.bigbluebutton.core.events.CoreEvent;
import org.bigbluebutton.core.events.SwitchedLayoutEvent;
import org.bigbluebutton.core.managers.UserManager;
import org.bigbluebutton.main.events.BBBEvent;
import org.bigbluebutton.main.events.MadePresenterEvent;
import org.bigbluebutton.main.events.SwitchedPresenterEvent;
import org.bigbluebutton.main.views.MainCanvas;
import org.bigbluebutton.modules.videoconf.business.TalkingButtonOverlay;
import org.bigbluebutton.modules.videoconf.events.CloseAllWindowsEvent;
import org.bigbluebutton.modules.videoconf.events.OpenVideoWindowEvent;
import org.bigbluebutton.modules.videoconf.events.StartBroadcastEvent;
import org.bigbluebutton.modules.videoconf.model.VideoConfOptions;
import org.bigbluebutton.util.i18n.ResourceUtil;
private static const LOGGER:ILogger = getClassLogger(AvatarWindow);
[Bindable] private var defaultWidth:Number = 320;
[Bindable] private var defaultHeight:Number = 240;
[Bindable] public var glowColor:String = "";
[Bindable] public var glowBlurSize:Number = 0;
private var avatarWidth:Number = 320;
private var avatarHeight:Number = 240;
private var videoconfOptions:VideoConfOptions = new VideoConfOptions();
private var windowType:String = "AvatarWindowType";
override public function getWindowType():String {
return windowType;
}
private var loader:Loader;
private var request:URLRequest;
private function loadAvatar():void {
request = new URLRequest(UsersUtil.getAvatarURL());
loader.contentLoaderInfo.addEventListener(Event.COMPLETE, onLoadingComplete);
loader.load(request);
}
private function onLoadingComplete(event:Event):void {
// Save the size of the avatar image.
avatarWidth = loader.content.width;
avatarHeight = loader.content.height;
onResize();
}
private function onCreationComplete():void {
this.glowColor = videoconfOptions.glowColor;
this.glowBlurSize = videoconfOptions.glowBlurSize;
loader = new Loader();
_videoHolder = new UIComponent();
_videoHolder.width = avatarWidth;
_videoHolder.height = avatarHeight;
this.addChild(_videoHolder);
_videoHolder.addChild(loader);
_video = new Video();
_video.width = avatarWidth;
_video.height = avatarHeight;
this.minWidth = _minWidth;
this.minHeight = _minHeight;
maximizeRestoreBtn.visible = false;
this.showCloseButton = videoconfOptions.showCloseButton;
this.resizable = false;
setAspectRatio(avatarWidth, avatarHeight);
addEventListener(MDIWindowEvent.RESIZE_START, onResizeStart);
addEventListener(MDIWindowEvent.RESIZE_END, onResizeEnd);
addEventListener(MouseEvent.MOUSE_OVER, showButtons);
addEventListener(MouseEvent.MOUSE_OUT, hideButtons);
startPublishing();
loadAvatar();
}
override protected function onResize():void {
super.onResize();
if (loader != null && _videoHolder != null) {
if (avatarIsSmallerThanWindow()) {
// The avatar image is smaller than the window. Just center the image.
_video.width = loader.width = avatarWidth;
_video.height = loader.height = avatarHeight;
} else {
// The avatar is bigger than the window. Fit into the window maintaining aspect ratio.
fitAvatarToWindow();
}
_video.x = loader.x = (this.width - PADDING_HORIZONTAL - loader.width) / 2;
_video.y = loader.y = (this.height - PADDING_VERTICAL - loader.height) / 2;
}
}
private function fitAvatarToWindow():void {
if (_videoHolder.width - PADDING_HORIZONTAL < _videoHolder.height - PADDING_VERTICAL) {
fitToHeightAndAdjustWidthToMaintainAspectRatio();
} else {
fitToWidthAndAdjustHeightToMaintainAspectRatio();
}
}
private function avatarIsSmallerThanWindow():Boolean {
return (avatarWidth < _videoHolder.width - PADDING_HORIZONTAL) && (avatarHeight < _videoHolder.height - PADDING_VERTICAL);
}
private function fitToWidthAndAdjustHeightToMaintainAspectRatio():void {
var aspect:Number = avatarHeight / avatarWidth;
_video.width = loader.width = _videoHolder.width - PADDING_HORIZONTAL;
// Maintain aspect-ratio
_video.height = loader.height = loader.width * aspect;
}
private function fitToHeightAndAdjustWidthToMaintainAspectRatio():void {
var aspect:Number = avatarWidth / avatarHeight;
_video.height = loader.height = _videoHolder.height - PADDING_VERTICAL;
// Maintain aspect-ratio
_video.width = loader.width = loader.height * aspect;
}
private function handleMadePresenterEvent(event:MadePresenterEvent):void {
LOGGER.debug("******** Avatar: HandleMadePresenter event *********");
updateControlButtons();
}
private function handleSwitchedPresenterEvent(event:SwitchedPresenterEvent):void {
LOGGER.debug("******** Avatar: handleSwitchedPresenterEvent event *********");
updateControlButtons();
}
private function handleNewRoleEvent(event:Event):void {
updateControlButtons();
}
private function handleUserVoiceMutedEvent(event:BBBEvent):void {
if (event.payload.userID == userID) {
userMuted(event.payload.muted);
}
}
private function handleUserTalkingEvent(event:CoreEvent):void {
if (event.message.userID == userID) {
if (event.message.talking) {
notTalkingEffect.end();
talkingEffect.play([this]);
simulateClick();
} else {
talkingEffect.end();
notTalkingEffect.play([this]);
}
}
}
private function startPublishing():void{
this.resizable = true;
onResize();
createButtons();
addControlButtons();
updateButtonsPosition();
}
override public function close(event:MouseEvent=null):void{
closeThisWindow();
super.close(event);
}
private function closeWindow(e:CloseAllWindowsEvent):void{
closeThisWindow();
}
private function closeThisWindow():void {
LOGGER.debug("* Closing avatar window for user [{0}] *", [userID]);
}
]]>
</mx:Script>
<mx:Glow id="talkingEffect" duration="500" alphaFrom="1.0" alphaTo="0.3"
blurXFrom="0.0" blurXTo="{glowBlurSize}" blurYFrom="0.0" blurYTo="{glowBlurSize}" color="{glowColor}"/>
<mx:Glow id="notTalkingEffect" duration="500" alphaFrom="0.3" alphaTo="1.0"
blurXFrom="{glowBlurSize}" blurXTo="0.0" blurYFrom="{glowBlurSize}" blurYTo="0.0" color="{glowColor}"/>
</pubVid:VideoWindowItf>

View File

@ -33,6 +33,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
creationComplete="onCreationComplete()">
<mate:Listener type="{ShortcutEvent.FOCUS_VIDEO_WINDOW}" method="focusWindow" />
<mate:Listener type="{SwitchedLayoutEvent.SWITCHED_LAYOUT_EVENT}" method="onLayoutChanged" />
<mx:Script>
<![CDATA[
@ -41,6 +42,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
import mx.core.UIComponent;
import org.bigbluebutton.core.KeyboardUtil;
import org.bigbluebutton.core.events.SwitchedLayoutEvent;
import org.bigbluebutton.main.events.ShortcutEvent;
import org.bigbluebutton.main.views.MainCanvas;
import org.bigbluebutton.modules.videoconf.model.VideoConfOptions;
@ -50,6 +52,8 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
private var keyCombos:Object;
private var disp:Dispatcher = new Dispatcher();
private var darkMode:Boolean;
private function onCreationComplete():void {
hotkeyCapture();
titleBarOverlay.tabIndex = videoOptions.baseTabIndex;
@ -89,7 +93,11 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
override protected function resourcesChanged():void {
super.resourcesChanged();
this.title = ResourceUtil.getInstance().getString("bbb.videodock.title");
if (!darkMode) {
this.title = ResourceUtil.getInstance().getString("bbb.videodock.title");
} else {
this.title = "";
}
if (titleBarOverlay != null) {
titleBarOverlay.accessibilityName = ResourceUtil.getInstance().getString('bbb.videoDock.titleBar');
@ -115,6 +123,21 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
focusManager.setFocus(titleBarOverlay);
}
private function onLayoutChanged(e:SwitchedLayoutEvent):void {
if(e.layoutID != "bbb.layout.name.videochat"){
setStyle("styleNameFocus", "videoDockStyleFocus");
setStyle("styleNameNoFocus", "videoDockStyleNoFocus");
showControls = true;
this.title = ResourceUtil.getInstance().getString("bbb.videodock.title");
} else {
setStyle("styleNameFocus", "videoDockStyleFocusChatLayout");
setStyle("styleNameNoFocus", "videoDockStyleNoFocusChatLayout");
showControls = false;
this.title = "";
}
styleChanged("styleName");
}
private function remoteMinimize(e:ShortcutEvent):void {
if (!minimized) {
this.minimize();

View File

@ -21,6 +21,7 @@ import NavBarContainer from '../nav-bar/container';
import ActionsBarContainer from '../actions-bar/container';
import MediaContainer from '../media/container';
import AudioModalContainer from '../audio-modal/container';
import ClosedCaptionsContainer from '/imports/ui/components/closed-captions/container';
const defaultProps = {
navbar: <NavBarContainer />,

View File

@ -8,6 +8,7 @@ import DeviceSelector from '/imports/ui/components/audio/device-selector/compone
import AudioStreamVolume from '/imports/ui/components/audio/audio-stream-volume/component';
import EnterAudioContainer from '/imports/ui/components/enter-audio/container';
import AudioTestContainer from '/imports/ui/components/audio-test/container';
import cx from 'classnames';
class AudioSettings extends React.Component {
constructor(props) {
@ -20,7 +21,7 @@ class AudioSettings extends React.Component {
this.state = {
inputDeviceId: undefined,
}
};
}
chooseAudio() {
@ -30,7 +31,7 @@ class AudioSettings extends React.Component {
handleInputChange(deviceId) {
console.log(`INPUT DEVICE CHANGED: ${deviceId}`);
this.setState({
inputDeviceId: deviceId
inputDeviceId: deviceId,
});
}
@ -50,7 +51,7 @@ class AudioSettings extends React.Component {
return (
<div>
<div className={styles.center}>
<div className={styles.topRow}>
<Button className={styles.backBtn}
label={intl.formatMessage(intlMessages.backLabel)}
icon={'left_arrow'}
@ -59,47 +60,69 @@ class AudioSettings extends React.Component {
ghost={true}
onClick={this.chooseAudio}
/>
<div className={styles.title}>
<div className={cx(styles.title, styles.chooseAudio)}>
<FormattedMessage
id="app.audio.audioSettings.titleLabel"
/>
</div>
</div>
<div className={styles.audioNote}>
<FormattedMessage
id="app.audio.audioSettings.descriptionLabel"
/>
<div className={styles.form}>
<div className={styles.row}>
<div className={styles.audioNote}>
<FormattedMessage
id="app.audio.audioSettings.descriptionLabel"
/>
</div>
</div>
<div className={styles.row}>
<div className={styles.col}>
<div className={styles.formElement}>
<label className={cx(styles.label, styles.labelSmall)}>
Microphone source
</label>
<DeviceSelector
value={this.state.inputDeviceId}
className={styles.select}
kind="audioinput"
onChange={this.handleInputChange} />
</div>
</div>
<div className={styles.col}>
<div className={styles.formElement}>
<label className={cx(styles.label, styles.labelSmall)}>
Speaker source
</label>
<DeviceSelector
value={this.state.outputDeviceId}
className={styles.select}
kind="audiooutput"
onChange={this.handleOutputChange} />
</div>
</div>
</div>
<div className={styles.row}>
<div className={styles.col}>
<div className={styles.formElement}>
<label className={cx(styles.label, styles.labelSmall)}>
Your audio stream volume
</label>
<AudioStreamVolume
deviceId={this.state.inputDeviceId}
className={styles.audioMeter} />
</div>
</div>
<div className={styles.col}>
<label className={styles.label}>&nbsp;</label>
<AudioTestContainer/>
</div>
</div>
</div>
<div className={styles.containerLeftHalfContent}>
<span className={styles.heading}>
<FormattedMessage
id="app.audio.audioSettings.microphoneSourceLabel"
/>
</span>
<DeviceSelector
className={styles.item}
kind="audioinput"
onChange={this.handleInputChange} />
<span className={styles.heading}>
<FormattedMessage
id="app.audio.audioSettings.microphoneStreamLabel"
/>
</span>
<AudioStreamVolume
className={styles.item}
deviceId={this.state.inputDeviceId} />
</div>
<div className={styles.containerRightHalfContent}>
<span className={styles.heading}>
<FormattedMessage
id="app.audio.audioSettings.speakerSourceLabel"
/>
</span>
<DeviceSelector
className={styles.item}
kind="audiooutput"
onChange={this.handleOutputChange} />
<AudioTestContainer />
<div className={styles.enterAudio}>
<EnterAudioContainer isFullAudio={true}/>
</div>
</div>

View File

@ -43,22 +43,22 @@ class JoinAudio extends React.Component {
const { intl } = this.props;
return (
<div>
<div className={styles.center}>
<div className={styles.closeBtn}>
<Button className={styles.closeBtn}
label={intl.formatMessage(intlMessages.closeLabel)}
icon={'close'}
size={'lg'}
circle={true}
hideLabel={true}
onClick={this.handleClose}
/>
<div>
<FormattedMessage
</div>
<div className={styles.title}>
<FormattedMessage
id="app.audioModal.audioChoiceLabel"
description="app.audioModal.audioChoiceDescription"
defaultMessage="How would you like to join the audio?"
/>
</div>
/>
</div>
<div className={styles.center}>
<Button className={styles.audioBtn}
@ -68,6 +68,9 @@ class JoinAudio extends React.Component {
size={'jumbo'}
onClick={this.openAudio}
/>
<span className={styles.verticalLine}>
</span>
<Button className={styles.audioBtn}
label={intl.formatMessage(intlMessages.listenOnlyLabel)}
icon={'listen'}

View File

@ -1,159 +1,164 @@
@import "../../stylesheets/variables/_all";
.center {
text-align: center;
font-size: $font-size-large;
padding-top: 40px;
display: flex;
justify-content: center;
align-items: center;
padding-top: 2rem;
}
.closeBtn {
position: absolute;
right: 10px;
top: 10px;
background-color: #FFFFFF;
border: none;
display: flex;
justify-content: flex-end;
i {
color: $color-gray-light;
}
}
// Modifies the close button style
Button.closeBtn span:first-child {
color: $color-gray-light;
background: none;
background-color: $color-primary;
border: none;
box-shadow: none;
Button.audioBtn {
i{
color: #3c5764;
}
}
// Modifies the audio button icon colour
Button.audioBtn span:first-child {
color: #25385D;
border: 5px solid $color-white;
background-color: $color-primary;
color: #1b3c4b;
background-color: #f1f8ff;
box-shadow: none;
border: 5px solid #f1f8ff;
}
// When hovering over a button of class audioBtn, change the border colour of first span-child
Button.audioBtn:hover span:first-child {
border: 5px solid $color-primary;
background-color: #f1f8ff;
}
// Modifies the button label text
Button.audioBtn span:last-child {
color: $color-gray-dark;
font-size: 30%;
color: black;
font-size: 0.8rem;
font-weight: 500;
}
Button.audioBtn:first-of-type {
margin-right: 70px;
margin-right: 5%;
}
Button.audioBtn:last-of-type {
margin-left: 5%;
}
.inner {
padding: 10px;
min-height: 350px;
min-width: 500px;
}
// Audio settings menu
.half {
width: 50%;
float: left;
padding-top: 30px;
}
div.half label {
font-size: 0.75em;
font-weight: 600;
color: $color-primary;
margin-bottom: 5px;
padding: 1em;
min-height: 20rem;
}
.backBtn {
position: absolute;
left: 10px;
top: 10px;
border: none;
box-shadow: none;
i {
color: $color-primary;
}
}
.playSound {
border: none;
box-shadow: none;
}
.enterBtn {
position: absolute;
bottom: 10px;
right: 10px;
border: none;
box-shadow: none;
}
.containerLeftHalf {
width: 50%;
float: left;
}
.containerRightHalf {
width: 50%;
float: right;
}
.containerFull{
width: 100%;
float: right;
}
.containerLeftHalfContent {
@extend .containerLeftHalf;
@extend .row;
}
.containerRightHalfContent {
@extend .containerRightHalf;
@extend .row;
}
.row {
height: 42px;
}
.item {
display: block;
margin-bottom: $md-padding-y;
width: 85%;
margin-bottom: 2em;
border-bottom: 3px solid;
border-top:0px;
border-left: 0px;
border-right: 0px;
outline:0px;
border-color: $color-gray-dark;
text-decoration-color: $color-gray-dark;
}
.title {
color: $color-gray-dark;
font-weight: 700;
font-size: $font-size-large;
margin-top: -1em;
.topRow {
align-items: center;
display: flex;
}
.audioNote {
color: $color-text;
display: inline-block;
margin-top: 1.75em;
margin-bottom: 2em;
font-size: 0.9rem;
}
.heading {
font-weight: 700;
font-size: $font-size-small;
display: inline-block;
margin-bottom: .5em;
.title {
text-align: center;
margin: auto;
color: black;
font-weight: 400;
font-size: 1.3rem;
}
.joinButton {
.form {
display: flex;
flex-flow: column;
padding: 2em;
}
.row {
display: flex;
flex-flow: row;
flex-grow: 1;
justify-content: space-between;
margin-bottom: 0.7rem;
}
.col {
display: flex;
flex-grow: 1;
flex-basis: 0;
margin-right: 1rem;
&:last-child {
margin-right: 0;
padding-right: 0.1rem;
padding-left: 4rem;
}
}
.labelSmall {
color: black;
font-size: 0.7rem;
font-weight: 600;
margin-bottom: 0.3rem;
}
.formElement {
position: relative;
left: 11em;
top: 3em;
display: flex;
flex-flow: column;
flex-grow: 1;
}
.select {
background-color: #fff;
border: 0;
border-bottom: 0.1rem solid $color-gray-lighter;
color: $color-gray-light;
width: 100%;
// appearance: none;
height: 1.75rem;
}
.audioMeter {
width: 100%;
}
.pullContentRight {
display: flex;
justify-content: flex-end;
flex-flow: row;
}
.verticalLine {
color: #f3f6f9;
border-left: 1px solid;
height: 5rem;
}
.enterAudio {
display: flex;
justify-content: flex-end;
margin-right: 2rem;
}
.chooseAudio {
position:absolute;
left:50%;
transform: translate(-50%, 0);
}

View File

@ -30,15 +30,24 @@ export default class Chat extends Component {
return (
<section className={styles.chat}>
<header className={styles.header}>
<Link
className={styles.closeChat}
to="/users"
role="button"
aria-label={closeChatLabel}>
<Icon iconName="left_arrow" /> {title}
</Link>
<div className={styles.title}>
<Link to="/users" role="button" aria-label={closeChatLabel}>
<Icon iconName="left_arrow"/> {title}
</Link>
</div>
<div className={styles.closeIcon}>
{
((this.props.chatID == 'public') ?
null :
<Link to="/users">
<Icon iconName="close" onClick={() => actions.handleClosePrivateChat(chatID)}/>
</Link>)
}
</div>
</header>
<MessageList
chatId={chatID}
messages={messages}

View File

@ -95,6 +95,9 @@ export default injectIntl(createContainer(({ params, intl }) => {
isChatLocked,
scrollPosition,
actions: {
handleClosePrivateChat: chatID => ChatService.closePrivateChat(chatID),
handleSendMessage: message => {
let sentMessage = ChatService.sendMessage(chatID, message);
ChatService.updateScrollPosition(chatID, null); //null so its scrolls to bottom

View File

@ -4,8 +4,10 @@ import Meetings from '/imports/api/meetings';
import Auth from '/imports/ui/services/auth';
import UnreadMessages from '/imports/ui/services/unread-messages';
import Storage from '/imports/ui/services/storage/session';
import { callServer } from '/imports/ui/services/api';
import _ from 'lodash';
const CHAT_CONFIG = Meteor.settings.public.chat;
const GROUPING_MESSAGES_WINDOW = CHAT_CONFIG.grouping_messages_window;
@ -20,6 +22,9 @@ const PUBLIC_CHAT_USERNAME = CHAT_CONFIG.public_username;
const ScrollCollection = new Mongo.Collection(null);
// session for closed chat list
const CLOSED_CHAT_LIST_KEY = 'closedChatList';
/* TODO: Same map is done in the user-list/service we should share this someway */
const mapUser = (user) => ({
@ -193,6 +198,13 @@ const sendMessage = (receiverID, message) => {
from_color: 0,
};
let currentClosedChats = Storage.getItem(CLOSED_CHAT_LIST_KEY);
// Remove the chat that user send messages from the session.
if (_.indexOf(currentClosedChats, receiver.id) > -1) {
Storage.setItem(CLOSED_CHAT_LIST_KEY, _.without(currentClosedChats, receiver.id));
}
callServer('sendChat', messagePayload);
return messagePayload;
@ -215,6 +227,17 @@ const updateUnreadMessage = (receiverID, timestamp) => {
return UnreadMessages.update(receiverID, timestamp);
};
const closePrivateChat = (chatID) => {
let currentClosedChats = Storage.getItem(CLOSED_CHAT_LIST_KEY) || [];
if (_.indexOf(currentClosedChats, chatID) < 0) {
currentClosedChats.push(chatID);
Storage.setItem(CLOSED_CHAT_LIST_KEY, currentClosedChats);
}
};
export default {
getPublicMessages,
getPrivateMessages,
@ -226,4 +249,5 @@ export default {
updateScrollPosition,
updateUnreadMessage,
sendMessage,
closePrivateChat,
};

View File

@ -6,23 +6,38 @@
display: flex;
flex-grow: 1;
flex-direction: column;
justify-content: space-around;
overflow: hidden;
}
.closeChat {
text-decoration: none;
}
.header {
margin-top: $lg-padding-x - $sm-padding-x;
margin-bottom: $lg-padding-x;
display: flex;
flex-direction: row;
align-items: center;
a {
text-decoration: none;
}
}
.title {
@extend %text-elipsis;
width: 90%;
> [class^="icon-bbb-"],
> [class*=" icon-bbb-"] {
font-size: 85%;
}
}
[class='icon-bbb-left_arrow'] {
padding-bottom: 5px;
.closeIcon {
position: relative;
}
[class='icon-bbb-left_arrow'],
[class='icon-bbb-close']{
padding-bottom: 5px;
}

View File

@ -3,10 +3,19 @@ import { findDOMNode } from 'react-dom';
import styles from './styles';
import DropdownTrigger from './trigger/component';
import DropdownContent from './content/component';
import Button from '/imports/ui/components/button/component';
import cx from 'classnames';
import { defineMessages, injectIntl } from 'react-intl';
const FOCUSABLE_CHILDREN = `[tabindex]:not([tabindex="-1"]), a, input, button`;
const intlMessages = defineMessages({
close: {
id: 'app.dropdown.close',
defaultMessage: 'Close',
},
});
const propTypes = {
/**
* The dropdown needs a trigger and a content component as childrens
@ -44,7 +53,7 @@ const defaultProps = {
isOpen: false,
};
export default class Dropdown extends Component {
class Dropdown extends Component {
constructor(props) {
super(props);
this.state = { isOpen: false, };
@ -113,7 +122,7 @@ export default class Dropdown extends Component {
}
render() {
const { children, className, style } = this.props;
const { children, className, style, intl } = this.props;
let trigger = children.find(x => x.type === DropdownTrigger);
let content = children.find(x => x.type === DropdownContent);
@ -137,6 +146,14 @@ export default class Dropdown extends Component {
<div style={style} className={cx(styles.dropdown, className)}>
{trigger}
{content}
{ this.state.isOpen ?
<Button
className={styles.close}
label={intl.formatMessage(intlMessages.close)}
size={'lg'}
color={'default'}
onClick={this.handleHide}
/> : null }
</div>
);
}
@ -144,3 +161,4 @@ export default class Dropdown extends Component {
Dropdown.propTypes = propTypes;
Dropdown.defaultProps = defaultProps;
export default injectIntl(Dropdown);

View File

@ -40,7 +40,9 @@ export default class DropdownContent extends Component {
style={style}
aria-expanded={this.props['aria-expanded']}
className={cx(styles.content, styles[placementName], className)}>
{boundChildren}
<div className={styles.scrollable}>
{boundChildren}
</div>
</div>
);
}

View File

@ -10,6 +10,11 @@
padding: ($line-height-computed / 2);
display: flex;
flex-direction: column;
@include mq($small-only) {
font-size: $font-size-large * 1.1;
padding: $line-height-computed;
}
}
@ -32,6 +37,11 @@
align-items: center;
justify-content: flex-start;
@include mq($small-only) {
padding: ($line-height-computed / 1.5) 0;
justify-content: center;
}
&:first-child {
padding-top: 0;
}

View File

@ -1,4 +1,5 @@
@import "../../stylesheets/variables/_all";
@import "../../stylesheets/mixins/_scrollable";
$dropdown-bg: $color-white;
$dropdown-color: $color-text;
@ -18,7 +19,6 @@ $dropdown-caret-height: 8px;
box-shadow: 0 6px 12px rgba(0, 0, 0, .175);
border: 1px solid rgba(0, 0, 0, .15);
padding: $line-height-computed / 2;
// min-width: 150px;
z-index: 1000;
&:after, &:before {
@ -35,10 +35,55 @@ $dropdown-caret-height: 8px;
&[aria-expanded="true"] {
display: block;
}
@include mq($small-only) {
z-index: 1015;
border-radius: 0;
background-color: #fff;
box-shadow: none;
position: fixed;
top: 0 !important;
left: 0 !important;
right: 0 !important;
bottom: 0 !important;
border: 0 !important;
padding: 0 !important;
margin: 0 0 $line-height-computed * 5.25 0 !important;
transform: translateX(0) translateY(0) !important;
&:after, &:before {
display: none !important;
}
}
}
.scrollable {
@include mq($small-only) {
@include scrollbox-vertical();
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
}
.trigger {}
.close {
display: none;
position: fixed;
bottom: 1rem;
z-index: 1011;
font-size: $font-size-large * 1.1;
width: calc(100% - #{($line-height-computed * 2)});
left: $line-height-computed;
box-shadow: 0 0 0 2rem #fff;
@include mq($small-only) {
display: block;
}
}
/* Placements
* ==========

View File

@ -1,5 +1,4 @@
import React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl';
import React, { Component } from 'react';
import Modal from '/imports/ui/components/modal/component';
import { Tab, Tabs, TabList, TabPanel } from 'react-tabs';
@ -9,10 +8,8 @@ import Application from '/imports/ui/components/settings/submenus/application/co
import Participants from '/imports/ui/components/settings/submenus/participants/component';
import Video from '/imports/ui/components/settings/submenus/video/component';
import Button from '../button/component';
import Icon from '../icon/component';
import styles from './styles';
import cx from 'classnames';
const propTypes = {
};

View File

@ -56,3 +56,13 @@
padding-top: 0;
padding-right: 0;
}
.testAudioBtn {
border: none;
padding-left: 0;
i {
color: $color-primary;
}
background-color: transparent;
color: $color-primary;
}

View File

@ -1,4 +1,4 @@
import React, { Component } from 'react';
import React from 'react';
import BaseMenu from '../base/component';
import styles from '../styles.scss';
@ -46,7 +46,7 @@ export default class AudioMenu extends BaseMenu {
render() {
return (
<div className={styles.tabContent}>
<div>
<div className={styles.header}>
<h3 className={styles.title}>Audio</h3>
</div>

View File

@ -2,15 +2,20 @@ import Users from '/imports/api/users';
import Chat from '/imports/api/chat';
import Auth from '/imports/ui/services/auth';
import UnreadMessages from '/imports/ui/services/unread-messages';
import Storage from '/imports/ui/services/storage/session';
import { EMOJI_STATUSES } from '/imports/utils/statuses.js';
import { callServer } from '/imports/ui/services/api';
import _ from 'lodash';
const CHAT_CONFIG = Meteor.settings.public.chat;
const USER_CONFIG = Meteor.settings.public.user;
const ROLE_MODERATOR = USER_CONFIG.role_moderator;
const PRIVATE_CHAT_TYPE = CHAT_CONFIG.type_private;
// session for closed chat list
const CLOSED_CHAT_LIST_KEY = 'closedChatList';
/* TODO: Same map is done in the chat/service we should share this someway */
const mapUser = user => ({
@ -164,18 +169,20 @@ const getUsers = () => {
.fetch();
return users
.map(u => u.user)
.map(mapUser)
.sort(sortUsers);
.map(u => u.user)
.map(mapUser)
.sort(sortUsers);
};
const getOpenChats = chatID => {
let openChats = Chat
.find({ 'message.chat_type': PRIVATE_CHAT_TYPE })
.fetch()
.map(mapOpenChats);
let currentUserId = Auth.userID;
if (chatID) {
openChats.push(chatID);
}
@ -191,6 +198,28 @@ const getOpenChats = chatID => {
return op;
});
let currentClosedChats = Storage.getItem(CLOSED_CHAT_LIST_KEY) || [];
let filteredChatList = [];
openChats.forEach((op) => {
// When a new private chat message is received, ensure the conversation view is restored.
if (op.unreadCounter > 0) {
if (_.indexOf(currentClosedChats, op.id) > -1) {
Storage.setItem(CLOSED_CHAT_LIST_KEY, _.without(currentClosedChats, op.id));
}
}
// Compare openChats with session and push it into filteredChatList
// if one of the openChat is not in session.
// It will pass to openChats.
if (_.indexOf(currentClosedChats, op.id) < 0) {
filteredChatList.push(op);
}
});
openChats = filteredChatList;
openChats.push({
id: 'public',
name: 'Public Chat',

View File

@ -112,6 +112,7 @@
"app.audio.listenOnly.backLabel": "Back",
"app.audio.listenOnly.closeLabel": "Close",
"app.error.kicked": "You have been kicked out of the meeting",
"app.dropdown.close": "Close",
"app.error.500": "Ops, something went wrong",
"app.error.404": "Not found",
"app.error.401": "Unauthorized",

View File

@ -5,7 +5,7 @@
-->
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1">
<metadata>
Created by FontForge 20161004 at Fri Dec 30 11:22:51 2016
Created by FontForge 20161004 at Thu Mar 23 10:01:15 2017
By Ghazi
Copyright (c) 2016, BlindSide Networks Inc.
</metadata>
@ -19,7 +19,7 @@ Copyright (c) 2016, BlindSide Networks Inc.
panose-1="2 0 5 3 0 0 0 0 0 0"
ascent="819"
descent="-205"
bbox="0 -205 1212 820"
bbox="0 -205 1024 820"
underline-thickness="51"
underline-position="-102"
unicode-range="U+0020-E932"
@ -31,20 +31,20 @@ Copyright (c) 2016, BlindSide Networks Inc.
<glyph glyph-name="space" unicode=" " horiz-adv-x="524"
/>
<glyph glyph-name="logout" unicode="&#xe900;"
d="M160 -117h84c19 0 30 -14 30 -32c0 -22 -7 -34 -30 -34h-84c-83 0 -150 66 -150 150v670c0 83 67 150 150 150h84c16 0 30 -17 30 -32c0 -16 -9 -32 -30 -32h-84c-47 0 -87 -40 -87 -86v-667c0 -47 40 -87 87 -87zM676 -9c-6 -5 -14 -8 -22 -8c-7 0 -15 3 -21 8
s-8 12 -8 19c0 9 5 20 12 27l233 233h-625c-22 0 -35 14 -35 30c0 17 13 30 35 30h625l-233 233c-7 7 -11 16 -11 25s4 17 11 23c6 5 14 8 21 8c8 0 16 -3 22 -9c14 -13 284 -283 310 -310c-30 -30 -272 -270 -314 -309z" />
d="M160 -117h84c19 0 30 -14 30 -32c0 -16 -7 -34 -30 -34h-84c-83 0 -150 66 -150 150v670c0 83 67 150 150 150h84c16 0 30 -17 30 -32c0 -16 -9 -32 -30 -32h-84c-47 0 -87 -40 -87 -86v-667c0 -47 40 -87 87 -87zM676 -9c-6 -5 -14 -8 -22 -8c-7 0 -15 3 -21 8
s-8 12 -8 19c0 9 5 20 12 27l233 233h-625c-22 0 -35 12 -35 30s13 30 35 30h625l-233 233c-7 7 -11 16 -11 25s4 17 11 23c6 5 14 8 21 8c8 0 16 -3 22 -9l310 -310c-30 -30 -314 -309 -314 -309z" />
<glyph glyph-name="more" unicode="&#xe902;"
d="M17 307c0 60 49 109 109 109c61 0 110 -49 110 -109s-49 -109 -110 -109c-60 0 -109 49 -109 109zM403 307c0 60 49 109 109 109s109 -49 109 -109s-49 -109 -109 -109s-109 49 -109 109zM788 307c0 60 49 109 110 109c60 0 109 -49 109 -109s-49 -109 -109 -109
c-61 0 -110 49 -110 109z" />
<glyph glyph-name="promote" unicode="&#xe903;" horiz-adv-x="1028"
d="M993 601c10 -8 11 -18 11 -28v-522c0 -14 -10 -33 -24 -33h-4c0 0 -520 129 -529 129v0c0 -65 -58 -110 -119 -110c-65 0 -120 55 -120 120v44l-128 31v-9c0 -15 -18 -27 -32 -27c-17 0 -31 12 -31 29v184c0 14 17 28 31 28c18 0 32 -11 32 -28v-17l886 215
c7 2 18 1 27 -6zM331 92c20 0 55 7 55 58l-113 27v-30c0 -31 27 -55 58 -55zM944 82v450l-864 -211v-31c287 -69 577 -139 864 -208z" />
d="M993 601c10 -8 11 -18 11 -28v-523c0 -14 -5 -20 -11 -25c-10 -8 -14 -8 -17 -7l-529 129c0 -65 -58 -110 -119 -110c-65 0 -120 55 -120 120v44l-128 31v-9c0 -15 -18 -27 -32 -27c-17 0 -31 12 -31 29v184c0 14 17 28 31 28c18 0 32 -11 32 -28v-17l886 215
c7 2 18 1 27 -6zM331 92c20 0 55 7 55 58l-113 27v-30c0 -31 27 -55 58 -55zM944 82v450l-864 -211v-31c287 -69 864 -208 864 -208z" />
<glyph glyph-name="video_off" unicode="&#xe904;"
d="M963 78l-236 106c-10 5 -20 17 -20 31v167c0 14 6 21 20 27l236 106c4 2 9 3 13 3c16 0 31 -14 31 -34v-378c0 -18 -15 -31 -31 -31c-4 0 -9 1 -13 3zM945 153v280l-174 -78v-123zM915 662l-212 -208v-410c0 -17 -14 -31 -31 -31h-409s-110 -110 -113 -112
c-5 -4 -12 -6 -18 -6c-10 0 -20 5 -26 13c-4 5 -6 11 -6 17c0 9 5 19 13 27l61 61h-123c-17 0 -31 14 -31 31v526c0 17 14 30 31 30h625c17 0 31 -13 31 -30v-24l160 160c5 5 13 8 20 8c8 0 17 -4 22 -9c8 -8 13 -17 13 -26c0 -6 -2 -12 -7 -17zM81 74h155l406 407v58h-561
v-465zM642 74v318l-314 -318h314z" />
<glyph glyph-name="user" unicode="&#xe905;"
d="M860 85c79 -55 123 -143 123 -239c0 -17 -14 -31 -31 -31h-880c-17 0 -31 14 -31 31c0 96 44 184 123 239c85 58 198 86 348 86s263 -28 348 -86zM106 -123v-4h812c-10 65 -44 120 -95 158c-75 51 -171 75 -311 75s-236 -21 -311 -72c-55 -38 -88 -96 -95 -157zM512 239
d="M860 85c79 -55 123 -143 123 -239c0 -17 -14 -31 -31 -31h-880c-17 0 -31 14 -31 31c0 96 37 179 123 239c115 79 198 86 348 86s263 -28 348 -86zM106 -123v-4h812c-10 65 -44 120 -95 158c-75 51 -171 75 -311 75s-236 -21 -311 -72c-55 -38 -88 -96 -95 -157zM512 239
c-147 0 -263 115 -263 273c0 151 112 290 263 290c154 0 263 -136 263 -290c0 -153 -120 -273 -263 -273zM307 515c0 -105 89 -211 202 -211s201 85 201 211c0 118 -78 222 -198 222c-114 0 -205 -110 -205 -222z" />
<glyph glyph-name="up_arrow" unicode="&#xe906;"
d="M659 502c8 -8 12 -18 12 -29c0 -9 -3 -18 -10 -25c-8 -8 -18 -13 -29 -13c-10 0 -20 4 -28 12l-221 222v-816c0 -20 -20 -39 -40 -39s-39 19 -39 39v816l-222 -222c-8 -8 -17 -11 -27 -11s-20 4 -28 11c-8 8 -12 17 -12 27s4 20 12 28l290 286c8 8 17 13 27 13
@ -65,9 +65,9 @@ d="M58 -205c-24 0 -41 17 -41 41c0 10 3 20 10 27l396 449l-396 439c-8 8 -11 16 -11
d="M973 802c20 0 34 -14 37 -34v-662c0 -20 -14 -35 -34 -35h-375l222 -201c8 -7 17 -20 17 -33c0 -6 -2 -12 -7 -18s-14 -8 -23 -8c-12 0 -24 4 -32 11l-232 209v-190c0 -19 -11 -36 -32 -36c-17 0 -33 16 -33 36v190c-20 -19 -196 -174 -236 -210c-6 -5 -16 -9 -27 -9
c-9 0 -18 2 -23 9c-4 5 -6 11 -6 17c0 11 5 23 16 32l222 198h-376c-20 0 -34 14 -34 34v666c0 20 14 34 34 34h922zM940 136v427h-858v-427h858zM82 631h858v106h-858v-106z" />
<glyph glyph-name="listen" unicode="&#xe90c;"
d="M1096 310c72 -51 116 -133 116 -218c0 -150 -119 -277 -273 -277c-17 0 -31 14 -31 31v485c0 17 14 31 31 31c31 0 61 -7 92 -17c0 227 -233 388 -416 388c-190 0 -393 -146 -417 -375c31 10 61 17 92 17c17 0 31 -13 31 -30v-485c0 -17 -14 -31 -31 -31
c-89 0 -171 41 -222 113c-35 49 -52 104 -52 160c0 85 40 169 114 225c0 263 222 475 485 475c266 0 481 -222 481 -492zM256 -106v416c-89 -14 -160 -85 -174 -174c-2 -12 -3 -24 -3 -36c0 -99 73 -188 177 -206zM973 -123c89 17 160 85 174 177c2 12 3 24 3 36
c0 100 -73 189 -177 207v-420z" />
d="M911 310c62 -44 96 -109 96 -180c0 -123 -99 -229 -225 -229c-14 0 -24 10 -24 24v402c0 14 10 24 24 24c24 0 51 -6 75 -13c-10 170 -140 307 -311 324c-11 1 -21 1 -31 1c-178 0 -332 -135 -348 -315c24 7 51 14 75 14c14 0 24 -11 24 -24v-403c0 -14 -10 -24 -24 -24
c-75 0 -143 34 -184 92c-30 41 -44 88 -44 134c0 71 33 142 95 187c7 219 185 393 403 396c222 -3 399 -184 399 -410zM215 -34v344c-72 -10 -133 -68 -143 -143c-2 -10 -3 -20 -3 -30c0 -81 60 -156 146 -171zM809 -51c75 17 133 71 143 150c2 10 3 20 3 30
c0 81 -60 156 -146 171v-351z" />
<glyph glyph-name="left_arrow" unicode="&#xe90d;"
d="M481 -188c-10 0 -20 4 -30 14l-427 486l430 480c7 9 18 13 29 13c10 0 21 -3 29 -10c9 -7 13 -18 13 -29c0 -10 -3 -21 -10 -29l-385 -425l382 -435c7 -8 10 -18 10 -27c0 -10 -4 -21 -13 -28c-7 -7 -18 -10 -28 -10z" />
<glyph glyph-name="happy" unicode="&#xe90e;"

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 31 KiB

0
record-and-playback/core/scripts/README Executable file → Normal file
View File

0
record-and-playback/core/scripts/bigbluebutton.yml Executable file → Normal file
View File

View File

@ -0,0 +1,101 @@
#!/usr/bin/ruby
# encoding: UTF-8
# Copyright ⓒ 2017 BigBlueButton Inc. and by respective authors.
#
# This file is part of BigBlueButton open source conferencing system.
#
# BigBlueButton 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 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/>.
require '../lib/recordandplayback'
require 'rubygems'
require 'yaml'
require 'fileutils'
# Number of seconds to delay archiving (red5 race condition workaround)
ARCHIVE_DELAY_SECONDS = 120
def archive_recorded_meetings(recording_dir)
recorded_done_files = Dir.glob("#{recording_dir}/status/recorded/*.done")
FileUtils.mkdir_p("#{recording_dir}/status/archived")
recorded_done_files.each do |recorded_done|
match = /([^\/]*).done$/.match(recorded_done)
meeting_id = match[1]
if File.mtime(recorded_done) + ARCHIVE_DELAY_SECONDS > Time.now
BigBlueButton.logger.info("Temporarily skipping #{meeting_id} for Red5 race workaround")
next
end
archived_done = "#{recording_dir}/status/archived/#{meeting_id}.done"
next if File.exists?(archived_done)
archived_norecord = "#{recording_dir}/status/archived/#{meeting_id}.norecord"
next if File.exists?(archived_norecord)
archived_fail = "#{recording_dir}/status/archived/#{meeting_id}.fail"
next if File.exists?(archived_fail)
BigBlueButton.redis_publisher.put_archive_started(meeting_id)
step_start_time = BigBlueButton.monotonic_clock
ret = BigBlueButton.exec_ret("ruby", "archive/archive.rb", "-m", meeting_id)
step_stop_time = BigBlueButton.monotonic_clock
step_time = step_stop_time - step_start_time
step_succeeded = (ret == 0 &&
(File.exists?(archived_done) ||
File.exists?(archived_norecord)))
BigBlueButton.redis_publisher.put_archive_ended(meeting_id, {
"success" => step_succeeded,
"step_time" => step_time
})
if step_succeeded
BigBlueButton.logger.info("Successfully archived #{meeting_id}")
FileUtils.rm_f(recorded_done)
else
BigBlueButton.logger.error("Failed to archive #{meeting_id}")
FileUtils.touch(archived_fail)
end
end
end
begin
props = YAML::load(File.open('bigbluebutton.yml'))
redis_host = props['redis_host']
redis_port = props['redis_port']
BigBlueButton.redis_publisher = BigBlueButton::RedisWrapper.new(redis_host, redis_port)
log_dir = props['log_dir']
recording_dir = props['recording_dir']
logger = Logger.new("#{log_dir}/bbb-rap-worker.log")
logger.level = Logger::INFO
BigBlueButton.logger = logger
BigBlueButton.logger.debug("Running rap-archive-worker...")
archive_recorded_meetings(recording_dir)
BigBlueButton.logger.debug("rap-archive-worker done")
rescue Exception => e
BigBlueButton.logger.error(e.message)
e.backtrace.each do |traceline|
BigBlueButton.logger.error(traceline)
end
end

View File

@ -0,0 +1,137 @@
#!/usr/bin/ruby
# encoding: UTF-8
# Copyright ⓒ 2017 BigBlueButton Inc. and by respective authors.
#
# This file is part of BigBlueButton open source conferencing system.
#
# BigBlueButton 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 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/>.
require '../lib/recordandplayback'
require 'rubygems'
require 'yaml'
require 'fileutils'
def process_archived_meetings(recording_dir)
sanity_done_files = Dir.glob("#{recording_dir}/status/sanity/*.done")
FileUtils.mkdir_p("#{recording_dir}/status/processed")
sanity_done_files.each do |sanity_done|
match = /([^\/]*).done$/.match(sanity_done)
meeting_id = match[1]
step_succeeded = true
# Iterate over the list of recording processing scripts to find available types
# For now, we look for the ".rb" extension - TODO other scripting languages?
Dir.glob("process/*.rb").sort.each do |process_script|
match2 = /([^\/]*).rb$/.match(process_script)
process_type = match2[1]
processed_done = "#{recording_dir}/status/processed/#{meeting_id}-#{process_type}.done"
next if File.exists?(processed_done)
processed_fail = "#{recording_dir}/status/processed/#{meeting_id}-#{process_type}.fail"
if File.exists?(processed_fail)
step_succeeded = false
next
end
BigBlueButton.redis_publisher.put_process_started(process_type, meeting_id)
# If the process directory exists, the script does nothing
FileUtils.rm_rf("#{recording_dir}/process/#{process_type}/#{meeting_id}")
step_start_time = BigBlueButton.monotonic_clock
ret = BigBlueButton.exec_ret("ruby", process_script, "-m", meeting_id)
step_stop_time = BigBlueButton.monotonic_clock
step_time = step_stop_time - step_start_time
IO.write("#{recording_dir}/process/#{process_type}/#{meeting_id}/processing_time", step_time)
step_succeeded = (ret == 0 and File.exists?(processed_done))
BigBlueButton.redis_publisher.put_process_ended(process_type, meeting_id, {
"success" => step_succeeded,
"step_time" => step_time
})
if step_succeeded
BigBlueButton.logger.info("Process format #{process_type} succeeded for #{meeting_id}")
BigBlueButton.logger.info("Process took #{step_time}ms")
else
BigBlueButton.logger.info("Process format #{process_type} failed for #{meeting_id}")
BigBlueButton.logger.info("Process took #{step_time}ms")
FileUtils.touch(processed_fail)
step_succeeded = false
end
end
if step_succeeded
post_process(meeting_id)
FileUtils.rm_f(sanity_done)
end
end
end
def post_process(meeting_id)
Dir.glob("post_process/*.rb").sort.each do |post_process_script|
match = /([^\/]*).rb$/.match(post_process_script)
post_type = match[1]
BigBlueButton.logger.info("Running post process script #{post_type}")
BigBlueButton.redis_publisher.put_post_process_started post_type, meeting_id
step_start_time = BigBlueButton.monotonic_clock
ret = BigBlueButton.exec_ret("ruby", post_process_script, "-m", meeting_id)
step_stop_time = BigBlueButton.monotonic_clock
step_time = step_stop_time - step_start_time
step_succeeded = (ret == 0)
BigBlueButton.redis_publisher.put_post_process_ended post_type, meeting_id, {
"success" => step_succeeded,
"step_time" => step_time
}
if not step_succeeded
BigBlueButton.logger.warn("Post process script #{post_process_script} failed")
end
end
end
begin
props = YAML::load(File.open('bigbluebutton.yml'))
redis_host = props['redis_host']
redis_port = props['redis_port']
BigBlueButton.redis_publisher = BigBlueButton::RedisWrapper.new(redis_host, redis_port)
log_dir = props['log_dir']
recording_dir = props['recording_dir']
logger = Logger.new("#{log_dir}/bbb-rap-worker.log")
logger.level = Logger::INFO
BigBlueButton.logger = logger
BigBlueButton.logger.debug("Running rap-process-worker...")
process_archived_meetings(recording_dir)
BigBlueButton.logger.debug("rap-process-worker done")
rescue Exception => e
BigBlueButton.logger.error(e.message)
e.backtrace.each do |traceline|
BigBlueButton.logger.error(traceline)
end
end

View File

@ -0,0 +1,136 @@
#!/usr/bin/ruby
# encoding: UTF-8
# Copyright ⓒ 2017 BigBlueButton Inc. and by respective authors.
#
# This file is part of BigBlueButton open source conferencing system.
#
# BigBlueButton 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 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/>.
require '../lib/recordandplayback'
require 'rubygems'
require 'yaml'
require 'fileutils'
def publish_processed_meetings(recording_dir)
processed_done_files = Dir.glob("#{recording_dir}/status/processed/*.done")
FileUtils.mkdir_p("#{recording_dir}/status/published")
processed_done_files.each do |processed_done|
match = /([^\/]*)-([^\/-]*).done$/.match(processed_done)
meeting_id = match[1]
publish_type = match[2]
step_succeeded = false
published_done = "#{recording_dir}/status/published/#{meeting_id}-#{publish_type}.done"
next if File.exists?(published_done)
published_fail = "#{recording_dir}/status/published/#{meeting_id}-#{publish_type}.fail"
next if File.exists?(published_fail)
publish_script = "publish/#{publish_type}.rb"
if File.exists?(publish_script)
BigBlueButton.redis_publisher.put_publish_started(publish_type, meeting_id)
# If the publish directory exists, the script does nothing
FileUtils.rm_rf("#{recording_dir}/publish/#{publish_type}/#{meeting_id}")
step_start_time = BigBlueButton.monotonic_clock
# For legacy reasons, the meeting ID passed to the publish script contains
# the playback format name.
ret = BigBlueButton.exec_ret("ruby", publish_script, "-m", "#{meeting_id}-#{publish_type}")
step_stop_time = BigBlueButton.monotonic_clock
step_time = step_stop_time - step_start_time
step_succeeded = (ret == 0 and File.exists?(published_done))
BigBlueButton.redis_publisher.put_publish_ended(publish_type, meeting_id, {
"success" => step_succeeded,
"step_time" => step_time
})
else
BigBlueButton.logger.warn("Processed recording found for type #{publish_type}, but no publish script exists")
step_succeeded = true
end
if step_succeeded
BigBlueButton.logger.info("Publish format #{publish_type} succeeded for #{meeting_id}")
FileUtils.rm_f(processed_done)
FileUtils.rm_rf("#{recording_dir}/process/#{publish_type}/#{meeting_id}")
FileUtils.rm_rf("#{recording_dir}/publish/#{publish_type}/#{meeting_id}")
# Check if this is the last format to be published
if Dir.glob("#{recording_dir}/status/processed/#{meeting_id}-*.done").length == 0
post_publish(meeting_id)
end
else
BigBlueButton.logger.info("Publish format #{publish_type} failed for #{meeting_id}")
FileUtils.touch(published_fail)
end
end
end
def post_publish(meeting_id)
Dir.glob("post_publish/*.rb").sort.each do |post_publish_script|
match = /([^\/]*).rb$/.match(post_publish_script)
post_type = match[1]
BigBlueButton.logger.info("Running post publish script #{post_type}")
BigBlueButton.redis_publisher.put_post_publish_started post_type, meeting_id
step_start_time = BigBlueButton.monotonic_clock
ret = BigBlueButton.exec_ret("ruby", post_publish_script, "-m", meeting_id)
step_stop_time = BigBlueButton.monotonic_clock
step_time = step_stop_time - step_start_time
step_succeeded = (ret == 0)
BigBlueButton.redis_publisher.put_post_publish_ended post_type, meeting_id, {
"success" => step_succeeded,
"step_time" => step_time
}
if not step_succeeded
BigBlueButton.logger.warn("Post publish script #{post_publish_script} failed")
end
end
end
begin
props = YAML::load(File.open('bigbluebutton.yml'))
redis_host = props['redis_host']
redis_port = props['redis_port']
BigBlueButton.redis_publisher = BigBlueButton::RedisWrapper.new(redis_host, redis_port)
log_dir = props['log_dir']
recording_dir = props['recording_dir']
logger = Logger.new("#{log_dir}/bbb-rap-worker.log")
logger.level = Logger::INFO
BigBlueButton.logger = logger
BigBlueButton.logger.debug("Running rap-publish-worker...")
publish_processed_meetings(recording_dir)
BigBlueButton.logger.debug("rap-publish-worker done")
rescue Exception => e
BigBlueButton.logger.error(e.message)
e.backtrace.each do |traceline|
BigBlueButton.logger.error(traceline)
end
end

View File

@ -0,0 +1,114 @@
#!/usr/bin/ruby
# encoding: UTF-8
# Copyright ⓒ 2017 BigBlueButton Inc. and by respective authors.
#
# This file is part of BigBlueButton open source conferencing system.
#
# BigBlueButton 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 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/>.
require '../lib/recordandplayback'
require 'rubygems'
require 'yaml'
require 'fileutils'
def sanity_archived_meetings(recording_dir)
archived_done_files = Dir.glob("#{recording_dir}/status/archived/*.done")
FileUtils.mkdir_p("#{recording_dir}/status/sanity")
archived_done_files.each do |archived_done|
match = /([^\/]*).done$/.match(archived_done)
meeting_id = match[1]
sanity_done = "#{recording_dir}/status/sanity/#{meeting_id}.done"
next if File.exists?(sanity_done)
sanity_fail = "#{recording_dir}/status/sanity/#{meeting_id}.fail"
next if File.exists?(sanity_fail)
BigBlueButton.redis_publisher.put_sanity_started(meeting_id)
step_start_time = BigBlueButton.monotonic_clock
ret = BigBlueButton.exec_ret("ruby", "sanity/sanity.rb", "-m", meeting_id)
step_stop_time = BigBlueButton.monotonic_clock
step_time = step_stop_time - step_start_time
step_succeeded = (ret == 0 && File.exists?(sanity_done))
BigBlueButton.redis_publisher.put_sanity_ended(meeting_id, {
"success" => step_succeeded,
"step_time" => step_time
})
if step_succeeded
BigBlueButton.logger.info("Successfully sanity checked #{meeting_id}")
post_archive(meeting_id)
FileUtils.rm_f(archived_done)
else
BigBlueButton.logger.error("Sanity check failed on #{meeting_id}")
FileUtils.touch(sanity_fail)
end
end
end
def post_archive(meeting_id)
Dir.glob("post_archive/*.rb").sort.each do |post_archive_script|
match = /([^\/]*).rb$/.match(post_archive_script)
post_type = match[1]
BigBlueButton.logger.info("Running post archive script #{post_type}")
BigBlueButton.redis_publisher.put_post_archive_started(post_type, meeting_id)
step_start_time = BigBlueButton.monotonic_clock
ret = BigBlueButton.exec_ret("ruby", post_archive_script, "-m", meeting_id)
step_stop_time = BigBlueButton.monotonic_clock
step_time = step_stop_time - step_start_time
step_succeeded = (ret == 0)
BigBlueButton.redis_publisher.put_post_archive_ended(post_type, meeting_id, {
"success" => step_succeeded,
"step_time" => step_time
})
if not step_succeeded
BigBlueButton.logger.warn("Post archive script #{post_archive_script} failed")
end
end
end
begin
props = YAML::load(File.open('bigbluebutton.yml'))
redis_host = props['redis_host']
redis_port = props['redis_port']
BigBlueButton.redis_publisher = BigBlueButton::RedisWrapper.new(redis_host, redis_port)
log_dir = props['log_dir']
recording_dir = props['recording_dir']
logger = Logger.new("#{log_dir}/bbb-rap-worker.log")
logger.level = Logger::INFO
BigBlueButton.logger = logger
BigBlueButton.logger.debug("Running rap-sanity-worker...")
sanity_archived_meetings(recording_dir)
BigBlueButton.logger.debug("rap-sanity-worker done")
rescue Exception => e
BigBlueButton.logger.error(e.message)
e.backtrace.each do |traceline|
BigBlueButton.logger.error(traceline)
end
end

View File

@ -1,362 +0,0 @@
#!/usr/bin/ruby
# encoding: UTF-8
# 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/>.
# Monit reduces the path, but we require tools that are often manually installed
# to /usr/local/bin. Add that to the path.
ENV['PATH'] += ':/usr/local/bin'
require '../lib/recordandplayback'
require 'rubygems'
require 'yaml'
require 'fileutils'
# Number of seconds to delay archiving (red5 race condition workaround)
ARCHIVE_DELAY_SECONDS = 120
def archive_recorded_meeting(recording_dir)
recorded_done_files = Dir.glob("#{recording_dir}/status/recorded/*.done")
FileUtils.mkdir_p("#{recording_dir}/status/archived")
recorded_done_files.each do |recorded_done|
match = /([^\/]*).done$/.match(recorded_done)
meeting_id = match[1]
if File.mtime(recorded_done) + ARCHIVE_DELAY_SECONDS > Time.now
BigBlueButton.logger.info("Temporarily skipping #{meeting_id} for Red5 race workaround")
next
end
archived_done = "#{recording_dir}/status/archived/#{meeting_id}.done"
next if File.exists?(archived_done)
archived_norecord = "#{recording_dir}/status/archived/#{meeting_id}.norecord"
next if File.exists?(archived_norecord)
archived_fail = "#{recording_dir}/status/archived/#{meeting_id}.fail"
next if File.exists?(archived_fail)
BigBlueButton.redis_publisher.put_archive_started meeting_id
step_start_time = BigBlueButton.monotonic_clock
ret = BigBlueButton.exec_ret("ruby", "archive/archive.rb", "-m", meeting_id)
step_stop_time = BigBlueButton.monotonic_clock
step_time = step_stop_time - step_start_time
step_succeeded = (ret == 0 && (File.exists?(archived_done) || File.exists?(archived_norecord)))
BigBlueButton.redis_publisher.put_archive_ended meeting_id, {
"success" => step_succeeded,
"step_time" => step_time
}
if step_succeeded
BigBlueButton.logger.info("Successfully archived #{meeting_id}")
FileUtils.rm(recorded_done)
else
BigBlueButton.logger.error("Failed to archive #{meeting_id}")
FileUtils.touch(archived_fail)
end
end
end
def sanity_archived_meeting(recording_dir)
archived_done_files = Dir.glob("#{recording_dir}/status/archived/*.done")
FileUtils.mkdir_p("#{recording_dir}/status/sanity")
archived_done_files.each do |archived_done|
match = /([^\/]*).done$/.match(archived_done)
meeting_id = match[1]
sanity_done = "#{recording_dir}/status/sanity/#{meeting_id}.done"
next if File.exists?(sanity_done)
sanity_fail = "#{recording_dir}/status/sanity/#{meeting_id}.fail"
next if File.exists?(sanity_fail)
BigBlueButton.redis_publisher.put_sanity_started meeting_id
step_start_time = BigBlueButton.monotonic_clock
ret = BigBlueButton.exec_ret("ruby", "sanity/sanity.rb", "-m", meeting_id)
step_stop_time = BigBlueButton.monotonic_clock
step_time = step_stop_time - step_start_time
step_succeeded = (ret == 0 && File.exists?(sanity_done))
BigBlueButton.redis_publisher.put_sanity_ended meeting_id, {
"success" => step_succeeded,
"step_time" => step_time
}
if step_succeeded
BigBlueButton.logger.info("Successfully sanity checked #{meeting_id}")
post_archive(meeting_id)
FileUtils.rm(archived_done)
else
BigBlueButton.logger.error("Sanity check failed on #{meeting_id}")
FileUtils.touch(sanity_fail)
end
end
end
def process_archived_meeting(recording_dir)
sanity_done_files = Dir.glob("#{recording_dir}/status/sanity/*.done")
FileUtils.mkdir_p("#{recording_dir}/status/processed")
sanity_done_files.each do |sanity_done|
match = /([^\/]*).done$/.match(sanity_done)
meeting_id = match[1]
step_succeeded = true
# Iterate over the list of recording processing scripts to find available types
# For now, we look for the ".rb" extension - TODO other scripting languages?
Dir.glob("process/*.rb").sort.each do |process_script|
match2 = /([^\/]*).rb$/.match(process_script)
process_type = match2[1]
processed_done = "#{recording_dir}/status/processed/#{meeting_id}-#{process_type}.done"
next if File.exists?(processed_done)
processed_fail = "#{recording_dir}/status/processed/#{meeting_id}-#{process_type}.fail"
if File.exists?(processed_fail)
step_succeeded = false
next
end
BigBlueButton.redis_publisher.put_process_started process_type, meeting_id
# If the process directory doesn't exist, the script does nothing
FileUtils.rm_rf("#{recording_dir}/process/#{process_type}/#{meeting_id}")
step_start_time = BigBlueButton.monotonic_clock
ret = BigBlueButton.exec_ret("ruby", process_script, "-m", meeting_id)
step_stop_time = BigBlueButton.monotonic_clock
step_time = step_stop_time - step_start_time
IO.write("#{recording_dir}/process/#{process_type}/#{meeting_id}/processing_time", step_time)
step_succeeded = (ret == 0 and File.exists?(processed_done))
BigBlueButton.redis_publisher.put_process_ended process_type, meeting_id, {
"success" => step_succeeded,
"step_time" => step_time
}
if step_succeeded
BigBlueButton.logger.info("Process format #{process_type} succeeded for #{meeting_id}")
BigBlueButton.logger.info("Process took #{step_time}ms")
else
BigBlueButton.logger.info("Process format #{process_type} failed for #{meeting_id}")
BigBlueButton.logger.info("Process took #{step_time}ms")
FileUtils.touch(processed_fail)
step_succeeded = false
end
end
if step_succeeded
post_process(meeting_id)
FileUtils.rm(sanity_done)
end
end
end
def publish_processed_meeting(recording_dir)
processed_done_files = Dir.glob("#{recording_dir}/status/processed/*.done")
FileUtils.mkdir_p("#{recording_dir}/status/published")
processed_done_files.each do |processed_done|
match = /([^\/]*)-([^\/-]*).done$/.match(processed_done)
meeting_id = match[1]
process_type = match[2]
# Since we publish all formats for a recording at once, we want to skip duplicates
# if there were multiple publish format done files for the same recording
if !File.exists?(processed_done)
BigBlueButton.logger.info("Processed done file for #{meeting_id} #{process_type} gone, assuming already processed")
next
end
publish_succeeded = true
Dir.glob("publish/*.rb").sort.each do |publish_script|
match2 = /([^\/]*).rb$/.match(publish_script)
publish_type = match2[1]
published_done = "#{recording_dir}/status/published/#{meeting_id}-#{publish_type}.done"
next if File.exists?(published_done)
published_fail = "#{recording_dir}/status/published/#{meeting_id}-#{publish_type}.fail"
if File.exists?(published_fail)
publish_succeeded = false
next
end
BigBlueButton.redis_publisher.put_publish_started publish_type, meeting_id
# If the publish directory doesn't exist, the script does nothing
FileUtils.rm_rf("#{recording_dir}/publish/#{publish_type}/#{meeting_id}")
step_start_time = BigBlueButton.monotonic_clock
# For legacy reasons, the meeting ID passed to the publish script contains
# the playback format name.
ret = BigBlueButton.exec_ret("ruby", publish_script, "-m", "#{meeting_id}-#{publish_type}")
step_stop_time = BigBlueButton.monotonic_clock
step_time = step_stop_time - step_start_time
step_succeeded = (ret == 0 and File.exists?(published_done))
BigBlueButton.redis_publisher.put_publish_ended publish_type, meeting_id, {
"success" => step_succeeded,
"step_time" => step_time
}
if step_succeeded
BigBlueButton.logger.info("Publish format #{publish_type} succeeded for #{meeting_id}")
else
BigBlueButton.logger.info("Publish format #{publish_type} failed for #{meeting_id}")
FileUtils.touch(published_fail)
publish_succeeded = false
end
end
if publish_succeeded
post_publish(meeting_id)
# Clean up the process done files
# Also clean up the publish and process work files
Dir.glob("process/*.rb").sort.each do |process_script|
match2 = /([^\/]*).rb$/.match(process_script)
process_type = match2[1]
this_processed_done = "#{recording_dir}/status/processed/#{meeting_id}-#{process_type}.done"
FileUtils.rm(this_processed_done)
FileUtils.rm_rf("#{recording_dir}/process/#{process_type}/#{meeting_id}")
end
Dir.glob("publish/*.rb").sort.each do |publish_script|
match2 = /([^\/]*).rb$/.match(publish_script)
publish_type = match2[1]
FileUtils.rm_rf("#{recording_dir}/publish/#{publish_type}/#{meeting_id}")
end
end
end
end
def post_archive(meeting_id)
Dir.glob("post_archive/*.rb").sort.each do |post_archive_script|
match = /([^\/]*).rb$/.match(post_archive_script)
post_type = match[1]
BigBlueButton.logger.info("Running post archive script #{post_type}")
BigBlueButton.redis_publisher.put_post_archive_started post_type, meeting_id
step_start_time = BigBlueButton.monotonic_clock
ret = BigBlueButton.exec_ret("ruby", post_archive_script, "-m", meeting_id)
step_stop_time = BigBlueButton.monotonic_clock
step_time = step_stop_time - step_start_time
step_succeeded = (ret == 0)
BigBlueButton.redis_publisher.put_post_archive_ended post_type, meeting_id, {
"success" => step_succeeded,
"step_time" => step_time
}
if not step_succeeded
BigBlueButton.logger.warn("Post archive script #{post_archive_script} failed")
end
end
end
def post_process(meeting_id)
Dir.glob("post_process/*.rb").sort.each do |post_process_script|
match = /([^\/]*).rb$/.match(post_process_script)
post_type = match[1]
BigBlueButton.logger.info("Running post process script #{post_type}")
BigBlueButton.redis_publisher.put_post_process_started post_type, meeting_id
step_start_time = BigBlueButton.monotonic_clock
ret = BigBlueButton.exec_ret("ruby", post_process_script, "-m", meeting_id)
step_stop_time = BigBlueButton.monotonic_clock
step_time = step_stop_time - step_start_time
step_succeeded = (ret == 0)
BigBlueButton.redis_publisher.put_post_process_ended post_type, meeting_id, {
"success" => step_succeeded,
"step_time" => step_time
}
if not step_succeeded
BigBlueButton.logger.warn("Post process script #{post_process_script} failed")
end
end
end
def post_publish(meeting_id)
Dir.glob("post_publish/*.rb").sort.each do |post_publish_script|
match = /([^\/]*).rb$/.match(post_publish_script)
post_type = match[1]
BigBlueButton.logger.info("Running post publish script #{post_type}")
BigBlueButton.redis_publisher.put_post_publish_started post_type, meeting_id
step_start_time = BigBlueButton.monotonic_clock
ret = BigBlueButton.exec_ret("ruby", post_publish_script, "-m", meeting_id)
step_stop_time = BigBlueButton.monotonic_clock
step_time = step_stop_time - step_start_time
step_succeeded = (ret == 0)
BigBlueButton.redis_publisher.put_post_publish_ended post_type, meeting_id, {
"success" => step_succeeded,
"step_time" => step_time
}
if not step_succeeded
BigBlueButton.logger.warn("Post publish script #{post_publish_script} failed")
end
end
end
begin
props = YAML::load(File.open('bigbluebutton.yml'))
redis_host = props['redis_host']
redis_port = props['redis_port']
BigBlueButton.redis_publisher = BigBlueButton::RedisWrapper.new(redis_host, redis_port)
log_dir = props['log_dir']
recording_dir = props['recording_dir']
logger = Logger.new("#{log_dir}/bbb-rap-worker.log",'daily' )
logger.level = Logger::INFO
BigBlueButton.logger = logger
BigBlueButton.logger.debug("Running rap-worker...")
archive_recorded_meeting(recording_dir)
sanity_archived_meeting(recording_dir)
process_archived_meeting(recording_dir)
publish_processed_meeting(recording_dir)
BigBlueButton.logger.debug("rap-worker done")
rescue Exception => e
BigBlueButton.logger.error(e.message)
e.backtrace.each do |traceline|
BigBlueButton.logger.error(traceline)
end
end

View File

@ -0,0 +1,10 @@
[Unit]
Description=BigBlueButton recording and playback archive worker
ConditionPathExistsGlob=/var/bigbluebutton/recording/status/recorded/*.done
[Service]
Type=simple
ExecStart=/usr/local/bigbluebutton/core/scripts/rap-archive-worker.rb
WorkingDirectory=/usr/local/bigbluebutton/core/scripts
User=tomcat7
Slice=bbb_record_core.slice

View File

@ -0,0 +1,10 @@
[Unit]
Description=BigBlueButton recording and playback process worker
ConditionPathExistsGlob=/var/bigbluebutton/recording/status/sanity/*.done
[Service]
Type=simple
ExecStart=/usr/local/bigbluebutton/core/scripts/rap-process-worker.rb
WorkingDirectory=/usr/local/bigbluebutton/core/scripts
User=tomcat7
Slice=bbb_record_core.slice

View File

@ -0,0 +1,10 @@
[Unit]
Description=BigBlueButton recording and playback publish worker
ConditionPathExistsGlob=/var/bigbluebutton/recording/status/processed/*.done
[Service]
Type=simple
ExecStart=/usr/local/bigbluebutton/core/scripts/rap-publish-worker.rb
WorkingDirectory=/usr/local/bigbluebutton/core/scripts
User=tomcat7
Slice=bbb_record_core.slice

View File

@ -0,0 +1,10 @@
[Unit]
Description=BigBlueButton recording and playback sanity check worker
ConditionPathExistsGlob=/var/bigbluebutton/recording/status/archived/*.done
[Service]
Type=simple
ExecStart=/usr/local/bigbluebutton/core/scripts/rap-sanity-worker.rb
WorkingDirectory=/usr/local/bigbluebutton/core/scripts
User=tomcat7
Slice=bbb_record_core.slice

View File

@ -0,0 +1,4 @@
[Unit]
Description=BigBlueButton recording & playback processing
Wants=bbb-rap-archive-worker.service bbb-rap-sanity-worker.service bbb-rap-process-worker.service bbb-rap-publish-worker.service
StopWhenUnneeded=true

View File

@ -0,0 +1,10 @@
[Unit]
Description=BigBlueButton record & playback processing timer
[Timer]
OnActiveSec=0
OnUnitInactiveSec=30s
Unit=bbb-record-core.target
[Install]
WantedBy=timers.target

View File

@ -0,0 +1,2 @@
[Unit]
Description = BigBlueButton Recording Processing