fix merge conflicts
This commit is contained in:
commit
eb3ac219e9
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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,8 +567,21 @@ 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();
|
||||
sort.fields = [new SortField("sequence", true, false, true)];
|
||||
|
@ -68,6 +68,8 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
|
||||
<mate:Listener type="{LockControlEvent.OPEN_LOCK_SETTINGS}" method="openLockSettingsWindow" />
|
||||
<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[
|
||||
@ -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>
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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 {
|
||||
|
||||
@ -282,47 +282,48 @@ 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 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();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
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;
|
||||
|
||||
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 + "]");
|
||||
}
|
||||
|
||||
return _currentLayout;
|
||||
}
|
||||
}
|
||||
}
|
@ -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{
|
||||
|
@ -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) {
|
||||
|
@ -7,21 +7,26 @@
|
||||
<mx:Script>
|
||||
<![CDATA[
|
||||
import com.asfusion.mate.events.Dispatcher;
|
||||
|
||||
|
||||
import mx.events.FlexEvent;
|
||||
|
||||
|
||||
import org.bigbluebutton.common.Images;
|
||||
import org.bigbluebutton.core.managers.UserManager;
|
||||
import org.bigbluebutton.main.events.BreakoutRoomEvent;
|
||||
import org.bigbluebutton.main.model.users.BreakoutRoom;
|
||||
import org.bigbluebutton.util.i18n.ResourceUtil;
|
||||
|
||||
|
||||
private var globalDispatch:Dispatcher = new Dispatcher();
|
||||
|
||||
[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 && 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 && moderator) || (!moderator && 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 && data.listenStatus != BreakoutRoom.OTHER && UserManager.getInstance().getConference().voiceJoined || data.listenStatus == BreakoutRoom.SELF}"
|
||||
icon="{images.transfer}"
|
||||
toolTip="{ResourceUtil.getInstance().getString('bbb.users.roomsGrid.transfer')}"
|
||||
click="listenToBreakoutRoom(event)" />
|
||||
</mx:HBox>
|
||||
|
@ -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 && amIModerator}"
|
||||
includeInLayout="{breakoutRoomsList.length > 0 && 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>
|
||||
|
@ -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>
|
@ -33,7 +33,8 @@ 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[
|
||||
import com.asfusion.mate.events.Dispatcher;
|
||||
@ -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();
|
||||
|
@ -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 />,
|
||||
|
@ -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}> </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>
|
||||
|
@ -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'}
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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}
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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
|
||||
* ==========
|
||||
|
@ -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 = {
|
||||
};
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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',
|
||||
|
@ -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",
|
||||
|
@ -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=""
|
||||
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=""
|
||||
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="" 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=""
|
||||
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=""
|
||||
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=""
|
||||
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=""
|
||||
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=""
|
||||
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=""
|
||||
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 31 KiB |
Binary file not shown.
Binary file not shown.
0
record-and-playback/core/scripts/README
Executable file → Normal file
0
record-and-playback/core/scripts/README
Executable file → Normal file
0
record-and-playback/core/scripts/bigbluebutton.yml
Executable file → Normal file
0
record-and-playback/core/scripts/bigbluebutton.yml
Executable file → Normal file
101
record-and-playback/core/scripts/rap-archive-worker.rb
Executable file
101
record-and-playback/core/scripts/rap-archive-worker.rb
Executable 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
|
137
record-and-playback/core/scripts/rap-process-worker.rb
Executable file
137
record-and-playback/core/scripts/rap-process-worker.rb
Executable 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
|
136
record-and-playback/core/scripts/rap-publish-worker.rb
Executable file
136
record-and-playback/core/scripts/rap-publish-worker.rb
Executable 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
|
||||
|
114
record-and-playback/core/scripts/rap-sanity-worker.rb
Executable file
114
record-and-playback/core/scripts/rap-sanity-worker.rb
Executable 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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
4
record-and-playback/core/systemd/bbb-record-core.target
Normal file
4
record-and-playback/core/systemd/bbb-record-core.target
Normal 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
|
10
record-and-playback/core/systemd/bbb-record-core.timer
Normal file
10
record-and-playback/core/systemd/bbb-record-core.timer
Normal 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
|
2
record-and-playback/core/systemd/bbb_record_core.slice
Normal file
2
record-and-playback/core/systemd/bbb_record_core.slice
Normal file
@ -0,0 +1,2 @@
|
||||
[Unit]
|
||||
Description = BigBlueButton Recording Processing
|
Loading…
Reference in New Issue
Block a user