Merge branch 'master' of https://github.com/bigbluebutton/bigbluebutton into fixdisplay

This commit is contained in:
bobakoftadeh 2019-01-30 22:28:29 +00:00
commit 106578f9dd
200 changed files with 14214 additions and 9437 deletions

View File

@ -44,7 +44,8 @@ class RunningMeeting(val props: DefaultProps, outGW: OutMessageGateway,
liveMeeting.guestsWaiting,
GuestPolicy(props.usersProp.guestPolicy, SystemUser.ID))
val outMsgRouter = new OutMsgRouter(props.recordProp.record, outGW)
private val recordEvents = props.recordProp.record || props.recordProp.keepEvents
val outMsgRouter = new OutMsgRouter(recordEvents, outGW)
val actorRef = context.actorOf(MeetingActor.props(props, eventBus, outMsgRouter, liveMeeting), props.meetingProp.intId)

View File

@ -13,7 +13,7 @@ case class BreakoutProps(parentId: String, sequence: Int, freeJoin: Boolean, bre
case class PasswordProp(moderatorPass: String, viewerPass: String)
case class RecordProp(record: Boolean, autoStartRecording: Boolean, allowStartStopRecording: Boolean)
case class RecordProp(record: Boolean, autoStartRecording: Boolean, allowStartStopRecording: Boolean, keepEvents: Boolean)
case class WelcomeProp(welcomeMsgTemplate: String, welcomeMsg: String, modOnlyMessage: String)

View File

@ -34,6 +34,7 @@ trait TestFixtures {
val dialNumber = "613-555-1234"
val maxUsers = 25
val muteOnStart = false
val keepEvents = false
val guestPolicy = "ALWAYS_ASK"
val metadata: collection.immutable.Map[String, String] = Map("foo" -> "bar", "bar" -> "baz", "baz" -> "foo")
@ -46,7 +47,7 @@ trait TestFixtures {
userInactivityInspectTimerInMinutes = userInactivityInspectTimerInMinutes, userInactivityThresholdInMinutes = userInactivityInspectTimerInMinutes, userActivitySignResponseDelayInMinutes = userActivitySignResponseDelayInMinutes)
val password = PasswordProp(moderatorPass = moderatorPassword, viewerPass = viewerPassword)
val recordProp = RecordProp(record = record, autoStartRecording = autoStartRecording,
allowStartStopRecording = allowStartStopRecording)
allowStartStopRecording = allowStartStopRecording, keepEvents = keepEvents)
val welcomeProp = WelcomeProp(welcomeMsgTemplate = welcomeMsgTemplate, welcomeMsg = welcomeMsg,
modOnlyMessage = modOnlyMessage)
val voiceProp = VoiceProp(telVoice = voiceConfId, voiceConf = voiceConfId, dialNumber = dialNumber, muteOnStart = muteOnStart)

View File

@ -110,6 +110,7 @@ public class MeetingService implements MessageListener {
private StunTurnService stunTurnService;
private RedisStorageService storeService;
private CallbackUrlService callbackUrlService;
private boolean keepEvents;
private ParamsProcessorUtil paramsProcessorUtil;
private PresentationUrlDownloadService presDownloadService;
@ -252,16 +253,20 @@ public class MeetingService implements MessageListener {
return false;
}
private boolean storeEvents(Meeting m) {
return m.isRecord() || keepEvents;
}
private void handleCreateMeeting(Meeting m) {
if (m.isBreakout()) {
Meeting parent = meetings.get(m.getParentMeetingId());
parent.addBreakoutRoom(m.getExternalId());
if (parent.isRecord()) {
if (storeEvents(parent)) {
storeService.addBreakoutRoom(parent.getInternalId(), m.getInternalId());
}
}
if (m.isRecord()) {
if (storeEvents(m)) {
Map<String, String> metadata = new TreeMap<>();
metadata.putAll(m.getMetadata());
// TODO: Need a better way to store these values for recordings
@ -310,7 +315,7 @@ public class MeetingService implements MessageListener {
m.getDialNumber(), m.getMaxUsers(), m.getMaxInactivityTimeoutMinutes(), m.getWarnMinutesBeforeMax(),
m.getMeetingExpireIfNoUserJoinedInMinutes(), m.getmeetingExpireWhenLastUserLeftInMinutes(),
m.getUserInactivityInspectTimerInMinutes(), m.getUserActivitySignResponseDelayInMinutes(),
m.getUserInactivityThresholdInMinutes(), m.getMuteOnStart());
m.getUserInactivityThresholdInMinutes(), m.getMuteOnStart(), keepEvents);
}
private String formatPrettyDate(Long timestamp) {
@ -522,6 +527,11 @@ public class MeetingService implements MessageListener {
if (m != null) {
m.setForciblyEnded(true);
processRecording(m);
if (keepEvents) {
// The creation of the ended tag must occur after the creation of the
// recorded tag to avoid concurrency issues at the recording scripts
recordingService.markAsEnded(m.getInternalId());
}
destroyMeeting(m.getInternalId());
meetings.remove(m.getInternalId());
removeUserSessions(m.getInternalId());
@ -985,4 +995,8 @@ public class MeetingService implements MessageListener {
public void setStunTurnService(StunTurnService s) {
stunTurnService = s;
}
public void setKeepEvents(boolean value) {
keepEvents = value;
}
}

View File

@ -126,6 +126,23 @@ public class RecordingService {
}
}
public void markAsEnded(String meetingId) {
String done = recordStatusDir + "/../ended/" + meetingId + ".done";
File doneFile = new File(done);
if (!doneFile.exists()) {
try {
doneFile.createNewFile();
if (!doneFile.exists())
log.error("Failed to create " + done + " file.");
} catch (IOException e) {
log.error("Exception occured when trying to create {} file.", done);
}
} else {
log.error(done + " file already exists.");
}
}
public List<RecordingMetadata> getRecordingsMetadata(List<String> recordIDs, List<String> states) {
List<RecordingMetadata> recs = new ArrayList<>();

View File

@ -25,7 +25,8 @@ public interface IBbbWebApiGWApp {
Integer userInactivityInspectTimerInMinutes,
Integer userInactivityThresholdInMinutes,
Integer userActivitySignResponseDelayInMinutes,
Boolean muteOnStart);
Boolean muteOnStart,
Boolean keepEvents);
void registerUser(String meetingID, String internalUserId, String fullname, String role,
String externUserID, String authToken, String avatarURL,

View File

@ -91,7 +91,8 @@ class BbbWebApiGWApp(
userInactivityInspectTimerInMinutes: java.lang.Integer,
userInactivityThresholdInMinutes: java.lang.Integer,
userActivitySignResponseDelayInMinutes: java.lang.Integer,
muteOnStart: java.lang.Boolean): Unit = {
muteOnStart: java.lang.Boolean,
keepEvents: java.lang.Boolean): Unit = {
val meetingProp = MeetingProp(name = meetingName, extId = extMeetingId, intId = meetingId,
isBreakout = isBreakout.booleanValue())
@ -108,7 +109,7 @@ class BbbWebApiGWApp(
val password = PasswordProp(moderatorPass = moderatorPass, viewerPass = viewerPass)
val recordProp = RecordProp(record = recorded.booleanValue(), autoStartRecording = autoStartRecording.booleanValue(),
allowStartStopRecording = allowStartStopRecording.booleanValue())
allowStartStopRecording = allowStartStopRecording.booleanValue(), keepEvents = keepEvents.booleanValue())
val breakoutProps = BreakoutProps(parentId = parentMeetingId, sequence = sequence.intValue(), freeJoin = freeJoin.booleanValue(), breakoutRooms = Vector())
val welcomeProp = WelcomeProp(welcomeMsgTemplate = welcomeMsgTemplate, welcomeMsg = welcomeMsg,
modOnlyMessage = modOnlyMessage)

0
bbb-fsesl-client/deploy.sh Normal file → Executable file
View File

View File

@ -63,7 +63,8 @@ public class CallAgent extends CallListenerAdapter implements CallStreamObserver
private String _destination;
private Boolean listeningToGlobal = false;
private IMessagingService messagingService;
private ForceHangupGlobalAudioUsersListener forceHangupGlobalAudioUsersListener;
private enum CallState {
UA_IDLE(0), UA_INCOMING_CALL(1), UA_OUTGOING_CALL(2), UA_ONCALL(3);
private final int state;
@ -404,15 +405,23 @@ public class CallAgent extends CallListenerAdapter implements CallStreamObserver
/** Callback function called when arriving a BYE request */
public void onCallClosing(Call call, Message bye) {
log.info("Received a BYE from the other end telling us to hangup.");
if (!isCurrentCall(call)) return;
closeVoiceStreams();
notifyListenersOfOnCallClosed();
callState = CallState.UA_IDLE;
log.info("Received a BYE from the other end telling us to hangup.");
// Reset local sdp for next call.
initSessionDescriptor();
if (!isCurrentCall(call)) return;
closeVoiceStreams();
notifyListenersOfOnCallClosed();
// FreeSWITCH initiated hangup of call. Hangup all listen only users.
// ralam jan 24, 2019
if (forceHangupGlobalAudioUsersListener != null) {
log.info("Forcing hangup for listen only users of of voice conf {}.", getDestination());
forceHangupGlobalAudioUsersListener.forceHangupGlobalAudioUsers(getDestination());
}
callState = CallState.UA_IDLE;
// Reset local sdp for next call.
initSessionDescriptor();
}
@ -451,7 +460,11 @@ public class CallAgent extends CallListenerAdapter implements CallStreamObserver
private boolean isCurrentCall(Call call) {
return this.call == call;
}
public void setForceHangupGlobalAudioUsersListener(ForceHangupGlobalAudioUsersListener listener) {
forceHangupGlobalAudioUsersListener = listener;
}
public void setCallStreamFactory(CallStreamFactory csf) {
this.callStreamFactory = csf;
}

View File

@ -0,0 +1,5 @@
package org.bigbluebutton.voiceconf.sip;
public interface ForceHangupGlobalAudioUsersListener {
void forceHangupGlobalAudioUsers(String voiceConf);
}

View File

@ -1,6 +1,7 @@
package org.bigbluebutton.voiceconf.sip;
import java.util.Collection;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.red5.app.sip.codecs.Codec;
@ -80,6 +81,13 @@ public class GlobalCall {
return null;
}
public static synchronized Collection<ListenOnlyUser> getAllListenOnlyUsers(String voiceConf) {
if (voiceConfToListenOnlyUsersMap.containsKey(voiceConf)) {
return voiceConfToListenOnlyUsersMap.get(voiceConf).getAllListenOnlyUsers();
}
return null;
}
public static Codec getRoomCodec(String roomName) {
return roomToCodecMap.get(roomName);
}

View File

@ -36,7 +36,7 @@ import org.red5.server.api.stream.IBroadcastStream;
* @author Richard Alam
*
*/
public class SipPeer implements SipRegisterAgentListener {
public class SipPeer implements SipRegisterAgentListener, ForceHangupGlobalAudioUsersListener {
private static Logger log = Red5LoggerFactory.getLogger(SipPeer.class, "sip");
private ClientConnectionManager clientConnManager;
@ -130,6 +130,7 @@ public class SipPeer implements SipRegisterAgentListener {
SipPeerProfile callerProfile = SipPeerProfile.copy(registeredProfile);
CallAgent ca = new CallAgent(this.clientRtpIp, sipProvider, callerProfile, audioconfProvider, clientId, messagingService);
ca.setClientConnectionManager(clientConnManager);
ca.setForceHangupGlobalAudioUsersListener(this);
ca.setCallStreamFactory(callStreamFactory);
callManager.add(ca);
@ -229,6 +230,15 @@ public class SipPeer implements SipRegisterAgentListener {
log.info("Successfully unregistered with Sip Server");
registered = false;
}
public void forceHangupGlobalAudioUsers(String voiceConf) {
Collection<ListenOnlyUser> listenOnlyUsers = GlobalCall.getAllListenOnlyUsers(voiceConf);
Iterator iter = listenOnlyUsers.iterator();
while (iter.hasNext()) {
ListenOnlyUser listenOnlyUser = (ListenOnlyUser) iter.next();
hangup(listenOnlyUser.clientId, true);
}
}
public void setCallStreamFactory(CallStreamFactory csf) {
callStreamFactory = csf;

View File

@ -1,5 +1,7 @@
package org.bigbluebutton.voiceconf.sip;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@ -23,4 +25,8 @@ public class VoiceConfToListenOnlyUsersMap {
public int numUsers() {
return listenOnlyUsers.size();
}
public Collection<ListenOnlyUser> getAllListenOnlyUsers() {
return listenOnlyUsers.values();
}
}

2
bigbluebutton-client/resources/config.xml.template Normal file → Executable file
View File

@ -11,7 +11,7 @@
localesConfig="http://HOST/client/conf/locales.xml"
localesDirectory="http://HOST/client/locale/"/>
<skinning url="http://HOST/client/branding/css/V2Theme.css.swf?v=VERSION" />
<branding logo="logos/logo.swf" copyright="&#169; 2018 &lt;u&gt;&lt;a href=&quot;http://HOST/home.html&quot; target=&quot;_blank&quot;&gt;BigBlueButton Inc.&lt;/a&gt;&lt;/u&gt; (build {0})" background="" toolbarColor="" showQuote="true"/>
<branding logo="logos/logo.swf" copyright="&#169; 2019 &lt;u&gt;&lt;a href=&quot;http://HOST/home.html&quot; target=&quot;_blank&quot;&gt;BigBlueButton Inc.&lt;/a&gt;&lt;/u&gt; (build {0})" background="" toolbarColor="" showQuote="true"/>
<shortcutKeys showButton="true" />
<browserVersions chrome="CHROME_VERSION" firefox="FIREFOX_VERSION" flash="FLASH_VERSION"/>
<layout showLogButton="false" defaultLayout="bbb.layout.name.defaultlayout"

File diff suppressed because it is too large Load Diff

View File

@ -534,12 +534,15 @@ function make_call(username, voiceBridge, server, callback, recall, isListenOnly
currentSession.mediaHandler.on('iceConnectionConnected', function() {
console.log('Received ICE status changed to connected');
if (callICEConnected === false) {
callICEConnected = true;
clearTimeout(iceConnectedTimeout);
if (callActive === true) {
callback({'status':'started'});
// Edge is only ready once the status is 'completed' so we need to skip this step
if (!bowser.msedge) {
callICEConnected = true;
clearTimeout(iceConnectedTimeout);
if (callActive === true) {
callback({'status':'started'});
}
clearTimeout(callTimeout);
}
clearTimeout(callTimeout);
}
});
@ -588,11 +591,7 @@ function releaseUserMedia() {
}
function isWebRTCAvailable() {
if (bowser.msedge) {
return false;
} else {
return SIP.WebRTC.isSupported();
}
return SIP.WebRTC.isSupported();
}
function getCallStatus() {

View File

@ -2,6 +2,8 @@ const isFirefox = typeof window.InstallTrigger !== 'undefined';
const isOpera = !!window.opera || navigator.userAgent.indexOf(' OPR/') >= 0;
const isChrome = !!window.chrome && !isOpera;
const isSafari = navigator.userAgent.indexOf('Safari') >= 0 && !isChrome;
const hasDisplayMedia = (typeof navigator.getDisplayMedia === 'function'
|| (navigator.mediaDevices && typeof navigator.mediaDevices.getDisplayMedia === 'function'));
const kurentoHandler = null;
const SEND_ROLE = "send";
const RECV_ROLE = "recv";
@ -610,56 +612,86 @@ Kurento.normalizeCallback = function (callback) {
// this function explains how to use above methods/objects
window.getScreenConstraints = function (sendSource, callback) {
const screenConstraints = { video: {}, audio: false };
let screenConstraints = { video: {}, audio: false };
// Limiting FPS to a range of 5-10 (5 ideal)
screenConstraints.video.frameRate = { ideal: 5, max: 10 };
screenConstraints.video.height = { max: kurentoManager.kurentoScreenshare.vid_max_height };
screenConstraints.video.width = { max: kurentoManager.kurentoScreenshare.vid_max_width };
const getDisplayMediaConstraints = function () {
// The fine-grained constraints (e.g.: frameRate) are supposed to go into
// the MediaStream because getDisplayMedia does not support them,
// so they're passed differently
kurentoManager.kurentoScreenshare.extensionInstalled = true;
optionalConstraints.width = { max: kurentoManager.kurentoScreenshare.vid_max_width };
optionalConstraints.height = { max: kurentoManager.kurentoScreenshare.vid_max_height };
optionalConstraints.frameRate = { ideal: 5, max: 10 };
let gDPConstraints = {
video: true,
optional: optionalConstraints
}
return gDPConstraints;
};
const optionalConstraints = [
{ googCpuOveruseDetection: true },
{ googCpuOveruseEncodeUsage: true },
{ googCpuUnderuseThreshold: 55 },
{ googCpuOveruseThreshold: 100 },
{ googPayloadPadding: true },
{ googScreencastMinBitrate: 600 },
{ googHighStartBitrate: true },
{ googHighBitrate: true },
{ googVeryHighBitrate: true },
];
if (isChrome) {
getChromeScreenConstraints((constraints) => {
if (!constraints) {
document.dispatchEvent(new Event('installChromeExtension'));
return;
}
if (!hasDisplayMedia) {
getChromeScreenConstraints((constraints) => {
if (!constraints) {
document.dispatchEvent(new Event('installChromeExtension'));
return;
}
const sourceId = constraints.streamId;
const sourceId = constraints.streamId;
kurentoManager.kurentoScreenshare.extensionInstalled = true;
kurentoManager.kurentoScreenshare.extensionInstalled = true;
// this statement sets gets 'sourceId" and sets "chromeMediaSourceId"
screenConstraints.video.chromeMediaSource = { exact: [sendSource] };
screenConstraints.video.chromeMediaSourceId = sourceId;
screenConstraints.optional = [
{ googCpuOveruseDetection: true },
{ googCpuOveruseEncodeUsage: true },
{ googCpuUnderuseThreshold: 55 },
{ googCpuOveruseThreshold: 100},
{ googPayloadPadding: true },
{ googScreencastMinBitrate: 600 },
{ googHighStartBitrate: true },
{ googHighBitrate: true },
{ googVeryHighBitrate: true }
];
// Re-wrap the video constraints into the mandatory object (latest adapter)
screenConstraints.video = {}
screenConstraints.video.mandatory = {};
screenConstraints.video.mandatory.maxFrameRate = 10;
screenConstraints.video.mandatory.maxHeight = kurentoManager.kurentoScreenshare.vid_max_height;
screenConstraints.video.mandatory.maxWidth = kurentoManager.kurentoScreenshare.vid_max_width;
screenConstraints.video.mandatory.chromeMediaSource = sendSource;
screenConstraints.video.mandatory.chromeMediaSourceId = sourceId;
screenConstraints.optional = optionalConstraints;
console.log('getScreenConstraints for Chrome returns => ', screenConstraints);
// now invoking native getUserMedia API
callback(null, screenConstraints);
}, chromeExtension);
console.log('getScreenConstraints for Chrome returns => ', screenConstraints);
// now invoking native getUserMedia API
callback(null, screenConstraints);
}, chromeExtension);
} else {
return callback(null, getDisplayMediaConstraints());
}
} else if (isFirefox) {
screenConstraints.video.mediaSource = 'window';
console.log('getScreenConstraints for Firefox returns => ', screenConstraints);
// now invoking native getUserMedia API
callback(null, screenConstraints);
} else if (isSafari) {
} else if (isSafari && !hasDisplayMedia) {
screenConstraints.video.mediaSource = 'screen';
console.log('getScreenConstraints for Safari returns => ', screenConstraints);
// now invoking native getUserMedia API
callback(null, screenConstraints);
} else if (hasDisplayMedia) {
// Falls back to getDisplayMedia if the browser supports it
return callback(null, getDisplayMediaConstraints());
}
};
@ -735,6 +767,10 @@ window.checkIfIncognito = function(isIncognito, isNotIncognito = function () {})
window.checkChromeExtInstalled = function (callback, chromeExtensionId) {
callback = Kurento.normalizeCallback(callback);
if (hasDisplayMedia) {
return callback(true);
}
if (typeof chrome === "undefined" || !chrome || !chrome.runtime) {
// No API, so no extension for sure
callback(false);

View File

@ -410,7 +410,27 @@ function WebRtcPeer(mode, options, callback) {
return callback(error);
constraints = [mediaConstraints];
constraints.unshift(constraints_);
getMedia(recursive.apply(undefined, constraints));
let gDMCallback = function(stream) {
stream.getTracks()[0].applyConstraints(constraints[0].optional)
.then(() => {
videoStream = stream;
start();
}).catch(() => {
videoStream = stream;
start();
});
}
if (typeof navigator.getDisplayMedia === 'function') {
navigator.getDisplayMedia(recursive.apply(undefined, constraints))
.then(gDMCallback)
.catch(callback);
} else if (navigator.mediaDevices && typeof navigator.mediaDevices.getDisplayMedia === 'function') {
navigator.mediaDevices.getDisplayMedia(recursive.apply(undefined, constraints))
.then(gDMCallback)
.catch(callback);
} else {
getMedia(recursive.apply(undefined, constraints));
}
}, guid);
}
} else {
@ -1045,7 +1065,7 @@ module.exports = function(stream, options) {
harker.setInterval = function(i) {
interval = i;
};
harker.stop = function() {
running = false;
harker.emit('volume_change', -100, threshold);
@ -1063,12 +1083,12 @@ module.exports = function(stream, options) {
// and emit events if changed
var looper = function() {
setTimeout(function() {
//check if stop has been called
if(!running) {
return;
}
var currentVolume = getMaxVolume(analyser, fftBins);
harker.emit('volume_change', currentVolume, threshold);
@ -4359,4 +4379,4 @@ WildEmitter.mixin = function (constructor) {
WildEmitter.mixin(WildEmitter);
},{}]},{},[2])(2)
});
});

View File

@ -110,6 +110,8 @@ package org.bigbluebutton.modules.screenshare.utils
ExternalInterface.addCallback("onSuccess", onSuccess);
// check if the extension exists
ExternalInterface.call("checkChromeExtInstalled", "onSuccess", WebRTCScreenshareUtility.chromeExtensionKey);
} else if (BrowserCheck.isEdge()) {
webRTCWorksAndConfigured("Edge, lets try");
} else {
cannotUseWebRTC("Web browser doesn't support WebRTC");
return;

View File

@ -65,6 +65,10 @@ package org.bigbluebutton.util.browser {
public static function isFirefox():Boolean {
return _browserName.toLowerCase() == "firefox";
}
public static function isEdge():Boolean {
return _browserName.toLowerCase() == "edge";
}
public static function isPuffinBelow46():Boolean {
return _browserName.toLowerCase() == "puffin" && String(_fullVersion).substr(0, 3) < "4.6";

View File

@ -72,6 +72,15 @@ if [ "$(id -u)" != "0" ]; then
fi
fi
if [ ! -f /etc/bigbluebutton/bigbluebutton-release ]; then
echo "#"
echo "# BigBlueButton does not appear to be installed. Could not"
echo "# locate:"
echo "#"
echo "# /usr/share/red5/webapps/bigbluebutton/WEB-INF/red5-web.xml"
exit 1
fi
source /etc/bigbluebutton/bigbluebutton-release
#
@ -312,8 +321,11 @@ stop_bigbluebutton () {
if [ -f /usr/lib/systemd/system/bbb-transcode-akka.service ]; then
BBB_TRANSCODE_AKKA=bbb-transcode-akka
fi
if [ -f /usr/share/etherpad-lite/settings.json ]; then
ETHERPAD=etherpad
fi
systemctl stop red5 $TOMCAT_SERVICE nginx freeswitch $REDIS_SERVICE bbb-apps-akka $BBB_TRANSCODE_AKKA bbb-fsesl-akka bbb-rap-archive-worker.service bbb-rap-process-worker.service bbb-rap-publish-worker.service bbb-rap-sanity-worker.service bbb-record-core.timer $HTML5 $WEBHOOKS
systemctl stop red5 $TOMCAT_SERVICE nginx freeswitch $REDIS_SERVICE bbb-apps-akka $BBB_TRANSCODE_AKKA bbb-fsesl-akka bbb-rap-archive-worker.service bbb-rap-process-worker.service bbb-rap-publish-worker.service bbb-rap-sanity-worker.service bbb-record-core.timer $HTML5 $WEBHOOKS $ETHERPAD
else
/etc/init.d/monit stop
@ -357,6 +369,21 @@ stop_bigbluebutton () {
}
start_bigbluebutton () {
#
# Apply any local configuration options (if exists)
#
if [ -x /etc/bigbluebutton/bbb-conf/apply-config.sh ]; then
echo -n "Applying updates in /etc/bigbluebutton/bbb-conf/apply-config.sh: "
/etc/bigbluebutton/bbb-conf/apply-config.sh
echo
fi
if grep -q "Failure to connect to CORE_DB sofia_reg_external" /opt/freeswitch/var/log/freeswitch/freeswitch.log; then
# See: http://docs.bigbluebutton.org/install/install.html#freeswitch-fails-to-bind-to-ipv4
echo "Clearing the FreeSWITCH database."
rm -rf /opt/freeswitch/var/lib/freeswitch/db/*
fi
echo "Starting BigBlueButton"
if command -v systemctl >/dev/null; then
if [ -f /usr/lib/systemd/system/bbb-html5.service ]; then
@ -368,8 +395,11 @@ start_bigbluebutton () {
if [ -f /usr/lib/systemd/system/bbb-transcode-akka.service ]; then
BBB_TRANSCODE_AKKA=bbb-transcode-akka
fi
if [ -f /usr/share/etherpad-lite/settings.json ]; then
ETHERPAD=etherpad
fi
systemctl start red5 $TOMCAT_SERVICE nginx freeswitch $REDIS_SERVICE bbb-apps-akka $BBB_TRANSCODE_AKKA bbb-fsesl-akka bbb-record-core.timer $HTML5 $WEBHOOKS
systemctl start red5 $TOMCAT_SERVICE nginx freeswitch $REDIS_SERVICE bbb-apps-akka $BBB_TRANSCODE_AKKA bbb-fsesl-akka bbb-record-core.timer $HTML5 $WEBHOOKS $ETHERPAD
if [ -f /usr/lib/systemd/system/bbb-html5.service ]; then
systemctl start mongod
sleep 3
@ -1526,6 +1556,22 @@ check_state() {
echo "#"
fi
if systemctl status freeswitch | grep -q SETSCHEDULER; then
echo "# Error: FreeSWITCH failed to start with SETSCHEDULER error, see"
echo "#"
echo "# http://docs.bigbluebutton.org/install/install.html#freeswitch-fails-to-start-with-a-setscheduler-error"
echo "#"
fi
NCPU=`nproc --all`
if [ "$NCPU" -lt "4" ]; then
echo "# Warning: found only $NCPU cores, whereas this sherver should have (at least) 4 CPU cores"
echo "# to run BigBlueButton in production."
echo "#"
echo "# http://docs.bigbluebutton.org/install/install.html#minimum-server-requirements"
echo "#"
fi
exit 0
}

View File

@ -355,6 +355,7 @@ if [ $DELETE ]; then
echo "deleting: $MEETING_ID"
# Remove the status files first to avoid issues with concurrent
# recording processing
rm -f /var/bigbluebutton/recording/status/ended/$MEETING_ID*
rm -f /var/bigbluebutton/recording/status/recorded/$MEETING_ID*
rm -f /var/bigbluebutton/recording/status/archived/$MEETING_ID*
rm -f /var/bigbluebutton/recording/status/sanity/$MEETING_ID*

View File

@ -42,23 +42,32 @@
<!-- Body -->
<div class='container'>
<!-- Welcome Message & Login Into Demo -->
<div class='row'>
<div class='span five'>
<h2>Welcome</h2>
<p> <a href="http://bigbluebutton.org/" target="_blank">BigBlueButton</a> is an open source web conferencing system for on-line learning. </p>
<p> We believe that every student with a web browser should have access to a high-quality on-line learning experience </p>
<p> We intend to make that possible with BigBlueButton.</p>
<div class='span six html5clientOnly'>
<div class='join-meeting '>
<h4>Try BigBlueButton via HTML5</h4>
<p>New School</p>
<h4>For Developers</h4>
<p> The BigBlueButton project is <a href="http://bigbluebutton.org/support">supported</a> by a community of developers that care about good design and a streamlined user experience. </p>
<p>See <a href="/demo/demo1.jsp" target="_blank">API examples </a> for how to integrate BigBlueButton with your project.</p>
<form name="form1" method="GET" onsubmit="return checkform(this);" action="/demo/demoHTML5.jsp">
<input type="text" id="username" required="" name="username" placeholder="Enter Your Name" size="29" class="field input-default" autofocus>
<input type="submit" value="Join" class="submit_btn button success large"><br>
<input type="hidden" name="isModerator" value="true">
<input type="hidden" name="action" value="create">
</form>
<a class="watch" href="https://www.youtube.com/watch?v=d5v6Uar79Yc" class="pull-right">Overview Video of New HTML5 Client</a>
</div>
</div>
<div class="span one"></div>
<div class='span six'>
<div class='join-meeting pull-right'>
<h4>Try BigBlueButton</h4>
<p>Join a demo session on this server.</p>
<div class='join-meeting'>
<h4>Try BigBlueButton via Flash</h4>
<p>Old School</p>
<form name="form1" method="GET" onsubmit="return checkform(this);" action="/demo/demo1.jsp">
<input type="text" id="username" required="" name="username" placeholder="Enter Your Name" size="29" class="field input-default" autofocus>
@ -66,13 +75,31 @@
<input type="hidden" name="action" value="create">
</form>
<a class="watch" href="#video" class="pull-right">New to BigBlueButton? Watch these videos.</a>
<a class="watch" href="#video" class="pull-right">Tutorial Videos</a>
</div>
</div>
</div>
<hr class="featurette-divider">
<!-- Welcome Message & Login Into Demo -->
<div class='row'>
<div >
<h2>Built for online learning</h2>
<p> BigBlueButton is an open source web conferencing system built for on-line learning.</p>
<p>BigBlueButton provides all the core capabilities you expect in a web conferencing system: real-time sharing of audio, video, presentation, and screen. BigBlueButton gives you many ways to engage users with interactive chat (public and private), multi-user whiteboard, shared notes, emojis, polling, and breakout rooms. You can record any session for later playback.</p>
<p> The big change in this release is the new <a href="https://www.youtube.com/watch?v=d5v6Uar79Yc">HTML5 client</a>. The HTML5 client runs on all devices: laptop, desktop, chromebook and mobile (Android 6.0+ and iOS 12.0+). The HTML5 client uses the browser's built-in support for <a href="https://webrtc.org/">web real-time communications (WebRTC)</a> to share audio, video, and screen with other users. Chrome and FireFox are the recommend as they have the best support for WebRTC.</p>
<p> Try out the HTML5 client above and give us <a href="https://docs.google.com/forms/d/1gFz5JdN3vD6jxhlVskFYgtEKEcexdDnUzpkwUXwQ4OY/viewform?usp=send_for">your feedback</a>. To learn more about BigBlueButton visit <a href="https://bigbluebutton.org/">bigbluebutton.org</a></p>
<h4>For Developers</h4>
<p> Under the hood, the HTML5 client uses the React for a responsive experience. The BigBlueButton project is <a href="http://bigbluebutton.org/support">supported</a> by a community of developers that care about good design and a streamlined user experience. See <a href="http://docs.bigbluebutton.org" target="_blank">our documentation site</a> for more information on how you can integrate BigBlueButton with your project.</p>
</div>
<div class="span one"></div>
</div>
<hr class="featurette-divider">
<!-- BigBlueButton Features -->
@ -217,19 +244,19 @@
</div>
<div class='span four video-item'>
<a href="https://www.youtube.com/watch?v=oh0bEk3YSwI" target="_blank">
<a href="https://www.youtube.com/watch?v=GeJbK1lvl2I" target="_blank">
<div class="video-btn"><i class="fa fa-play-circle-o"></i></div>
<img src="images/bbb-viewer-overview.jpg" alt="BigBlueButton Viewer Overview Video"/>
</a>
<h3><a href="https://www.youtube.com/watch?v=oh0bEk3YSwI;feature=youtu.be" title="Student Overview" target="_blank">Viewer Overview</a></h3>
<h3><a href="https://www.youtube.com/watch?v=GeJbK1lvl2I;feature=youtu.be" title="Student Overview" target="_blank">Viewer Overview</a></h3>
</div>
<div class='span four last video-item'>
<a href="https://www.youtube.com/watch?v=J9mbw00P9W0&feature=youtu.be" target="_blank">
<a href="https://www.youtube.com/watch?v=758xaFdeoN0&feature=youtu.be" target="_blank">
<div class="video-btn"><i class="fa fa-play-circle-o"></i></div>
<img src="images/bbb-presenter-overview.jpg" alt="Moderator/Presenter Overview Video"/>
</a>
<h3><a href="https://www.youtube.com/watch?v=J9mbw00P9W0&feature=youtu.be" title="Moderator/Presenter Overview" target="_blank">Moderator/Presenter Overview</a></h3>
<h3><a href="https://www.youtube.com/watch?v=758xaFdeoN0&feature=youtu.be" title="Moderator/Presenter Overview" target="_blank">Moderator/Presenter Overview</a></h3>
</div>
</div>
</div>
@ -261,8 +288,8 @@
<div class="row">
<div class="span twelve center">
<p>Copyright &copy; 2018 BigBlueButton Inc.<br>
<small>Version <a href="http://docs.bigbluebutton.org/">2.0-RC2</a></small>
<p>Copyright &copy; 2019 BigBlueButton Inc.<br>
<small>Version <a href="http://docs.bigbluebutton.org/">2.2-dev</a></small>
</p>
</div>
</div>
@ -270,3 +297,4 @@
</footer>
</body>
</html>

8490
bigbluebutton-html5/client/compatibility/adapter.js Normal file → Executable file

File diff suppressed because it is too large Load Diff

View File

@ -1,601 +0,0 @@
var userID, callerIdName=null, conferenceVoiceBridge, userAgent=null, userMicMedia, userWebcamMedia, currentSession=null, callTimeout, callActive, callICEConnected, iceConnectedTimeout, callFailCounter, callPurposefullyEnded, uaConnected, transferTimeout, iceGatheringTimeout;
var inEchoTest = true;
var html5StunTurn = null;
function webRTCCallback(message) {
switch (message.status) {
case 'succeded':
BBB.webRTCCallSucceeded();
break;
case 'failed':
if (message.errorcode !== 1004) {
message.cause = null;
}
monitorTracksStop();
BBB.webRTCCallFailed(inEchoTest, message.errorcode, message.cause);
break;
case 'ended':
monitorTracksStop();
BBB.webRTCCallEnded(inEchoTest);
break;
case 'started':
monitorTracksStart();
BBB.webRTCCallStarted(inEchoTest);
break;
case 'connecting':
BBB.webRTCCallConnecting(inEchoTest);
break;
case 'waitingforice':
BBB.webRTCCallWaitingForICE(inEchoTest);
break;
case 'transferring':
BBB.webRTCCallTransferring(inEchoTest);
break;
case 'mediarequest':
BBB.webRTCMediaRequest();
break;
case 'mediasuccess':
BBB.webRTCMediaSuccess();
break;
case 'mediafail':
BBB.webRTCMediaFail();
break;
}
}
function callIntoConference(voiceBridge, callback, isListenOnly, stunTurn = null) {
// root of the call initiation process from the html5 client
// Flash will not pass in the listen only field. For html5 it is optional. Assume NOT listen only if no state passed
if (isListenOnly == null) {
isListenOnly = false;
}
// if additional stun configuration is passed, store the information
if (stunTurn != null) {
html5StunTurn = {
stunServers: stunTurn.stun,
turnServers: stunTurn.turn,
};
}
// reset callerIdName
callerIdName = null;
if (!callerIdName) {
BBB.getMyUserInfo(function(userInfo) {
console.log("User info callback [myUserID=" + userInfo.myUserID
+ ",myUsername=" + userInfo.myUsername + ",myAvatarURL=" + userInfo.myAvatarURL
+ ",myRole=" + userInfo.myRole + ",amIPresenter=" + userInfo.amIPresenter
+ ",dialNumber=" + userInfo.dialNumber + ",voiceBridge=" + userInfo.voiceBridge
+ ",isListenOnly=" + isListenOnly + "].");
userID = userInfo.myUserID;
callerIdName = userInfo.myUserID + "-bbbID-" + userInfo.myUsername;
if (isListenOnly) {
//prepend the callerIdName so it is recognized as a global audio user
callerIdName = "GLOBAL_AUDIO_" + callerIdName;
}
conferenceVoiceBridge = userInfo.voiceBridge
if (voiceBridge === "9196") {
voiceBridge = voiceBridge + conferenceVoiceBridge;
} else {
voiceBridge = conferenceVoiceBridge;
}
console.log(callerIdName);
webrtc_call(callerIdName, voiceBridge, callback, isListenOnly);
});
} else {
if (voiceBridge === "9196") {
voiceBridge = voiceBridge + conferenceVoiceBridge;
} else {
voiceBridge = conferenceVoiceBridge;
}
webrtc_call(callerIdName, voiceBridge, callback, isListenOnly);
}
}
function joinWebRTCVoiceConference() {
console.log("Joining to the voice conference directly");
inEchoTest = false;
// set proper callbacks to previously created user agent
if(userAgent) {
setUserAgentListeners(webRTCCallback);
}
callIntoConference(conferenceVoiceBridge, webRTCCallback);
}
function leaveWebRTCVoiceConference() {
console.log("Leaving the voice conference");
webrtc_hangup();
}
function startWebRTCAudioTest(){
console.log("Joining the audio test first");
inEchoTest = true;
callIntoConference("9196", webRTCCallback);
}
function stopWebRTCAudioTest(){
console.log("Stopping webrtc audio test");
webrtc_hangup();
}
function stopWebRTCAudioTestJoinConference(){
console.log("Transferring from audio test to conference");
webRTCCallback({'status': 'transferring'});
transferTimeout = setTimeout( function() {
console.log("Call transfer failed. No response after 3 seconds");
webRTCCallback({'status': 'failed', 'errorcode': 1008});
releaseUserMedia();
currentSession = null;
if (userAgent != null) {
var userAgentTemp = userAgent;
userAgent = null;
userAgentTemp.stop();
}
}, 5000);
BBB.listen("UserJoinedVoiceEvent", userJoinedVoiceHandler);
currentSession.dtmf(1);
inEchoTest = false;
}
function userJoinedVoiceHandler(event) {
console.log("UserJoinedVoiceHandler - " + event);
if (inEchoTest === false && userID === event.userID) {
BBB.unlisten("UserJoinedVoiceEvent", userJoinedVoiceHandler);
clearTimeout(transferTimeout);
webRTCCallback({'status': 'started'});
}
}
function createUA(username, server, callback, makeCallFunc) {
if (userAgent) {
console.log("User agent already created");
return;
}
console.log("Fetching STUN/TURN server info for user agent");
console.log(html5StunTurn);
if (html5StunTurn != null) {
createUAWithStuns(username, server, callback, html5StunTurn, makeCallFunc);
return;
}
BBB.getSessionToken(function(sessionToken) {
$.ajax({
dataType: 'json',
url: '/bigbluebutton/api/stuns',
data: {sessionToken:sessionToken}
}).done(function(data) {
var stunsConfig = {};
stunsConfig['stunServers'] = ( data['stunServers'] ? data['stunServers'].map(function(data) {
return data['url'];
}) : [] );
stunsConfig['turnServers'] = ( data['turnServers'] ? data['turnServers'].map(function(data) {
return {
'urls': data['url'],
'username': data['username'],
'password': data['password']
};
}) : [] );
//stunsConfig['remoteIceCandidates'] = ( data['remoteIceCandidates'] ? data['remoteIceCandidates'].map(function(data) {
// return data['ip'];
//}) : [] );
createUAWithStuns(username, server, callback, stunsConfig, makeCallFunc);
}).fail(function(data, textStatus, errorThrown) {
BBBLog.error("Could not fetch stun/turn servers", {error: textStatus, user: callerIdName, voiceBridge: conferenceVoiceBridge});
callback({'status':'failed', 'errorcode': 1009});
});
});
}
function createUAWithStuns(username, server, callback, stunsConfig, makeCallFunc) {
console.log("Creating new user agent");
/* VERY IMPORTANT
* - You must escape the username because spaces will cause the connection to fail
* - We are connecting to the websocket through an nginx redirect instead of directly to 5066
*/
var configuration = {
uri: 'sip:' + encodeURIComponent(username) + '@' + server,
wsServers: ('https:' == document.location.protocol ? 'wss://' : 'ws://') + server + '/ws',
displayName: username,
register: false,
traceSip: true,
autostart: false,
userAgentString: "BigBlueButton",
stunServers: stunsConfig['stunServers'],
turnServers: stunsConfig['turnServers'],
//artificialRemoteIceCandidates: stunsConfig['remoteIceCandidates']
};
uaConnected = false;
userAgent = new SIP.UA(configuration);
setUserAgentListeners(callback, makeCallFunc);
userAgent.start();
};
function setUserAgentListeners(callback, makeCallFunc) {
console.log("resetting UA callbacks");
userAgent.removeAllListeners('connected');
userAgent.on('connected', function() {
uaConnected = true;
callback({'status':'succeded'});
makeCallFunc();
});
userAgent.removeAllListeners('disconnected');
userAgent.on('disconnected', function() {
if (userAgent) {
if (userAgent != null) {
var userAgentTemp = userAgent;
userAgent = null;
userAgentTemp.stop();
}
if (uaConnected) {
callback({'status':'failed', 'errorcode': 1001}); // WebSocket disconnected
} else {
callback({'status':'failed', 'errorcode': 1002}); // Could not make a WebSocket connection
}
}
});
};
function getUserMicMedia(getUserMicMediaSuccess, getUserMicMediaFailure) {
if (userMicMedia == undefined) {
if (SIP.WebRTC.isSupported()) {
SIP.WebRTC.getUserMedia({audio:true, video:false}, getUserMicMediaSuccess, getUserMicMediaFailure);
} else {
console.log("getUserMicMedia: webrtc not supported");
getUserMicMediaFailure("WebRTC is not supported");
}
} else {
console.log("getUserMicMedia: mic already set");
getUserMicMediaSuccess(userMicMedia);
}
};
function webrtc_call(username, voiceBridge, callback, isListenOnly) {
if (!isWebRTCAvailable()) {
callback({'status': 'failed', 'errorcode': 1003}); // Browser version not supported
return;
}
if (isListenOnly == null) { // assume NOT listen only unless otherwise stated
isListenOnly = false;
}
var server = window.document.location.hostname;
console.log("user " + username + " calling to " + voiceBridge);
var makeCallFunc = function() {
// only make the call when both microphone and useragent have been created
// for listen only, stating listen only is a viable substitute for acquiring user media control
if ((isListenOnly||userMicMedia) && userAgent)
make_call(username, voiceBridge, server, callback, false, isListenOnly);
};
// Reset userAgent so we can successfully switch between listenOnly and listen+speak modes
userAgent = null;
if (!userAgent) {
createUA(username, server, callback, makeCallFunc);
}
// if the user requests to proceed as listen only (does not require media) or media is already acquired,
// proceed with making the call
if (isListenOnly || userMicMedia != null) {
makeCallFunc();
} else {
callback({'status':'mediarequest'});
getUserMicMedia(function(stream) {
console.log("getUserMicMedia: success");
userMicMedia = stream;
callback({'status':'mediasuccess'});
makeCallFunc();
}, function(e) {
console.error("getUserMicMedia: failure - " + e);
callback({'status':'mediafail', 'cause': e});
}
);
}
}
function make_call(username, voiceBridge, server, callback, recall, isListenOnly) {
if (isListenOnly == null) {
isListenOnly = false;
}
if (userAgent == null) {
console.log("userAgent is still null. Delaying call");
var callDelayTimeout = setTimeout( function() {
make_call(username, voiceBridge, server, callback, recall, isListenOnly);
}, 100);
return;
}
if (!userAgent.isConnected()) {
console.log("Trying to make call, but UserAgent hasn't connected yet. Delaying call");
userAgent.once('connected', function() {
console.log("UserAgent has now connected, retrying the call");
make_call(username, voiceBridge, server, callback, recall, isListenOnly);
});
return;
}
if (currentSession) {
console.log('Active call detected ignoring second make_call');
return;
}
// Make an audio/video call:
console.log("Setting options.. ");
var options = {};
if (isListenOnly) {
// create necessary options for a listen only stream
var stream = null;
// handle the web browser
// create a stream object through the browser separated from user media
if (typeof webkitMediaStream !== 'undefined') {
// Google Chrome
stream = new webkitMediaStream;
} else {
// Firefox
audioContext = new window.AudioContext;
stream = audioContext.createMediaStreamDestination().stream;
}
options = {
media: {
stream: stream, // use the stream created above
constraints: {
audio: true,
video: false
},
render: {
remote: document.getElementById('remote-media')
}
},
// a list of our RTC Connection constraints
RTCConstraints: {
// our constraints are mandatory. We must received audio and must not receive audio
mandatory: {
OfferToReceiveAudio: true,
OfferToReceiveVideo: false
}
}
};
} else {
options = {
media: {
stream: userMicMedia,
constraints: {
audio: true,
video: false
},
render: {
remote: document.getElementById('remote-media')
}
}
};
}
callTimeout = setTimeout(function() {
console.log('Ten seconds without updates sending timeout code');
callback({'status':'failed', 'errorcode': 1006}); // Failure on call
releaseUserMedia();
currentSession = null;
if (userAgent != null) {
var userAgentTemp = userAgent;
userAgent = null;
userAgentTemp.stop();
}
}, 10000);
callActive = false;
callICEConnected = false;
callPurposefullyEnded = false;
callFailCounter = 0;
console.log("Calling to " + voiceBridge + "....");
currentSession = userAgent.invite('sip:' + voiceBridge + '@' + server, options);
// Only send the callback if it's the first try
if (recall === false) {
console.log('call connecting');
callback({'status':'connecting'});
} else {
console.log('call connecting again');
}
/*
iceGatheringTimeout = setTimeout(function() {
console.log('Thirty seconds without ICE gathering finishing');
callback({'status':'failed', 'errorcode': 1011}); // ICE Gathering Failed
releaseUserMedia();
currentSession = null;
if (userAgent != null) {
var userAgentTemp = userAgent;
userAgent = null;
userAgentTemp.stop();
}
}, 30000);
*/
currentSession.mediaHandler.on('iceGatheringComplete', function() {
clearTimeout(iceGatheringTimeout);
});
// The connecting event fires before the listener can be added
currentSession.on('connecting', function(){
clearTimeout(callTimeout);
});
currentSession.on('progress', function(response){
console.log('call progress: ' + response);
clearTimeout(callTimeout);
});
currentSession.on('failed', function(response, cause){
console.log('call failed with cause: '+ cause);
if (currentSession) {
releaseUserMedia();
if (callActive === false) {
callback({'status':'failed', 'errorcode': 1004, 'cause': cause}); // Failure on call
currentSession = null;
if (userAgent != null) {
var userAgentTemp = userAgent;
userAgent = null;
userAgentTemp.stop();
}
} else {
callActive = false;
//currentSession.bye();
currentSession = null;
if (userAgent != null) {
userAgent.stop();
}
}
}
clearTimeout(callTimeout);
});
currentSession.on('bye', function(request){
callActive = false;
if (currentSession) {
console.log('call ended ' + currentSession.endTime);
releaseUserMedia();
if (callPurposefullyEnded === true) {
callback({'status':'ended'});
} else {
callback({'status':'failed', 'errorcode': 1005}); // Call ended unexpectedly
}
clearTimeout(callTimeout);
currentSession = null;
} else {
console.log('bye event already received');
}
});
currentSession.on('cancel', function(request) {
callActive = false;
if (currentSession) {
console.log('call canceled');
releaseUserMedia();
clearTimeout(callTimeout);
currentSession = null;
} else {
console.log('cancel event already received');
}
});
currentSession.on('accepted', function(data){
callActive = true;
console.log('BigBlueButton call accepted');
if (callICEConnected === true) {
callback({'status':'started'});
} else {
callback({'status':'waitingforice'});
console.log('Waiting for ICE negotiation');
iceConnectedTimeout = setTimeout(function() {
console.log('5 seconds without ICE finishing');
callback({'status':'failed', 'errorcode': 1010}); // ICE negotiation timeout
releaseUserMedia();
currentSession = null;
if (userAgent != null) {
var userAgentTemp = userAgent;
userAgent = null;
userAgentTemp.stop();
}
}, 5000);
}
clearTimeout(callTimeout);
});
currentSession.mediaHandler.on('iceConnectionFailed', function() {
console.log('received ice negotiation failed');
callback({'status':'failed', 'errorcode': 1007}); // Failure on call
releaseUserMedia();
currentSession = null;
clearTimeout(iceConnectedTimeout);
if (userAgent != null) {
var userAgentTemp = userAgent;
userAgent = null;
userAgentTemp.stop();
}
clearTimeout(callTimeout);
});
// Some browsers use status of 'connected', others use 'completed', and a couple use both
currentSession.mediaHandler.on('iceConnectionConnected', function() {
console.log('Received ICE status changed to connected');
if (callICEConnected === false) {
callICEConnected = true;
clearTimeout(iceConnectedTimeout);
if (callActive === true) {
callback({'status':'started'});
}
clearTimeout(callTimeout);
}
});
currentSession.mediaHandler.on('iceConnectionCompleted', function() {
console.log('Received ICE status changed to completed');
if (callICEConnected === false) {
callICEConnected = true;
clearTimeout(iceConnectedTimeout);
if (callActive === true) {
callback({'status':'started'});
}
clearTimeout(callTimeout);
}
});
}
function webrtc_hangup(callback) {
callPurposefullyEnded = true;
console.log("Hanging up current session");
if (callback) {
currentSession.on('bye', callback);
}
try {
currentSession.bye();
} catch (err) {
console.log("Forcing to cancel current session");
currentSession.cancel();
}
}
function releaseUserMedia() {
if (!!userMicMedia) {
console.log("Releasing media tracks");
userMicMedia.getAudioTracks().forEach(function(track) {
track.stop();
});
userMicMedia.getVideoTracks().forEach(function(track) {
track.stop();
});
userMicMedia = null;
}
}
function isWebRTCAvailable() {
if (bowser.msedge) {
return false;
} else {
return SIP.WebRTC.isSupported();
}
}
function getCallStatus() {
return currentSession;
}

View File

@ -3,6 +3,8 @@ const isOpera = !!window.opera || navigator.userAgent.indexOf(' OPR/') >= 0;
const isChrome = !!window.chrome && !isOpera;
const isSafari = navigator.userAgent.indexOf('Safari') >= 0 && !isChrome;
const isElectron = navigator.userAgent.toLowerCase().indexOf(' electron/') > -1;
const hasDisplayMedia = (typeof navigator.getDisplayMedia === 'function'
|| (navigator.mediaDevices && typeof navigator.mediaDevices.getDisplayMedia === 'function'));
const kurentoHandler = null;
Kurento = function (
@ -620,7 +622,7 @@ Kurento.normalizeCallback = function (callback) {
// this function explains how to use above methods/objects
window.getScreenConstraints = function (sendSource, callback) {
const screenConstraints = { video: {}, audio: false };
let screenConstraints = { video: {}, audio: false };
// Limiting FPS to a range of 5-10 (5 ideal)
screenConstraints.video.frameRate = { ideal: 5, max: 10 };
@ -643,6 +645,23 @@ window.getScreenConstraints = function (sendSource, callback) {
});
};
const getDisplayMediaConstraints = function () {
// The fine-grained constraints (e.g.: frameRate) are supposed to go into
// the MediaStream because getDisplayMedia does not support them,
// so they're passed differently
kurentoManager.kurentoScreenshare.extensionInstalled = true;
optionalConstraints.width = { max: kurentoManager.kurentoScreenshare.vid_max_width };
optionalConstraints.height = { max: kurentoManager.kurentoScreenshare.vid_max_height };
optionalConstraints.frameRate = { ideal: 5, max: 10 };
let gDPConstraints = {
video: true,
optional: optionalConstraints
}
return gDPConstraints;
};
const optionalConstraints = [
{ googCpuOveruseDetection: true },
{ googCpuOveruseEncodeUsage: true },
@ -669,25 +688,34 @@ window.getScreenConstraints = function (sendSource, callback) {
}
if (isChrome) {
const extensionKey = kurentoManager.getChromeExtensionKey();
getChromeScreenConstraints(extensionKey).then((constraints) => {
if (!constraints) {
document.dispatchEvent(new Event('installChromeExtension'));
return;
}
if (!hasDisplayMedia) {
const extensionKey = kurentoManager.getChromeExtensionKey();
getChromeScreenConstraints(extensionKey).then((constraints) => {
if (!constraints) {
document.dispatchEvent(new Event('installChromeExtension'));
return;
}
const sourceId = constraints.streamId;
const sourceId = constraints.streamId;
kurentoManager.kurentoScreenshare.extensionInstalled = true;
kurentoManager.kurentoScreenshare.extensionInstalled = true;
// this statement sets gets 'sourceId" and sets "chromeMediaSourceId"
screenConstraints.video.chromeMediaSource = { exact: [sendSource] };
screenConstraints.video.chromeMediaSourceId = sourceId;
screenConstraints.optional = optionalConstraints;
// Re-wrap the video constraints into the mandatory object (latest adapter)
screenConstraints.video = {}
screenConstraints.video.mandatory = {};
screenConstraints.video.mandatory.maxFrameRate = 10;
screenConstraints.video.mandatory.maxHeight = kurentoManager.kurentoScreenshare.vid_max_height;
screenConstraints.video.mandatory.maxWidth = kurentoManager.kurentoScreenshare.vid_max_width;
screenConstraints.video.mandatory.chromeMediaSource = sendSource;
screenConstraints.video.mandatory.chromeMediaSourceId = sourceId;
screenConstraints.optional = optionalConstraints;
console.log('getScreenConstraints for Chrome returns => ', screenConstraints);
return callback(null, screenConstraints);
});
console.log('getScreenConstraints for Chrome returns => ', screenConstraints);
return callback(null, screenConstraints);
});
} else {
return callback(null, getDisplayMediaConstraints());
}
}
if (isFirefox) {
@ -698,9 +726,14 @@ window.getScreenConstraints = function (sendSource, callback) {
return callback(null, screenConstraints);
}
// Falls back to getDisplayMedia if the browser supports it
if (hasDisplayMedia) {
return callback(null, getDisplayMediaConstraints());
}
if (isSafari) {
// At this time (version 11.1), Safari doesn't support screenshare.
document.dispatchEvent(new Event('safariScreenshareNotSupported'));
return document.dispatchEvent(new Event('safariScreenshareNotSupported'));
}
};

View File

@ -193,6 +193,15 @@ function WebRtcPeer(mode, options, callback) {
});
if (!pc) {
pc = new RTCPeerConnection(configuration);
//Add Transceiver for Webview on IOS
const userAgent = window.navigator.userAgent.toLocaleLowerCase();
if ((userAgent.indexOf('iphone') > -1 || userAgent.indexOf('ipad') > -1) && userAgent.indexOf('safari') == -1) {
try {
pc.addTransceiver('video');
} catch(e) {}
}
if (useDataChannels && !dataChannel) {
var dcId = 'WebRtcPeer-' + self.id;
var dcOptions = undefined;
@ -258,6 +267,28 @@ function WebRtcPeer(mode, options, callback) {
};
this.generateOffer = function (callback) {
callback = callback.bind(this);
const descriptionCallback = () => {
var localDescription = pc.localDescription;
// logger.debug('Local description set', localDescription.sdp);
if (multistream && usePlanB) {
localDescription = interop.toUnifiedPlan(localDescription);
logger.debug('offer::origPlanB->UnifiedPlan', dumpSDP(localDescription));
}
callback(null, localDescription.sdp, self.processAnswer.bind(self));
}
const userAgent = window.navigator.userAgent.toLocaleLowerCase();
const isSafari = ((userAgent.indexOf('iphone') > -1 || userAgent.indexOf('ipad') > -1) || browser.name.toLowerCase() == 'safari');
// Bind the SDP release to the gathering state on Safari-based envs
if (isSafari) {
pc.onicegatheringstatechange = function (event) {
if(event.target.iceGatheringState == "complete") {
descriptionCallback();
}
}
}
var offerAudio = true;
var offerVideo = true;
if (mediaConstraints) {
@ -274,14 +305,12 @@ function WebRtcPeer(mode, options, callback) {
// logger.debug('Created SDP offer');
offer = mangleSdpToAddSimulcast(offer);
return pc.setLocalDescription(offer);
}).then(function () {
var localDescription = pc.localDescription;
// logger.debug('Local description set', localDescription.sdp);
if (multistream && usePlanB) {
localDescription = interop.toUnifiedPlan(localDescription);
logger.debug('offer::origPlanB->UnifiedPlan', dumpSDP(localDescription));
}).then(() => {
// The Safari offer release was already binded to the gathering state
if (isSafari) {
return;
}
callback(null, localDescription.sdp, self.processAnswer.bind(self));
descriptionCallback();
}).catch(callback);
};
this.getLocalSessionDescriptor = function () {
@ -432,7 +461,27 @@ function WebRtcPeer(mode, options, callback) {
return callback(error);
constraints = [mediaConstraints];
constraints.unshift(constraints_);
getMedia(recursive.apply(undefined, constraints));
let gDMCallback = function(stream) {
stream.getTracks()[0].applyConstraints(constraints[0].optional)
.then(() => {
videoStream = stream;
start();
}).catch(() => {
videoStream = stream;
start();
});
}
if (typeof navigator.getDisplayMedia === 'function') {
navigator.getDisplayMedia(recursive.apply(undefined, constraints))
.then(gDMCallback)
.catch(callback);
} else if (navigator.mediaDevices && typeof navigator.mediaDevices.getDisplayMedia === 'function') {
navigator.mediaDevices.getDisplayMedia(recursive.apply(undefined, constraints))
.then(gDMCallback)
.catch(callback);
} else {
getMedia(recursive.apply(undefined, constraints));
}
}, guid);
}
} else {
@ -1194,180 +1243,180 @@ if (typeof Object.create === 'function') {
// Does nothing at all.
},{}],11:[function(require,module,exports){
/*!
* @name JavaScript/NodeJS Merge v1.2.0
* @author yeikos
* @repository https://github.com/yeikos/js.merge
* Copyright 2014 yeikos - MIT license
* https://raw.github.com/yeikos/js.merge/master/LICENSE
*/
;(function(isNode) {
/**
* Merge one or more objects
* @param bool? clone
* @param mixed,... arguments
* @return object
*/
var Public = function(clone) {
return merge(clone === true, false, arguments);
}, publicName = 'merge';
/**
* Merge two or more objects recursively
* @param bool? clone
* @param mixed,... arguments
* @return object
*/
Public.recursive = function(clone) {
return merge(clone === true, true, arguments);
};
/**
* Clone the input removing any reference
* @param mixed input
* @return mixed
*/
Public.clone = function(input) {
var output = input,
type = typeOf(input),
index, size;
if (type === 'array') {
output = [];
size = input.length;
for (index=0;index<size;++index)
output[index] = Public.clone(input[index]);
} else if (type === 'object') {
output = {};
for (index in input)
output[index] = Public.clone(input[index]);
}
return output;
};
/**
* Merge two objects recursively
* @param mixed input
* @param mixed extend
* @return mixed
*/
function merge_recursive(base, extend) {
if (typeOf(base) !== 'object')
return extend;
for (var key in extend) {
if (typeOf(base[key]) === 'object' && typeOf(extend[key]) === 'object') {
base[key] = merge_recursive(base[key], extend[key]);
} else {
base[key] = extend[key];
}
}
return base;
}
/**
* Merge two or more objects
* @param bool clone
* @param bool recursive
* @param array argv
* @return object
*/
function merge(clone, recursive, argv) {
var result = argv[0],
size = argv.length;
if (clone || typeOf(result) !== 'object')
result = {};
for (var index=0;index<size;++index) {
var item = argv[index],
type = typeOf(item);
if (type !== 'object') continue;
for (var key in item) {
var sitem = clone ? Public.clone(item[key]) : item[key];
if (recursive) {
result[key] = merge_recursive(result[key], sitem);
} else {
result[key] = sitem;
}
}
}
return result;
}
/**
* Get type of variable
* @param mixed input
* @return string
*
* @see http://jsperf.com/typeofvar
*/
function typeOf(input) {
return ({}).toString.call(input).slice(8, -1).toLowerCase();
}
if (isNode) {
module.exports = Public;
} else {
window[publicName] = Public;
}
/*!
* @name JavaScript/NodeJS Merge v1.2.0
* @author yeikos
* @repository https://github.com/yeikos/js.merge
* Copyright 2014 yeikos - MIT license
* https://raw.github.com/yeikos/js.merge/master/LICENSE
*/
;(function(isNode) {
/**
* Merge one or more objects
* @param bool? clone
* @param mixed,... arguments
* @return object
*/
var Public = function(clone) {
return merge(clone === true, false, arguments);
}, publicName = 'merge';
/**
* Merge two or more objects recursively
* @param bool? clone
* @param mixed,... arguments
* @return object
*/
Public.recursive = function(clone) {
return merge(clone === true, true, arguments);
};
/**
* Clone the input removing any reference
* @param mixed input
* @return mixed
*/
Public.clone = function(input) {
var output = input,
type = typeOf(input),
index, size;
if (type === 'array') {
output = [];
size = input.length;
for (index=0;index<size;++index)
output[index] = Public.clone(input[index]);
} else if (type === 'object') {
output = {};
for (index in input)
output[index] = Public.clone(input[index]);
}
return output;
};
/**
* Merge two objects recursively
* @param mixed input
* @param mixed extend
* @return mixed
*/
function merge_recursive(base, extend) {
if (typeOf(base) !== 'object')
return extend;
for (var key in extend) {
if (typeOf(base[key]) === 'object' && typeOf(extend[key]) === 'object') {
base[key] = merge_recursive(base[key], extend[key]);
} else {
base[key] = extend[key];
}
}
return base;
}
/**
* Merge two or more objects
* @param bool clone
* @param bool recursive
* @param array argv
* @return object
*/
function merge(clone, recursive, argv) {
var result = argv[0],
size = argv.length;
if (clone || typeOf(result) !== 'object')
result = {};
for (var index=0;index<size;++index) {
var item = argv[index],
type = typeOf(item);
if (type !== 'object') continue;
for (var key in item) {
var sitem = clone ? Public.clone(item[key]) : item[key];
if (recursive) {
result[key] = merge_recursive(result[key], sitem);
} else {
result[key] = sitem;
}
}
}
return result;
}
/**
* Get type of variable
* @param mixed input
* @return string
*
* @see http://jsperf.com/typeofvar
*/
function typeOf(input) {
return ({}).toString.call(input).slice(8, -1).toLowerCase();
}
if (isNode) {
module.exports = Public;
} else {
window[publicName] = Public;
}
})(typeof module === 'object' && module && typeof module.exports === 'object' && module.exports);
},{}],12:[function(require,module,exports){
/**
@ -4265,159 +4314,159 @@ uuid.unparse = unparse;
module.exports = uuid;
},{"./rng":22}],24:[function(require,module,exports){
/*
WildEmitter.js is a slim little event emitter by @henrikjoreteg largely based
on @visionmedia's Emitter from UI Kit.
Why? I wanted it standalone.
I also wanted support for wildcard emitters like this:
emitter.on('*', function (eventName, other, event, payloads) {
});
emitter.on('somenamespace*', function (eventName, payloads) {
});
Please note that callbacks triggered by wildcard registered events also get
the event name as the first argument.
*/
module.exports = WildEmitter;
function WildEmitter() { }
WildEmitter.mixin = function (constructor) {
var prototype = constructor.prototype || constructor;
prototype.isWildEmitter= true;
// Listen on the given `event` with `fn`. Store a group name if present.
prototype.on = function (event, groupName, fn) {
this.callbacks = this.callbacks || {};
var hasGroup = (arguments.length === 3),
group = hasGroup ? arguments[1] : undefined,
func = hasGroup ? arguments[2] : arguments[1];
func._groupName = group;
(this.callbacks[event] = this.callbacks[event] || []).push(func);
return this;
};
// Adds an `event` listener that will be invoked a single
// time then automatically removed.
prototype.once = function (event, groupName, fn) {
var self = this,
hasGroup = (arguments.length === 3),
group = hasGroup ? arguments[1] : undefined,
func = hasGroup ? arguments[2] : arguments[1];
function on() {
self.off(event, on);
func.apply(this, arguments);
}
this.on(event, group, on);
return this;
};
// Unbinds an entire group
prototype.releaseGroup = function (groupName) {
this.callbacks = this.callbacks || {};
var item, i, len, handlers;
for (item in this.callbacks) {
handlers = this.callbacks[item];
for (i = 0, len = handlers.length; i < len; i++) {
if (handlers[i]._groupName === groupName) {
//console.log('removing');
// remove it and shorten the array we're looping through
handlers.splice(i, 1);
i--;
len--;
}
}
}
return this;
};
// Remove the given callback for `event` or all
// registered callbacks.
prototype.off = function (event, fn) {
this.callbacks = this.callbacks || {};
var callbacks = this.callbacks[event],
i;
if (!callbacks) return this;
// remove all handlers
if (arguments.length === 1) {
delete this.callbacks[event];
return this;
}
// remove specific handler
i = callbacks.indexOf(fn);
callbacks.splice(i, 1);
if (callbacks.length === 0) {
delete this.callbacks[event];
}
return this;
};
/// Emit `event` with the given args.
// also calls any `*` handlers
prototype.emit = function (event) {
this.callbacks = this.callbacks || {};
var args = [].slice.call(arguments, 1),
callbacks = this.callbacks[event],
specialCallbacks = this.getWildcardCallbacks(event),
i,
len,
item,
listeners;
if (callbacks) {
listeners = callbacks.slice();
for (i = 0, len = listeners.length; i < len; ++i) {
if (!listeners[i]) {
break;
}
listeners[i].apply(this, args);
}
}
if (specialCallbacks) {
len = specialCallbacks.length;
listeners = specialCallbacks.slice();
for (i = 0, len = listeners.length; i < len; ++i) {
if (!listeners[i]) {
break;
}
listeners[i].apply(this, [event].concat(args));
}
}
return this;
};
// Helper for for finding special wildcard event handlers that match the event
prototype.getWildcardCallbacks = function (eventName) {
this.callbacks = this.callbacks || {};
var item,
split,
result = [];
for (item in this.callbacks) {
split = item.split('*');
if (item === '*' || (split.length === 2 && eventName.slice(0, split[0].length) === split[0])) {
result = result.concat(this.callbacks[item]);
}
}
return result;
};
};
WildEmitter.mixin(WildEmitter);
/*
WildEmitter.js is a slim little event emitter by @henrikjoreteg largely based
on @visionmedia's Emitter from UI Kit.
Why? I wanted it standalone.
I also wanted support for wildcard emitters like this:
emitter.on('*', function (eventName, other, event, payloads) {
});
emitter.on('somenamespace*', function (eventName, payloads) {
});
Please note that callbacks triggered by wildcard registered events also get
the event name as the first argument.
*/
module.exports = WildEmitter;
function WildEmitter() { }
WildEmitter.mixin = function (constructor) {
var prototype = constructor.prototype || constructor;
prototype.isWildEmitter= true;
// Listen on the given `event` with `fn`. Store a group name if present.
prototype.on = function (event, groupName, fn) {
this.callbacks = this.callbacks || {};
var hasGroup = (arguments.length === 3),
group = hasGroup ? arguments[1] : undefined,
func = hasGroup ? arguments[2] : arguments[1];
func._groupName = group;
(this.callbacks[event] = this.callbacks[event] || []).push(func);
return this;
};
// Adds an `event` listener that will be invoked a single
// time then automatically removed.
prototype.once = function (event, groupName, fn) {
var self = this,
hasGroup = (arguments.length === 3),
group = hasGroup ? arguments[1] : undefined,
func = hasGroup ? arguments[2] : arguments[1];
function on() {
self.off(event, on);
func.apply(this, arguments);
}
this.on(event, group, on);
return this;
};
// Unbinds an entire group
prototype.releaseGroup = function (groupName) {
this.callbacks = this.callbacks || {};
var item, i, len, handlers;
for (item in this.callbacks) {
handlers = this.callbacks[item];
for (i = 0, len = handlers.length; i < len; i++) {
if (handlers[i]._groupName === groupName) {
//console.log('removing');
// remove it and shorten the array we're looping through
handlers.splice(i, 1);
i--;
len--;
}
}
}
return this;
};
// Remove the given callback for `event` or all
// registered callbacks.
prototype.off = function (event, fn) {
this.callbacks = this.callbacks || {};
var callbacks = this.callbacks[event],
i;
if (!callbacks) return this;
// remove all handlers
if (arguments.length === 1) {
delete this.callbacks[event];
return this;
}
// remove specific handler
i = callbacks.indexOf(fn);
callbacks.splice(i, 1);
if (callbacks.length === 0) {
delete this.callbacks[event];
}
return this;
};
/// Emit `event` with the given args.
// also calls any `*` handlers
prototype.emit = function (event) {
this.callbacks = this.callbacks || {};
var args = [].slice.call(arguments, 1),
callbacks = this.callbacks[event],
specialCallbacks = this.getWildcardCallbacks(event),
i,
len,
item,
listeners;
if (callbacks) {
listeners = callbacks.slice();
for (i = 0, len = listeners.length; i < len; ++i) {
if (!listeners[i]) {
break;
}
listeners[i].apply(this, args);
}
}
if (specialCallbacks) {
len = specialCallbacks.length;
listeners = specialCallbacks.slice();
for (i = 0, len = listeners.length; i < len; ++i) {
if (!listeners[i]) {
break;
}
listeners[i].apply(this, [event].concat(args));
}
}
return this;
};
// Helper for for finding special wildcard event handlers that match the event
prototype.getWildcardCallbacks = function (eventName) {
this.callbacks = this.callbacks || {};
var item,
split,
result = [];
for (item in this.callbacks) {
split = item.split('*');
if (item === '*' || (split.length === 2 && eventName.slice(0, split[0].length) === split[0])) {
result = result.concat(this.callbacks[item]);
}
}
return result;
};
};
WildEmitter.mixin(WildEmitter);
},{}]},{},[2])(2)
});

11
bigbluebutton-html5/client/compatibility/sip.js Executable file → Normal file
View File

@ -11564,6 +11564,13 @@ MediaHandler.prototype = Object.create(SIP.MediaHandler.prototype, {
self.ready = false;
methodName = self.hasOffer('remote') ? 'createAnswer' : 'createOffer';
if(constraints.offerToReceiveAudio) {
//Needed for Safari on webview
try {
pc.addTransceiver('audio');
} catch (e) {}
}
return SIP.Utils.promisify(pc, methodName, true)(constraints)
.catch(function methodError(e) {
self.emit('peerConnection-' + methodName + 'Failed', e);
@ -11611,7 +11618,9 @@ MediaHandler.prototype = Object.create(SIP.MediaHandler.prototype, {
try {
streams = [].concat(streams);
streams.forEach(function (stream) {
this.peerConnection.addStream(stream);
try {
this.peerConnection.addStream(stream);
} catch (e) {}
}, this);
} catch(e) {
this.logger.error('error adding stream');

View File

@ -4,7 +4,7 @@ import { Tracker } from 'meteor/tracker';
import BaseAudioBridge from './base';
import logger from '/imports/startup/client/logger';
import { fetchStunTurnServers } from '/imports/utils/fetchStunTurnServers';
import browser from 'browser-detect';
const MEDIA = Meteor.settings.public.media;
const MEDIA_TAG = MEDIA.mediaTag;
@ -14,6 +14,13 @@ const CALL_HANGUP_MAX_RETRIES = MEDIA.callHangupMaximumRetries;
const CONNECTION_TERMINATED_EVENTS = ['iceConnectionFailed', 'iceConnectionClosed'];
const CALL_CONNECT_NOTIFICATION_TIMEOUT = 500;
const logConnector = (level, category, label, content) => {
if (level === 'log')
level = "info";
logger[level]({logCode: 'sipjs_log'}, '[' + category + '] ' + content);
};
export default class SIPBridge extends BaseAudioBridge {
constructor(userData) {
super(userData);
@ -199,6 +206,7 @@ export default class SIPBridge extends BaseAudioBridge {
wsServers: `${(protocol === 'https:' ? 'wss://' : 'ws://')}${hostname}/ws`,
log: {
builtinEnabled: false,
connector: logConnector
},
displayName: callerIdName,
register: false,
@ -259,10 +267,8 @@ export default class SIPBridge extends BaseAudioBridge {
},
},
RTCConstraints: {
mandatory: {
OfferToReceiveAudio: true,
OfferToReceiveVideo: false,
},
offerToReceiveAudio: true,
offerToReceiveVideo: false,
},
};
@ -273,7 +279,14 @@ export default class SIPBridge extends BaseAudioBridge {
return new Promise((resolve) => {
const { mediaHandler } = currentSession;
const connectionCompletedEvents = ['iceConnectionCompleted', 'iceConnectionConnected'];
let connectionCompletedEvents = ['iceConnectionCompleted', 'iceConnectionConnected'];
// Edge sends a connected first and then a completed, but the call isn't ready until
// the completed comes in. Due to the way that we have the listeners set up, the only
// way to ignore one status is to not listen for it.
if (browser().name === 'edge') {
connectionCompletedEvents = ['iceConnectionCompleted'];
}
const handleConnectionCompleted = () => {
connectionCompletedEvents.forEach(e => mediaHandler.off(e, handleConnectionCompleted));
// We have to delay notifying that the call is connected because it is sometimes not
@ -295,9 +308,9 @@ export default class SIPBridge extends BaseAudioBridge {
});
}
const mappedCause = cause in this.errorCodes ?
this.errorCodes[cause] :
this.baseErrorCodes.GENERIC_ERROR;
const mappedCause = cause in this.errorCodes
? this.errorCodes[cause]
: this.baseErrorCodes.GENERIC_ERROR;
return this.callback({
status: this.baseCallStates.failed,

View File

@ -3,7 +3,7 @@ import { check } from 'meteor/check';
import RedisPubSub from '/imports/startup/server/redis';
import Breakouts from '/imports/api/breakouts';
export default function requestJoinURL(credentials, { breakoutId }) {
export default function requestJoinURL(credentials, { breakoutId, userId: userIdToInvite }) {
const REDIS_CONFIG = Meteor.settings.private.redis;
const CHANNEL = REDIS_CONFIG.channels.toAkkaApps;
@ -12,16 +12,19 @@ export default function requestJoinURL(credentials, { breakoutId }) {
check(meetingId, String);
check(requesterUserId, String);
check(requesterToken, String);
const userId = userIdToInvite || requesterUserId;
const Breakout = Breakouts.findOne({ breakoutId });
const BreakoutUser = Breakout.users.filter(user => user.userId === requesterUserId).shift();
if (BreakoutUser) return BreakoutUser.redirectToHtml5JoinURL;
const BreakoutUser = Breakout.users.filter(user => user.userId === userId).shift();
if (BreakoutUser) return null;
const eventName = 'RequestBreakoutJoinURLReqMsg';
return RedisPubSub.publishUserMessage(
CHANNEL, eventName, meetingId, requesterUserId,
{
meetingId,
breakoutId,
userId: requesterUserId,
userId,
},
);
}

View File

@ -0,0 +1,5 @@
import { Meteor } from 'meteor/meteor';
const Streamer = new Meteor.Streamer('videos');
export default Streamer;

View File

@ -0,0 +1,18 @@
import ExternalVideoStreamer from '/imports/api/external-videos';
import Users from '/imports/api/users';
import Logger from '/imports/startup/server/logger';
import './methods';
ExternalVideoStreamer.allowRead('all');
ExternalVideoStreamer.allowWrite('all');
const allowFromPresenter = (eventName, { userId }) => {
const user = Users.findOne({ userId });
const ret = user && user.presenter;
Logger.debug('ExternalVideo Streamer auth userid:', userId, ' event: ', eventName, ' suc: ', ret);
return ret || eventName === 'viewerJoined';
};
ExternalVideoStreamer.allowEmit(allowFromPresenter);

View File

@ -0,0 +1,8 @@
import { Meteor } from 'meteor/meteor';
import startWatchingExternalVideo from './methods/startWatchingExternalVideo';
import stopWatchingExternalVideo from './methods/stopWatchingExternalVideo';
Meteor.methods({
startWatchingExternalVideo,
stopWatchingExternalVideo,
});

View File

@ -0,0 +1,26 @@
import { Meteor } from 'meteor/meteor';
import { check } from 'meteor/check';
import Logger from '/imports/startup/server/logger';
import Meetings from '/imports/api/meetings';
import RedisPubSub from '/imports/startup/server/redis';
export default function startStream(credentials, options) {
const REDIS_CONFIG = Meteor.settings.private.redis;
const CHANNEL = REDIS_CONFIG.channels.toAkkaApps;
const EVENT_NAME = 'StartExternalVideoMsg';
const { meetingId, requesterUserId } = credentials;
const { externalVideoUrl } = options;
Logger.info(' user sharing a new youtube video: ', credentials);
check(meetingId, String);
check(requesterUserId, String);
check(externalVideoUrl, String);
Meetings.update({ meetingId }, { $set: { externalVideoUrl } });
const payload = { externalVideoUrl };
return RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload);
}

View File

@ -0,0 +1,23 @@
import { Meteor } from 'meteor/meteor';
import { check } from 'meteor/check';
import Logger from '/imports/startup/server/logger';
import Meetings from '/imports/api/meetings';
import RedisPubSub from '/imports/startup/server/redis';
export default function startStream(credentials) {
const REDIS_CONFIG = Meteor.settings.private.redis;
const CHANNEL = REDIS_CONFIG.channels.toAkkaApps;
const EVENT_NAME = 'StopExternalVideoMsg';
const { meetingId, requesterUserId } = credentials;
Logger.info(' user sharing a new youtube video: ', credentials);
check(meetingId, String);
check(requesterUserId, String);
Meetings.update({ meetingId }, { $set: { externalVideoUrl: null } });
const payload = {};
RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload);
}

View File

@ -11,20 +11,19 @@ export default function clearGroupChatMsg(meetingId, chatId) {
if (chatId) {
GroupChatMsg.remove({ meetingId, chatId }, () => {
Logger.info(`Cleared GroupChatMsg (${meetingId}, ${chatId})`);
const clearMsg = {
color: '0',
timestamp: Date.now(),
correlationId: `${PUBLIC_CHAT_SYSTEM_ID}-${Date.now()}`,
sender: {
id: PUBLIC_CHAT_SYSTEM_ID,
name: '',
},
message: CHAT_CLEAR_MESSAGE,
};
return addGroupChatMsg(meetingId, PUBLIC_GROUP_CHAT_ID, clearMsg);
});
const clearMsg = {
color: '0',
timestamp: Date.now(),
correlationId: `${PUBLIC_CHAT_SYSTEM_ID}-${Date.now()}`,
sender: {
id: PUBLIC_CHAT_SYSTEM_ID,
name: '',
},
message: CHAT_CLEAR_MESSAGE,
};
return addGroupChatMsg(meetingId, PUBLIC_GROUP_CHAT_ID, clearMsg);
}
if (meetingId) {

View File

@ -6,6 +6,7 @@ import handleMeetingDestruction from './handlers/meetingDestruction';
import handleMeetingLocksChange from './handlers/meetingLockChange';
import handleUserLockChange from './handlers/userLockChange';
import handleRecordingStatusChange from './handlers/recordingStatusChange';
import handleRecordingTimerChange from './handlers/recordingTimerChange';
import handleChangeWebcamOnlyModerator from './handlers/webcamOnlyModerator';
RedisPubSub.on('MeetingCreatedEvtMsg', handleMeetingCreation);
@ -15,4 +16,5 @@ RedisPubSub.on('MeetingDestroyedEvtMsg', handleMeetingDestruction);
RedisPubSub.on('LockSettingsInMeetingChangedEvtMsg', handleMeetingLocksChange);
RedisPubSub.on('UserLockedInMeetingEvtMsg', handleUserLockChange);
RedisPubSub.on('RecordingStatusChangedEvtMsg', handleRecordingStatusChange);
RedisPubSub.on('UpdateRecordingTimerEvtMsg', handleRecordingTimerChange);
RedisPubSub.on('WebcamsOnlyForModeratorChangedEvtMsg', handleChangeWebcamOnlyModerator);

View File

@ -0,0 +1,29 @@
import { check } from 'meteor/check';
import Meetings from '/imports/api/meetings';
import Logger from '/imports/startup/server/logger';
export default function handleRecordingStatusChange({ body }, meetingId) {
const { time } = body;
check(meetingId, String);
check(body, {
time: Number,
});
const selector = {
meetingId,
};
const modifier = {
$set: { 'recordProp.time': time },
};
const cb = (err) => {
if (err) {
Logger.error(`Changing recording time: ${err}`);
}
};
return Meetings.upsert(selector, modifier, cb);
}

View File

@ -1,5 +1,5 @@
import flat from 'flat';
import { check } from 'meteor/check';
import { check, Match } from 'meteor/check';
import Meetings from '/imports/api/meetings';
import Logger from '/imports/startup/server/logger';
@ -42,11 +42,11 @@ export default function addMeeting(meeting) {
modOnlyMessage: String,
welcomeMsgTemplate: String,
},
recordProp: {
recordProp: Match.ObjectIncluding({
allowStartStopRecording: Boolean,
autoStartRecording: Boolean,
record: Boolean,
},
record: Boolean
}),
password: {
viewerPass: String,
moderatorPass: String,

View File

@ -17,7 +17,7 @@ export default function addUserSettings(credentials, meetingId, userId, settings
'forceListenOnly',
'skipCheck',
'clientTitle',
'lockOnJoin', // NOT IMPLEMENTED YET
'lockOnJoin',
'askForFeedbackOnLogout',
// BRANDING
'displayBrandingArea',

View File

@ -20,4 +20,3 @@ RedisPubSub.on('GuestsWaitingForApprovalEvtMsg', handleGuestsWaitingForApproval)
RedisPubSub.on('GuestsWaitingApprovedEvtMsg', handleGuestApproved);
RedisPubSub.on('UserEjectedFromMeetingEvtMsg', handleUserEjected);
RedisPubSub.on('UserRoleChangedEvtMsg', handleChangeRole);

View File

@ -40,6 +40,7 @@ export default function handleValidateAuthToken({ body }, meetingId) {
$set: {
validated: valid,
approved: !waitForApproval,
loginTime: Date.now(),
},
};

View File

@ -4,6 +4,7 @@ import setEmojiStatus from './methods/setEmojiStatus';
import assignPresenter from './methods/assignPresenter';
import changeRole from './methods/changeRole';
import removeUser from './methods/removeUser';
import toggleUserLock from './methods/toggleUserLock';
Meteor.methods({
setEmojiStatus,
@ -11,4 +12,5 @@ Meteor.methods({
changeRole,
removeUser,
validateAuthToken,
toggleUserLock,
});

View File

@ -0,0 +1,29 @@
import { Meteor } from 'meteor/meteor';
import { check } from 'meteor/check';
import RedisPubSub from '/imports/startup/server/redis';
import Logger from '/imports/startup/server/logger';
export default function toggleUserLock(credentials, userId, lock) {
const REDIS_CONFIG = Meteor.settings.private.redis;
const CHANNEL = REDIS_CONFIG.channels.toAkkaApps;
const EVENT_NAME = 'LockUserInMeetingCmdMsg';
const { meetingId, requesterUserId: lockedBy } = credentials;
check(meetingId, String);
check(lockedBy, String);
check(userId, String);
check(lock, Boolean);
const payload = {
lockedBy,
userId,
lock,
};
Logger.verbose(`User ${lockedBy} updated lock status from ${userId} to ${lock}
in meeting ${meetingId}`);
return RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, lockedBy, payload);
}

View File

@ -78,7 +78,6 @@ export default function addUser(meetingId, user) {
roles: [ROLE_VIEWER.toLowerCase()],
sortName: user.name.trim().toLowerCase(),
color,
logTime: Date.now(),
},
flat(user),
),

View File

@ -45,11 +45,16 @@ class Base extends Component {
this.updateLoadingState = this.updateLoadingState.bind(this);
}
componentWillUpdate() {
const { approved } = this.props;
componentDidUpdate(prevProps) {
const { ejected, approved } = this.props;
const { loading } = this.state;
if (approved && loading) this.updateLoadingState(false);
if (prevProps.ejected || ejected) {
Session.set('codeError', '403');
Session.set('isMeetingEnded', true);
}
}
updateLoadingState(loading = false) {
@ -173,6 +178,7 @@ const BaseContainer = withTracker(() => {
const subscriptionsReady = subscriptionsHandlers.every(handler => handler.ready());
return {
approved: Users.findOne({ userId: Auth.userID, approved: true, guest: true }),
ejected: Users.findOne({ userId: Auth.userID, ejected: true }),
meetingEnded: Session.get('isMeetingEnded'),
locale,
subscriptionsReady,

View File

@ -1,9 +1,49 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { IntlProvider } from 'react-intl';
import { IntlProvider, addLocaleData } from 'react-intl';
import Settings from '/imports/ui/services/settings';
import LoadingScreen from '/imports/ui/components/loading-screen/component';
// currently supported locales.
import bg from 'react-intl/locale-data/bg';
import de from 'react-intl/locale-data/de';
import el from 'react-intl/locale-data/el';
import en from 'react-intl/locale-data/en';
import es from 'react-intl/locale-data/es';
import fa from 'react-intl/locale-data/fa';
import fr from 'react-intl/locale-data/fr';
import id from 'react-intl/locale-data/id';
import it from 'react-intl/locale-data/it';
import ja from 'react-intl/locale-data/ja';
import km from 'react-intl/locale-data/km';
import pl from 'react-intl/locale-data/pl';
import pt from 'react-intl/locale-data/pt';
import ru from 'react-intl/locale-data/ru';
import tr from 'react-intl/locale-data/tr';
import uk from 'react-intl/locale-data/uk';
import zh from 'react-intl/locale-data/zh';
addLocaleData([
...bg,
...de,
...el,
...en,
...es,
...fa,
...fr,
...id,
...it,
...ja,
...km,
...pl,
...pt,
...ru,
...tr,
...uk,
...zh,
]);
const propTypes = {
locale: PropTypes.string,
children: PropTypes.element.isRequired,
@ -29,14 +69,18 @@ class IntlStartup extends Component {
}
componentWillMount() {
this.fetchLocalizedMessages(this.props.locale);
const { locale } = this.props;
this.fetchLocalizedMessages(locale);
}
componentWillUpdate(nextProps) {
if (!this.state.fetching
&& this.state.normalizedLocale
&& nextProps.locale.toLowerCase() !== this.state.normalizedLocale.toLowerCase()) {
this.fetchLocalizedMessages(nextProps.locale);
const { fetching, normalizedLocale } = this.state;
const { locale } = nextProps;
if (!fetching
&& normalizedLocale
&& locale.toLowerCase() !== normalizedLocale.toLowerCase()) {
this.fetchLocalizedMessages(locale);
}
}
@ -69,9 +113,12 @@ class IntlStartup extends Component {
}
render() {
return this.state.fetching ? <LoadingScreen /> : (
<IntlProvider locale={DEFAULT_LANGUAGE} messages={this.state.messages}>
{this.props.children}
const { fetching, normalizedLocale, messages } = this.state;
const { children } = this.props;
return fetching ? <LoadingScreen /> : (
<IntlProvider locale={normalizedLocale} messages={messages}>
{children}
</IntlProvider>
);
}

View File

@ -52,6 +52,7 @@ WebApp.connectHandlers.use('/locale', (req, res) => {
messages = Object.assign(messages, JSON.parse(data));
normalizedLocale = locale;
} catch (e) {
Logger.error(`'Could not process locale ${locale}:${e}`);
// Getting here means the locale is not available on the files.
}
});
@ -71,6 +72,7 @@ WebApp.connectHandlers.use('/locales', (req, res) => {
name: Langmap[locale].nativeName,
}));
} catch (e) {
Logger.error(`'Could not process locales error: ${e}`);
// Getting here means the locale is not available on the files.
}

View File

@ -28,7 +28,7 @@ const makeEnvelope = (channel, eventName, header, body) => {
const makeDebugger = enabled => (message) => {
if (!enabled) return;
Logger.info(`REDIS: ${message}`);
Logger.debug(`REDIS: ${message}`);
};
class MettingMessageQueue {
@ -166,9 +166,6 @@ class RedisPubSub {
return;
}
// Please keep this log until the message handling is solid
console.warn(` ~~~~ REDIS RECEIVED: ${eventName} ${message}`);
const queueId = meetingId || NO_MEETING_ID;
if (!(queueId in this.mettingsQueues)) {
@ -232,9 +229,6 @@ class RedisPubSub {
const envelope = makeEnvelope(channel, eventName, header, payload);
// Please keep this log until the message handling is solid
console.warn(` ~~~~ REDIS PUBLISHING: ${envelope}`);
return this.pub.publish(channel, envelope, RedisPubSub.handlePublishError);
}
}

View File

@ -10,27 +10,24 @@ import DropdownList from '/imports/ui/components/dropdown/list/component';
import DropdownListItem from '/imports/ui/components/dropdown/list/item/component';
import PresentationUploaderContainer from '/imports/ui/components/presentation/presentation-uploader/container';
import { withModalMounter } from '/imports/ui/components/modal/service';
import getFromUserSettings from '/imports/ui/services/users-settings';
import withShortcutHelper from '/imports/ui/components/shortcut-help/service';
import BreakoutRoom from '../create-breakout-room/component';
import { styles } from '../styles';
import ActionBarService from '../service';
import ExternalVideoModal from '/imports/ui/components/external-video-player/modal/container';
const propTypes = {
isUserPresenter: PropTypes.bool.isRequired,
intl: intlShape.isRequired,
mountModal: PropTypes.func.isRequired,
isUserModerator: PropTypes.bool.isRequired,
allowStartStopRecording: PropTypes.bool.isRequired,
isRecording: PropTypes.bool.isRequired,
record: PropTypes.bool.isRequired,
toggleRecording: PropTypes.func.isRequired,
meetingIsBreakout: PropTypes.bool.isRequired,
hasBreakoutRoom: PropTypes.bool.isRequired,
createBreakoutRoom: PropTypes.func.isRequired,
meetingName: PropTypes.string.isRequired,
shortcuts: PropTypes.string.isRequired,
users: PropTypes.arrayOf(PropTypes.object).isRequired,
handleTakePresenter: PropTypes.func.isRequired,
};
const intlMessages = defineMessages({
@ -62,14 +59,6 @@ const intlMessages = defineMessages({
id: 'app.actionsBar.actionsDropdown.stopDesktopShareDesc',
description: 'adds context to stop desktop share option',
},
startRecording: {
id: 'app.actionsBar.actionsDropdown.startRecording',
description: 'start recording option',
},
stopRecording: {
id: 'app.actionsBar.actionsDropdown.stopRecording',
description: 'stop recording option',
},
pollBtnLabel: {
id: 'app.actionsBar.actionsDropdown.pollBtnLabel',
description: 'poll menu toggle button label',
@ -86,6 +75,22 @@ const intlMessages = defineMessages({
id: 'app.actionsBar.actionsDropdown.createBreakoutRoomDesc',
description: 'Description of create breakout room option',
},
invitationItem: {
id: 'app.invitation.title',
description: 'invitation to breakout title',
},
takePresenter: {
id: 'app.actionsBar.actionsDropdown.takePresenter',
description: 'Label for take presenter role option',
},
takePresenterDesc: {
id: 'app.actionsBar.actionsDropdown.takePresenterDesc',
description: 'Description of take presenter role option',
},
externalVideoLabel: {
id: 'app.actionsBar.actionsDropdown.shareExternalVideo',
description: 'Share external video button',
},
});
class ActionsDropdown extends Component {
@ -93,21 +98,17 @@ class ActionsDropdown extends Component {
super(props);
this.handlePresentationClick = this.handlePresentationClick.bind(this);
this.handleCreateBreakoutRoomClick = this.handleCreateBreakoutRoomClick.bind(this);
}
this.onCreateBreakouts = this.onCreateBreakouts.bind(this);
this.onInvitationUsers = this.onInvitationUsers.bind(this);
componentWillMount() {
this.presentationItemId = _.uniqueId('action-item-');
this.recordId = _.uniqueId('action-item-');
this.pollId = _.uniqueId('action-item-');
this.createBreakoutRoomId = _.uniqueId('action-item-');
}
this.takePresenterId = _.uniqueId('action-item-');
componentDidMount() {
if (Meteor.settings.public.allowOutsideCommands.toggleRecording
|| getFromUserSettings('outsideToggleRecording', false)) {
ActionBarService.connectRecordingObserver();
window.addEventListener('message', ActionBarService.processOutsideToggleRecording);
}
this.handlePresentationClick = this.handlePresentationClick.bind(this);
this.handleCreateBreakoutRoomClick = this.handleCreateBreakoutRoomClick.bind(this);
}
componentWillUpdate(nextProps) {
@ -118,17 +119,25 @@ class ActionsDropdown extends Component {
}
}
onCreateBreakouts() {
return this.handleCreateBreakoutRoomClick(false);
}
onInvitationUsers() {
return this.handleCreateBreakoutRoomClick(true);
}
getAvailableActions() {
const {
intl,
isUserPresenter,
isUserModerator,
allowStartStopRecording,
isRecording,
record,
toggleRecording,
allowExternalVideo,
meetingIsBreakout,
hasBreakoutRoom,
getUsersNotAssigned,
users,
handleTakePresenter,
} = this.props;
const {
@ -136,16 +145,26 @@ class ActionsDropdown extends Component {
pollBtnDesc,
presentationLabel,
presentationDesc,
startRecording,
stopRecording,
createBreakoutRoom,
createBreakoutRoomDesc,
invitationItem,
takePresenter,
takePresenterDesc,
} = intlMessages;
const {
formatMessage,
} = intl;
const canCreateBreakout = isUserModerator
&& !meetingIsBreakout
&& !hasBreakoutRoom;
const canInviteUsers = isUserModerator
&& !meetingIsBreakout
&& hasBreakoutRoom
&& getUsersNotAssigned(users).length;
return _.compact([
(isUserPresenter
? (
@ -160,7 +179,15 @@ class ActionsDropdown extends Component {
}}
/>
)
: null),
: (
<DropdownListItem
icon="presentation"
label={formatMessage(takePresenter)}
description={formatMessage(takePresenterDesc)}
key={this.takePresenterId}
onClick={() => handleTakePresenter()}
/>
)),
(isUserPresenter
? (
<DropdownListItem
@ -173,51 +200,72 @@ class ActionsDropdown extends Component {
/>
)
: null),
(record && isUserModerator && allowStartStopRecording
(isUserPresenter && allowExternalVideo
? (
<DropdownListItem
icon="record"
label={formatMessage(isRecording
? stopRecording : startRecording)}
description={formatMessage(isRecording
? stopRecording : startRecording)}
key={this.recordId}
onClick={toggleRecording}
icon="video"
label={intl.formatMessage(intlMessages.externalVideoLabel)}
description="External Video"
key="external-video"
onClick={this.handleExternalVideoClick}
/>
)
: null),
(isUserModerator && !meetingIsBreakout && !hasBreakoutRoom
(canCreateBreakout
? (
<DropdownListItem
icon="rooms"
label={formatMessage(createBreakoutRoom)}
description={formatMessage(createBreakoutRoomDesc)}
key={this.createBreakoutRoomId}
onClick={this.handleCreateBreakoutRoomClick}
onClick={this.onCreateBreakouts}
/>
)
: null),
(canInviteUsers
? (
<DropdownListItem
icon="rooms"
label={formatMessage(invitationItem)}
key={this.createBreakoutRoomId}
onClick={this.onInvitationUsers}
/>
)
: null),
]);
}
handleExternalVideoClick = () => {
this.props.mountModal(<ExternalVideoModal />);
}
handlePresentationClick() {
const { mountModal } = this.props;
mountModal(<PresentationUploaderContainer />);
}
handleCreateBreakoutRoomClick() {
handleCreateBreakoutRoomClick(isInvitation) {
const {
createBreakoutRoom,
mountModal,
meetingName,
users,
getUsersNotAssigned,
getBreakouts,
sendInvitation,
} = this.props;
mountModal(
<BreakoutRoom
createBreakoutRoom={createBreakoutRoom}
meetingName={meetingName}
users={users}
{...{
createBreakoutRoom,
meetingName,
users,
getUsersNotAssigned,
isInvitation,
getBreakouts,
sendInvitation,
}}
/>,
);
}

View File

@ -4,7 +4,9 @@ import { styles } from './styles.scss';
import DesktopShare from './desktop-share/component';
import ActionsDropdown from './actions-dropdown/component';
import AudioControlsContainer from '../audio/audio-controls/container';
import JoinVideoOptionsContainer from '../video-provider/video-menu/container';
import JoinVideoOptionsContainer from '../video-provider/video-button/container';
import PresentationOptionsContainer from './presentation-options/component';
class ActionsBar extends React.PureComponent {
render() {
@ -25,6 +27,12 @@ class ActionsBar extends React.PureComponent {
hasBreakoutRoom,
meetingName,
users,
isLayoutSwapped,
toggleSwapLayout,
getUsersNotAssigned,
sendInvitation,
getBreakouts,
handleTakePresenter,
} = this.props;
const {
@ -34,6 +42,8 @@ class ActionsBar extends React.PureComponent {
} = recordSettingsList;
const actionBarClasses = {};
const { enableExternalVideo } = Meteor.settings.public.app;
actionBarClasses[styles.centerWithActions] = isUserPresenter;
actionBarClasses[styles.center] = true;
@ -44,6 +54,7 @@ class ActionsBar extends React.PureComponent {
isUserPresenter,
isUserModerator,
allowStartStopRecording,
allowExternalVideo: enableExternalVideo,
isRecording,
record,
toggleRecording,
@ -52,6 +63,10 @@ class ActionsBar extends React.PureComponent {
hasBreakoutRoom,
meetingName,
users,
getUsersNotAssigned,
sendInvitation,
getBreakouts,
handleTakePresenter,
}}
/>
</div>
@ -78,6 +93,16 @@ class ActionsBar extends React.PureComponent {
}}
/>
</div>
<div className={styles.right}>
{ isLayoutSwapped
? (
<PresentationOptionsContainer
toggleSwapLayout={toggleSwapLayout}
/>
)
: null
}
</div>
</div>
);
}

View File

@ -3,11 +3,14 @@ import { withTracker } from 'meteor/react-meteor-data';
import getFromUserSettings from '/imports/ui/services/users-settings';
import Meetings from '/imports/api/meetings';
import Auth from '/imports/ui/services/auth';
import { makeCall } from '/imports/ui/services/api';
import ActionsBar from './component';
import Service from './service';
import VideoService from '../video-provider/service';
import { shareScreen, unshareScreen, isVideoBroadcasting } from '../screenshare/service';
import MediaService, { getSwapLayout } from '../media/service';
const ActionsBarContainer = props => <ActionsBar {...props} />;
export default withTracker(() => {
@ -23,6 +26,7 @@ export default withTracker(() => {
},
});
return {
isUserPresenter: Service.isUserPresenter(),
isUserModerator: Service.isUserModerator(),
@ -40,5 +44,11 @@ export default withTracker(() => {
hasBreakoutRoom: Service.hasBreakoutRoom(),
meetingName: Service.meetingName(),
users: Service.users(),
isLayoutSwapped: getSwapLayout(),
toggleSwapLayout: MediaService.toggleSwapLayout,
sendInvitation: Service.sendInvitation,
getBreakouts: Service.getBreakouts,
getUsersNotAssigned: Service.getUsersNotAssigned,
handleTakePresenter: Service.takePresenterRole,
};
})(ActionsBarContainer);

View File

@ -77,6 +77,14 @@ const intlMessages = defineMessages({
id: 'app.audio.backLabel',
description: 'Back label',
},
invitationTitle: {
id: 'app.invitation.title',
description: 'isInvitationto breakout title',
},
invitationConfirm: {
id: 'app.invitation.confirm',
description: 'Invitation to breakout confirm button label',
},
});
const MIN_BREAKOUT_ROOMS = 2;
const MAX_BREAKOUT_ROOMS = 8;
@ -94,6 +102,7 @@ class BreakoutRoom extends Component {
this.setFreeJoin = this.setFreeJoin.bind(this);
this.getUserByRoom = this.getUserByRoom.bind(this);
this.onAssignRandomly = this.onAssignRandomly.bind(this);
this.onInviteBreakout = this.onInviteBreakout.bind(this);
this.renderUserItemByRoom = this.renderUserItemByRoom.bind(this);
this.renderRoomsGrid = this.renderRoomsGrid.bind(this);
this.renderBreakoutForm = this.renderBreakoutForm.bind(this);
@ -103,7 +112,9 @@ class BreakoutRoom extends Component {
this.renderMobile = this.renderMobile.bind(this);
this.renderButtonSetLevel = this.renderButtonSetLevel.bind(this);
this.renderSelectUserScreen = this.renderSelectUserScreen.bind(this);
this.renderTitle = this.renderTitle.bind(this);
this.handleDismiss = this.handleDismiss.bind(this);
this.setInvitationConfig = this.setInvitationConfig.bind(this);
this.state = {
numberOfRooms: MIN_BREAKOUT_ROOMS,
@ -116,10 +127,18 @@ class BreakoutRoom extends Component {
preventClosing: true,
valid: true,
};
this.breakoutFormId = _.uniqueId('breakout-form-');
this.freeJoinId = _.uniqueId('free-join-check-');
this.btnLevelId = _.uniqueId('btn-set-level-');
}
componentDidMount() {
const { isInvitation } = this.props;
this.setRoomUsers();
if (isInvitation) {
this.setInvitationConfig();
}
}
componentDidUpdate(prevProps, prevstate) {
@ -140,7 +159,7 @@ class BreakoutRoom extends Component {
freeJoin,
} = this.state;
if (users.length === this.getUserByRoom(0).length) {
if (users.length === this.getUserByRoom(0).length && !freeJoin) {
this.setState({ valid: false });
return;
}
@ -160,17 +179,46 @@ class BreakoutRoom extends Component {
Session.set('isUserListOpen', true);
}
onInviteBreakout() {
const { getBreakouts, sendInvitation } = this.props;
const { users } = this.state;
const breakouts = getBreakouts();
if (users.length === this.getUserByRoom(0).length) {
this.setState({ valid: false });
return;
}
breakouts.forEach((breakout) => {
const { breakoutId } = breakout;
const breakoutUsers = this.getUserByRoom(breakout.sequence);
breakoutUsers.forEach(user => sendInvitation(breakoutId, user.userId));
});
this.setState({ preventClosing: false });
}
onAssignRandomly() {
const { numberOfRooms } = this.state;
return this.getUserByRoom(0)
.filter(user => !user.isModerator)
.forEach(user => this.changeUserRoom(user.userId, Math.floor(Math.random() * (numberOfRooms) + 1)));
}
setInvitationConfig() {
const { getBreakouts } = this.props;
this.setState({
numberOfRooms: getBreakouts().length,
formFillLevel: 2,
});
}
setRoomUsers() {
const { users } = this.props;
const roomUsers = users.map(user => ({
const { users, getUsersNotAssigned } = this.props;
const roomUsers = getUsersNotAssigned(users).map(user => ({
userId: user.userId,
userName: user.name,
isModerator: user.moderator,
room: 0,
}));
@ -208,7 +256,7 @@ class BreakoutRoom extends Component {
changeUserRoom(userId, room) {
const { users } = this.state;
const idxUser = users.findIndex(user => user.userId === userId);
users[idxUser].room = room;
this.setState({ users });
@ -251,7 +299,7 @@ class BreakoutRoom extends Component {
};
return (
<div className={styles.boxContainer}>
<div className={styles.boxContainer} key="rooms-grid-">
<label htmlFor="BreakoutRoom" className={!valid ? styles.changeToWarn : null}>
<p
className={styles.freeJoinLabel}
@ -277,7 +325,7 @@ class BreakoutRoom extends Component {
<div className={styles.breakoutBox} onDrop={drop(value)} onDragOver={allowDrop}>
{this.renderUserItemByRoom(value)}
</div>
</label>
</label>
))
}
</div>
@ -285,13 +333,18 @@ class BreakoutRoom extends Component {
}
renderBreakoutForm() {
const { intl } = this.props;
const {
intl,
isInvitation,
} = this.props;
const {
numberOfRooms,
durationTime,
} = this.state;
if (isInvitation) return null;
return (
<div className={styles.breakoutSettings}>
<div className={styles.breakoutSettings} key={this.breakoutFormId}>
<label htmlFor="numberOfRooms">
<p className={styles.labelText}>{intl.formatMessage(intlMessages.numberOfRooms)}</p>
<select
@ -341,7 +394,13 @@ class BreakoutRoom extends Component {
</span>
</div>
</label>
<span className={styles.randomText} role="button" onClick={this.onAssignRandomly}>{intl.formatMessage(intlMessages.randomlyAssign)}</span>
<span
className={styles.randomText}
role="button"
onClick={this.onAssignRandomly}
>
{intl.formatMessage(intlMessages.randomlyAssign)}
</span>
</div>
);
}
@ -363,10 +422,11 @@ class BreakoutRoom extends Component {
}
renderFreeJoinCheck() {
const { intl } = this.props;
const { intl, isInvitation } = this.props;
if (isInvitation) return null;
const { freeJoin } = this.state;
return (
<label htmlFor="freeJoinCheckbox" className={styles.freeJoinLabel}>
<label htmlFor="freeJoinCheckbox" className={styles.freeJoinLabel} key={this.freeJoinId}>
<input
type="checkbox"
className={styles.freeJoinCheckbox}
@ -416,7 +476,7 @@ class BreakoutRoom extends Component {
}
renderRoomSortList() {
const { intl } = this.props;
const { intl, isInvitation } = this.props;
const { numberOfRooms } = this.state;
const onClick = roomNumber => this.setState({ formFillLevel: 3, roomSelected: roomNumber });
return (
@ -440,7 +500,7 @@ class BreakoutRoom extends Component {
))
}
</span>
{this.renderButtonSetLevel(1, intl.formatMessage(intlMessages.backLabel))}
{ isInvitation || this.renderButtonSetLevel(1, intl.formatMessage(intlMessages.backLabel))}
</div>
);
}
@ -478,12 +538,22 @@ class BreakoutRoom extends Component {
size="lg"
label={label}
onClick={() => this.setState({ formFillLevel: level })}
key={this.btnLevelId}
/>
);
}
render() {
renderTitle() {
const { intl } = this.props;
return (
<p className={styles.subTitle}>
{intl.formatMessage(intlMessages.breakoutRoomDesc)}
</p>
);
}
render() {
const { intl, isInvitation } = this.props;
const { preventClosing } = this.state;
const BROWSER_RESULTS = browser();
@ -491,11 +561,17 @@ class BreakoutRoom extends Component {
return (
<Modal
title={intl.formatMessage(intlMessages.breakoutRoomTitle)}
title={
isInvitation
? intl.formatMessage(intlMessages.invitationTitle)
: intl.formatMessage(intlMessages.breakoutRoomTitle)
}
confirm={
{
label: intl.formatMessage(intlMessages.confirmButton),
callback: this.onCreateBreakouts,
label: isInvitation
? intl.formatMessage(intlMessages.invitationConfirm)
: intl.formatMessage(intlMessages.confirmButton),
callback: isInvitation ? this.onInviteBreakout : this.onCreateBreakouts,
}
}
dismiss={{
@ -505,9 +581,7 @@ class BreakoutRoom extends Component {
preventClosing={preventClosing}
>
<div className={styles.content}>
<p className={styles.subTitle}>
{intl.formatMessage(intlMessages.breakoutRoomDesc)}
</p>
{isInvitation || this.renderTitle()}
{isMobileBrowser ? this.renderMobile() : this.renderDesktop()}
</div>
</Modal>

View File

@ -0,0 +1,39 @@
import React from 'react';
import PropTypes from 'prop-types';
import { defineMessages, injectIntl, intlShape } from 'react-intl';
import Button from '/imports/ui/components/button/component';
import { styles } from '../styles';
const propTypes = {
intl: intlShape.isRequired,
toggleSwapLayout: PropTypes.func.isRequired,
};
const intlMessages = defineMessages({
restorePresentationLabel: {
id: 'app.actionsBar.actionsDropdown.restorePresentationLabel',
description: 'Restore Presentation option label',
},
restorePresentationDesc: {
id: 'app.actionsBar.actionsDropdown.restorePresentationDesc',
description: 'button to restore presentation after it has been closed',
},
});
const PresentationOptionsContainer = ({ intl, toggleSwapLayout }) => (
<Button
className={styles.button}
icon="presentation"
label={intl.formatMessage(intlMessages.restorePresentationLabel)}
description={intl.formatMessage(intlMessages.restorePresentationDesc)}
color="primary"
hideLabel
circle
size="lg"
onClick={toggleSwapLayout}
id="restore-presentation"
/>
);
PresentationOptionsContainer.propTypes = propTypes;
export default injectIntl(PresentationOptionsContainer);

View File

@ -4,32 +4,21 @@ import { makeCall } from '/imports/ui/services/api';
import Meetings from '/imports/api/meetings';
import Breakouts from '/imports/api/breakouts';
const processOutsideToggleRecording = (e) => {
switch (e.data) {
case 'c_record': {
makeCall('toggleRecording');
break;
}
case 'c_recording_status': {
const recordingState = Meetings.findOne({ meetingId: Auth.meetingID }).recordProp.recording;
const recordingMessage = recordingState ? 'recordingStarted' : 'recordingStopped';
this.window.parent.postMessage({ response: recordingMessage }, '*');
break;
}
default: {
// console.log(e.data);
}
}
};
const connectRecordingObserver = () => {
// notify on load complete
this.window.parent.postMessage({ response: 'readyToConnect' }, '*');
const getBreakouts = () => Breakouts.find({ parentMeetingId: Auth.meetingID })
.fetch()
.sort((a, b) => a.sequence - b.sequence);
const getUsersNotAssigned = (users) => {
const breakouts = getBreakouts();
const breakoutUsers = breakouts
.reduce((acc, value) => [...acc, ...value.users], [])
.map(u => u.userId);
return users.filter(u => !breakoutUsers.includes(u.intId));
};
const takePresenterRole = () => makeCall('assignPresenter', Auth.userID);
export default {
connectRecordingObserver: () => connectRecordingObserver(),
isUserPresenter: () => Users.findOne({ userId: Auth.userID }).presenter,
isUserModerator: () => Users.findOne({ userId: Auth.userID }).moderator,
recordSettingsList: () => Meetings.findOne({ meetingId: Auth.meetingID }).recordProp,
@ -38,6 +27,9 @@ export default {
users: () => Users.find({ connectionStatus: 'online' }).fetch(),
hasBreakoutRoom: () => Breakouts.find({ parentMeetingId: Auth.meetingID }).fetch().length > 0,
toggleRecording: () => makeCall('toggleRecording'),
processOutsideToggleRecording: arg => processOutsideToggleRecording(arg),
createBreakoutRoom: (numberOfRooms, durationInMinutes, freeJoin = true, record = false) => makeCall('createBreakoutRoom', numberOfRooms, durationInMinutes, freeJoin, record),
sendInvitation: (breakoutId, userId) => makeCall('requestJoinURL', { breakoutId, userId }),
getBreakouts,
getUsersNotAssigned,
takePresenterRole,
};

View File

@ -2,13 +2,15 @@
.actionsbar,
.left,
.center {
.center,
.right {
display: flex;
flex-direction: row;
}
.left,
.center {
.center,
.right {
flex: 1;
justify-content: center;
@ -29,6 +31,12 @@
}
}
.right {
position: absolute;
bottom: var(--sm-padding-x);
right: var(--sm-padding-x);
}
.centerWithActions {
@include mq($xsmall-only) {
justify-content: flex-end;
@ -57,4 +65,4 @@
span:hover {
border: 1.5px solid rgba(255,255,255, .5) !important;
}
}
}

View File

@ -15,6 +15,9 @@ import ChatAlertContainer from '../chat/alert/container';
import { styles } from './styles';
const MOBILE_MEDIA = 'only screen and (max-width: 40em)';
const APP_CONFIG = Meteor.settings.public.app;
const DESKTOP_FONT_SIZE = APP_CONFIG.desktopFontSize;
const MOBILE_FONT_SIZE = APP_CONFIG.mobileFontSize;
const intlMessages = defineMessages({
userListLabel: {
@ -36,7 +39,6 @@ const intlMessages = defineMessages({
});
const propTypes = {
fontSize: PropTypes.string,
navbar: PropTypes.element,
sidebar: PropTypes.element,
media: PropTypes.element,
@ -49,7 +51,6 @@ const propTypes = {
};
const defaultProps = {
fontSize: '16px',
navbar: null,
sidebar: null,
media: null,
@ -70,13 +71,14 @@ class App extends Component {
}
componentDidMount() {
const { locale, fontSize } = this.props;
const { locale } = this.props;
const BROWSER_RESULTS = browser();
const isMobileBrowser = BROWSER_RESULTS.mobile || BROWSER_RESULTS.os.includes('Android');
Modal.setAppElement('#app');
document.getElementsByTagName('html')[0].lang = locale;
document.getElementsByTagName('html')[0].style.fontSize = fontSize;
document.getElementsByTagName('html')[0].style.fontSize = isMobileBrowser ? MOBILE_FONT_SIZE : DESKTOP_FONT_SIZE;
const BROWSER_RESULTS = browser();
const body = document.getElementsByTagName('body')[0];
if (BROWSER_RESULTS && BROWSER_RESULTS.name) {
body.classList.add(`browser-${BROWSER_RESULTS.name}`);
@ -192,14 +194,14 @@ class App extends Component {
render() {
const {
customStyle, customStyleUrl, micsLocked,
customStyle, customStyleUrl, openPanel,
} = this.props;
return (
<main className={styles.main}>
<NotificationsBarContainer />
<section className={styles.wrapper}>
<div className={styles.content}>
<div className={openPanel ? styles.content : styles.noPanelContent}>
{this.renderNavBar()}
{this.renderMedia()}
{this.renderActionsBar()}
@ -209,7 +211,7 @@ class App extends Component {
</section>
<PollingContainer />
<ModalContainer />
{micsLocked ? null : <AudioContainer />}
<AudioContainer />
<ToastContainer />
<ChatAlertContainer />
{customStyleUrl ? <link rel="stylesheet" type="text/css" href={customStyleUrl} /> : null}

View File

@ -116,7 +116,6 @@ export default injectIntl(withModalMounter(withTracker(({ intl, baseControls })
chatIsOpen: Session.equals('openPanel', 'chat'),
openPanel: Session.get('openPanel'),
userListIsOpen: !Session.equals('openPanel', ''),
micsLocked: (currentUserIsLocked && meeting.lockSettingsProp.disableMic),
};
})(AppContainer)));

View File

@ -9,6 +9,7 @@
--bars-padding: calc(var(--lg-padding-x) - .45rem); // -.45 so user-list and chat title is aligned with the presentation title
--userlist-handle-width: 5px; // 5px so user-list and chat resize handle render as the same size
--poll-pane-min-width: 20em;
--panel-margin-left: 0.1em;
}
.main {
@ -57,7 +58,7 @@
}
}
.content {
.content, .noPanelContent {
@extend %full-page;
order: 3;
@ -89,13 +90,16 @@
}
}
.content{
margin-left: var(--panel-margin-left);
}
.userList {
@extend %full-page;
@extend %text-elipsis;
z-index: 2;
overflow: visible;
overflow: visible;
order: 1;
@include mq($small-only) {
@ -123,6 +127,7 @@
.poll,
.breakoutRoom,
.note,
.chat {
@extend %full-page;
@ -176,7 +181,7 @@
order: 2;
flex-direction: row;
position: relative;
margin-left: var(--panel-margin-right);
@include mq($portrait) {
flex-direction: column;
}

View File

@ -44,12 +44,7 @@ export default withModalMounter(withTracker(({ mountModal }) => ({
glow: Service.isTalking() && !Service.isMuted(),
handleToggleMuteMicrophone: () => Service.toggleMuteMicrophone(),
handleJoinAudio: () => {
const meeting = Meetings.findOne({ meetingId: Auth.meetingID });
const currentUser = Users.findOne({ userId: Auth.userID });
const currentUserIsLocked = mapUser(currentUser).isLocked;
const micsLocked = (currentUserIsLocked && meeting.lockSettingsProp.disableMic);
return micsLocked ? Service.joinListenOnly() : mountModal(<AudioModalContainer />);
return Service.isConnected() ? Service.joinListenOnly() : mountModal(<AudioModalContainer />);
},
handleLeaveAudio: () => Service.exitAudio(),
}))(AudioControlsContainer));

View File

@ -1,6 +1,6 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import ModalBase from '/imports/ui/components/modal/base/component';
import Modal from '/imports/ui/components/modal/simple/component';
import Button from '/imports/ui/components/button/component';
import { defineMessages, injectIntl, intlShape } from 'react-intl';
import { styles } from './styles';
@ -9,6 +9,7 @@ import AudioSettings from '../audio-settings/component';
import EchoTest from '../echo-test/component';
import Help from '../help/component';
const propTypes = {
intl: intlShape.isRequired,
closeModal: PropTypes.func.isRequired,
@ -97,15 +98,7 @@ class AudioModal extends Component {
hasError: false,
};
const {
intl,
closeModal,
joinEchoTest,
exitAudio,
leaveEchoTest,
changeInputDevice,
changeOutputDevice,
} = props;
const { intl } = props;
this.handleGoToAudioOptions = this.handleGoToAudioOptions.bind(this);
this.handleGoToAudioSettings = this.handleGoToAudioSettings.bind(this);
@ -114,12 +107,6 @@ class AudioModal extends Component {
this.handleJoinMicrophone = this.handleJoinMicrophone.bind(this);
this.handleJoinListenOnly = this.handleJoinListenOnly.bind(this);
this.skipAudioOptions = this.skipAudioOptions.bind(this);
this.closeModal = closeModal;
this.joinEchoTest = joinEchoTest;
this.exitAudio = exitAudio;
this.leaveEchoTest = leaveEchoTest;
this.changeInputDevice = changeInputDevice;
this.changeOutputDevice = changeOutputDevice;
this.contents = {
echoTest: {
@ -142,6 +129,7 @@ class AudioModal extends Component {
joinFullAudioImmediately,
joinFullAudioEchoTest,
forceListenOnlyAttendee,
audioLocked,
} = this.props;
if (joinFullAudioImmediately) {
@ -152,7 +140,7 @@ class AudioModal extends Component {
this.handleGoToEchoTest();
}
if (forceListenOnlyAttendee) {
if (forceListenOnlyAttendee || audioLocked) {
this.handleJoinListenOnly();
}
}
@ -160,10 +148,11 @@ class AudioModal extends Component {
componentWillUnmount() {
const {
isEchoTest,
exitAudio,
} = this.props;
if (isEchoTest) {
this.exitAudio();
exitAudio();
}
}
@ -175,7 +164,8 @@ class AudioModal extends Component {
}
handleGoToAudioSettings() {
this.leaveEchoTest().then(() => {
const { leaveEchoTest } = this.props;
leaveEchoTest().then(() => {
this.setState({
content: 'settings',
});
@ -199,13 +189,14 @@ class AudioModal extends Component {
const {
inputDeviceId,
outputDeviceId,
joinEchoTest,
} = this.props;
this.setState({
hasError: false,
});
return this.joinEchoTest().then(() => {
return joinEchoTest().then(() => {
console.log(inputDeviceId, outputDeviceId);
this.setState({
content: 'echoTest',
@ -260,10 +251,10 @@ class AudioModal extends Component {
return (
isConnecting ||
forceListenOnlyAttendee ||
joinFullAudioImmediately ||
joinFullAudioEchoTest
isConnecting
|| forceListenOnlyAttendee
|| joinFullAudioImmediately
|| joinFullAudioEchoTest
) && !content && !hasError;
}
@ -276,28 +267,34 @@ class AudioModal extends Component {
audioLocked,
} = this.props;
const showMicrophone = forceListenOnlyAttendee || audioLocked;
return (
<span className={styles.audioOptions}>
{!forceListenOnlyAttendee ?
<Button
className={styles.audioBtn}
label={intl.formatMessage(intlMessages.microphoneLabel)}
icon="unmute"
circle
size="jumbo"
disabled={audioLocked}
onClick={skipCheck ? this.handleJoinMicrophone : this.handleGoToEchoTest}
/>
{!showMicrophone
? (
<Button
className={styles.audioBtn}
label={intl.formatMessage(intlMessages.microphoneLabel)}
icon="unmute"
circle
size="jumbo"
disabled={audioLocked}
onClick={skipCheck ? this.handleJoinMicrophone : this.handleGoToEchoTest}
/>
)
: null}
{listenOnlyMode ?
<Button
className={styles.audioBtn}
label={intl.formatMessage(intlMessages.listenOnlyLabel)}
icon="listen"
circle
size="jumbo"
onClick={this.handleJoinListenOnly}
/>
{listenOnlyMode
? (
<Button
className={styles.audioBtn}
label={intl.formatMessage(intlMessages.listenOnlyLabel)}
icon="listen"
circle
size="jumbo"
onClick={this.handleJoinListenOnly}
/>
)
: null}
</span>
);
@ -318,7 +315,8 @@ class AudioModal extends Component {
<div className={styles.warning}>!</div>
<h4 className={styles.main}>{intl.formatMessage(intlMessages.iOSError)}</h4>
<div className={styles.text}>{intl.formatMessage(intlMessages.iOSErrorDescription)}</div>
<div className={styles.text}>{intl.formatMessage(intlMessages.iOSErrorRecommendation)}
<div className={styles.text}>
{intl.formatMessage(intlMessages.iOSErrorRecommendation)}
</div>
</div>);
}
@ -326,9 +324,9 @@ class AudioModal extends Component {
return (
<div className={styles.connecting} role="alert">
<span>
{!isEchoTest ?
intl.formatMessage(intlMessages.connecting) :
intl.formatMessage(intlMessages.connectingEchoTest)
{!isEchoTest
? intl.formatMessage(intlMessages.connecting)
: intl.formatMessage(intlMessages.connectingEchoTest)
}
</span>
<span className={styles.connectingAnimation} />
@ -354,15 +352,18 @@ class AudioModal extends Component {
isEchoTest,
inputDeviceId,
outputDeviceId,
joinEchoTest,
changeInputDevice,
changeOutputDevice,
} = this.props;
return (
<AudioSettings
handleBack={this.handleGoToAudioOptions}
handleRetry={this.handleRetryGoToEchoTest}
joinEchoTest={this.joinEchoTest}
changeInputDevice={this.changeInputDevice}
changeOutputDevice={this.changeOutputDevice}
joinEchoTest={joinEchoTest}
changeInputDevice={changeInputDevice}
changeOutputDevice={changeOutputDevice}
isConnecting={isConnecting}
isConnected={isConnected}
isEchoTest={isEchoTest}
@ -385,6 +386,7 @@ class AudioModal extends Component {
intl,
showPermissionsOvelay,
isIOSChrome,
closeModal,
} = this.props;
const { content } = this.state;
@ -392,40 +394,37 @@ class AudioModal extends Component {
return (
<span>
{showPermissionsOvelay ? <PermissionsOverlay /> : null}
<ModalBase
<Modal
overlayClassName={styles.overlay}
className={styles.modal}
onRequestClose={this.closeModal}
onRequestClose={closeModal}
hideBorder
>
{!this.skipAudioOptions() ?
{!this.skipAudioOptions()
<header
data-test="audioModalHeader"
className={styles.header}
>{
isIOSChrome ? null :
<h3 className={styles.title}>
{content ?
this.contents[content].title :
intl.formatMessage(intlMessages.audioChoiceLabel)}
</h3>
? (
<header
data-test="audioModalHeader"
className={styles.header}
>
{
isIOSChrome ? null
: (
<h3 className={styles.title}>
{content
? this.contents[content].title
: intl.formatMessage(intlMessages.audioChoiceLabel)}
</h3>
)
}
<Button
data-test="modalBaseCloseButton"
className={styles.closeBtn}
label={intl.formatMessage(intlMessages.closeLabel)}
icon="close"
size="md"
hideLabel
onClick={this.closeModal}
/>
</header>
</header>
)
: null
}
<div className={styles.content}>
{this.renderContent()}
</div>
</ModalBase>
</Modal>
</span>
);
}

View File

@ -118,6 +118,7 @@
text-align: center;
font-weight: 400;
font-size: 1.3rem;
color: var(--color-background);
white-space: normal;
@include mq($small-only) {

View File

@ -2,7 +2,7 @@
.testAudioBtn {
--hover-color: #0c5cb2;
margin: 0 !important;
background-color: transparent;
color: var(--color-primary);
font-weight: normal;
@ -16,6 +16,7 @@
&:hover,
&:focus,
&:active {
border: none;
background-color: transparent !important;
color: var(--hover-color) !important;
i {

View File

@ -65,6 +65,7 @@ class AuthenticatedHandler extends Component {
}
componentDidMount() {
if (Session.get('codeError')) return this.changeState(true);
AuthenticatedHandler.authenticatedRouteHandler((value, error) => {
if (error) AuthenticatedHandler.setError(error);
this.changeState(true);

View File

@ -65,19 +65,35 @@ class BreakoutRoom extends Component {
}
componentDidUpdate() {
const { breakoutRoomUser } = this.props;
if (this.state.waiting && !this.state.generated) {
const breakoutUser = breakoutRoomUser(this.state.requestedBreakoutId);
const {
breakoutRoomUser,
breakoutRooms,
closeBreakoutPanel,
} = this.props;
const {
waiting,
generated,
requestedBreakoutId,
} = this.state;
if (breakoutRooms.length <= 0) closeBreakoutPanel();
if (waiting && !generated) {
const breakoutUser = breakoutRoomUser(requestedBreakoutId);
if (!breakoutUser) return;
if (breakoutUser.redirectToHtml5JoinURL !== '') {
_.delay(() => this.setState({ generated: true, waiting: false }), 1000);
}
}
}
getBreakoutURL(breakoutId) {
const { requestJoinURL, breakoutRoomUser } = this.props;
const { waiting } = this.state;
const hasUser = breakoutRoomUser(breakoutId);
if (!hasUser && !this.state.waiting) {
if (!hasUser && !waiting) {
this.setState(
{ waiting: true, requestedBreakoutId: breakoutId },
() => requestJoinURL(breakoutId),
@ -96,6 +112,7 @@ class BreakoutRoom extends Component {
transferToBreakout(breakoutId);
this.setState({ joinedAudioOnly: true, breakoutId });
}
returnBackToMeeeting(breakoutId) {
const { transferUserToMeeting, meetingId } = this.props;
transferUserToMeeting(breakoutId, meetingId);
@ -119,9 +136,9 @@ class BreakoutRoom extends Component {
const moderatorJoinedAudio = isMicrophoneUser && isModerator;
const disable = waiting && requestedBreakoutId !== breakoutId;
const audioAction = joinedAudioOnly ?
() => this.returnBackToMeeeting(breakoutId) :
() => this.transferUserToBreakoutRoom(breakoutId);
const audioAction = joinedAudioOnly
? () => this.returnBackToMeeeting(breakoutId)
: () => this.transferUserToBreakoutRoom(breakoutId);
return (
<div className={styles.breakoutActions}>
<Button
@ -133,18 +150,17 @@ class BreakoutRoom extends Component {
className={styles.joinButton}
/>
{
moderatorJoinedAudio ?
[
moderatorJoinedAudio
? [
('|'),
(
<Button
label={
moderatorJoinedAudio &&
stateBreakoutId === breakoutId &&
joinedAudioOnly
?
intl.formatMessage(intlMessages.breakoutReturnAudio) :
intl.formatMessage(intlMessages.breakoutJoinAudio)
moderatorJoinedAudio
&& stateBreakoutId === breakoutId
&& joinedAudioOnly
? intl.formatMessage(intlMessages.breakoutReturnAudio)
: intl.formatMessage(intlMessages.breakoutJoinAudio)
}
className={styles.button}
onClick={audioAction}
@ -163,10 +179,15 @@ class BreakoutRoom extends Component {
intl,
} = this.props;
const {
waiting,
requestedBreakoutId,
} = this.state;
const roomItems = breakoutRooms.map(item => (
<div className={styles.content} key={`breakoutRoomList-${item.breakoutId}`}>
<span>{intl.formatMessage(intlMessages.breakoutRoom, item.sequence.toString())}</span>
{this.state.waiting && this.state.requestedBreakoutId === item.breakoutId ? (
{waiting && requestedBreakoutId === item.breakoutId ? (
<span>
{intl.formatMessage(intlMessages.generatingURL)}
<span className={styles.connectingAnimation} />
@ -192,13 +213,12 @@ class BreakoutRoom extends Component {
render() {
const {
intl, endAllBreakouts, breakoutRooms, isModerator, closeBreakoutPanel,
intl, endAllBreakouts, isModerator, closeBreakoutPanel,
} = this.props;
if (breakoutRooms.length <= 0) return null;
return (
<div className={styles.panel}>
<div className={styles.header} role="button" onClick={closeBreakoutPanel} >
<span >
<span>
<Icon iconName="left_arrow" />
{intl.formatMessage(intlMessages.breakoutTitle)}
</span>
@ -206,8 +226,8 @@ class BreakoutRoom extends Component {
{this.renderBreakoutRooms()}
{this.renderDuration()}
{
isModerator ?
(
isModerator
? (
<Button
color="primary"
size="lg"

View File

@ -260,7 +260,7 @@
}
.circle {
--btn-sm-padding-x: calc(var(--lg-padding-x) / 2.75);
--btn-sm-padding-x: calc(var(--sm-padding-x) / 2.75);
--btn-md-padding-x: calc(var(--md-padding-x) / 2.75);
--btn-lg-padding-x: calc(var(--lg-padding-x) / 2.75);
--btn-jumbo-padding-x: calc(var(--jumbo-padding-x) / 2.75);

View File

@ -1,41 +1,33 @@
import React from 'react';
import _ from 'lodash';
import PropTypes from 'prop-types';
const propTypes = {
play: PropTypes.bool.isRequired,
count: PropTypes.number.isRequired,
};
class ChatAudioAlert extends React.Component {
constructor(props) {
super(props);
this.audio = new Audio(`${Meteor.settings.public.app.basename}/resources/sounds/notify.mp3`);
this.handleAudioLoaded = this.handleAudioLoaded.bind(this);
this.playAudio = this.playAudio.bind(this);
this.componentDidUpdate = _.debounce(this.playAudio, 2000);
}
componentDidMount() {
this.audio.addEventListener('loadedmetadata', this.handleAudioLoaded);
}
shouldComponentUpdate(nextProps) {
return nextProps.count > this.props.count;
}
componentWillUnmount() {
this.audio.removeEventListener('loadedmetadata', this.handleAudioLoaded);
}
handleAudioLoaded() {
this.componentDidUpdate = _.debounce(this.playAudio, this.audio.duration * 1000);
this.componentDidUpdate = this.playAudio;
}
playAudio() {
if (!this.props.play) return;
const { play } = this.props;
if (!play) return;
this.audio.play();
}

View File

@ -10,9 +10,10 @@ import Service from '../service';
import { styles } from '../styles';
const propTypes = {
disableNotify: PropTypes.bool.isRequired,
openChats: PropTypes.arrayOf(PropTypes.object).isRequired,
disableAudio: PropTypes.bool.isRequired,
pushAlertDisabled: PropTypes.bool.isRequired,
activeChats: PropTypes.arrayOf(PropTypes.object).isRequired,
audioAlertDisabled: PropTypes.bool.isRequired,
joinTimestamp: PropTypes.number.isRequired,
};
const intlMessages = defineMessages({
@ -34,51 +35,105 @@ const intlMessages = defineMessages({
},
});
const PUBLIC_KEY = 'public';
const PRIVATE_KEY = 'private';
const ALERT_INTERVAL = 5000; // 5 seconds
const ALERT_DURATION = 4000; // 4 seconds
class ChatAlert extends PureComponent {
constructor(props) {
super(props);
this.state = {
notified: Service.getNotified(PRIVATE_KEY),
publicNotified: Service.getNotified(PUBLIC_KEY),
alertEnabledTimestamp: props.joinTimestamp,
lastAlertTimestampByChat: {},
pendingNotificationsByChat: {},
};
}
componentWillReceiveProps(nextProps) {
componentDidUpdate(prevProps) {
const {
openChats,
disableNotify,
pushAlertDisabled,
activeChats,
joinTimestamp,
} = this.props;
if (nextProps.disableNotify === false && disableNotify === true) {
const loadMessages = {};
openChats
.forEach((c) => {
loadMessages[c.id] = c.unreadCounter;
});
this.setState({ notified: loadMessages });
const {
alertEnabledTimestamp,
lastAlertTimestampByChat,
pendingNotificationsByChat,
} = this.state;
// Avoid alerting messages received before enabling alerts
if (prevProps.pushAlertDisabled && !pushAlertDisabled) {
const newAlertEnabledTimestamp = Service.getLastMessageTimestampFromChatList(activeChats);
this.setAlertEnabledTimestamp(newAlertEnabledTimestamp);
return;
}
const notifiedToClear = {};
openChats
.filter(c => c.unreadCounter === 0)
.forEach((c) => {
notifiedToClear[c.id] = 0;
// Keep track of messages that was not alerted yet
const unalertedMessagesByChatId = {};
activeChats
.filter(chat => chat.id !== Session.get('idChatOpen'))
.filter(chat => chat.unreadCounter > 0)
.forEach((chat) => {
const chatId = (chat.id === 'public') ? 'MAIN-PUBLIC-GROUP-CHAT' : chat.id;
const thisChatUnreadMessages = UnreadMessages.getUnreadMessages(chatId);
unalertedMessagesByChatId[chatId] = thisChatUnreadMessages.filter((msg) => {
const messageChatId = (msg.chatId === 'MAIN-PUBLIC-GROUP-CHAT') ? msg.chatId : msg.sender;
const retorno = (msg
&& msg.timestamp > alertEnabledTimestamp
&& msg.timestamp > joinTimestamp
&& msg.timestamp > (lastAlertTimestampByChat[messageChatId] || 0)
);
return retorno;
});
if (!unalertedMessagesByChatId[chatId].length) delete unalertedMessagesByChatId[chatId];
});
this.setState(({ notified }) => ({
notified: {
...notified,
...notifiedToClear,
},
}), () => {
Service.setNotified(PRIVATE_KEY, this.state.notified);
const lastUnalertedMessageTimestampByChat = {};
Object.keys(unalertedMessagesByChatId).forEach((chatId) => {
lastUnalertedMessageTimestampByChat[chatId] = unalertedMessagesByChatId[chatId]
.reduce(Service.maxTimestampReducer, 0);
});
// Keep track of chats that need to be alerted now (considering alert interval)
const chatsWithPendingAlerts = Object.keys(lastUnalertedMessageTimestampByChat)
.filter(chatId => lastUnalertedMessageTimestampByChat[chatId]
> ((lastAlertTimestampByChat[chatId] || 0) + ALERT_INTERVAL)
&& !(chatId in pendingNotificationsByChat));
if (!chatsWithPendingAlerts.length) return;
const newPendingNotificationsByChat = Object.assign({},
...chatsWithPendingAlerts.map(chatId => ({ [chatId]: unalertedMessagesByChatId[chatId] })));
// Mark messages as alerted
const newLastAlertTimestampByChat = { ...lastAlertTimestampByChat };
chatsWithPendingAlerts.forEach(
(chatId) => {
newLastAlertTimestampByChat[chatId] = lastUnalertedMessageTimestampByChat[chatId];
},
);
if (!pushAlertDisabled) {
this.setChatMessagesState(newPendingNotificationsByChat, newLastAlertTimestampByChat);
}
}
setAlertEnabledTimestamp(newAlertEnabledTimestamp) {
const { alertEnabledTimestamp } = this.state;
if (newAlertEnabledTimestamp > 0 && alertEnabledTimestamp !== newAlertEnabledTimestamp) {
this.setState({ alertEnabledTimestamp: newAlertEnabledTimestamp });
}
}
setChatMessagesState(pendingNotificationsByChat, lastAlertTimestampByChat) {
this.setState({ pendingNotificationsByChat, lastAlertTimestampByChat });
}
mapContentText(message) {
const {
intl,
@ -86,12 +141,13 @@ class ChatAlert extends PureComponent {
const contentMessage = message
.map((content) => {
if (content.text === 'PUBLIC_CHAT_CLEAR') return intl.formatMessage(intlMessages.publicChatClear);
/* this code is to remove html tags that come in the server's messangens */
/* this code is to remove html tags that come in the server's messages */
const tempDiv = document.createElement('div');
tempDiv.innerHTML = content.text;
const textWithoutTag = tempDiv.innerText;
return textWithoutTag;
});
return contentMessage;
}
@ -102,155 +158,64 @@ class ChatAlert extends PureComponent {
<div className={styles.contentMessage}>
{
this.mapContentText(message)
.reduce((acc, text) => [...acc, (<br />), text], []).splice(1)
.reduce((acc, text) => [...acc, (<br key={_.uniqueId('br_')} />), text], [])
}
</div>
</div>
);
}
notifyPrivateChat() {
const {
disableNotify,
openChats,
intl,
} = this.props;
if (disableNotify) return;
const hasUnread = ({ unreadCounter }) => unreadCounter > 0;
const isNotNotified = ({ id, unreadCounter }) => unreadCounter !== this.state.notified[id];
const isPrivate = ({ id }) => id !== PUBLIC_KEY;
const thisChatClosed = ({ id }) => !Session.equals('idChatOpen', id);
const chatsNotify = openChats
.filter(hasUnread)
.filter(isNotNotified)
.filter(isPrivate)
.filter(thisChatClosed)
.map(({
id,
name,
unreadCounter,
...rest
}) => ({
...rest,
name,
unreadCounter,
id,
message: intl.formatMessage(intlMessages.appToastChatPrivate),
}));
return (
<span>
{
chatsNotify.map(({ id, message, name }) => {
const getChatmessages = UnreadMessages.getUnreadMessages(id)
.filter(({ fromTime, fromUserId }) => fromTime > (this.state.notified[fromUserId] || 0));
const reduceMessages = Service
.reduceAndMapGroupMessages(getChatmessages);
if (!reduceMessages.length) return null;
const flatMessages = _.flatten(reduceMessages
.map(msg => this.createMessage(name, msg.content)));
const limitingMessages = flatMessages;
return (<ChatPushAlert
key={_.uniqueId('id-')}
chatId={id}
content={limitingMessages}
message={<span >{message}</span>}
onOpen={() => {
this.setState(({ notified }) => ({
notified: {
...notified,
[id]: new Date().getTime(),
},
}), () => {
Service.setNotified(PRIVATE_KEY, this.state.notified);
});
}}
/>);
})
}
</span>
);
}
notifyPublicChat() {
const {
publicUserId,
intl,
disableNotify,
} = this.props;
const publicUnread = UnreadMessages.getUnreadMessages(publicUserId);
const publicUnreadReduced = Service.reduceAndMapGroupMessages(publicUnread);
if (disableNotify) return;
if (!Service.hasUnreadMessages(publicUserId)) return;
if (Session.equals('idChatOpen', PUBLIC_KEY)) return;
const checkIfBeenNotified = ({ sender, time }) =>
time > (this.state.publicNotified[sender.id] || 0);
const chatsNotify = publicUnreadReduced
.map(msg => ({
...msg,
sender: {
name: msg.sender ? msg.sender.name : intl.formatMessage(intlMessages.appToastChatSystem),
...msg.sender,
},
}))
.filter(checkIfBeenNotified);
return (
<span>
{
chatsNotify.map(({ sender, time, content }) =>
(<ChatPushAlert
key={time}
chatId={PUBLIC_KEY}
name={sender.name}
message={
<span >
{ intl.formatMessage(intlMessages.appToastChatPublic) }
</span>
}
content={this.createMessage(sender.name, content)}
onOpen={() => {
this.setState(({ notified, publicNotified }) => ({
...notified,
publicNotified: {
...publicNotified,
[sender.id]: time,
},
}), () => {
Service.setNotified(PUBLIC_KEY, this.state.publicNotified);
});
}}
/>))
}
</span>
);
}
render() {
const {
openChats,
disableAudio,
audioAlertDisabled,
pushAlertDisabled,
intl,
} = this.props;
const unreadMessagesCount = openChats
.map(chat => chat.unreadCounter)
.reduce((a, b) => a + b, 0);
const shouldPlayAudio = !disableAudio && unreadMessagesCount > 0;
const {
pendingNotificationsByChat,
} = this.state;
const shouldPlay = Object.keys(pendingNotificationsByChat).length > 0;
return (
<span>
<ChatAudioAlert play={shouldPlayAudio} count={unreadMessagesCount} />
{ this.notifyPublicChat() }
{ this.notifyPrivateChat() }
{!audioAlertDisabled ? <ChatAudioAlert play={shouldPlay} /> : null}
{
!pushAlertDisabled
? Object.keys(pendingNotificationsByChat)
.map((chatId) => {
// Only display the latest group of messages (up to 5 messages)
const reducedMessage = Service
.reduceAndMapGroupMessages(pendingNotificationsByChat[chatId].slice(-5)).pop();
if (!reducedMessage) return null;
const content = this
.createMessage(reducedMessage.sender.name, reducedMessage.content);
return (
<ChatPushAlert
key={chatId}
chatId={chatId}
content={content}
title={
(chatId === 'MAIN-PUBLIC-GROUP-CHAT')
? <span>{intl.formatMessage(intlMessages.appToastChatPublic)}</span>
: <span>{intl.formatMessage(intlMessages.appToastChatPrivate)}</span>
}
onOpen={
() => {
let pendingNotifications = pendingNotificationsByChat;
delete pendingNotifications[chatId];
pendingNotifications = { ...pendingNotifications };
this.setState({ pendingNotificationsByChat: pendingNotifications });
}}
alertDuration={ALERT_DURATION}
/>
);
})
: null
}
</span>
);
}

View File

@ -3,6 +3,9 @@ import { withTracker } from 'meteor/react-meteor-data';
import UserListService from '/imports/ui/components/user-list/service';
import Settings from '/imports/ui/services/settings';
import ChatAlert from './component';
import ChatService from '/imports/ui/components/chat/service.js';
import Auth from '/imports/ui/services/auth';
import Users from '/imports/api/users';
const ChatAlertContainer = props => (
<ChatAlert {...props} />
@ -10,12 +13,13 @@ const ChatAlertContainer = props => (
export default withTracker(() => {
const AppSettings = Settings.application;
const openChats = UserListService.getOpenChats();
const activeChats = UserListService.getActiveChats();
const loginTime = Users.findOne({ userId: Auth.userID }).loginTime;
return {
disableAudio: !AppSettings.chatAudioAlerts,
disableNotify: !AppSettings.chatPushAlerts,
openChats,
audioAlertDisabled: !AppSettings.chatAudioAlerts,
pushAlertDisabled: !AppSettings.chatPushAlerts,
activeChats,
publicUserId: Meteor.settings.public.chat.public_group_id,
joinTimestamp: loginTime,
};
})(memo(ChatAlertContainer));

View File

@ -4,37 +4,44 @@ import _ from 'lodash';
import injectNotify from '/imports/ui/components/toast/inject-notify/component';
import { Session } from 'meteor/session';
const ALERT_INTERVAL = 2000; // 2 seconds
const ALERT_LIFETIME = 4000; // 4 seconds
const propTypes = {
notify: PropTypes.func.isRequired,
onOpen: PropTypes.func.isRequired,
chatId: PropTypes.string.isRequired,
message: PropTypes.node.isRequired,
title: PropTypes.node.isRequired,
content: PropTypes.node.isRequired,
alertDuration: PropTypes.number.isRequired,
};
class ChatPushAlert extends PureComponent {
static link(message, chatId) {
static link(title, chatId) {
let chat = chatId;
if (chat === 'MAIN-PUBLIC-GROUP-CHAT') {
chat = 'public';
}
return (
<div
key={chatId}
role="button"
aria-label={message}
aria-label={title}
tabIndex={0}
onClick={() => {
Session.set('openPanel', 'chat');
Session.set('idChatOpen', chatId);
Session.set('idChatOpen', chat);
}}
onKeyPress={() => null}
>
{ message }
{title}
</div>
);
}
constructor(props) {
super(props);
this.showNotify = _.debounce(this.showNotify.bind(this), ALERT_INTERVAL);
this.showNotify = this.showNotify.bind(this);
this.componentDidMount = this.showNotify;
this.componentDidUpdate = this.showNotify;
@ -45,15 +52,16 @@ class ChatPushAlert extends PureComponent {
notify,
onOpen,
chatId,
message,
title,
content,
alertDuration,
} = this.props;
return notify(
ChatPushAlert.link(message, chatId),
ChatPushAlert.link(title, chatId),
'info',
'chat',
{ onOpen, autoClose: ALERT_LIFETIME },
{ onOpen, autoClose: alertDuration },
ChatPushAlert.link(content, chatId),
true,
);

View File

@ -56,6 +56,7 @@ const Chat = (props) => {
>
<Button
onClick={() => {
Session.set('idChatOpen', '');
Session.set('openPanel', 'userlist');
}}
aria-label={intl.formatMessage(intlMessages.hideChatLabel, { 0: title })}
@ -75,6 +76,7 @@ const Chat = (props) => {
hideLabel
onClick={() => {
actions.handleClosePrivateChat(chatID);
Session.set('idChatOpen', '');
Session.set('openPanel', 'userlist');
}}
aria-label={intl.formatMessage(intlMessages.closeChatLabel, { 0: title })}

View File

@ -59,7 +59,7 @@ export default injectIntl(withTracker(({ intl }) => {
messages = ChatService.getPublicGroupMessages();
const time = user.logTime;
const time = user.loginTime;
const welcomeId = `welcome-msg-${time}`;
const welcomeMsg = {

View File

@ -118,6 +118,7 @@ class MessageForm extends PureComponent {
const { message } = this.state;
let msg = message.trim();
if (disabled
|| msg.length === 0
|| msg.length < minMessageLength
@ -132,11 +133,13 @@ class MessageForm extends PureComponent {
div.appendChild(document.createTextNode(msg));
msg = div.innerHTML;
return handleSendMessage(msg)
.then(() => this.setState({
return (
handleSendMessage(msg),
this.setState({
message: '',
hasErrors: false,
}));
})
);
}
render() {
@ -184,7 +187,7 @@ class MessageForm extends PureComponent {
/>
</div>
<div className={styles.info}>
{ hasErrors ? <span id="message-input-error">{error}</span> : null }
{hasErrors ? <span id="message-input-error">{error}</span> : null}
</div>
</form>
);

View File

@ -24,6 +24,7 @@ const eventsToBeBound = [
];
const isElementInViewport = (el) => {
if (!el) return false;
const rect = el.getBoundingClientRect();
const prefetchHeight = 125;
@ -87,8 +88,8 @@ export default class MessageListItem extends Component {
}
shouldComponentUpdate(nextProps, nextState) {
if(!this.props.scrollArea && nextProps.scrollArea) return true;
else return !nextState.preventRender && nextState.pendingChanges;
if (!this.props.scrollArea && nextProps.scrollArea) return true;
return !nextState.preventRender && nextState.pendingChanges;
}
renderSystemMessage() {
@ -120,7 +121,7 @@ export default class MessageListItem extends Component {
const {
user,
messages,
time
time,
} = this.props;
const dateTime = new Date(time);

View File

@ -18,13 +18,14 @@ const eventsToBeBound = [
];
const isElementInViewport = (el) => {
if (!el) return false;
const rect = el.getBoundingClientRect();
return (
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
rect.right <= (window.innerWidth || document.documentElement.clientWidth)
rect.top >= 0
&& rect.left >= 0
&& rect.bottom <= (window.innerHeight || document.documentElement.clientHeight)
&& rect.right <= (window.innerWidth || document.documentElement.clientWidth)
);
};
@ -45,7 +46,7 @@ export default class MessageListItem extends PureComponent {
if (isElementInViewport(node)) {
this.props.handleReadMessage(this.props.time);
if(scrollArea) {
if (scrollArea) {
eventsToBeBound.forEach(
e => scrollArea.removeEventListener(e, this.handleMessageInViewport),
);
@ -76,7 +77,7 @@ export default class MessageListItem extends PureComponent {
(e) => { scrollArea.addEventListener(e, this.handleMessageInViewport, false); },
);
}
}
}
componentDidMount() {
this.listenToUnreadMessages();
@ -114,7 +115,6 @@ export default class MessageListItem extends PureComponent {
/>
);
}
}
MessageListItem.propTypes = propTypes;

View File

@ -18,6 +18,7 @@ const PUBLIC_CHAT_ID = CHAT_CONFIG.public_id;
const PUBLIC_GROUP_CHAT_ID = CHAT_CONFIG.public_group_id;
const PRIVATE_CHAT_TYPE = CHAT_CONFIG.type_private;
const PUBLIC_CHAT_USER_ID = CHAT_CONFIG.system_userid;
const PUBLIC_CHAT_CLEAR = CHAT_CONFIG.system_messages_keys.chat_clear;
const ScrollCollection = new Mongo.Collection(null);
@ -91,16 +92,13 @@ const reduceGroupMessages = (previous, current) => {
return previous.concat(currentMessage);
};
const reduceAndMapGroupMessages = messages =>
(messages.reduce(reduceGroupMessages, []).map(mapGroupMessage));
const reduceAndMapGroupMessages = messages => (messages
.reduce(reduceGroupMessages, []).map(mapGroupMessage));
const getPublicGroupMessages = () => {
const publicGroupMessages = GroupChatMsg.find({
chatId: PUBLIC_GROUP_CHAT_ID,
}, {
sort: ['timestamp'],
}).fetch();
}, { sort: ['timestamp'] }).fetch();
return publicGroupMessages;
};
@ -122,9 +120,7 @@ const getPrivateGroupMessages = () => {
messages = GroupChatMsg.find({
chatId,
}, {
sort: ['timestamp'],
}).fetch();
}, { sort: ['timestamp'] }).fetch();
}
return reduceAndMapGroupMessages(messages, []);
@ -140,8 +136,8 @@ const isChatLocked = (receiverID) => {
const isPubChatLocked = meeting.lockSettingsProp.disablePubChat;
const isPrivChatLocked = meeting.lockSettingsProp.disablePrivChat;
return mapUser(user).isLocked &&
((isPublic && isPubChatLocked) || (!isPublic && isPrivChatLocked));
return mapUser(user).isLocked
&& ((isPublic && isPubChatLocked) || (!isPublic && isPrivChatLocked));
}
return false;
@ -205,11 +201,10 @@ const getScrollPosition = (receiverID) => {
return scroll.position;
};
const updateScrollPosition =
position => ScrollCollection.upsert(
{ receiver: Session.get('idChatOpen') },
{ $set: { position } },
);
const updateScrollPosition = position => ScrollCollection.upsert(
{ receiver: Session.get('idChatOpen') },
{ $set: { position } },
);
const updateUnreadMessage = (timestamp) => {
const chatID = Session.get('idChatOpen') || PUBLIC_CHAT_ID;
@ -248,8 +243,27 @@ const htmlDecode = (input) => {
};
// Export the chat as [Hour:Min] user: message
const exportChat = messageList => (
messageList.map((message) => {
const exportChat = (messageList) => {
const { welcomeProp } = getMeeting();
const { loginTime } = getUser(Auth.userID);
const { welcomeMsg } = welcomeProp;
const clearMessage = messageList.filter(message => message.message === PUBLIC_CHAT_CLEAR);
const hasClearMessage = clearMessage.length;
if (!hasClearMessage || (hasClearMessage && clearMessage[0].timestamp < loginTime)) {
messageList.push({
timestamp: loginTime,
message: welcomeMsg,
type: SYSTEM_CHAT_TYPE,
sender: PUBLIC_CHAT_USER_ID,
});
}
messageList.sort((a, b) => a.timestamp - b.timestamp);
return messageList.map((message) => {
const date = new Date(message.timestamp);
const hour = date.getHours().toString().padStart(2, 0);
const min = date.getMinutes().toString().padStart(2, 0);
@ -259,31 +273,41 @@ const exportChat = messageList => (
}
const userName = message.sender === PUBLIC_CHAT_USER_ID ? '' : `${getUser(message.sender).name} :`;
return `${hourMin} ${userName} ${htmlDecode(message.message)}`;
}).join('\n')
);
}).join('\n');
};
const setNotified = (chatType, item) => {
const notified = Storage.getItem('notified');
const key = 'notified';
const userChat = { [chatType]: item };
if (notified) {
Storage.setItem(key, {
...notified,
...userChat,
});
return;
const getUnreadMessagesFromChatId = chatId => UnreadMessages.getUnreadMessages(chatId);
const getAllMessages = (chatID) => {
const filter = {
sender: { $ne: Auth.userID },
};
if (chatID === PUBLIC_GROUP_CHAT_ID) {
filter.chatId = { $eq: chatID };
} else {
const privateChat = GroupChat.findOne({ users: { $all: [chatID, Auth.userID] } });
filter.chatId = { $ne: PUBLIC_GROUP_CHAT_ID };
if (privateChat) {
filter.chatId = privateChat.chatId;
}
}
Storage.setItem(key, {
...userChat,
});
const messages = GroupChatMsg.find(filter).fetch();
return messages;
};
const getNotified = (chat) => {
const key = 'notified';
const notified = Storage.getItem(key);
if (notified) return notified[chat] || {};
return {};
};
const getlastMessage = lastMessages => lastMessages.sort((a,
b) => a.timestamp - b.timestamp).pop();
const maxTimestampReducer = (max, el) => ((el.timestamp > max) ? el.timestamp : max);
const maxNumberReducer = (max, el) => ((el > max) ? el : max);
const getLastMessageTimestampFromChatList = activeChats => activeChats
.map(chat => ((chat.id === 'public') ? 'MAIN-PUBLIC-GROUP-CHAT' : chat.id))
.map(chatId => getAllMessages(chatId).reduce(maxTimestampReducer, 0))
.reduce(maxNumberReducer, 0);
export default {
reduceAndMapGroupMessages,
@ -302,6 +326,10 @@ export default {
removeFromClosedChatsSession,
exportChat,
clearPublicChatHistory,
setNotified,
getNotified,
getlastMessage,
getUnreadMessagesFromChatId,
getAllMessages,
maxTimestampReducer,
maxNumberReducer,
getLastMessageTimestampFromChatList,
};

View File

@ -1,9 +1,17 @@
import React, { Component } from 'react';
import React from 'react';
import PropTypes from 'prop-types';
import { defineMessages, injectIntl } from 'react-intl';
import injectWbResizeEvent from '/imports/ui/components/presentation/resize-wrapper/component';
import { styles } from './styles.scss';
class ClosedCaptions extends Component {
const intlMessages = defineMessages({
noLocaleSelected: {
id: 'app.submenu.closedCaptions.noLocaleSelected',
description: 'label for selected language for closed captions',
},
});
class ClosedCaptions extends React.PureComponent {
constructor(props) {
super(props);
@ -25,13 +33,14 @@ class ClosedCaptions extends Component {
}
renderCaptions(caption) {
const { fontFamily, fontSize, fontColor } = this.props;
const text = caption.captions;
const captionStyles = {
whiteSpace: 'pre-wrap',
wordWrap: 'break-word',
fontFamily: this.props.fontFamily,
fontSize: this.props.fontSize,
color: this.props.fontColor,
fontFamily,
fontSize,
color: fontColor,
};
return (
@ -48,12 +57,15 @@ class ClosedCaptions extends Component {
locale,
captions,
backgroundColor,
intl,
} = this.props;
return (
<div disabled className={styles.ccbox}>
<div className={styles.title}>
<p> {locale} </p>
<p>
{ locale || intl.formatMessage(intlMessages.noLocaleSelected) }
</p>
</div>
<div
ref={(ref) => { this.refCCScrollArea = ref; }}
@ -69,7 +81,7 @@ class ClosedCaptions extends Component {
}
}
export default injectWbResizeEvent(ClosedCaptions);
export default injectIntl(injectWbResizeEvent(ClosedCaptions));
ClosedCaptions.propTypes = {
backgroundColor: PropTypes.string.isRequired,
@ -83,12 +95,15 @@ ClosedCaptions.propTypes = {
).isRequired,
}).isRequired,
).isRequired,
locale: PropTypes.string.isRequired,
locale: PropTypes.string,
fontColor: PropTypes.string.isRequired,
fontSize: PropTypes.string.isRequired,
fontFamily: PropTypes.string.isRequired,
intl: PropTypes.shape({
formatMessage: PropTypes.func.isRequired,
}).isRequired,
};
ClosedCaptions.defaultProps = {
locale: 'Locale is not selected',
locale: undefined,
};

View File

@ -4,7 +4,7 @@ import Settings from '/imports/ui/services/settings';
import _ from 'lodash';
const getCCData = () => {
const meetingID = Auth.meetingID;
const { meetingID } = Auth;
const ccSettings = Settings.cc;

View File

@ -53,7 +53,7 @@ const propTypes = {
const defaultProps = {
children: null,
isOpen: false,
keepOpen: null,
onShow: noop,
onHide: noop,
autoFocus: false,
@ -78,10 +78,12 @@ class Dropdown extends Component {
onShow,
onHide,
} = this.props;
const { isOpen } = this.state;
if (this.state.isOpen && !prevState.isOpen) { onShow(); }
if (isOpen && !prevState.isOpen) { onShow(); }
if (!this.state.isOpen && prevState.isOpen) { onHide(); }
if (!isOpen && prevState.isOpen) { onHide(); }
}
handleShow() {
@ -99,16 +101,29 @@ class Dropdown extends Component {
}
handleWindowClick(event) {
const { keepOpen, onHide } = this.props;
const { isOpen } = this.state;
const triggerElement = findDOMNode(this.trigger);
const contentElement = findDOMNode(this.content);
const closeDropdown = this.props.isOpen && this.state.isOpen && triggerElement.contains(event.target);
const preventHide = this.props.isOpen && contentElement.contains(event.target) || !triggerElement;
if (closeDropdown) {
return this.props.onHide();
if (keepOpen === null) {
if (triggerElement.contains(event.target)) {
return;
}
}
if (contentElement && preventHide) {
if (triggerElement && triggerElement.contains(event.target)) {
if (keepOpen) return onHide();
if (isOpen) return this.handleHide();
}
if (keepOpen && isOpen && !contentElement.contains(event.target)) {
onHide();
this.handleHide();
return;
}
if (keepOpen !== null) {
return;
}
@ -116,7 +131,8 @@ class Dropdown extends Component {
}
handleToggle() {
return this.state.isOpen ? this.handleHide() : this.handleShow();
const { isOpen } = this.state;
return isOpen ? this.handleHide() : this.handleShow();
}
render() {
@ -125,15 +141,18 @@ class Dropdown extends Component {
className,
style,
intl,
keepOpen,
...otherProps
} = this.props;
const { isOpen } = this.state;
let trigger = children.find(x => x.type === DropdownTrigger);
let content = children.find(x => x.type === DropdownContent);
trigger = React.cloneElement(trigger, {
ref: (ref) => { this.trigger = ref; },
dropdownIsOpen: this.state.isOpen,
dropdownIsOpen: isOpen,
dropdownToggle: this.handleToggle,
dropdownShow: this.handleShow,
dropdownHide: this.handleHide,
@ -141,12 +160,14 @@ class Dropdown extends Component {
content = React.cloneElement(content, {
ref: (ref) => { this.content = ref; },
'aria-expanded': this.state.isOpen,
dropdownIsOpen: this.state.isOpen,
'aria-expanded': isOpen,
dropdownIsOpen: isOpen,
dropdownToggle: this.handleToggle,
dropdownShow: this.handleShow,
dropdownHide: this.handleHide,
});
const showCloseBtn = (isOpen && keepOpen) || (isOpen && keepOpen === null);
return (
<div
@ -162,7 +183,7 @@ class Dropdown extends Component {
>
{trigger}
{content}
{this.state.isOpen ?
{showCloseBtn ?
<Button
className={styles.close}
label={intl.formatMessage(intlMessages.close)}

View File

@ -0,0 +1,71 @@
import React from 'react';
import PropTypes from 'prop-types';
import { defineMessages, injectIntl, intlShape } from 'react-intl';
import Button from '/imports/ui/components/button/component';
import Modal from '/imports/ui/components/modal/simple/component';
import { styles } from './styles';
const intlMessages = defineMessages({
endMeetingTitle: {
id: 'app.endMeeting.title',
description: 'end meeting title',
},
endMeetingDescription: {
id: 'app.endMeeting.description',
description: 'end meeting description',
},
yesLabel: {
id: 'app.endMeeting.yesLabel',
description: 'label for yes button for end meeting',
},
noLabel: {
id: 'app.endMeeting.noLabel',
description: 'label for no button for end meeting',
},
});
const propTypes = {
intl: intlShape.isRequired,
closeModal: PropTypes.func.isRequired,
endMeeting: PropTypes.func.isRequired,
};
class EndMeetingComponent extends React.PureComponent {
render() {
const { intl, closeModal, endMeeting } = this.props;
return (
<Modal
overlayClassName={styles.overlay}
className={styles.modal}
onRequestClose={closeModal}
hideBorder
title={intl.formatMessage(intlMessages.endMeetingTitle)}
>
<div className={styles.container}>
<div className={styles.description}>
{intl.formatMessage(intlMessages.endMeetingDescription)}
</div>
<div className={styles.footer}>
<Button
color="primary"
className={styles.button}
label={intl.formatMessage(intlMessages.yesLabel)}
onClick={endMeeting}
/>
<Button
label={intl.formatMessage(intlMessages.noLabel)}
className={styles.button}
onClick={closeModal}
/>
</div>
</div>
</Modal>
);
}
}
EndMeetingComponent.propTypes = propTypes;
export default injectIntl(EndMeetingComponent);

View File

@ -0,0 +1,19 @@
import React from 'react';
import { withTracker } from 'meteor/react-meteor-data';
import { withModalMounter } from '/imports/ui/components/modal/service';
import { makeCall } from '/imports/ui/services/api';
import EndMeetingComponent from './component';
const EndMeetingContainer = props => <EndMeetingComponent {...props} />;
export default withModalMounter(withTracker(({ mountModal }) => ({
closeModal() {
mountModal(null);
},
endMeeting: () => {
makeCall('endMeeting');
mountModal(null);
},
}))(EndMeetingContainer));

View File

@ -0,0 +1,58 @@
@import '/imports/ui/stylesheets/mixins/focus';
@import '/imports/ui/stylesheets/variables/_all';
@import "/imports/ui/components/modal/simple/styles";
:root {
--description-margin: 3.5rem;
}
.title {
color: var(--color-gray-dark);
font-weight: var(--headings-font-weight);
font-size: var(--jumbo-padding-y);
}
.container {
display: flex;
align-items: center;
flex-direction: column;
padding: 0;
margin-top: 0;
margin-right: var(--description-margin);
margin-left: var(--description-margin);
margin-bottom: var(--lg-padding-x);
}
.footer {
display:flex;
}
.button {
padding-right: var(--jumbo-padding-y);
padding-left: var(--jumbo-padding-y);
margin-right: var(--sm-padding-x);
}
.description {
text-align: center;
line-height: var(--line-height-base);
color: var(--color-gray);
margin-bottom: var(--jumbo-padding-y)
}
.modal {
@extend .modal;
padding: var(--sm-padding-y);
}
.overlay {
@extend .overlay;
}
.header {
margin: 0;
padding: 0;
border: none;
line-height: var(--title-position-left);
margin-bottom: var(--lg-padding-y);
}

View File

@ -4,6 +4,7 @@ import { defineMessages, injectIntl } from 'react-intl';
import { Meteor } from 'meteor/meteor';
import Button from '/imports/ui/components/button/component';
import logoutRouteHandler from '/imports/utils/logoutRouteHandler';
import { Session } from 'meteor/session';
import { styles } from './styles';
const intlMessages = defineMessages({
@ -18,6 +19,9 @@ const intlMessages = defineMessages({
401: {
id: 'app.error.401',
},
400: {
id: 'app.error.400',
},
leave: {
id: 'app.error.leaveLabel',
description: 'aria-label for leaving',
@ -55,18 +59,27 @@ class ErrorScreen extends React.PureComponent {
return (
<div className={styles.background}>
<h1>
<h1 className={styles.codeError}>
{code}
</h1>
<h1 className={styles.message}>
{formatedMessage}
</h1>
<div className={styles.separator} />
<div>
{children}
</div>
{
!Session.get('errorMessageDescription') || (
<div className={styles.sessionMessage}>
{Session.get('errorMessageDescription')}
</div>)
}
<div>
<Button
size="sm"
color="primary"
className={styles.button}
onClick={logoutRouteHandler}
label={intl.formatMessage(intlMessages.leave)}
/>

View File

@ -19,7 +19,35 @@
}
.message {
text-transform: uppercase;
margin: 0;
text-transform: capitalize;
color: var(--color-gray-light);
font-size: 1.25rem;
font-weight: 600;
font-weight: 400;
}
.sessionMessage {
@extend .message;
font-size: var(--font-size-small);
margin-bottom: 1.5rem;
}
.codeError {
margin: 0;
font-size: 5rem;
color: var(--color-white);
}
.separator {
height: 0;
width: 5rem;
border: 1px solid var(--color-gray-lighter);
margin: 1.5rem 0 1.5rem 0;
align-self: center;
opacity: .75;
}
.button {
width: 9rem;
height: 2rem;
}

View File

@ -0,0 +1,211 @@
import React, { Component } from 'react';
import injectWbResizeEvent from '/imports/ui/components/presentation/resize-wrapper/component';
import YouTube from 'react-youtube';
import Vimeo from 'react-vimeo';
import { sendMessage, onMessage } from './service';
const { PlayerState } = YouTube;
class VideoPlayer extends Component {
constructor(props) {
super(props);
this.player = null;
this.syncInterval = null;
this.playerState = PlayerState.UNSTARTED;
this.presenterCommand = false;
this.preventStateChange = false;
this.opts = {
playerVars: {
width: '100%',
height: '100%',
autoplay: 1,
modestbranding: true,
rel: 0,
ecver: 2,
},
};
this.keepSync = this.keepSync.bind(this);
this.handleResize = this.handleResize.bind(this);
this.handleOnReady = this.handleOnReady.bind(this);
this.handleStateChange = this.handleStateChange.bind(this);
this.resizeListener = () => {
setTimeout(this.handleResize, 0);
};
}
componentDidMount() {
window.addEventListener('resize', this.resizeListener);
}
componentDidUpdate(nextProps) {
if (!nextProps.videoId) {
clearInterval(this.syncInterval);
}
}
componentWillUnmount() {
window.removeEventListener('resize', this.resizeListener);
clearInterval(this.syncInterval);
this.player = null;
this.refPlayer = null;
}
handleResize() {
if (!this.player || !this.refPlayer) {
return;
}
const el = this.refPlayer;
const parent = el.parentElement;
const w = parent.clientWidth;
const h = parent.clientHeight;
const idealW = h * 16 / 9;
if (idealW > w) {
this.player.setSize(w, w * 9 / 16);
} else {
this.player.setSize(idealW, h);
}
}
keepSync() {
const { isPresenter } = this.props;
if (isPresenter) {
this.syncInterval = setInterval(() => {
const curTime = this.player.getCurrentTime();
const rate = this.player.getPlaybackRate();
sendMessage('playerUpdate', { rate, time: curTime, state: this.playerState });
}, 2000);
} else {
onMessage('play', ({ time }) => {
this.presenterCommand = true;
if (this.player) {
this.player.seekTo(time, true);
this.playerState = PlayerState.PLAYING;
this.player.playVideo();
}
});
onMessage('stop', ({ time }) => {
this.presenterCommand = true;
if (this.player) {
this.playerState = PlayerState.PAUSED;
this.player.seekTo(time, true);
this.player.pauseVideo();
}
});
onMessage('playerUpdate', (data) => {
if (!this.player) {
return;
}
if (data.rate !== this.player.getPlaybackRate()) {
this.player.setPlaybackRate(data.rate);
}
if (Math.abs(this.player.getCurrentTime() - data.time) > 2) {
this.player.seekTo(data.time, true);
}
if (this.playerState !== data.state) {
this.presenterCommand = true;
this.playerState = data.state;
if (this.playerState === PlayerState.PLAYING) {
this.player.playVideo();
} else {
this.player.pauseVideo();
}
}
});
onMessage('changePlaybackRate', (rate) => {
this.player.setPlaybackRate(rate);
});
}
}
handleOnReady(event) {
const { isPresenter } = this.props;
this.player = event.target;
this.player.pauseVideo();
this.keepSync();
if (!isPresenter) {
sendMessage('viewerJoined');
} else {
this.player.playVideo();
}
this.handleResize();
}
handleStateChange(event) {
const { isPresenter } = this.props;
const curTime = this.player.getCurrentTime();
if (this.preventStateChange && [PlayerState.PLAYING, PlayerState.PAUSED].includes(event.data)) {
this.preventStateChange = false;
return;
}
if (this.playerState === event.data) {
return;
}
if (event.data === PlayerState.PLAYING) {
if (isPresenter) {
sendMessage('play', { time: curTime });
this.playerState = event.data;
} else if (!this.presenterCommand) {
this.player.seekTo(curTime, true);
this.preventStateChange = true;
this.player.pauseVideo();
} else {
this.playerState = event.data;
this.presenterCommand = false;
}
} else if (event.data === PlayerState.PAUSED) {
if (isPresenter) {
sendMessage('stop', { time: curTime });
this.playerState = event.data;
} else if (!this.presenterCommand) {
this.player.seekTo(curTime);
this.preventStateChange = true;
this.player.playVideo();
} else {
this.playerState = event.data;
this.presenterCommand = false;
}
}
}
render() {
const { videoId } = this.props;
const { opts, handleOnReady, handleStateChange } = this;
return (
<div
id="youtube-video-player"
data-test="videoPlayer"
ref={(ref) => { this.refPlayer = ref; }}
>
<YouTube
videoId={videoId}
opts={opts}
onReady={handleOnReady}
onStateChange={handleStateChange}
/>
</div>
);
}
}
export default injectWbResizeEvent(VideoPlayer);

View File

@ -0,0 +1,25 @@
import React from 'react';
import { defineMessages, injectIntl } from 'react-intl';
import { withTracker } from 'meteor/react-meteor-data';
import ExternalVideo from './component';
const intlMessages = defineMessages({
title: {
id: 'app.externalVideo.title',
description: '',
},
});
const ExternalVideoContainer = props => (
<ExternalVideo {...props}>
{props.children}
</ExternalVideo>
);
export default injectIntl(withTracker(({ params, intl }) => {
const title = intl.formatMessage(intlMessages.title);
return {
title,
};
})(ExternalVideoContainer));

View File

@ -0,0 +1,154 @@
import React, { Component } from 'react';
import { withModalMounter } from '/imports/ui/components/modal/service';
import ModalBase from '/imports/ui/components/modal/base/component';
import Button from '/imports/ui/components/button/component';
import { defineMessages, injectIntl } from 'react-intl';
import { isUrlValid, getUrlFromVideoId } from '../service';
import { styles } from './styles';
const intlMessages = defineMessages({
start: {
id: 'app.externalVideo.start',
description: 'Share youtube video',
},
stop: {
id: 'app.externalVideo.stop',
description: 'Stop sharing video',
},
urlError: {
id: 'app.externalVideo.urlError',
description: 'Not a video URL error',
},
input: {
id: 'app.externalVideo.input',
description: 'Video URL',
},
title: {
id: 'app.externalVideo.title',
description: 'Modal title',
},
close: {
id: 'app.externalVideo.close',
description: 'Close',
},
});
class ExternalVideoModal extends Component {
constructor(props) {
super(props);
const { videoId } = props;
this.state = {
url: getUrlFromVideoId(videoId),
sharing: videoId,
};
this.startWatchingHandler = this.startWatchingHandler.bind(this);
this.stopWatchingHandler = this.stopWatchingHandler.bind(this);
this.updateVideoUrlHandler = this.updateVideoUrlHandler.bind(this);
this.renderUrlError = this.renderUrlError.bind(this);
this.updateVideoUrlHandler = this.updateVideoUrlHandler.bind(this);
}
startWatchingHandler() {
const { startWatching, closeModal } = this.props;
const { url } = this.state;
startWatching(url);
closeModal();
}
stopWatchingHandler() {
const { stopWatching, closeModal } = this.props;
stopWatching();
closeModal();
}
updateVideoUrlHandler(ev) {
this.setState({ url: ev.target.value });
}
renderUrlError() {
const { intl } = this.props;
const { url } = this.state;
const valid = (!url || url.length <= 3) || isUrlValid(url);
return (
!valid
? (
<div className={styles.urlError}>
{intl.formatMessage(intlMessages.urlError)}
</div>
)
: null
);
}
render() {
const { intl, videoId, closeModal } = this.props;
const { url, sharing } = this.state;
const startDisabled = !isUrlValid(url) || (getUrlFromVideoId(videoId) === url);
return (
<ModalBase
overlayClassName={styles.overlay}
className={styles.modal}
onRequestClose={closeModal}
>
<header data-test="videoModealHeader" className={styles.header}>
<h3 className={styles.title}>{intl.formatMessage(intlMessages.title)}</h3>
<Button
data-test="modalBaseCloseButton"
className={styles.closeBtn}
label={intl.formatMessage(intlMessages.close)}
icon="close"
size="md"
hideLabel
onClick={closeModal}
/>
</header>
<div className={styles.content}>
<div className={styles.videoUrl}>
<label htmlFor="video-modal-input" id="video-modal-input">
{intl.formatMessage(intlMessages.input)}
<input
id="video-modal-input"
onChange={this.updateVideoUrlHandler}
name="video-modal-input"
value={url}
/>
</label>
</div>
<div className={styles.content}>
{this.renderUrlError()}
</div>
<Button
className={styles.startBtn}
label={intl.formatMessage(intlMessages.start)}
onClick={this.startWatchingHandler}
disabled={startDisabled}
/>
<Button
className={styles.stopBtn}
label={intl.formatMessage(intlMessages.stop)}
onClick={this.stopWatchingHandler}
disabled={!sharing}
/>
</div>
</ModalBase>
);
}
}
export default injectIntl(withModalMounter(ExternalVideoModal));

View File

@ -0,0 +1,16 @@
import React from 'react';
import { withTracker } from 'meteor/react-meteor-data';
import { withModalMounter } from '/imports/ui/components/modal/service';
import ExternalVideoModal from './component';
import { startWatching, stopWatching, getVideoId } from '../service';
const ExternalVideoModalContainer = props => <ExternalVideoModal {...props} />;
export default withModalMounter(withTracker(({ mountModal }) => ({
closeModal: () => {
mountModal(null);
},
startWatching,
stopWatching,
videoId: getVideoId(),
}))(ExternalVideoModalContainer));

View File

@ -0,0 +1,162 @@
@import "/imports/ui/stylesheets/variables/_all";
@import "/imports/ui/components/modal/simple/styles";
.header {
margin: 0;
padding: 0;
border: none;
line-height: 2rem;
}
.content {
justify-content: center;
padding-bottom: 0;
margin-right: auto;
margin-left: auto;
width: 100%;
}
.videoOptions {
margin-top: auto;
margin-bottom: auto;
display: flex;
}
.overlay {
@extend .overlay;
}
.modal {
@extend .modal;
padding: 1.5rem;
min-height: 20rem;
}
.closeBtn {
position: relative;
background-color: var(--color-white);
i {
color: var(--color-gray-light);
}
&:focus,
&:hover{
background-color: var(--color-gray-lighter);
i{
color: var(--color-gray);
}
}
}
.warning{
text-align: center;
font-weight: var(--headings-font-weight);
font-size: 5rem;
white-space: normal;
}
.text{
margin: var(--line-height-computed);
text-align: center;
}
.main {
margin: var(--line-height-computed);
text-align: center;
font-size: var(--font-size-large);
}
.startBtn {
&:focus {
outline: none !important;
}
i{
color: #3c5764;
}
margin: 0;
width: 40%;
display: block;
position: absolute;
bottom: 20px;
color: var(--color-white) !important;
background-color: var(--color-link) !important;
}
.stopBtn {
&:focus {
outline: none !important;
}
i{
color: red;
}
margin-left: 50%;
width: 40%;
display: block;
position: absolute;
bottom: 20px;
color: var(--color-white) !important;
background-color: red !important;
}
.title {
text-align: center;
font-weight: 400;
font-size: 1.3rem;
white-space: normal;
@include mq(var(--small-only)) {
font-size: 1rem;
padding: 0 1rem;
}
}
.videoUrl {
label {
display: block;
}
input {
display: block;
margin: 10px 0 10px 0;
padding: 0.4em;
background-color: #F1F8FF;
line-height: 2rem;
width: 100%;
font-family: inherit;
font-weight: inherit;
border: none;
border-radius: 0.4rem;
transition: box-shadow var(--transitionDuration);
}
input:focus {
outline: none;
box-shadow: 0.2rem 0.8rem 1.6rem 600;
}
span {
font-weight: 600;
}
}
.urlError {
color: red;
padding: 1em;
transition: 1s;
}

View File

@ -0,0 +1,61 @@
import Meetings from '/imports/api/meetings';
import Auth from '/imports/ui/services/auth';
import ExternalVideoStreamer from '/imports/api/external-videos';
import { makeCall } from '/imports/ui/services/api';
const YOUTUBE_PREFIX = 'https://youtube.com/watch?v=';
const isUrlEmpty = url => !url || url.length === 0;
const isUrlValid = (url) => {
const regexp = RegExp('^(https?://)?(www.)?(youtube.com|youtu.?be)/.+$');
return !isUrlEmpty(url) && url.match(regexp);
};
const getUrlFromVideoId = id => (id ? `${YOUTUBE_PREFIX}${id}` : '');
const videoIdFromUrl = (url) => {
const urlObj = new URL(url);
const params = new URLSearchParams(urlObj.search);
return params.get('v');
};
const startWatching = (url) => {
const externalVideoUrl = videoIdFromUrl(url);
makeCall('startWatchingExternalVideo', { externalVideoUrl });
};
const stopWatching = () => {
makeCall('stopWatchingExternalVideo');
};
const sendMessage = (event, data) => {
ExternalVideoStreamer.emit(event, {
...data,
meetingId: Auth.meetingID,
userId: Auth.userID,
});
};
const onMessage = (message, func) => {
ExternalVideoStreamer.on(message, func);
};
const getVideoId = () => {
const meetingId = Auth.meetingID;
const meeting = Meetings.findOne({ meetingId });
return meeting && meeting.externalVideoUrl;
};
export {
sendMessage,
onMessage,
getVideoId,
getUrlFromVideoId,
isUrlValid,
startWatching,
stopWatching,
};

View File

@ -0,0 +1,52 @@
@import "/imports/ui/stylesheets/mixins/focus";
@import "/imports/ui/stylesheets/variables/_all";
$icon-offset: -.4em;
.note {
background-color: #fff;
padding-top: var(--md-padding-x);
display: flex;
flex-grow: 1;
flex-direction: column;
justify-content: space-around;
overflow: hidden;
}
.header {
display: flex;
flex-direction: row;
align-items: left;
flex-shrink: 0;
padding-left: var(--md-padding-x);
padding-right: var(--md-padding-x);
a {
@include elementFocus(var(--color-primary));
padding-bottom: var(--sm-padding-y);
padding-left: var(--sm-padding-y);
text-decoration: none;
display: block;
}
[class^="icon-bbb-"],
[class*=" icon-bbb-"] {
font-size: 85%;
}
}
.title {
@extend %text-elipsis;
flex: 1;
}
iframe {
display: flex;
flex-flow: column;
flex-grow: 1;
flex-shrink: 1;
position: relative;
overflow-x: hidden;
overflow-y: auto;
border-style: none;
}

View File

@ -13,6 +13,7 @@ const propTypes = {
};
const APP_CONFIG = Meteor.settings.public.app;
const { showParticipantsOnLogin } = APP_CONFIG;
class JoinHandler extends Component {
static setError(codeError) {
@ -41,8 +42,10 @@ class JoinHandler extends Component {
async fetchToken() {
const urlParams = new URLSearchParams(window.location.search);
const sessionToken = urlParams.get('sessionToken');
if (!sessionToken) {
JoinHandler.setError('404');
JoinHandler.setError('400');
Session.set('errorMessageDescription', 'Session token was not provided');
}
// Old credentials stored in memory were being used when joining a new meeting
@ -108,27 +111,26 @@ class JoinHandler extends Component {
const fetchContent = await fetch(url, { credentials: 'same-origin' });
const parseToJson = await fetchContent.json();
const { response } = parseToJson;
setLogoutURL(response);
if (response.returncode !== 'FAILED') {
await setAuth(response);
await setCustomData(response);
setLogoURL(response);
logUserInfo();
const { showParticipantsOnLogin } = APP_CONFIG;
if (showParticipantsOnLogin) {
if (showParticipantsOnLogin && !deviceInfo.type().isPhone) {
Session.set('openPanel', 'chat');
Session.set('idChatOpen', '');
if (deviceInfo.type().isPhone) Session.set('openPanel', '');
} else {
Session.set('openPanel', '');
}
logger.info(`User successfully went through main.joinRouteHandler with [${JSON.stringify(response)}].`);
} else {
const e = new Error('Session not found');
const e = new Error(response.message);
if (!Session.get('codeError')) Session.set('errorMessageDescription', response.message);
logger.error(`User faced [${e}] on main.joinRouteHandler. Error was:`, JSON.stringify(response));
}
this.changeToJoin(true);

View File

@ -1,9 +1,8 @@
import React, { Component } from 'react';
import React from 'react';
import { defineMessages, injectIntl } from 'react-intl';
import Button from '/imports/ui/components/button/component';
import Toggle from '/imports/ui/components/switch/component';
import cx from 'classnames';
import ModalBase from '/imports/ui/components/modal/base/component';
import Modal from '/imports/ui/components/modal/simple/component';
import { styles } from './styles';
const intlMessages = defineMessages({
@ -51,48 +50,29 @@ const intlMessages = defineMessages({
id: 'app.lock-viewers.PrivateChatLable',
description: 'description for close button',
},
layoutLable: {
id: 'app.lock-viewers.Layout',
description: 'description for close button',
},
});
class LockViewersComponent extends Component {
constructor(props) {
super(props);
class LockViewersComponent extends React.PureComponent {
render() {
const {
intl,
meeting,
closeModal,
toggleLockSettings,
toggleWebcamsOnlyForModerator,
} = props;
this.closeModal = closeModal;
this.toggleLockSettings = toggleLockSettings;
this.toggleWebcamsOnlyForModerator = toggleWebcamsOnlyForModerator;
}
render() {
const { intl, meeting } = this.props;
} = this.props;
return (
<ModalBase
<Modal
overlayClassName={styles.overlay}
className={styles.modal}
onRequestClose={this.closeModal}
onRequestClose={closeModal}
hideBorder
>
<div className={styles.container}>
<div className={styles.header}>
<div className={styles.title}>{intl.formatMessage(intlMessages.lockViewersTitle)}</div>
<Button
data-test="modalBaseCloseButton"
className={styles.closeBtn}
label={intl.formatMessage(intlMessages.closeLabel)}
icon="close"
size="md"
hideLabel
onClick={this.closeModal}
/>
</div>
<div className={styles.description}>
{`${intl.formatMessage(intlMessages.lockViewersDescription)}`}
@ -117,9 +97,8 @@ class LockViewersComponent extends Component {
icons={false}
defaultChecked={meeting.lockSettingsProp.disableCam}
onChange={() => {
meeting.lockSettingsProp.disableCam =
!meeting.lockSettingsProp.disableCam;
this.toggleLockSettings(meeting);
meeting.lockSettingsProp.disableCam = !meeting.lockSettingsProp.disableCam;
toggleLockSettings(meeting);
}}
ariaLabel={intl.formatMessage(intlMessages.webcamLabel)}
/>
@ -140,9 +119,8 @@ class LockViewersComponent extends Component {
icons={false}
defaultChecked={meeting.usersProp.webcamsOnlyForModerator}
onChange={() => {
meeting.usersProp.webcamsOnlyForModerator =
!meeting.usersProp.webcamsOnlyForModerator;
this.toggleWebcamsOnlyForModerator(meeting);
meeting.usersProp.webcamsOnlyForModerator = !meeting.usersProp.webcamsOnlyForModerator;
toggleWebcamsOnlyForModerator(meeting);
}}
ariaLabel={intl.formatMessage(intlMessages.otherViewersWebcamLabel)}
/>
@ -163,9 +141,8 @@ class LockViewersComponent extends Component {
icons={false}
defaultChecked={meeting.lockSettingsProp.disableMic}
onChange={() => {
meeting.lockSettingsProp.disableMic =
!meeting.lockSettingsProp.disableMic;
this.toggleLockSettings(meeting);
meeting.lockSettingsProp.disableMic = !meeting.lockSettingsProp.disableMic;
toggleLockSettings(meeting);
}}
ariaLabel={intl.formatMessage(intlMessages.microphoneLable)}
/>
@ -186,9 +163,8 @@ class LockViewersComponent extends Component {
icons={false}
defaultChecked={meeting.lockSettingsProp.disablePubChat}
onChange={() => {
meeting.lockSettingsProp.disablePubChat =
!meeting.lockSettingsProp.disablePubChat;
this.toggleLockSettings(meeting);
meeting.lockSettingsProp.disablePubChat = !meeting.lockSettingsProp.disablePubChat;
toggleLockSettings(meeting);
}}
ariaLabel={intl.formatMessage(intlMessages.publicChatLabel)}
/>
@ -209,41 +185,17 @@ class LockViewersComponent extends Component {
icons={false}
defaultChecked={meeting.lockSettingsProp.disablePrivChat}
onChange={() => {
meeting.lockSettingsProp.disablePrivChat =
!meeting.lockSettingsProp.disablePrivChat;
this.toggleLockSettings(meeting);
meeting.lockSettingsProp.disablePrivChat = !meeting.lockSettingsProp.disablePrivChat;
toggleLockSettings(meeting);
}}
ariaLabel={intl.formatMessage(intlMessages.privateChatLable)}
/>
</div>
</div>
</div>
<div className={styles.row}>
<div className={styles.col} aria-hidden="true">
<div className={styles.formElement}>
<div className={styles.label}>
{intl.formatMessage(intlMessages.layoutLable)}
</div>
</div>
</div>
<div className={styles.col}>
<div className={cx(styles.formElement, styles.pullContentRight)}>
<Toggle
icons={false}
defaultChecked={meeting.lockSettingsProp.lockedLayout}
onChange={() => {
meeting.lockSettingsProp.lockedLayout =
!meeting.lockSettingsProp.lockedLayout;
this.toggleLockSettings(meeting);
}}
ariaLabel={intl.formatMessage(intlMessages.layoutLable)}
/>
</div>
</div>
</div>
</div>
</div>
</ModalBase>
</Modal>
);
}
}

View File

@ -9,7 +9,6 @@
}
.title {
position: relative;
left: var(--title-position-left);
color: var(--color-gray-dark);
font-weight: bold;
@ -24,6 +23,7 @@
.container {
margin: var(--modal-margin);
margin-top: 0;
margin-bottom: var(--lg-padding-x);
}

View File

@ -1,91 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { defineMessages, injectIntl } from 'react-intl';
import Button from '/imports/ui/components/button/component';
import Modal from '/imports/ui/components/modal/fullscreen/component';
import { styles } from './styles';
const propTypes = {
handleEndMeeting: PropTypes.func.isRequired,
confirmLeaving: PropTypes.func.isRequired,
intl: PropTypes.shape({
formatMessage: PropTypes.func.isRequired,
}).isRequired,
showEndMeeting: PropTypes.bool.isRequired,
};
const intlMessages = defineMessages({
title: {
id: 'app.leaveConfirmation.title',
description: 'Leave session modal title',
},
message: {
id: 'app.leaveConfirmation.message',
description: 'message for leaving session',
},
confirmLabel: {
id: 'app.leaveConfirmation.confirmLabel',
description: 'Confirmation button label',
},
confirmDesc: {
id: 'app.leaveConfirmation.confirmDesc',
description: 'adds context to confim option',
},
dismissLabel: {
id: 'app.leaveConfirmation.dismissLabel',
description: 'Dismiss button label',
},
dismissDesc: {
id: 'app.leaveConfirmation.dismissDesc',
description: 'adds context to dismiss option',
},
endMeetingLabel: {
id: 'app.leaveConfirmation.endMeetingLabel',
description: 'End meeting button label',
},
endMeetingAriaLabel: {
id: 'app.leaveConfirmation.endMeetingAriaLabel',
description: 'End meeting button aria label',
},
endMeetingDesc: {
id: 'app.leaveConfirmation.endMeetingDesc',
description: 'adds context to end meeting option',
},
});
const LeaveConfirmation = ({
intl,
handleEndMeeting,
showEndMeeting,
confirmLeaving,
}) => (
<Modal
title={intl.formatMessage(intlMessages.title)}
confirm={{
callback: confirmLeaving,
label: intl.formatMessage(intlMessages.confirmLabel),
description: intl.formatMessage(intlMessages.confirmDesc),
}}
dismiss={{
callback: () => null,
label: intl.formatMessage(intlMessages.dismissLabel),
description: intl.formatMessage(intlMessages.dismissDesc),
}}
>
{intl.formatMessage(intlMessages.message)}
{showEndMeeting ?
<Button
className={styles.endMeeting}
label={intl.formatMessage(intlMessages.endMeetingLabel)}
onClick={handleEndMeeting}
aria-describedby="modalEndMeetingDesc"
/> : null
}
<div id="modalEndMeetingDesc" hidden>{intl.formatMessage(intlMessages.endMeetingDesc)}</div>
</Modal>
);
LeaveConfirmation.propTypes = propTypes;
export default injectIntl(LeaveConfirmation);

View File

@ -1,27 +0,0 @@
import React from 'react';
import { meetingIsBreakout } from '/imports/ui/components/app/service';
import { withTracker } from 'meteor/react-meteor-data';
import { Session } from 'meteor/session';
import LogoutConfirmation from './component';
import {
isModerator,
endMeeting,
} from './service';
const LogoutConfirmationContainer = props => (
<LogoutConfirmation {...props} />
);
export default withTracker(() => {
const confirmLeaving = () => {
Session.set('isMeetingEnded', true);
Session.set('codeError', '430');
};
return {
showEndMeeting: !meetingIsBreakout() && isModerator(),
handleEndMeeting: endMeeting,
confirmLeaving,
};
})(LogoutConfirmationContainer);

View File

@ -1,20 +0,0 @@
import { makeCall } from '/imports/ui/services/api/index';
import Users from '/imports/api/users';
import mapUser from '/imports/ui/services/user/mapUser';
import Auth from '/imports/ui/services/auth';
const endMeeting = () => {
makeCall('endMeeting', Auth.credentials);
};
const isModerator = () => {
const currentUserId = Auth.userID;
const currentUser = Users.findOne({ userId: currentUserId });
return (currentUser) ? mapUser(currentUser).isModerator : null;
};
export {
endMeeting,
isModerator,
};

View File

@ -1,8 +0,0 @@
%btn {
flex: 0 1 48%;
}
.endMeeting {
@extend %btn;
margin-left: 5rem;
}

View File

@ -7,12 +7,13 @@ import { notify } from '/imports/ui/services/notification';
import VideoService from '/imports/ui/components/video-provider/service';
import getFromUserSettings from '/imports/ui/services/users-settings';
import { withModalMounter } from '/imports/ui/components/modal/service';
import VideoPreviewContainer from '/imports/ui/components/video-preview/container';
import Media from './component';
import MediaService, { getSwapLayout } from './service';
import PresentationPodsContainer from '../presentation-pod/container';
import ScreenshareContainer from '../screenshare/container';
import DefaultContent from '../presentation/default-content/component';
import ExternalVideoContainer from '../external-video-player/container';
import { getVideoId } from '../external-video-player/service';
const LAYOUT_CONFIG = Meteor.settings.public.layout;
const KURENTO_CONFIG = Meteor.settings.public.kurento;
@ -80,8 +81,9 @@ class MediaContainer extends Component {
const chromeErrorElement = (
<div>
{intl.formatMessage(intlMessages.chromeExtensionError)}{' '}
<a href={CHROME_EXTENSION_LINK} target="_blank">
{intl.formatMessage(intlMessages.chromeExtensionError)}
{' '}
<a href={CHROME_EXTENSION_LINK} target="_blank" rel="noopener noreferrer">
{intl.formatMessage(intlMessages.chromeExtensionErrorLink)}
</a>
</div>
@ -99,7 +101,7 @@ class MediaContainer extends Component {
}
}
export default withModalMounter(withTracker(({ mountModal }) => {
export default withModalMounter(withTracker(() => {
const { dataSaving } = Settings;
const { viewParticipantsWebcams, viewScreenshare } = dataSaving;
@ -118,7 +120,7 @@ export default withModalMounter(withTracker(({ mountModal }) => {
}
const usersVideo = VideoService.getAllUsersVideo();
if (MediaService.shouldShowOverlay() && usersVideo.length) {
if (MediaService.shouldShowOverlay() && usersVideo.length && viewParticipantsWebcams) {
data.floatingOverlay = usersVideo.length < 2;
data.hideOverlay = usersVideo.length === 0;
}
@ -129,7 +131,20 @@ export default withModalMounter(withTracker(({ mountModal }) => {
if (data.swapLayout) {
data.floatingOverlay = true;
data.hideOverlay = hidePresentation;
data.hideOverlay = true;
}
if (data.isScreensharing) {
data.floatingOverlay = false;
}
if (MediaService.shouldShowExternalVideo()) {
data.children = (
<ExternalVideoContainer
isPresenter={MediaService.isUserPresenter()}
videoId={getVideoId()}
/>
);
}
MediaContainer.propTypes = propTypes;

View File

@ -1,5 +1,6 @@
import Presentations from '/imports/api/presentations';
import { isVideoBroadcasting } from '/imports/ui/components/screenshare/service';
import { getVideoId } from '/imports/ui/components/external-video-player/service';
import Auth from '/imports/ui/services/auth';
import Users from '/imports/api/users';
import Settings from '/imports/ui/services/settings';
@ -30,6 +31,10 @@ function shouldShowScreenshare() {
return isVideoBroadcasting() && getFromUserSettings('enableScreensharing', KURENTO_CONFIG.enableScreensharing);
}
function shouldShowExternalVideo() {
return getVideoId() && Meteor.settings.public.app.enableExternalVideo;
}
function shouldShowOverlay() {
return getFromUserSettings('enableVideo', KURENTO_CONFIG.enableVideo);
}
@ -64,6 +69,7 @@ export default {
getPresentationInfo,
shouldShowWhiteboard,
shouldShowScreenshare,
shouldShowExternalVideo,
shouldShowOverlay,
isUserPresenter,
isVideoBroadcasting,

Some files were not shown because too many files have changed in this diff Show More