Compare commits

..

3 Commits

Author SHA1 Message Date
Andrew Welker
38cb86218c Merge branch 'development' into hotfix/add-zoomroom-share-info 2022-09-26 08:29:05 -06:00
Andrew Welker
c0c67f6c15 Merge pull request #996 from PepperDash/hotfix/zoom-room-privacy-mute-notincall
Hotfix/zoom room privacy mute notincall
2022-09-08 16:08:13 -06:00
Andrew Welker
eb3922aa43 Merge branch 'development' into hotfix/zoom-room-privacy-mute-notincall 2022-09-08 15:08:44 -06:00
6 changed files with 106 additions and 224 deletions

View File

@@ -1,56 +1,61 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Crestron.SimplSharp;
namespace PepperDash.Essentials.Core
{
/// <summary>
/// Describes the functionality required to prompt a user to enter a password
/// </summary>
public interface IPasswordPrompt
{
/// <summary>
/// Notifies when a password is required or is entered incorrectly
/// </summary>
event EventHandler<PasswordPromptEventArgs> PasswordRequired;
/// <summary>
/// Submits the password
/// </summary>
/// <param name="password"></param>
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Crestron.SimplSharp;
namespace PepperDash.Essentials.Core
{
/// <summary>
/// Describes the functionality required to prompt a user to enter a password
/// </summary>
public interface IPasswordPrompt
{
/// <summary>
/// Notifies when a password is required or is entered incorrectly
/// </summary>
event EventHandler<PasswordPromptEventArgs> PasswordRequired;
/// <summary>
/// Submits the password
/// </summary>
/// <param name="password"></param>
void SubmitPassword(string password);
}
public class PasswordPromptEventArgs : EventArgs
{
/// <summary>
/// Indicates if the last submitted password was incorrect
/// </summary>
public bool LastAttemptWasIncorrect { get; private set; }
/// <summary>
/// Indicates that the login attempt has failed
/// Cancels the password submission
/// </summary>
public bool LoginAttemptFailed { get; private set; }
/// <summary>
/// Indicates that the process was cancelled and the prompt should be dismissed
/// </summary>
public bool LoginAttemptCancelled { get; private set; }
/// <summary>
/// A message to be displayed to the user
/// </summary>
public string Message { get; private set; }
public PasswordPromptEventArgs(bool lastAttemptIncorrect, bool loginFailed, bool loginCancelled, string message)
{
LastAttemptWasIncorrect = lastAttemptIncorrect;
LoginAttemptFailed = loginFailed;
LoginAttemptCancelled = loginCancelled;
Message = message;
}
}
void CancelPasswordPrompt();
}
public class PasswordPromptEventArgs : EventArgs
{
/// <summary>
/// Indicates if the last submitted password was incorrect
/// </summary>
public bool LastAttemptWasIncorrect { get; private set; }
/// <summary>
/// Indicates that the login attempt has failed
/// </summary>
public bool LoginAttemptFailed { get; private set; }
/// <summary>
/// Indicates that the process was cancelled and the prompt should be dismissed
/// </summary>
public bool LoginAttemptCancelled { get; private set; }
/// <summary>
/// A message to be displayed to the user
/// </summary>
public string Message { get; private set; }
public PasswordPromptEventArgs(bool lastAttemptIncorrect, bool loginFailed, bool loginCancelled, string message)
{
LastAttemptWasIncorrect = lastAttemptIncorrect;
LoginAttemptFailed = loginFailed;
LoginAttemptCancelled = loginCancelled;
Message = message;
}
}
}

View File

@@ -43,12 +43,10 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec.Interfaces
[JsonProperty("isLocked", NullValueHandling = NullValueHandling.Ignore)]
public Boolean IsLocked { get; private set; }
[JsonProperty("isRecording", NullValueHandling = NullValueHandling.Ignore)]
public Boolean IsRecording { get; private set; }
[JsonProperty("canRecord", NullValueHandling = NullValueHandling.Ignore)]
public Boolean CanRecord { get; private set; }
public Boolean IsRecording { get; private set; }
public MeetingInfo(string id, string name, string host, string password, string shareStatus, bool isHost, bool isSharingMeeting, bool waitingForHost, bool isLocked, bool isRecording, bool canRecord)
public MeetingInfo(string id, string name, string host, string password, string shareStatus, bool isHost, bool isSharingMeeting, bool waitingForHost, bool isLocked, bool isRecording)
{
Id = id;
Name = name;
@@ -59,8 +57,7 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec.Interfaces
IsSharingMeeting = isSharingMeeting;
WaitingForHost = waitingForHost;
IsLocked = isLocked;
IsRecording = isRecording;
CanRecord = CanRecord;
IsRecording = isRecording;
}
}

View File

@@ -448,7 +448,6 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec.ZoomRoom
private bool _isAirHostClientConnected;
private bool _isSharingBlackMagic;
private bool _isDirectPresentationConnected;
private bool _isBlackMagicConnected;
public string directPresentationPairingCode { get; set; }
@@ -485,18 +484,7 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec.ZoomRoom
}
}
public bool isBlackMagicConnected
{
get { return _isBlackMagicConnected; }
set
{
if (value != _isBlackMagicConnected)
{
_isBlackMagicConnected = value;
NotifyPropertyChanged("isBlackMagicConnected");
}
}
}
public bool isBlackMagicConnected { get; set; }
public bool isBlackMagicDataAvailable { get; set; }
public bool isDirectPresentationConnected
@@ -639,7 +627,6 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec.ZoomRoom
// backer variables
private bool _can_Switch_Speaker_View;
private bool _can_Switch_Wall_View;
private bool _can_Switch_Strip_View;
private bool _can_Switch_Share_On_All_Screens;
private bool _can_Switch_Floating_Share_Content;
private bool _is_In_First_Page;
@@ -727,23 +714,6 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec.ZoomRoom
}
}
[JsonProperty("can_Switch_Strip_View")]
public bool can_Switch_Strip_View
{
get
{
return _can_Switch_Strip_View;
}
set
{
if (value != _can_Switch_Strip_View)
{
_can_Switch_Strip_View = value;
NotifyPropertyChanged("can_Switch_Strip_View");
}
}
}
[JsonProperty("is_In_First_Page")]
public bool is_In_First_Page
{
@@ -805,43 +775,11 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec.ZoomRoom
public class CallRecordInfo : NotifiableObject
{
private bool _meetingIsBeingRecorded;
private bool _canRecord;
private bool _emailRequired;
public bool canRecord { get; set; }
public bool emailRequired { get; set; }
public bool amIRecording { get; set; }
public bool canRecord
{
get
{
return _canRecord;
}
set
{
if (value != _canRecord)
{
_canRecord = value;
NotifyPropertyChanged("canRecord");
}
}
}
public bool emailRequired
{
get
{
return _emailRequired;
}
set
{
if (value != _emailRequired)
{
_emailRequired = value;
NotifyPropertyChanged("emailRequired");
}
}
}
public bool meetingIsBeingRecorded
{
get
@@ -860,17 +798,6 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec.ZoomRoom
}
}
/// <summary>
/// Indicates if recording is allowed (when meeting capable and and email is not required to be entered by the user)
/// </summary>
public bool AllowRecord
{
get
{
return canRecord && !emailRequired;
}
}
public CallRecordInfo()
{
Debug.Console(2, Debug.ErrorLogLevel.Notice, "********************************************* CallRecordInfo() ******************************************");

View File

@@ -529,9 +529,6 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec.ZoomRoom
/// </summary>
private void SetUpCallFeedbackActions()
{
Status.Sharing.PropertyChanged -= HandleSharingStateUpdate;
Status.Sharing.PropertyChanged += HandleSharingStateUpdate;
Status.Call.Sharing.PropertyChanged -= HandleSharingStateUpdate;
Status.Call.Sharing.PropertyChanged += HandleSharingStateUpdate;
@@ -544,7 +541,7 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec.ZoomRoom
private void HandleCallRecordInfoStateUpdate(object sender, PropertyChangedEventArgs a)
{
if (a.PropertyName == "meetingIsBeingRecorded" || a.PropertyName == "emailRequired" || a.PropertyName == "canRecord")
if (a.PropertyName == "meetingIsBeingRecorded")
{
MeetingIsRecordingFeedback.FireUpdate();
@@ -557,7 +554,7 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec.ZoomRoom
MeetingInfo.IsSharingMeeting,
MeetingInfo.WaitingForHost,
MeetingIsLockedFeedback.BoolValue,
MeetingIsRecordingFeedback.BoolValue, Status.Call.CallRecordInfo.AllowRecord);
MeetingIsRecordingFeedback.BoolValue);
MeetingInfo = meetingInfo;
}
}
@@ -583,10 +580,10 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec.ZoomRoom
private void HandleSharingStateUpdate(object sender, PropertyChangedEventArgs a)
{
//if (a.PropertyName != "State")
//{
// return;
//}
if (a.PropertyName != "State")
{
return;
}
SharingContentIsOnFeedback.FireUpdate();
ReceivingContent.FireUpdate();
@@ -596,19 +593,21 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec.ZoomRoom
// Update the share status of the meeting info
if (MeetingInfo == null)
{
MeetingInfo = new MeetingInfo("", "", "", "", GetSharingStatus(), GetIsHostMyself(), true, false, MeetingIsLockedFeedback.BoolValue, MeetingIsRecordingFeedback.BoolValue, Status.Call.CallRecordInfo.AllowRecord);
var sharingStatus = GetSharingStatus();
MeetingInfo = new MeetingInfo("", "", "", "", sharingStatus, GetIsHostMyself(), true, false, MeetingIsLockedFeedback.BoolValue, MeetingIsRecordingFeedback.BoolValue);
return;
}
var meetingInfo = new MeetingInfo(MeetingInfo.Id, MeetingInfo.Name, Participants.Host != null ? Participants.Host.Name : "None",
MeetingInfo.Password, GetSharingStatus(), GetIsHostMyself(), MeetingInfo.IsSharingMeeting, MeetingInfo.WaitingForHost, MeetingIsLockedFeedback.BoolValue, MeetingIsRecordingFeedback.BoolValue, Status.Call.CallRecordInfo.AllowRecord);
MeetingInfo.Password, GetSharingStatus(), GetIsHostMyself(), MeetingInfo.IsSharingMeeting, MeetingInfo.WaitingForHost, MeetingIsLockedFeedback.BoolValue, MeetingIsRecordingFeedback.BoolValue);
MeetingInfo = meetingInfo;
}
catch (Exception e)
{
Debug.Console(1, this, "Error processing state property update. {0}", e.Message);
Debug.Console(2, this, e.StackTrace);
MeetingInfo = new MeetingInfo("", "", "", "", "None", false, false, false, MeetingIsLockedFeedback.BoolValue, MeetingIsRecordingFeedback.BoolValue, false);
MeetingInfo = new MeetingInfo("", "", "", "", "None", false, false, false, MeetingIsLockedFeedback.BoolValue, MeetingIsRecordingFeedback.BoolValue);
}
}
@@ -715,7 +714,7 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec.ZoomRoom
MeetingInfo.IsSharingMeeting,
MeetingInfo.WaitingForHost,
MeetingIsLockedFeedback.BoolValue,
MeetingIsRecordingFeedback.BoolValue, Status.Call.CallRecordInfo.AllowRecord
MeetingIsRecordingFeedback.BoolValue
);
}
};
@@ -777,7 +776,7 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec.ZoomRoom
MeetingInfo.IsSharingMeeting,
MeetingInfo.WaitingForHost,
MeetingIsLockedFeedback.BoolValue,
MeetingIsRecordingFeedback.BoolValue, Status.Call.CallRecordInfo.AllowRecord);
MeetingIsRecordingFeedback.BoolValue);
MeetingInfo = meetingInfo;
break;
}
@@ -811,10 +810,8 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec.ZoomRoom
Debug.Console(1, this, "Status.Layout.PropertyChanged a.PropertyName: {0}", a.PropertyName);
switch (a.PropertyName.ToLower())
{
case "can_Switch_speaker_view":
case "can_switch_speaker_view":
case "can_switch_wall_view":
case "can_switch_strip_view":
case "video_type":
case "can_switch_share_on_all_screens":
{
ComputeAvailableLayouts();
@@ -830,7 +827,7 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec.ZoomRoom
LayoutViewIsOnLastPageFeedback.FireUpdate();
break;
}
case "can_switch_floating_share_content":
case "can_Switch_Floating_Share_Content":
{
CanSwapContentWithThumbnailFeedback.FireUpdate();
break;
@@ -1471,15 +1468,13 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec.ZoomRoom
MeetingInfo.Id,
MeetingInfo.Name,
Participants.Host.Name,
MeetingInfo.Password,
GetSharingStatus(),
MeetingInfo.Password,
MeetingInfo.ShareStatus,
GetIsHostMyself(),
MeetingInfo.IsSharingMeeting,
MeetingInfo.WaitingForHost,
MeetingIsLockedFeedback.BoolValue,
MeetingIsRecordingFeedback.BoolValue,
Status.Call.CallRecordInfo.AllowRecord
);
MeetingIsRecordingFeedback.BoolValue);
MeetingInfo = meetingInfo;
PrintCurrentCallParticipants();
@@ -1564,7 +1559,7 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec.ZoomRoom
{
JsonConvert.PopulateObject(responseObj.ToString(), Status.Call.Sharing);
SetDefaultLayout();
SetLayout();
break;
}
@@ -1694,14 +1689,14 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec.ZoomRoom
if (MeetingInfo == null)
{
MeetingInfo = new MeetingInfo("Waiting For Host", "Waiting For Host", "Waiting For Host", "",
GetSharingStatus(), false, false, true, MeetingIsLockedFeedback.BoolValue, MeetingIsRecordingFeedback.BoolValue, Status.Call.CallRecordInfo.AllowRecord);
GetSharingStatus(), false, false, true, MeetingIsLockedFeedback.BoolValue, MeetingIsRecordingFeedback.BoolValue);
UpdateCallStatus();
break;
}
MeetingInfo = new MeetingInfo("Waiting For Host", "Waiting For Host", "Waiting For Host", "",
GetSharingStatus(), false, false, true, MeetingIsLockedFeedback.BoolValue, MeetingIsRecordingFeedback.BoolValue, Status.Call.CallRecordInfo.AllowRecord);
GetSharingStatus(), false, false, true, MeetingIsLockedFeedback.BoolValue, MeetingIsRecordingFeedback.BoolValue);
UpdateCallStatus();
@@ -1711,12 +1706,12 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec.ZoomRoom
if (MeetingInfo == null)
{
MeetingInfo = new MeetingInfo("Waiting For Host", "Waiting For Host", "Waiting For Host", "",
GetSharingStatus(), false, false, false, MeetingIsLockedFeedback.BoolValue, MeetingIsRecordingFeedback.BoolValue, Status.Call.CallRecordInfo.AllowRecord);
GetSharingStatus(), false, false, false, MeetingIsLockedFeedback.BoolValue, MeetingIsRecordingFeedback.BoolValue);
break;
}
MeetingInfo = new MeetingInfo(MeetingInfo.Id, MeetingInfo.Name, MeetingInfo.Host, MeetingInfo.Password,
GetSharingStatus(), GetIsHostMyself(), false, false, MeetingIsLockedFeedback.BoolValue, MeetingIsRecordingFeedback.BoolValue, Status.Call.CallRecordInfo.AllowRecord);
GetSharingStatus(), GetIsHostMyself(), false, false, MeetingIsLockedFeedback.BoolValue, MeetingIsRecordingFeedback.BoolValue);
break;
}
@@ -1806,7 +1801,7 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec.ZoomRoom
if (result.Success)
{
MeetingInfo = new MeetingInfo("", "", "", "", GetSharingStatus(), true, true, MeetingInfo.WaitingForHost, MeetingIsLockedFeedback.BoolValue, MeetingIsRecordingFeedback.BoolValue, Status.Call.CallRecordInfo.AllowRecord);
MeetingInfo = new MeetingInfo("", "", "", "", "", true, true, MeetingInfo.WaitingForHost, MeetingIsLockedFeedback.BoolValue, MeetingIsRecordingFeedback.BoolValue);
break;
}
@@ -1940,7 +1935,7 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec.ZoomRoom
}
}
private void SetDefaultLayout()
private void SetLayout()
{
if (!_props.AutoDefaultLayouts) return;
@@ -1953,13 +1948,8 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec.ZoomRoom
}
else
{
if (_props.DefaultCallLayout == (_props.DefaultCallLayout & AvailableLayouts))
{
SendText(String.Format("zconfiguration call layout style: {0}",
_props.DefaultCallLayout));
}
else
Debug.Console(0, this, "Unable to set default Layout. {0} not currently an available layout based on meeting state", _props.DefaultCallLayout);
SendText(String.Format("zconfiguration call layout style: {0}",
_props.DefaultCallLayout));
}
}
@@ -2157,12 +2147,10 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec.ZoomRoom
!String.Equals(Status.Call.Info.meeting_type,"NORMAL"),
false,
MeetingIsLockedFeedback.BoolValue,
MeetingIsRecordingFeedback.BoolValue, Status.Call.CallRecordInfo.AllowRecord
MeetingIsRecordingFeedback.BoolValue
);
SetDefaultLayout();
}
// TODO [ ] Issue #868
else if (item.Status == eCodecCallStatus.Disconnected)
{
MeetingInfo = new MeetingInfo(
@@ -2175,7 +2163,7 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec.ZoomRoom
false,
false,
false,
false, Status.Call.CallRecordInfo.AllowRecord
false
);
}
@@ -2183,6 +2171,11 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec.ZoomRoom
Debug.Console(1, this, "[OnCallStatusChange] Current Call Status: {0}",
Status.Call != null ? Status.Call.Status.ToString() : "no call");
if (_props.AutoDefaultLayouts)
{
SetLayout();
}
}
private string GetSharingStatus()
@@ -2395,17 +2388,6 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec.ZoomRoom
/// <param name="joinMap"></param>
public void LinkZoomRoomToApi(BasicTriList trilist, ZoomRoomJoinMap joinMap)
{
var meetingInfoCodec = this as IHasMeetingInfo;
if (meetingInfoCodec != null)
{
trilist.SetBool(joinMap.MeetingCanRecord.JoinNumber, meetingInfoCodec.MeetingInfo.CanRecord);
meetingInfoCodec.MeetingInfoChanged += (o, a) =>
{
trilist.SetBool(joinMap.MeetingCanRecord.JoinNumber, a.Info.CanRecord);
};
}
var recordingCodec = this as IHasMeetingRecordingWithPrompt;
if (recordingCodec != null)
{
@@ -3282,7 +3264,7 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec.ZoomRoom
// There is no property that directly reports if strip mode is valid, but API stipulates
// that strip mode is available if the number of screens is 1
if (Status.NumberOfScreens.NumOfScreens == 1 || Status.Layout.can_Switch_Strip_View || Status.Layout.video_type.ToLower() == "strip")
if (Status.NumberOfScreens.NumOfScreens == 1)
{
availableLayouts |= zConfiguration.eLayoutStyle.Strip;
}
@@ -3297,15 +3279,10 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec.ZoomRoom
var handler = LayoutInfoChanged;
if (handler != null)
{
var currentLayout = zConfiguration.eLayoutStyle.None;
currentLayout = (zConfiguration.eLayoutStyle)Enum.Parse(typeof(zConfiguration.eLayoutStyle), string.IsNullOrEmpty(LocalLayoutFeedback.StringValue) ? "None" : LocalLayoutFeedback.StringValue, true);
handler(this, new LayoutInfoChangedEventArgs()
{
AvailableLayouts = AvailableLayouts,
CurrentSelectedLayout = currentLayout,
CurrentSelectedLayout = (zConfiguration.eLayoutStyle)Enum.Parse(typeof(zConfiguration.eLayoutStyle),string.IsNullOrEmpty(LocalLayoutFeedback.StringValue) ? "None" : LocalLayoutFeedback.StringValue , true),
LayoutViewIsOnFirstPage = LayoutViewIsOnFirstPageFeedback.BoolValue,
LayoutViewIsOnLastPage = LayoutViewIsOnLastPageFeedback.BoolValue,
CanSwapContentWithThumbnail = CanSwapContentWithThumbnailFeedback.BoolValue,
@@ -3438,6 +3415,11 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec.ZoomRoom
//OnPasswordRequired(false, false, true, "");
}
public void CancelPasswordPrompt()
{
OnPasswordRequired(false, false, true, "Login Cancelled");
}
void OnPasswordRequired(bool lastAttemptIncorrect, bool loginFailed, bool loginCancelled, string message)
{
var handler = PasswordRequired;
@@ -3586,18 +3568,6 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec.ZoomRoom
void OnShareInfoChanged(zStatus.Sharing status)
{
Debug.Console(2, this,
@"ShareInfoChanged:
isSharingHDMI: {0}
isSharingAirplay: {1}
AirplayPassword: {2}
OSD Display State: {3}
",
status.isSharingBlackMagic,
status.isAirHostClientConnected,
status.password,
status.dispState);
var handler = ShareInfoChanged;
if (handler != null)
{

View File

@@ -373,20 +373,6 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec.ZoomRoom
JoinType = eJoinType.Digital
});
[JoinName("MeetingCanRecord")]
public JoinDataComplete MeetingCanRecord = new JoinDataComplete(
new JoinData
{
JoinNumber = 246,
JoinSpan = 1
},
new JoinMetadata
{
Description = "When high, indicated that the current meeting can be recorded",
JoinCapabilities = eJoinCapabilities.ToSIMPL,
JoinType = eJoinType.Digital
});
#region Sharing Status

View File

@@ -4,7 +4,6 @@ using System.Linq;
using System.Text;
using Crestron.SimplSharp;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using PepperDash.Core;
using PepperDash.Essentials.Core;
@@ -30,13 +29,11 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec.ZoomRoom
/* This layout will be selected when Sharing starts (either from Far end or locally)*/
[JsonProperty("defaultSharingLayout")]
[JsonConverter(typeof(StringEnumConverter))]
public zConfiguration.eLayoutStyle DefaultSharingLayout { get; set; }
public string DefaultSharingLayout { get; set; }
//This layout will be selected when a call is connected and no content is being shared
[JsonProperty("defaultCallLayout")]
[JsonConverter(typeof(StringEnumConverter))]
public zConfiguration.eLayoutStyle DefaultCallLayout { get; set; }
public string DefaultCallLayout { get; set; }
[JsonProperty("minutesBeforeMeetingStart")]
public int MinutesBeforeMeetingStart { get; set; }