Files
Neil Dorin 0e5cecbfc3 fix(essentials): #901 Refines initial synchronization sequence
Makes sure that the AddedContact feedback subscription is excluded right away, but later included after the phonebook is retrieved.  This should ensure we get the initial phonebook and all subsequent additions at runtime while also minimizing initial data volume.
2022-02-11 14:16:40 -07:00

3684 lines
112 KiB
C#

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using Crestron.SimplSharp;
using Crestron.SimplSharpPro.CrestronThread;
using Crestron.SimplSharpPro.DeviceSupport;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using PepperDash.Core;
using PepperDash.Essentials.Core;
using PepperDash.Essentials.Core.Bridges;
using PepperDash.Essentials.Core.Config;
using PepperDash.Essentials.Core.DeviceTypeInterfaces;
using PepperDash.Essentials.Core.Routing;
using PepperDash.Essentials.Core.Queues;
using PepperDash.Essentials.Devices.Common.Cameras;
using PepperDash.Essentials.Devices.Common.Codec;
using PepperDash.Essentials.Devices.Common.VideoCodec.Cisco;
using PepperDash.Essentials.Devices.Common.VideoCodec.Interfaces;
namespace PepperDash.Essentials.Devices.Common.VideoCodec.ZoomRoom
{
public class ZoomRoom : VideoCodecBase, IHasCodecSelfView, IHasDirectoryHistoryStack, ICommunicationMonitor,
IRouting,
IHasScheduleAwareness, IHasCodecCameras, IHasParticipants, IHasCameraOff, IHasCameraMute, IHasCameraAutoMode,
IHasFarEndContentStatus, IHasSelfviewPosition, IHasPhoneDialing, IHasZoomRoomLayouts, IHasParticipantPinUnpin,
IHasParticipantAudioMute, IHasSelfviewSize, IPasswordPrompt, IHasStartMeeting, IHasMeetingInfo, IHasPresentationOnlyMeeting,
IHasMeetingLock, IHasMeetingRecording
{
private const long MeetingRefreshTimer = 60000;
public uint DefaultMeetingDurationMin { get; private set; }
/// <summary>
/// CR LF
/// </summary>
private const string EchoDelimiter = "\x0D\x0A\x0D\x0A";
private const string SendDelimiter = "\x0D";
/// <summary>
/// CR LF } CR LF
/// </summary>
private const string JsonDelimiter = "\x0D\x0A\x7D\x0D\x0A";
private string[] Delimiters = new string[] { EchoDelimiter, JsonDelimiter, "OK\x0D\x0A", "end\x0D\x0A" };
//"echo off\x0D\x0A\x0A\x0D\x0A"
private readonly GenericQueue _receiveQueue;
//private readonly CrestronQueue<string> _receiveQueue;
//private readonly Thread _receiveThread;
private readonly ZoomRoomSyncState _syncState;
public bool CommDebuggingIsOn;
private CodecDirectory _currentDirectoryResult;
private uint _jsonCurlyBraceCounter;
private bool _jsonFeedbackMessageIsIncoming;
private StringBuilder _jsonMessage;
private int _previousVolumeLevel;
private CameraBase _selectedCamera;
private string _lastDialedMeetingNumber;
private readonly ZoomRoomPropertiesConfig _props;
public ZoomRoom(DeviceConfig config, IBasicCommunication comm)
: base(config)
{
DefaultMeetingDurationMin = 30;
_props = JsonConvert.DeserializeObject<ZoomRoomPropertiesConfig>(config.Properties.ToString());
_receiveQueue = new GenericQueue(Key + "-rxQueue", Thread.eThreadPriority.MediumPriority, 2048);
Communication = comm;
if (_props.CommunicationMonitorProperties != null)
{
CommunicationMonitor = new GenericCommunicationMonitor(this, Communication,
_props.CommunicationMonitorProperties);
}
else
{
CommunicationMonitor = new GenericCommunicationMonitor(this, Communication, 30000, 120000, 300000,
"zStatus SystemUnit" + SendDelimiter);
}
DeviceManager.AddDevice(CommunicationMonitor);
Status = new ZoomRoomStatus();
Configuration = new ZoomRoomConfiguration();
CodecInfo = new ZoomRoomInfo(Status, Configuration);
_syncState = new ZoomRoomSyncState(Key + "--Sync", this);
_syncState.InitialSyncCompleted += SyncState_InitialSyncCompleted;
_syncState.FirstJsonResponseReceived += (o, a) => SetUpSyncQueries();
PhonebookSyncState = new CodecPhonebookSyncState(Key + "--PhonebookSync");
PhonebookSyncState.InitialSyncCompleted += (o, a) => ResubscribeForAddedContacts();
PortGather = new CommunicationGather(Communication, Delimiters) {IncludeDelimiter = true};
PortGather.LineReceived += Port_LineReceived;
CodecOsdIn = new RoutingInputPort(RoutingPortNames.CodecOsd,
eRoutingSignalType.Audio | eRoutingSignalType.Video,
eRoutingPortConnectionType.Hdmi, new Action(StopSharing), this);
Output1 = new RoutingOutputPort(RoutingPortNames.AnyVideoOut,
eRoutingSignalType.Audio | eRoutingSignalType.Video,
eRoutingPortConnectionType.Hdmi, null, this);
Output2 = new RoutingOutputPort(RoutingPortNames.AnyVideoOut,
eRoutingSignalType.Video,
eRoutingPortConnectionType.DisplayPort, null, this);
SelfviewIsOnFeedback = new BoolFeedback(SelfViewIsOnFeedbackFunc);
CameraIsOffFeedback = new BoolFeedback(CameraIsOffFeedbackFunc);
CameraIsMutedFeedback = CameraIsOffFeedback;
CameraAutoModeIsOnFeedback = new BoolFeedback(CameraAutoModeIsOnFeedbackFunc);
CodecSchedule = new CodecScheduleAwareness(MeetingRefreshTimer);
if (_props.MinutesBeforeMeetingStart > 0)
{
CodecSchedule.MeetingWarningMinutes = _props.MinutesBeforeMeetingStart;
}
ReceivingContent = new BoolFeedback(FarEndIsSharingContentFeedbackFunc);
SelfviewPipPositionFeedback = new StringFeedback(SelfviewPipPositionFeedbackFunc);
// TODO: #714 [ ] SelfviewPipSizeFeedback
SelfviewPipSizeFeedback = new StringFeedback(SelfviewPipSizeFeedbackFunc);
SetUpFeedbackActions();
Cameras = new List<CameraBase>();
SetUpDirectory();
Participants = new CodecParticipants();
SupportsCameraOff = true; // Always allow turning off the camera for zoom calls?
SupportsCameraAutoMode = _props.SupportsCameraAutoMode;
PhoneOffHookFeedback = new BoolFeedback(PhoneOffHookFeedbackFunc);
CallerIdNameFeedback = new StringFeedback(CallerIdNameFeedbackFunc);
CallerIdNumberFeedback = new StringFeedback(CallerIdNumberFeedbackFunc);
LocalLayoutFeedback = new StringFeedback(LocalLayoutFeedbackFunc);
LayoutViewIsOnFirstPageFeedback = new BoolFeedback(LayoutViewIsOnFirstPageFeedbackFunc);
LayoutViewIsOnLastPageFeedback = new BoolFeedback(LayoutViewIsOnLastPageFeedbackFunc);
CanSwapContentWithThumbnailFeedback = new BoolFeedback(CanSwapContentWithThumbnailFeedbackFunc);
ContentSwappedWithThumbnailFeedback = new BoolFeedback(ContentSwappedWithThumbnailFeedbackFunc);
NumberOfScreensFeedback = new IntFeedback(NumberOfScreensFeedbackFunc);
MeetingIsLockedFeedback = new BoolFeedback(() => Configuration.Call.Lock.Enable );
MeetingIsRecordingFeedback = new BoolFeedback(() => Status.Call.CallRecordInfo.meetingIsBeingRecorded );
}
public CommunicationGather PortGather { get; private set; }
public ZoomRoomStatus Status { get; private set; }
public ZoomRoomConfiguration Configuration { get; private set; }
//CTimer LoginMessageReceivedTimer;
//CTimer RetryConnectionTimer;
/// <summary>
/// Gets and returns the scaled volume of the codec
/// </summary>
protected override Func<int> VolumeLevelFeedbackFunc
{
get
{
return () =>
{
var scaledVol = CrestronEnvironment.ScaleWithLimits(Configuration.Audio.Output.Volume, 100, 0, 65535, 0);
if (Configuration.Audio.Output.Volume != 0)
{
Debug.Console(2, this, "Storing previous volume level as: {0}, scaled: {1}", Configuration.Audio.Output.Volume,
scaledVol);
_previousVolumeLevel = scaledVol; // Store the previous level for recall
}
return scaledVol;
};
}
}
protected override Func<bool> PrivacyModeIsOnFeedbackFunc
{
get { return () => Configuration.Call.Microphone.Mute; }
}
protected override Func<bool> StandbyIsOnFeedbackFunc
{
get { return () => false; }
}
protected override Func<string> SharingSourceFeedbackFunc
{
get { return () => Status.Sharing.dispState; }
}
protected override Func<bool> SharingContentIsOnFeedbackFunc
{
get { return () => Status.Call.Sharing.IsSharing; }
}
protected Func<bool> FarEndIsSharingContentFeedbackFunc
{
get { return () => Status.Call.Sharing.State == zEvent.eSharingState.Receiving; }
}
protected override Func<bool> MuteFeedbackFunc
{
get { return () => Configuration.Audio.Output.Volume == 0; }
}
//protected Func<bool> RoomIsOccupiedFeedbackFunc
//{
// get
// {
// return () => false;
// }
//}
//protected Func<int> PeopleCountFeedbackFunc
//{
// get
// {
// return () => 0;
// }
//}
protected Func<bool> SelfViewIsOnFeedbackFunc
{
get { return () => !Configuration.Video.HideConfSelfVideo; }
}
protected Func<bool> CameraIsOffFeedbackFunc
{
get { return () => Configuration.Call.Camera.Mute; }
}
protected Func<bool> CameraAutoModeIsOnFeedbackFunc
{
get { return () => false; }
}
protected Func<string> SelfviewPipPositionFeedbackFunc
{
get
{
return
() =>
_currentSelfviewPipPosition != null
? _currentSelfviewPipPosition.Command ?? "Unknown"
: "Unknown";
}
}
// TODO: #714 [ ] SelfviewPipSizeFeedbackFunc
protected Func<string> SelfviewPipSizeFeedbackFunc
{
get
{
return
() =>
_currentSelfviewPipSize != null
? _currentSelfviewPipSize.Command ?? "Unknown"
: "Unknown";
}
}
protected Func<bool> LocalLayoutIsProminentFeedbackFunc
{
get { return () => false; }
}
public RoutingInputPort CodecOsdIn { get; private set; }
public RoutingOutputPort Output1 { get; private set; }
public RoutingOutputPort Output2 { get; private set; }
#region ICommunicationMonitor Members
public StatusMonitorBase CommunicationMonitor { get; private set; }
#endregion
#region IHasCodecCameras Members
public event EventHandler<CameraSelectedEventArgs> CameraSelected;
public List<CameraBase> Cameras { get; private set; }
public CameraBase SelectedCamera
{
get { return _selectedCamera; }
private set
{
_selectedCamera = value;
SelectedCameraFeedback.FireUpdate();
ControllingFarEndCameraFeedback.FireUpdate();
var handler = CameraSelected;
if (handler != null)
{
handler(this, new CameraSelectedEventArgs(_selectedCamera));
}
}
}
public StringFeedback SelectedCameraFeedback { get; private set; }
public void SelectCamera(string key)
{
if (Cameras == null)
{
return;
}
var camera = Cameras.FirstOrDefault(c => c.Key.IndexOf(key, StringComparison.OrdinalIgnoreCase) > -1);
if (camera != null)
{
Debug.Console(1, this, "Selected Camera with key: '{0}'", camera.Key);
SelectedCamera = camera;
}
else
{
Debug.Console(1, this, "Unable to select camera with key: '{0}'", key);
}
}
public CameraBase FarEndCamera { get; private set; }
public BoolFeedback ControllingFarEndCameraFeedback { get; private set; }
#endregion
#region IHasCodecSelfView Members
public BoolFeedback SelfviewIsOnFeedback { get; private set; }
public void GetSelfViewMode()
{
SendText("zConfiguration Video hide_conf_self_video");
}
public void SelfViewModeOn()
{
SendText("zConfiguration Video hide_conf_self_video: off");
}
public void SelfViewModeOff()
{
SendText("zConfiguration Video hide_conf_self_video: on");
}
public void SelfViewModeToggle()
{
if (SelfviewIsOnFeedback.BoolValue)
{
SelfViewModeOff();
}
else
{
SelfViewModeOn();
}
}
#endregion
#region IHasDirectoryHistoryStack Members
public event EventHandler<DirectoryEventArgs> DirectoryResultReturned;
public CodecDirectory DirectoryRoot { get; private set; }
public CodecDirectory CurrentDirectoryResult
{
get { return _currentDirectoryResult; }
private set
{
_currentDirectoryResult = value;
Debug.Console(2, this, "CurrentDirectoryResult Updated. ResultsFolderId: {0}",
_currentDirectoryResult.ResultsFolderId);
CurrentDirectoryResultIsNotDirectoryRoot.FireUpdate();
OnDirectoryResultReturned(_currentDirectoryResult);
}
}
public CodecPhonebookSyncState PhonebookSyncState { get; private set; }
public void SearchDirectory(string searchString)
{
var directoryResults = new CodecDirectory();
directoryResults.AddContactsToDirectory(
DirectoryRoot.CurrentDirectoryResults.FindAll(
c => c.Name.IndexOf(searchString, 0, StringComparison.OrdinalIgnoreCase) > -1));
DirectoryBrowseHistoryStack.Clear();
CurrentDirectoryResult = directoryResults;
}
public void GetDirectoryFolderContents(string folderId)
{
var directoryResults = new CodecDirectory {ResultsFolderId = folderId};
directoryResults.AddContactsToDirectory(
DirectoryRoot.CurrentDirectoryResults.FindAll(c => c.ParentFolderId.Equals(folderId)));
DirectoryBrowseHistoryStack.Push(_currentDirectoryResult);
CurrentDirectoryResult = directoryResults;
}
public void SetCurrentDirectoryToRoot()
{
DirectoryBrowseHistoryStack.Clear();
CurrentDirectoryResult = DirectoryRoot;
}
public void GetDirectoryParentFolderContents()
{
if (DirectoryBrowseHistoryStack.Count == 0)
{
return;
}
var currentDirectory = DirectoryBrowseHistoryStack.Pop();
CurrentDirectoryResult = currentDirectory;
}
public BoolFeedback CurrentDirectoryResultIsNotDirectoryRoot { get; private set; }
public List<CodecDirectory> DirectoryBrowseHistory { get; private set; }
public Stack<CodecDirectory> DirectoryBrowseHistoryStack { get; private set; }
#endregion
#region IHasScheduleAwareness Members
public CodecScheduleAwareness CodecSchedule { get; private set; }
public void GetSchedule()
{
GetBookings();
}
#endregion
#region IRouting Members
public void ExecuteSwitch(object inputSelector, object outputSelector, eRoutingSignalType signalType)
{
ExecuteSwitch(inputSelector);
}
#endregion
private void SyncState_InitialSyncCompleted(object sender, EventArgs e)
{
SetUpRouting();
SetIsReady();
}
private void SetUpCallFeedbackActions()
{
Status.Call.Sharing.PropertyChanged += HandleSharingStateUpdate;
Status.Call.PropertyChanged += (o, a) =>
{
if (a.PropertyName == "Info")
{
Debug.Console(1, this, "Updating Call Status");
UpdateCallStatus();
}
};
}
private void HandleSharingStateUpdate(object sender, PropertyChangedEventArgs a)
{
if (a.PropertyName != "State")
{
return;
}
SharingContentIsOnFeedback.FireUpdate();
ReceivingContent.FireUpdate();
try
{
// Update the share status of the meeting info
if (MeetingInfo == null)
{
var sharingStatus = GetSharingStatus();
MeetingInfo = new MeetingInfo("", "", "", "", sharingStatus, GetIsHostMyself(), true, false, MeetingIsLockedFeedback.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);
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);
}
}
/// <summary>
/// Subscribes to the PropertyChanged events on the state objects and fires the corresponding feedbacks.
/// </summary>
private void SetUpFeedbackActions()
{
Configuration.Audio.Output.PropertyChanged += (o, a) =>
{
if (a.PropertyName == "Volume")
{
VolumeLevelFeedback.FireUpdate();
MuteFeedback.FireUpdate();
}
};
Configuration.Call.Microphone.PropertyChanged += (o, a) =>
{
if (a.PropertyName == "Mute")
{
PrivacyModeIsOnFeedback.FireUpdate();
}
};
Configuration.Video.PropertyChanged += (o, a) =>
{
if (a.PropertyName == "HideConfSelfVideo")
{
SelfviewIsOnFeedback.FireUpdate();
}
};
Configuration.Video.Camera.PropertyChanged += (o, a) =>
{
if (a.PropertyName == "SelectedId")
{
SelectCamera(Configuration.Video.Camera.SelectedId);
// this will in turn fire the affected feedbacks
}
};
Configuration.Call.Camera.PropertyChanged += (o, a) =>
{
Debug.Console(1, this, "Configuration.Call.Camera.PropertyChanged: {0}", a.PropertyName);
if (a.PropertyName != "Mute") return;
CameraIsOffFeedback.FireUpdate();
CameraAutoModeIsOnFeedback.FireUpdate();
};
Configuration.Call.Layout.PropertyChanged += (o, a) =>
{
switch (a.PropertyName)
{
case "Position":
{
ComputeSelfviewPipPositionStatus();
SelfviewPipPositionFeedback.FireUpdate();
break;
}
case "ShareThumb":
{
ContentSwappedWithThumbnailFeedback.FireUpdate();
OnLayoutInfoChanged();
break;
}
case "Style":
{
LocalLayoutFeedback.FireUpdate();
OnLayoutInfoChanged();
break;
}
case "Size":
{
// TODO: #714 [ ] SetupFeedbackActions >> Size
ComputeSelfviewPipSizeStatus();
SelfviewPipSizeFeedback.FireUpdate();
break;
}
}
};
Configuration.Call.Lock.PropertyChanged += (o, a) =>
{
if (a.PropertyName == "Enable")
{
MeetingIsLockedFeedback.FireUpdate();
MeetingInfo = new MeetingInfo
(
MeetingInfo.Id,
MeetingInfo.Name,
MeetingInfo.Host,
MeetingInfo.Password,
GetSharingStatus(),
MeetingInfo.IsHost,
MeetingInfo.IsSharingMeeting,
MeetingInfo.WaitingForHost,
MeetingIsLockedFeedback.BoolValue
);
}
};
// This is to deal with incorrect object structure coming back from the Zoom Room on v 5.6.3
Configuration.Client.Call.Layout.PropertyChanged += (o, a) =>
{
switch (a.PropertyName)
{
case "Position":
{
ComputeSelfviewPipPositionStatus();
SelfviewPipPositionFeedback.FireUpdate();
break;
}
case "ShareThumb":
{
ContentSwappedWithThumbnailFeedback.FireUpdate();
OnLayoutInfoChanged();
break;
}
case "Style":
{
LocalLayoutFeedback.FireUpdate();
OnLayoutInfoChanged();
break;
}
}
};
Status.Call.Sharing.PropertyChanged += (o, a) =>
{
if (a.PropertyName == "State")
{
SharingContentIsOnFeedback.FireUpdate();
ReceivingContent.FireUpdate();
}
};
Status.Call.PropertyChanged += (o, a) =>
{
switch(a.PropertyName)
{
case "Info":
{
Debug.Console(1, this, "Updating Call Status");
UpdateCallStatus();
break;
}
case "Status":
{
UpdateCallStatus();
break;
}
}
};
Status.Call.CallRecordInfo.PropertyChanged += (o, a) =>
{
if (a.PropertyName == "meetingIsBeingRecorded")
{
MeetingIsRecordingFeedback.FireUpdate();
}
};
Status.Sharing.PropertyChanged += (o, a) =>
{
switch (a.PropertyName)
{
case "dispState":
SharingSourceFeedback.FireUpdate();
break;
case "password":
break;
case "isAirHostClientConnected":
case "isDirectPresentationConnected":
case "isSharingBlackMagic":
{
Debug.Console(2, this, "Updating sharing status: {0}", a.PropertyName);
SharingContentIsOnFeedback.FireUpdate();
if (MeetingInfo == null)
{
//Ignoring for now, as the CallInfo return will create the appropriate value
return;
}
// Update the share status of the meeting info
var meetingInfo = new MeetingInfo(MeetingInfo.Id,
MeetingInfo.Name,
MeetingInfo.Host,
MeetingInfo.Password,
GetSharingStatus(),
GetIsHostMyself(),
MeetingInfo.IsSharingMeeting,
MeetingInfo.WaitingForHost,
MeetingIsLockedFeedback.BoolValue);
MeetingInfo = meetingInfo;
break;
}
}
};
Status.PhoneCall.PropertyChanged += (o, a) =>
{
switch (a.PropertyName)
{
case "IsIncomingCall":
Debug.Console(1, this, "Incoming Phone Call: {0}", Status.PhoneCall.IsIncomingCall);
break;
case "PeerDisplayName":
Debug.Console(1, this, "Peer Display Name: {0}", Status.PhoneCall.PeerDisplayName);
CallerIdNameFeedback.FireUpdate();
break;
case "PeerNumber":
Debug.Console(1, this, "Peer Number: {0}", Status.PhoneCall.PeerNumber);
CallerIdNumberFeedback.FireUpdate();
break;
case "OffHook":
Debug.Console(1, this, "Phone is OffHook: {0}", Status.PhoneCall.OffHook);
PhoneOffHookFeedback.FireUpdate();
break;
}
};
Status.Layout.PropertyChanged += (o, a) =>
{
Debug.Console(1, this, "Status.Layout.PropertyChanged a.PropertyName: {0}", a.PropertyName);
switch (a.PropertyName.ToLower())
{
case "can_switch_speaker_view":
case "can_switch_wall_view":
case "can_switch_share_on_all_screens":
{
ComputeAvailableLayouts();
break;
}
case "is_in_first_page":
{
LayoutViewIsOnFirstPageFeedback.FireUpdate();
break;
}
case "is_in_last_page":
{
LayoutViewIsOnLastPageFeedback.FireUpdate();
break;
}
case "can_Switch_Floating_Share_Content":
{
CanSwapContentWithThumbnailFeedback.FireUpdate();
break;
}
}
OnLayoutInfoChanged();
};
Status.NumberOfScreens.PropertyChanged += (o, a) =>
{
switch (a.PropertyName)
{
case "NumberOfScreens":
{
NumberOfScreensFeedback.FireUpdate();
break;
}
}
};
}
private void SetUpDirectory()
{
DirectoryRoot = new CodecDirectory() {ResultsFolderId = "root"};
CurrentDirectoryResultIsNotDirectoryRoot = new BoolFeedback(() => CurrentDirectoryResult.ResultsFolderId != "root");
CurrentDirectoryResult = DirectoryRoot;
DirectoryBrowseHistory = new List<CodecDirectory>();
DirectoryBrowseHistoryStack = new Stack<CodecDirectory>();
}
private void SetUpRouting()
{
// Set up input ports
CreateOsdSource();
InputPorts.Add(CodecOsdIn);
// Set up output ports
OutputPorts.Add(Output1);
OutputPorts.Add(Output2);
}
/// <summary>
/// Creates the fake OSD source, and connects it's AudioVideo output to the CodecOsdIn input
/// to enable routing
/// </summary>
private void CreateOsdSource()
{
OsdSource = new DummyRoutingInputsDevice(Key + "[osd]");
DeviceManager.AddDevice(OsdSource);
var tl = new TieLine(OsdSource.AudioVideoOutputPort, CodecOsdIn);
TieLineCollection.Default.Add(tl);
//foreach(var input in Status.Video.
}
/// <summary>
/// Starts the HTTP feedback server and syncronizes state of codec
/// </summary>
/// <returns></returns>
public override bool CustomActivate()
{
CrestronConsole.AddNewConsoleCommand(SetCommDebug, "SetCodecCommDebug", "0 for Off, 1 for on",
ConsoleAccessLevelEnum.AccessOperator);
if (!_props.DisablePhonebookAutoDownload)
{
CrestronConsole.AddNewConsoleCommand(s => SendText("zCommand Phonebook List Offset: 0 Limit: 10000"),
"GetZoomRoomContacts", "Triggers a refresh of the codec phonebook",
ConsoleAccessLevelEnum.AccessOperator);
}
CrestronConsole.AddNewConsoleCommand(s => GetBookings(), "GetZoomRoomBookings",
"Triggers a refresh of the booking data for today", ConsoleAccessLevelEnum.AccessOperator);
return base.CustomActivate();
}
#region Overrides of Device
public override void Initialize()
{
var socket = Communication as ISocketStatus;
if (socket != null)
{
socket.ConnectionChange += socket_ConnectionChange;
}
CommDebuggingIsOn = false;
Communication.Connect();
CommunicationMonitor.Start();
}
#endregion
public void SetCommDebug(string s)
{
if (s == "1")
{
CommDebuggingIsOn = true;
Debug.Console(1, this, "Comm Debug Enabled.");
}
else
{
CommDebuggingIsOn = false;
Debug.Console(1, this, "Comm Debug Disabled.");
}
}
private void socket_ConnectionChange(object sender, GenericSocketStatusChageEventArgs e)
{
Debug.Console(1, this, "Socket status change {0}", e.Client.ClientStatus);
if (e.Client.IsConnected)
{
}
else
{
_syncState.CodecDisconnected();
PhonebookSyncState.CodecDisconnected();
}
}
public void SendText(string command)
{
if (CommDebuggingIsOn)
{
Debug.Console(1, this, "Sending: '{0}'", command);
}
Communication.SendText(command + SendDelimiter);
}
/// <summary>
/// Gathers responses and enqueues them.
/// </summary>
/// <param name="dev"></param>
/// <param name="args"></param>
private void Port_LineReceived(object dev, GenericCommMethodReceiveTextArgs args)
{
//Debug.Console(0, this, "Port_LineReceived");
if (args.Delimiter != JsonDelimiter)
{
// Debug.Console(0, this,
//@"Non JSON response:
//Delimiter: {0}
//{1}", ComTextHelper.GetDebugText(args.Delimiter), args.Text);
ProcessNonJsonResponse(args.Text);
return;
}
else
{
// Debug.Console(0, this,
//@"JSON response:
//Delimiter: {0}
//{1}", ComTextHelper.GetDebugText(args.Delimiter), args.Text);
_receiveQueue.Enqueue(new ProcessStringMessage(args.Text, DeserializeResponse));
//_receiveQueue.Enqueue(new ProcessStringMessage(args.Text, ProcessMessage));
}
}
/// <summary>
/// Queues the initial queries to be sent upon connection
/// </summary>
private void SetUpSyncQueries()
{
// zStatus
_syncState.AddQueryToQueue("zStatus Call Status");
_syncState.AddQueryToQueue("zStatus Audio Input Line");
_syncState.AddQueryToQueue("zStatus Audio Output Line");
_syncState.AddQueryToQueue("zStatus Video Camera Line");
_syncState.AddQueryToQueue("zStatus Video Optimizable");
_syncState.AddQueryToQueue("zStatus Capabilities");
_syncState.AddQueryToQueue("zStatus Sharing");
_syncState.AddQueryToQueue("zStatus CameraShare");
_syncState.AddQueryToQueue("zStatus Call Layout");
_syncState.AddQueryToQueue("zStatus Call ClosedCaption Available");
_syncState.AddQueryToQueue("zStatus NumberOfScreens");
// zConfiguration
_syncState.AddQueryToQueue("zConfiguration Call Sharing optimize_video_sharing");
_syncState.AddQueryToQueue("zConfiguration Call Microphone Mute");
_syncState.AddQueryToQueue("zConfiguration Call Camera Mute");
_syncState.AddQueryToQueue("zConfiguration Audio Input SelectedId");
_syncState.AddQueryToQueue("zConfiguration Audio Input is_sap_disabled");
_syncState.AddQueryToQueue("zConfiguration Audio Input reduce_reverb");
_syncState.AddQueryToQueue("zConfiguration Audio Input volume");
_syncState.AddQueryToQueue("zConfiguration Audio Output selectedId");
_syncState.AddQueryToQueue("zConfiguration Audio Output volume");
_syncState.AddQueryToQueue("zConfiguration Video hide_conf_self_video");
_syncState.AddQueryToQueue("zConfiguration Video Camera selectedId");
_syncState.AddQueryToQueue("zConfiguration Video Camera Mirror");
_syncState.AddQueryToQueue("zConfiguration Client appVersion");
_syncState.AddQueryToQueue("zConfiguration Client deviceSystem");
_syncState.AddQueryToQueue("zConfiguration Call Layout ShareThumb");
_syncState.AddQueryToQueue("zConfiguration Call Layout Style");
_syncState.AddQueryToQueue("zConfiguration Call Layout Size");
_syncState.AddQueryToQueue("zConfiguration Call Layout Position");
_syncState.AddQueryToQueue("zConfiguration Call Lock Enable");
_syncState.AddQueryToQueue("zConfiguration Call MuteUserOnEntry Enable");
_syncState.AddQueryToQueue("zConfiguration Call ClosedCaption FontSize ");
_syncState.AddQueryToQueue("zConfiguration Call ClosedCaption Visible");
// zCommand
if (!_props.DisablePhonebookAutoDownload)
{
_syncState.AddQueryToQueue("zCommand Phonebook List Offset: 0 Limit: 10000");
}
_syncState.AddQueryToQueue("zCommand Bookings List");
_syncState.AddQueryToQueue("zCommand Call ListParticipants");
_syncState.AddQueryToQueue("zCommand Call Info");
_syncState.StartSync();
}
private void SetupSession()
{
// disable echo of commands
SendText("echo off");
// switch to json format
// set feedback exclusions
// Currently the feedback exclusions don't work when using the API in JSON response mode
// But leave these here in case the API gets updated in the future
// These may work as of 5.9.4
// In 5.9.4 we're getting sent an AddedContact message for every contact in the phonebook on connect, which is redunant and way too much data
// We want to exclude these messages right away until after we've retrieved the entire phonebook and then we can re-enable them
SendText("zFeedback Register Op: ex Path: /Event/Phonebook/AddedContact");
SendText("zFeedback Register Op: ex Path: /Event/InfoResult/Info/callin_country_list");
SendText("zFeedback Register Op: ex Path: /Event/InfoResult/Info/callout_country_list");
SendText("zFeedback Register Op: ex Path: /Event/InfoResult/Info/toll_free_callinLlist");
SendText("zStatus SystemUnit");
}
/// <summary>
/// Removes the feedback exclusion for added contacts
/// </summary>
private void ResubscribeForAddedContacts()
{
SendText("zFeedback Register Op: in Path: /Event/Phonebook/AddedContact");
}
/// <summary>
/// Processes non-JSON responses as their are received
/// </summary>
/// <param name="response"></param>
private void ProcessNonJsonResponse(string response)
{
if (response.Contains("client_loop: send disconnect: Broken pipe"))
{
Debug.Console(1, this, Debug.ErrorLogLevel.Error,
"Zoom Room Controller or App connected. Essentials will NOT control the Zoom Room until it is disconnected.");
return;
}
if (!_syncState.InitialSyncComplete)
{
if(response.ToLower().Contains("*r login successful"))
{
_syncState.LoginResponseReceived();
SendText("format json");
SetupSession();
}
//switch (response.Trim().ToLower()) // remove the whitespace
//{
// case "*r login successful":
// {
// _syncState.LoginMessageReceived();
// //// Fire up a thread to send the intial commands.
// //CrestronInvoke.BeginInvoke(o =>
// //{
// // disable echo of commands
// SendText("echo off");
// // switch to json format
// SendText("format json");
// // set feedback exclusions
// // Currently the feedback exclusions don't work when using the API in JSON response mode
// // But leave these here in case the API gets updated in the future
// // These may work as of 5.9.4
// if (_props.DisablePhonebookAutoDownload)
// {
// SendText("zFeedback Register Op: ex Path: /Event/Phonebook/AddedContact");
// }
// SendText("zFeedback Register Op: ex Path: /Event/InfoResult/Info/callin_country_list");
// SendText("zFeedback Register Op: ex Path: /Event/InfoResult/Info/callout_country_list");
// SendText("zFeedback Register Op: ex Path: /Event/InfoResult/Info/toll_free_callinLlist");
// //});
// break;
// }
//}
}
}
/// <summary>
/// Processes messages as they are dequeued
/// </summary>
/// <param name="message"></param>
private void ProcessMessage(string message)
{
// Counts the curly braces
if (message.Contains("client_loop: send disconnect: Broken pipe"))
{
Debug.Console(1, this, Debug.ErrorLogLevel.Error,
"Zoom Room Controller or App connected. Essentials will NOT control the Zoom Room until it is disconnected.");
return;
}
if (message.Contains('{'))
{
_jsonCurlyBraceCounter++;
}
if (message.Contains('}'))
{
_jsonCurlyBraceCounter--;
}
//Debug.Console(2, this, "JSON Curly Brace Count: {0}", _jsonCurlyBraceCounter);
if (!_jsonFeedbackMessageIsIncoming && message.Trim('\x20') == "{" + EchoDelimiter)
// Check for the beginning of a new JSON message
{
_jsonFeedbackMessageIsIncoming = true;
_jsonCurlyBraceCounter = 1; // reset the counter for each new message
_jsonMessage = new StringBuilder();
_jsonMessage.Append(message);
if (CommDebuggingIsOn)
{
Debug.Console(2, this, "Incoming JSON message...");
}
return;
}
if (_jsonFeedbackMessageIsIncoming && message.Trim('\x20') == "}" + EchoDelimiter)
// Check for the end of a JSON message
{
_jsonMessage.Append(message);
if (_jsonCurlyBraceCounter == 0)
{
_jsonFeedbackMessageIsIncoming = false;
if (CommDebuggingIsOn)
{
Debug.Console(2, this, "Complete JSON Received:\n{0}", _jsonMessage.ToString());
}
// Forward the complete message to be deserialized
DeserializeResponse(_jsonMessage.ToString());
}
//JsonMessage = new StringBuilder();
return;
}
// NOTE: This must happen after the above conditions have been checked
// Append subsequent partial JSON fragments to the string builder
if (_jsonFeedbackMessageIsIncoming)
{
_jsonMessage.Append(message);
//Debug.Console(1, this, "Building JSON:\n{0}", JsonMessage.ToString());
return;
}
if (CommDebuggingIsOn)
{
Debug.Console(1, this, "Non-JSON response: '{0}'", message);
}
_jsonCurlyBraceCounter = 0; // reset on non-JSON response
if (!_syncState.InitialSyncComplete)
{
switch (message.Trim().ToLower()) // remove the whitespace
{
case "*r login successful":
{
_syncState.LoginResponseReceived();
// Fire up a thread to send the intial commands.
CrestronInvoke.BeginInvoke(o =>
{
// Currently the feedback exclusions don't work when using the API in JSON response mode
// But leave these here in case the API gets updated in the future
Thread.Sleep(100);
// disable echo of commands
SendText("echo off");
Thread.Sleep(100);
// set feedback exclusions
SendText("zFeedback Register Op: ex Path: /Event/InfoResult/Info/callin_country_list");
Thread.Sleep(100);
SendText("zFeedback Register Op: ex Path: /Event/InfoResult/Info/callout_country_list");
Thread.Sleep(100);
SendText("zFeedback Register Op: ex Path: /Event/InfoResult/Info/toll_free_callinLlist");
Thread.Sleep(100);
if (_props.DisablePhonebookAutoDownload)
{
SendText("zFeedback Register Op: ex Path: /Event/Phonebook/AddedContact");
}
// switch to json format
SendText("format json");
});
break;
}
}
}
}
/// <summary>
/// Deserializes a JSON formatted response
/// </summary>
/// <param name="response"></param>
private void DeserializeResponse(string response)
{
try
{
var trimmedResponse = response.Trim();
if (trimmedResponse.Length <= 0)
{
return;
}
var message = JObject.Parse(trimmedResponse);
if (!_syncState.FirstJsonResponseWasReceived)
{
_syncState.ReceivedFirstJsonResponse();
}
var eType =
(eZoomRoomResponseType)
Enum.Parse(typeof (eZoomRoomResponseType), message["type"].Value<string>(), true);
var topKey = message["topKey"].Value<string>();
var responseObj = message[topKey];
Debug.Console(1, this, "{0} Response Received. topKey: '{1}'\n{2}", eType, topKey, responseObj.ToString().Replace("\n", CrestronEnvironment.NewLine));
switch (eType)
{
case eZoomRoomResponseType.zConfiguration:
{
switch (topKey.ToLower())
{
case "call":
{
JsonConvert.PopulateObject(responseObj.ToString(), Configuration.Call);
break;
}
case "audio":
{
JsonConvert.PopulateObject(responseObj.ToString(), Configuration.Audio);
break;
}
case "video":
{
JsonConvert.PopulateObject(responseObj.ToString(), Configuration.Video);
break;
}
case "client":
{
JsonConvert.PopulateObject(responseObj.ToString(), Configuration.Client);
break;
}
default:
{
break;
}
}
break;
}
case eZoomRoomResponseType.zCommand:
{
switch (topKey.ToLower())
{
case "inforesult":
{
JsonConvert.PopulateObject(responseObj.ToString(), Status.Call.Info);
break;
}
case "phonebooklistresult":
{
// This result will always be the complete contents of the directory and never
// A subset of the results via a search
JsonConvert.PopulateObject(responseObj.ToString(), Status.Phonebook);
var directoryResults =
zStatus.Phonebook.ConvertZoomContactsToGeneric(Status.Phonebook.Contacts);
if (!PhonebookSyncState.InitialSyncComplete)
{
PhonebookSyncState.InitialPhonebookFoldersReceived();
PhonebookSyncState.PhonebookRootEntriesReceived();
PhonebookSyncState.SetPhonebookHasFolders(true);
PhonebookSyncState.SetNumberOfContacts(Status.Phonebook.Contacts.Count);
}
if (directoryResults.ResultsFolderId != "root")
{
directoryResults.ResultsFolderId = "root";
}
DirectoryRoot = directoryResults;
CurrentDirectoryResult = directoryResults;
break;
}
case "listparticipantsresult":
{
Debug.Console(1, this, "JTokenType: {0}", responseObj.Type);
switch (responseObj.Type)
{
case JTokenType.Array:
Status.Call.Participants =
JsonConvert.DeserializeObject<List<zCommand.ListParticipant>>(
responseObj.ToString());
break;
case JTokenType.Object:
{
// this is a single participant event notification
var participant =
JsonConvert.DeserializeObject<zCommand.ListParticipant>(
responseObj.ToString());
if (participant != null)
{
Debug.Console(1, this,
"[DeserializeResponse] zCommands.listparticipantresult - participant.event: {0} **********************************",
participant.Event);
Debug.Console(1, this,
"[DeserializeResponse] zCommands.listparticipantresult - participant.event: {0} - UserId: {1} Name: {2} IsHost: {3}",
participant.Event, participant.UserId, participant.UserName, participant.IsHost);
switch (participant.Event)
{
case "ZRCUserChangedEventUserInfoUpdated":
case "ZRCUserChangedEventLeftMeeting":
{
var existingParticipant =
Status.Call.Participants.FirstOrDefault(
p => p.UserId.Equals(participant.UserId));
if (existingParticipant != null)
{
switch (participant.Event)
{
case "ZRCUserChangedEventLeftMeeting":
Status.Call.Participants.Remove(existingParticipant);
break;
case "ZRCUserChangedEventUserInfoUpdated":
JsonConvert.PopulateObject(responseObj.ToString(),
existingParticipant);
break;
}
}
}
break;
case "ZRCUserChangedEventJoinedMeeting":
{
var existingParticipant =
Status.Call.Participants.FirstOrDefault(p => p.UserId.Equals(participant.UserId));
if (existingParticipant != null)
{
Debug.Console(1, this,
"[DeserializeResponse] zCommands.listparticipantresult - participant.event: {0} ...updating matching UserId participant with UserId: {1} UserName: {2}",
participant.Event, participant.UserId, participant.UserName);
JsonConvert.PopulateObject(responseObj.ToString(), existingParticipant);
}
else
{
Debug.Console(1, this,
"[DeserializeResponse] zCommands.listparticipantresult - participant.event: {0} ...adding participant with UserId: {1} UserName: {2}",
participant.Event, participant.UserId, participant.UserName);
Status.Call.Participants.Add(participant);
}
break;
}
}
Debug.Console(1, this,
"[DeserializeResponse] zCommands.listparticipantresult - participant.event: {0} ***********************************",
participant.Event);
}
}
break;
}
var participants =
zCommand.ListParticipant.GetGenericParticipantListFromParticipantsResult(
Status.Call.Participants);
Participants.CurrentParticipants = participants;
// Update the share status of the meeting info
var meetingInfo = new MeetingInfo(
MeetingInfo.Id,
MeetingInfo.Name,
Participants.Host.Name,
MeetingInfo.Password,
MeetingInfo.ShareStatus,
GetIsHostMyself(),
MeetingInfo.IsSharingMeeting,
MeetingInfo.WaitingForHost,
MeetingIsLockedFeedback.BoolValue);
MeetingInfo = meetingInfo;
PrintCurrentCallParticipants();
break;
}
default:
{
break;
}
}
break;
}
case eZoomRoomResponseType.zEvent:
{
switch (topKey.ToLower())
{
case "phonebook":
{
if (responseObj["Updated Contact"] != null)
{
var updatedContact =
JsonConvert.DeserializeObject<zStatus.Contact>(
responseObj["Updated Contact"].ToString());
var existingContact =
Status.Phonebook.Contacts.FirstOrDefault(c => c.Jid.Equals(updatedContact.Jid));
if (existingContact != null)
{
// Update existing contact
JsonConvert.PopulateObject(responseObj["Updated Contact"].ToString(),
existingContact);
}
}
else if (responseObj["Added Contact"] != null)
{
var jToken = responseObj["Updated Contact"];
if (jToken != null)
{
var newContact =
JsonConvert.DeserializeObject<zStatus.Contact>(
jToken.ToString());
// Add a new contact
Status.Phonebook.Contacts.Add(newContact);
}
}
break;
}
case "bookingslistresult":
{
if (!_syncState.InitialSyncComplete)
{
_syncState.LastQueryResponseReceived();
}
var codecBookings = JsonConvert.DeserializeObject<List<zCommand.BookingsListResult>>(
responseObj.ToString());
if (codecBookings != null && codecBookings.Count > 0)
{
CodecSchedule.Meetings = zCommand.GetGenericMeetingsFromBookingResult(
codecBookings, CodecSchedule.MeetingWarningMinutes);
}
else
{
//need to clear the list if it's empty
CodecSchedule.Meetings = new List<Meeting>();
}
break;
}
case "bookings updated":
{
GetBookings();
break;
}
case "sharingstate":
{
JsonConvert.PopulateObject(responseObj.ToString(), Status.Call.Sharing);
SetLayout();
break;
}
case "incomingcallindication":
{
var incomingCall =
JsonConvert.DeserializeObject<zEvent.IncomingCallIndication>(responseObj.ToString());
if (incomingCall != null)
{
var newCall = new CodecActiveCallItem
{
Direction = eCodecCallDirection.Incoming,
Status = eCodecCallStatus.Ringing,
Type = eCodecCallType.Video,
Name = incomingCall.callerName,
Id = incomingCall.callerJID
};
ActiveCalls.Add(newCall);
OnCallStatusChange(newCall);
}
break;
}
case "treatedincomingcallindication":
{
var incomingCall =
JsonConvert.DeserializeObject<zEvent.IncomingCallIndication>(responseObj.ToString());
if (incomingCall != null)
{
var existingCall =
ActiveCalls.FirstOrDefault(c => c.Id.Equals(incomingCall.callerJID));
if (existingCall != null)
{
existingCall.Status = !incomingCall.accepted
? eCodecCallStatus.Disconnected
: eCodecCallStatus.Connecting;
OnCallStatusChange(existingCall);
}
UpdateCallStatus();
}
break;
}
case "calldisconnect":
{
var disconnectEvent =
JsonConvert.DeserializeObject<zEvent.CallDisconnect>(responseObj.ToString());
Debug.Console(1, this,
"[DeserializeResponse] zEvent.calldisconnect ********************************************");
Debug.Console(1, this, "[DeserializeResponse] zEvent.calldisconnect - disconnectEvent.Successful: {0}",
disconnectEvent.Successful);
if (disconnectEvent.Successful)
{
if (ActiveCalls.Count > 0)
{
var activeCall = ActiveCalls.FirstOrDefault(c => c.IsActiveCall);
if (activeCall != null)
{
Debug.Console(1, this,
"[DeserializeResponse] zEvent.calldisconnect - ActiveCalls.Count: {0} activeCall.Id: {1}, activeCall.Number: {2} activeCall.Name: {3}, activeCall.IsActive: {4}",
ActiveCalls.Count, activeCall.Id, activeCall.Number, activeCall.Name, activeCall.IsActiveCall);
activeCall.Status = eCodecCallStatus.Disconnected;
OnCallStatusChange(activeCall);
}
}
}
Debug.Console(1, this,
"[DeserializeResponse] zEvent.calldisconnect ********************************************");
UpdateCallStatus();
break;
}
case "callconnecterror":
{
UpdateCallStatus();
break;
}
case "videounmuterequest":
{
// TODO: notify room of a request to unmute video
break;
}
case "meetingneedspassword":
{
var meetingNeedsPassword =
responseObj.ToObject<zEvent.MeetingNeedsPassword>();
if (meetingNeedsPassword.NeedsPassword)
{
var prompt = "Password required to join this meeting. Please enter the meeting password.";
OnPasswordRequired(meetingNeedsPassword.WrongAndRetry, false, false, prompt);
}
else
{
OnPasswordRequired(false, false, true, "");
}
break;
}
case "needwaitforhost":
{
Status.NeedWaitForHost = JsonConvert.DeserializeObject<zEvent.NeedWaitForHost>(responseObj.ToString());
Debug.Console(1, this, "WaitingForHost: {0}", Status.NeedWaitForHost.Wait);
if (Status.NeedWaitForHost.Wait)
{
if (MeetingInfo == null)
{
MeetingInfo = new MeetingInfo("Waiting For Host", "Waiting For Host", "Waiting For Host", "",
GetSharingStatus(), false, false, true, MeetingIsLockedFeedback.BoolValue);
UpdateCallStatus();
break;
}
MeetingInfo = new MeetingInfo("Waiting For Host", "Waiting For Host", "Waiting For Host", "",
GetSharingStatus(), false, false, true, MeetingIsLockedFeedback.BoolValue);
UpdateCallStatus();
break;
}
if (MeetingInfo == null)
{
MeetingInfo = new MeetingInfo("Waiting For Host", "Waiting For Host", "Waiting For Host", "",
GetSharingStatus(), false, false, false, MeetingIsLockedFeedback.BoolValue);
break;
}
MeetingInfo = new MeetingInfo(MeetingInfo.Id, MeetingInfo.Name, MeetingInfo.Host, MeetingInfo.Password,
GetSharingStatus(), GetIsHostMyself(), false, false, MeetingIsLockedFeedback.BoolValue);
break;
}
case "openvideofailforhoststop":
{
// TODO: notify user that host has disabled unmuting video
break;
}
case "updatedcallrecordinfo":
{
JsonConvert.PopulateObject(responseObj.ToString(), Status.Call.CallRecordInfo);
break;
}
case "phonecallstatus":
{
JsonConvert.PopulateObject(responseObj.ToString(), Status.PhoneCall);
break;
}
case "pinstatusofscreennotification":
{
var status = responseObj.ToObject<zEvent.PinStatusOfScreenNotification>();
Debug.Console(1, this, "Pin Status notification for UserId: {0}, ScreenIndex: {1}", status.PinnedUserId,
status.ScreenIndex);
Participant alreadyPinnedParticipant = null;
// Check for a participant already pinned to the same screen index.
if (status.PinnedUserId > 0)
{
alreadyPinnedParticipant =
Participants.CurrentParticipants.FirstOrDefault(p => p.ScreenIndexIsPinnedToFb.Equals(status.ScreenIndex));
// Make sure that the already pinned participant isn't the same ID as for this message. If true, clear the pinned fb.
if (alreadyPinnedParticipant != null && alreadyPinnedParticipant.UserId != status.PinnedUserId)
{
Debug.Console(1, this, "Participant: {0} with id: {1} already pinned to screenIndex {2}. Clearing pinned fb.",
alreadyPinnedParticipant.Name, alreadyPinnedParticipant.UserId,
alreadyPinnedParticipant.ScreenIndexIsPinnedToFb);
alreadyPinnedParticipant.IsPinnedFb = false;
alreadyPinnedParticipant.ScreenIndexIsPinnedToFb = -1;
}
}
var participant = Participants.CurrentParticipants.FirstOrDefault(p => p.UserId.Equals(status.PinnedUserId));
if (participant != null)
{
participant.IsPinnedFb = true;
participant.ScreenIndexIsPinnedToFb = status.ScreenIndex;
}
else
{
participant =
Participants.CurrentParticipants.FirstOrDefault(p => p.ScreenIndexIsPinnedToFb.Equals(status.ScreenIndex));
if (participant == null && alreadyPinnedParticipant == null)
{
Debug.Console(1, this, "no matching participant found by pinned_user_id: {0} or screen_index: {1}",
status.PinnedUserId, status.ScreenIndex);
return;
}
else if (participant != null)
{
Debug.Console(2, this, "Unpinning {0} with id: {1} from screen index: {2}", participant.Name,
participant.UserId, status.ScreenIndex);
participant.IsPinnedFb = false;
participant.ScreenIndexIsPinnedToFb = -1;
}
}
// fire the event as we've modified the participants list
Participants.OnParticipantsChanged();
break;
}
case "startlocalpresentmeeting":
{
var result = JsonConvert.DeserializeObject<zEvent.StartLocalPresentMeeting>(responseObj.ToString());
if (result.Success)
{
MeetingInfo = new MeetingInfo("", "", "", "", "", true, true, MeetingInfo.WaitingForHost, MeetingIsLockedFeedback.BoolValue);
break;
}
break;
}
default:
{
break;
}
}
break;
}
case eZoomRoomResponseType.zStatus:
{
switch (topKey.ToLower())
{
case "login":
{
_syncState.LoginResponseReceived();
SetupSession();
JsonConvert.PopulateObject(responseObj.ToString(), Status.Login);
break;
}
case "systemunit":
{
JsonConvert.PopulateObject(responseObj.ToString(), Status.SystemUnit);
break;
}
case "call":
{
JsonConvert.PopulateObject(responseObj.ToString(), Status.Call);
Debug.Console(1, this,
"[DeserializeResponse] zStatus.call - Status.Call.Info.meeting_id: {0} Status.Call.Info.meeting_list_item.meetingName: {1}",
Status.Call.Info.meeting_id, Status.Call.Info.meeting_list_item.meetingName);
foreach (var participant in Status.Call.Participants)
{
Debug.Console(1, this,
"[DeserializeResponse] zStatus.call - Status.Call.Participants participant.UserId: {0} participant.UserName: {1}",
participant.UserId, participant.UserName);
}
UpdateCallStatus();
break;
}
case "capabilities":
{
JsonConvert.PopulateObject(responseObj.ToString(), Status.Capabilities);
break;
}
case "sharing":
{
JsonConvert.PopulateObject(responseObj.ToString(), Status.Sharing);
break;
}
case "numberofscreens":
{
JsonConvert.PopulateObject(responseObj.ToString(), Status.NumberOfScreens);
break;
}
case "video":
{
JsonConvert.PopulateObject(responseObj.ToString(), Status.Video);
break;
}
case "camerashare":
{
JsonConvert.PopulateObject(responseObj.ToString(), Status.CameraShare);
break;
}
case "layout":
{
JsonConvert.PopulateObject(responseObj.ToString(), Status.Layout);
break;
}
case "audio input line":
{
JsonConvert.PopulateObject(responseObj.ToString(), Status.AudioInputs);
break;
}
case "audio output line":
{
JsonConvert.PopulateObject(responseObj.ToString(), Status.AudioOuputs);
break;
}
case "video camera line":
{
JsonConvert.PopulateObject(responseObj.ToString(), Status.Cameras);
if (!_syncState.CamerasHaveBeenSetUp)
{
SetUpCameras();
}
break;
}
default:
{
break;
}
}
break;
}
default:
{
Debug.Console(1, "Unknown Response Type:");
break;
}
}
}
catch (Exception ex)
{
Debug.Console(1, this, "Error Deserializing feedback: {0}", ex.Message);
Debug.Console(2, this, "{0}", ex);
if (ex.InnerException != null)
{
Debug.Console(1, this,"Error Deserializing feedback inner exception: {0}", ex.InnerException.Message);
Debug.Console(2, this, "{0}", ex.InnerException.StackTrace);
}
}
}
private void SetLayout()
{
if (!_props.AutoDefaultLayouts) return;
if (
(Status.Call.Sharing.State == zEvent.eSharingState.Receiving ||
Status.Call.Sharing.State == zEvent.eSharingState.Sending))
{
SendText(String.Format("zconfiguration call layout style: {0}",
_props.DefaultSharingLayout));
}
else
{
SendText(String.Format("zconfiguration call layout style: {0}",
_props.DefaultCallLayout));
}
}
/// <summary>
/// Retrieves the current call participants list
/// </summary>
public void GetCurrentCallParticipants()
{
SendText("zCommand Call ListParticipants");
}
/// <summary>
/// Prints the current call particiapnts list
/// </summary>
public void PrintCurrentCallParticipants()
{
if (Debug.Level <= 0) return;
Debug.Console(1, this, "*************************** Call Participants **************************");
foreach (var participant in Participants.CurrentParticipants)
{
Debug.Console(1, this, "UserId: {3} Name: {0} Audio: {1} IsHost: {2}",
participant.Name, participant.AudioMuteFb, participant.IsHost, participant.UserId);
}
Debug.Console(1, this, "************************************************************************");
}
/// <summary>
/// Retrieves bookings list
/// </summary>
private void GetBookings()
{
SendText("zCommand Bookings List");
}
/// <summary>
/// Updates the current call status
/// </summary>
private void UpdateCallStatus()
{
Debug.Console(1, this,
"[UpdateCallStatus] Current Call Status: {0} Active Call Count: {1} Need Wait For Host: {2}",
Status.Call != null ? Status.Call.Status.ToString() : "no call", ActiveCalls.Count, Status.NeedWaitForHost.Wait);
if (Status.Call != null)
{
var callStatus = Status.Call.Status;
// If not crrently in a meeting, intialize the call object
if (callStatus != zStatus.eCallStatus.IN_MEETING && callStatus != zStatus.eCallStatus.CONNECTING_MEETING)
{
//Debug.Console(1, this, "[UpdateCallStatus] Creating new Status.Call object");
Status.Call = new zStatus.Call {Status = callStatus};
OnCallStatusChange(new CodecActiveCallItem() {Status = eCodecCallStatus.Disconnected});
SetUpCallFeedbackActions();
}
if (ActiveCalls.Count == 0)
{
if (callStatus == zStatus.eCallStatus.CONNECTING_MEETING ||
callStatus == zStatus.eCallStatus.IN_MEETING)
{
var newStatus = eCodecCallStatus.Unknown;
switch (callStatus)
{
case zStatus.eCallStatus.CONNECTING_MEETING:
newStatus = eCodecCallStatus.Connecting;
break;
case zStatus.eCallStatus.IN_MEETING:
newStatus = eCodecCallStatus.Connected;
break;
}
if (!string.IsNullOrEmpty(Status.Call.Info.meeting_id))
{
var newCall = new CodecActiveCallItem
{
Name = Status.Call.Info.meeting_list_item.meetingName,
Number = Status.Call.Info.meeting_list_item.meetingNumber,
Id = Status.Call.Info.meeting_id,
Status = newStatus,
Type = eCodecCallType.Video,
};
if (!String.IsNullOrEmpty(_lastDialedMeetingNumber))
{
_lastDialedMeetingNumber = String.Empty;
}
ActiveCalls.Add(newCall);
OnCallStatusChange(newCall);
} else if (String.IsNullOrEmpty(Status.Call.Info.meeting_id) && Status.NeedWaitForHost.Wait)
{
var newCall = new CodecActiveCallItem
{
Name = "Waiting For Host",
Number = "Waiting For Host",
Id = "Waiting For Host",
Status = newStatus,
Type = eCodecCallType.Video,
};
if (!String.IsNullOrEmpty(_lastDialedMeetingNumber))
{
_lastDialedMeetingNumber = String.Empty;
}
ActiveCalls.Add(newCall);
OnCallStatusChange(newCall);
}
}
}
else
{
var existingCall = ActiveCalls.FirstOrDefault(c => !c.Status.Equals(eCodecCallStatus.Ringing));
switch (callStatus)
{
case zStatus.eCallStatus.IN_MEETING:
if (Status.NeedWaitForHost.Wait)
{
Status.NeedWaitForHost.Wait = false;
}
existingCall.Status = eCodecCallStatus.Connected;
break;
case zStatus.eCallStatus.NOT_IN_MEETING:
if (Status.NeedWaitForHost.Wait)
{
Status.NeedWaitForHost.Wait = false;
}
existingCall.Status = eCodecCallStatus.Disconnected;
break;
}
Debug.Console(1, this, "[UpdateCallStatus] ELSE ActiveCalls.Count == {1} - Current Call Status: {0}",
Status.Call != null ? Status.Call.Status.ToString() : "no call", ActiveCalls.Count);
OnCallStatusChange(existingCall);
}
}
Debug.Console(1, this, "[UpdateCallStatus] Active Calls ------------------------------");
// Clean up any disconnected calls left in the list
for (int i = 0; i < ActiveCalls.Count; i++)
{
var call = ActiveCalls[i];
Debug.Console(1, this,
@"ID: {1}
Number: {5}
Name: {0}
IsActive: {2}
Status: {3}
Direction: {4}
IsActiveCall: {6}", call.Name, call.Id, call.IsActiveCall, call.Status, call.Direction, call.Number,
call.IsActiveCall);
if (!call.IsActiveCall)
{
Debug.Console(1, this, "[UpdateCallStatus] Removing Inactive call.Id: {1} call.Name: {0}", call.Name, call.Id);
ActiveCalls.Remove(call);
}
}
Debug.Console(1, this, "[UpdateCallStatus] Active Calls ------------------------------");
//clear participants list after call cleanup
var emptyList = new List<Participant>();
Participants.CurrentParticipants = emptyList;
if (ActiveCalls.Count > 0) GetCurrentCallParticipants();
}
protected override void OnCallStatusChange(CodecActiveCallItem item)
{
if (item.Status == eCodecCallStatus.Connected)
{
var host = "";
if (Participants.Host != null)
host = Participants.Host.Name;
MeetingInfo = new MeetingInfo(
Status.Call.Info.meeting_id,
Status.Call.Info.meeting_list_item.meetingName,
host,
Status.Call.Info.meeting_password,
GetSharingStatus(),
GetIsHostMyself(),
!String.Equals(Status.Call.Info.meeting_type,"NORMAL"),
false,
MeetingIsLockedFeedback.BoolValue
);
}
// TODO [ ] Issue #868
else if (item.Status == eCodecCallStatus.Disconnected)
{
MeetingInfo = new MeetingInfo(
string.Empty,
string.Empty,
string.Empty,
string.Empty,
string.Empty,
false,
false,
false,
false
);
}
base.OnCallStatusChange(item);
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()
{
string sharingState = "None";
try
{
if (Status.Call.Sharing.State == zEvent.eSharingState.Receiving)
{
sharingState = "Receiving Content";
}
if (Status.Sharing.isAirHostClientConnected)
{
sharingState = "Sharing AirPlay";
}
if (Status.Sharing.isDirectPresentationConnected)
{
sharingState = "Sharing Laptop";
}
if (Status.Sharing.isSharingBlackMagic)
{
sharingState = "Sharing HDMI Source";
}
return sharingState;
}
catch (Exception e)
{
Debug.Console(1, this, "Exception getting sharing status: {0}", e.Message);
Debug.Console(2, this, "{0}", e.StackTrace);
return sharingState;
}
}
/// <summary>
/// Will return true if the host is myself (this zoom room)
/// </summary>
/// <returns></returns>
private bool GetIsHostMyself()
{
try
{
if (Participants.CurrentParticipants.Count == 0)
{
Debug.Console(2, this, "No current participants");
return false;
}
var host = Participants.Host;
if(host == null)
{
Debug.Console(2, this, "Host is currently null");
return false;
}
Debug.Console(2, this, "Host is: '{0}' IsMyself?: {1}", host.Name, host.IsMyself);
return host.IsMyself;
}
catch (Exception e)
{
Debug.Console(1, "Exception getting isHost: {0}", e.Message);
Debug.Console(2, "{0}", e.StackTrace);
return false;
}
}
public override void StartSharing()
{
SendText("zCommand Call Sharing HDMI Start");
}
/// <summary>
/// Stops sharing the current presentation
/// </summary>
public override void StopSharing()
{
if (Status.Sharing.isSharingBlackMagic)
{
SendText("zCommand Call Sharing HDMI Stop");
}
else
{
SendText("zCommand Call Sharing Disconnect");
}
}
public override void PrivacyModeOn()
{
SendText("zConfiguration Call Microphone Mute: on");
}
public override void PrivacyModeOff()
{
SendText("zConfiguration Call Microphone Mute: off");
}
public override void PrivacyModeToggle()
{
if (PrivacyModeIsOnFeedback.BoolValue)
{
PrivacyModeOff();
}
else
{
PrivacyModeOn();
}
}
public override void MuteOff()
{
Debug.Console(2, this, "Unmuting to previous level: {0}", _previousVolumeLevel);
SetVolume((ushort) _previousVolumeLevel);
}
public override void MuteOn()
{
SetVolume(0);
}
public override void MuteToggle()
{
if (MuteFeedback.BoolValue)
{
MuteOff();
}
else
{
MuteOn();
}
}
/// <summary>
/// Increments the voluem
/// </summary>
/// <param name="pressRelease"></param>
public override void VolumeUp(bool pressRelease)
{
// TODO: Implment volume decrement that calls SetVolume()
}
/// <summary>
/// Decrements the volume
/// </summary>
/// <param name="pressRelease"></param>
public override void VolumeDown(bool pressRelease)
{
// TODO: Implment volume decrement that calls SetVolume()
}
/// <summary>
/// Scales the level and sets the codec to the specified level within its range
/// </summary>
/// <param name="level">level from slider (0-65535 range)</param>
public override void SetVolume(ushort level)
{
var scaledLevel = CrestronEnvironment.ScaleWithLimits(level, 65535, 0, 100, 0);
SendText(string.Format("zConfiguration Audio Output volume: {0}", scaledLevel));
}
/// <summary>
/// Recalls the default volume on the codec
/// </summary>
public void VolumeSetToDefault()
{
}
/// <summary>
///
/// </summary>
public override void StandbyActivate()
{
// No corresponding function on device
}
/// <summary>
///
/// </summary>
public override void StandbyDeactivate()
{
// No corresponding function on device
}
public override void LinkToApi(BasicTriList trilist, uint joinStart, string joinMapKey, EiscApiAdvanced bridge)
{
var joinMap = new ZoomRoomJoinMap(joinStart);
var customJoins = JoinMapHelper.TryGetJoinMapAdvancedForDevice(joinMapKey);
if (customJoins != null)
{
joinMap.SetCustomJoinData(customJoins);
}
if (bridge != null)
{
bridge.AddJoinMap(Key, joinMap);
}
LinkVideoCodecToApi(this, trilist, joinMap);
LinkZoomRoomToApi(trilist, joinMap);
}
/// <summary>
/// Links all the specific Zoom functionality to the API bridge
/// </summary>
/// <param name="trilist"></param>
/// <param name="joinMap"></param>
public void LinkZoomRoomToApi(BasicTriList trilist, ZoomRoomJoinMap joinMap)
{
var layoutsCodec = this as IHasZoomRoomLayouts;
if (layoutsCodec != null)
{
layoutsCodec.LayoutInfoChanged += (o, a) =>
{
trilist.SetBool(joinMap.LayoutGalleryIsAvailable.JoinNumber,
zConfiguration.eLayoutStyle.Gallery == (a.AvailableLayouts & zConfiguration.eLayoutStyle.Gallery));
trilist.SetBool(joinMap.LayoutSpeakerIsAvailable.JoinNumber,
zConfiguration.eLayoutStyle.Speaker == (a.AvailableLayouts & zConfiguration.eLayoutStyle.Speaker));
trilist.SetBool(joinMap.LayoutStripIsAvailable.JoinNumber, zConfiguration.eLayoutStyle.Strip
==
(a.AvailableLayouts & zConfiguration.eLayoutStyle.Strip));
trilist.SetBool(joinMap.LayoutShareAllIsAvailable.JoinNumber, zConfiguration.eLayoutStyle.ShareAll
==
(a.AvailableLayouts &
zConfiguration.eLayoutStyle.ShareAll));
// pass the names used to set the layout through the bridge
trilist.SetString(joinMap.LayoutGalleryIsAvailable.JoinNumber, zConfiguration.eLayoutStyle.Gallery.ToString());
trilist.SetString(joinMap.LayoutSpeakerIsAvailable.JoinNumber, zConfiguration.eLayoutStyle.Speaker.ToString());
trilist.SetString(joinMap.LayoutStripIsAvailable.JoinNumber, zConfiguration.eLayoutStyle.Strip.ToString());
trilist.SetString(joinMap.LayoutShareAllIsAvailable.JoinNumber, zConfiguration.eLayoutStyle.ShareAll.ToString());
};
trilist.SetSigFalseAction(joinMap.SwapContentWithThumbnail.JoinNumber, () => layoutsCodec.SwapContentWithThumbnail());
layoutsCodec.CanSwapContentWithThumbnailFeedback.LinkInputSig(
trilist.BooleanInput[joinMap.CanSwapContentWithThumbnail.JoinNumber]);
layoutsCodec.ContentSwappedWithThumbnailFeedback.LinkInputSig(
trilist.BooleanInput[joinMap.SwapContentWithThumbnail.JoinNumber]);
layoutsCodec.LayoutViewIsOnFirstPageFeedback.LinkInputSig(
trilist.BooleanInput[joinMap.LayoutIsOnFirstPage.JoinNumber]);
layoutsCodec.LayoutViewIsOnLastPageFeedback.LinkInputSig(trilist.BooleanInput[joinMap.LayoutIsOnLastPage.JoinNumber]);
trilist.SetSigFalseAction(joinMap.LayoutTurnToNextPage.JoinNumber, () => layoutsCodec.LayoutTurnNextPage());
trilist.SetSigFalseAction(joinMap.LayoutTurnToPreviousPage.JoinNumber, () => layoutsCodec.LayoutTurnPreviousPage());
trilist.SetSigFalseAction(joinMap.GetAvailableLayouts.JoinNumber, () => layoutsCodec.GetAvailableLayouts());
trilist.SetStringSigAction(joinMap.GetSetCurrentLayout.JoinNumber, (s) =>
{
try
{
var style = (zConfiguration.eLayoutStyle) Enum.Parse(typeof (zConfiguration.eLayoutStyle), s, true);
SetLayout(style);
}
catch (Exception e)
{
Debug.Console(1, this, "Unable to parse '{0}' to zConfiguration.eLayoutStyle: {1}", s, e);
}
});
layoutsCodec.LocalLayoutFeedback.LinkInputSig(trilist.StringInput[joinMap.GetSetCurrentLayout.JoinNumber]);
}
var pinCodec = this as IHasParticipantPinUnpin;
if (pinCodec != null)
{
pinCodec.NumberOfScreensFeedback.LinkInputSig(trilist.UShortInput[joinMap.NumberOfScreens.JoinNumber]);
// Set the value of the local property to be used when pinning a participant
trilist.SetUShortSigAction(joinMap.ScreenIndexToPinUserTo.JoinNumber, (u) => ScreenIndexToPinUserTo = u);
}
var layoutSizeCodec = this as IHasSelfviewSize;
if (layoutSizeCodec != null)
{
trilist.SetSigFalseAction(joinMap.GetSetSelfviewPipSize.JoinNumber, layoutSizeCodec.SelfviewPipSizeToggle);
trilist.SetStringSigAction(joinMap.GetSetSelfviewPipSize.JoinNumber, (s) =>
{
try
{
var size = (zConfiguration.eLayoutSize) Enum.Parse(typeof (zConfiguration.eLayoutSize), s, true);
var cmd = SelfviewPipSizes.FirstOrDefault(c => c.Command.Equals(size.ToString()));
SelfviewPipSizeSet(cmd);
}
catch (Exception e)
{
Debug.Console(1, this, "Unable to parse '{0}' to zConfiguration.eLayoutSize: {1}", s, e);
}
});
layoutSizeCodec.SelfviewPipSizeFeedback.LinkInputSig(trilist.StringInput[joinMap.GetSetSelfviewPipSize.JoinNumber]);
}
PasswordRequired += (device, args) =>
{
if (args.LoginAttemptCancelled)
{
trilist.SetBool(joinMap.ShowPasswordPrompt.JoinNumber, false);
return;
}
if (!string.IsNullOrEmpty(args.Message))
{
trilist.SetString(joinMap.PasswordPromptMessage.JoinNumber, args.Message);
}
if (args.LoginAttemptFailed)
{
trilist.SetBool(joinMap.PasswordLoginFailed.JoinNumber, true);
return;
}
trilist.SetBool(joinMap.ShowPasswordPrompt.JoinNumber, true);
};
MeetingInfoChanged += (device, args) =>
{
trilist.SetString(joinMap.MeetingInfoId.JoinNumber, args.Info.Id);
trilist.SetString(joinMap.MeetingInfoHost.JoinNumber, args.Info.Host);
trilist.SetString(joinMap.MeetingInfoPassword.JoinNumber, args.Info.Password);
trilist.SetBool(joinMap.IsHost.JoinNumber, args.Info.IsHost);
trilist.SetBool(joinMap.ShareOnlyMeeting.JoinNumber, args.Info.IsSharingMeeting);
trilist.SetBool(joinMap.WaitingForHost.JoinNumber, args.Info.WaitingForHost);
//trilist.SetString(joinMap.CurrentSource.JoinNumber, args.Info.ShareStatus);
};
trilist.SetSigTrueAction(joinMap.StartMeetingNow.JoinNumber, () => StartMeeting(0));
trilist.SetSigTrueAction(joinMap.ShareOnlyMeeting.JoinNumber, StartSharingOnlyMeeting);
trilist.SetSigTrueAction(joinMap.StartNormalMeetingFromSharingOnlyMeeting.JoinNumber, StartNormalMeetingFromSharingOnlyMeeting);
// not sure if this would be needed here, should be handled by VideoCodecBase.cs LinkToApi methods
//DirectoryResultReturned += (device, args) =>
//{
// // add logic here if necessary when event fires
//};
trilist.SetStringSigAction(joinMap.SubmitPassword.JoinNumber, SubmitPassword);
PasswordRequired += (devices, args) =>
{
if (args.LoginAttemptCancelled)
{
trilist.SetBool(joinMap.ShowPasswordPrompt.JoinNumber, false);
return;
}
if (!string.IsNullOrEmpty(args.Message))
{
trilist.SetString(joinMap.PasswordPromptMessage.JoinNumber, args.Message);
}
if (args.LoginAttemptFailed)
{
// login attempt failed
return;
}
trilist.SetBool(joinMap.PasswordIncorrect.JoinNumber, args.LastAttemptWasIncorrect);
trilist.SetBool(joinMap.ShowPasswordPrompt.JoinNumber, true);
};
trilist.OnlineStatusChange += (device, args) =>
{
if (!args.DeviceOnLine) return;
ComputeAvailableLayouts();
layoutsCodec.LocalLayoutFeedback.FireUpdate();
layoutsCodec.CanSwapContentWithThumbnailFeedback.FireUpdate();
layoutsCodec.ContentSwappedWithThumbnailFeedback.FireUpdate();
layoutsCodec.LayoutViewIsOnFirstPageFeedback.FireUpdate();
layoutsCodec.LayoutViewIsOnLastPageFeedback.FireUpdate();
pinCodec.NumberOfScreensFeedback.FireUpdate();
layoutSizeCodec.SelfviewPipSizeFeedback.FireUpdate();
};
}
public override void ExecuteSwitch(object selector)
{
var action = selector as Action;
if (action == null)
{
return;
}
action();
}
public void AcceptCall()
{
var incomingCall =
ActiveCalls.FirstOrDefault(
c => c.Status.Equals(eCodecCallStatus.Ringing) && c.Direction.Equals(eCodecCallDirection.Incoming));
AcceptCall(incomingCall);
}
public override void AcceptCall(CodecActiveCallItem call)
{
SendText(string.Format("zCommand Call Accept callerJID: {0}", call.Id));
call.Status = eCodecCallStatus.Connected;
OnCallStatusChange(call);
UpdateCallStatus();
}
public void RejectCall()
{
var incomingCall =
ActiveCalls.FirstOrDefault(
c => c.Status.Equals(eCodecCallStatus.Ringing) && c.Direction.Equals(eCodecCallDirection.Incoming));
RejectCall(incomingCall);
}
public override void RejectCall(CodecActiveCallItem call)
{
SendText(string.Format("zCommand Call Reject callerJID: {0}", call.Id));
call.Status = eCodecCallStatus.Disconnected;
OnCallStatusChange(call);
UpdateCallStatus();
}
public override void Dial(Meeting meeting)
{
Debug.Console(1, this, "Dialing meeting.Id: {0} Title: {1}", meeting.Id, meeting.Title);
_lastDialedMeetingNumber = meeting.Id;
SendText(string.Format("zCommand Dial Start meetingNumber: {0}", meeting.Id));
}
public override void Dial(string number)
{
Debug.Console(2, this, "Dialing number: {0}", number);
_lastDialedMeetingNumber = number;
SendText(string.Format("zCommand Dial Join meetingNumber: {0}", number));
}
/// <summary>
/// Dials a meeting with a password
/// </summary>
/// <param name="number"></param>
/// <param name="password"></param>
public void Dial(string number, string password)
{
Debug.Console(2, this, "Dialing meeting number: {0} with password: {1}", number, password);
SendText(string.Format("zCommand Dial Join meetingNumber: {0} password: {1}", number, password));
}
/// <summary>
/// Invites a contact to either a new meeting (if not already in a meeting) or the current meeting.
/// Currently only invites a single user
/// </summary>
/// <param name="contact"></param>
public override void Dial(IInvitableContact contact)
{
var ic = contact as InvitableDirectoryContact;
if (ic != null)
{
Debug.Console(1, this, "Attempting to Dial (Invite): {0}", ic.Name);
if (!IsInCall)
{
SendText(string.Format("zCommand Invite Duration: {0} user: {1}", DefaultMeetingDurationMin,
ic.ContactId));
}
else
{
SendText(string.Format("zCommand Call invite user: {0}", ic.ContactId));
}
}
}
/// <summary>
/// Invites contacts to a new meeting for a specified duration
/// </summary>
/// <param name="contacts"></param>
/// <param name="duration"></param>
public void InviteContactsToNewMeeting(List<InvitableDirectoryContact> contacts, uint duration)
{
if(duration == 0)
{
duration = DefaultMeetingDurationMin;
}
StringBuilder message = new StringBuilder();
// Add the prefix
message.Append(string.Format("zCommand Invite Duration: {0}", duration));
// Add each invitee
foreach (var contact in contacts)
{
var invitee = string.Format(" user: {0}", contact.ContactId);
message.Append(invitee);
}
SendText(message.ToString());
}
/// <summary>
/// Invites contacts to an existing meeting
/// </summary>
/// <param name="contacts"></param>
public void InviteContactsToExistingMeeting(List<InvitableDirectoryContact> contacts)
{
StringBuilder message = new StringBuilder();
// Add the prefix
message.Append(string.Format("zCommand Call Invite"));
// Add each invitee
foreach (var contact in contacts)
{
var invitee = string.Format(" user: {0}", contact.ContactId);
message.Append(invitee);
}
SendText(message.ToString());
}
/// <summary>
/// Starts a PMI Meeting for the specified duration (or default meeting duration if 0 is specified)
/// </summary>
/// <param name="duration">duration of meeting</param>
public void StartMeeting(uint duration)
{
uint dur = DefaultMeetingDurationMin;
if (duration > 0)
dur = duration;
SendText(string.Format("zCommand Dial StartPmi Duration: {0}", dur));
}
public void LeaveMeeting()
{
SendText("zCommand Call Leave");
}
public override void EndCall(CodecActiveCallItem call)
{
SendText("zCommand Call Disconnect");
}
public override void EndAllCalls()
{
SendText("zCommand Call Disconnect");
}
public override void SendDtmf(string s)
{
SendDtmfToPhone(s);
}
/// <summary>
/// Call when directory results are updated
/// </summary>
/// <param name="result"></param>
private void OnDirectoryResultReturned(CodecDirectory result)
{
try
{
Debug.Console(2, this, "OnDirectoryResultReturned");
var directoryResult = new CodecDirectory();
// If result is Root, create a copy and filter out contacts whose parent folder is not root
if (!CurrentDirectoryResultIsNotDirectoryRoot.BoolValue)
{
Debug.Console(2, this, "Filtering DirectoryRoot to remove contacts for display");
directoryResult.ResultsFolderId = result.ResultsFolderId;
directoryResult.AddFoldersToDirectory(result.Folders);
directoryResult.AddContactsToDirectory(
result.Contacts.Where((c) => c.ParentFolderId == result.ResultsFolderId).ToList());
}
else
{
directoryResult = result;
}
Debug.Console(2, this, "Updating directoryResult. IsOnRoot: {0}",
!CurrentDirectoryResultIsNotDirectoryRoot.BoolValue);
// This will return the latest results to all UIs. Multiple indendent UI Directory browsing will require a different methodology
var handler = DirectoryResultReturned;
if (handler != null)
{
handler(this, new DirectoryEventArgs
{
Directory = directoryResult,
DirectoryIsOnRoot = !CurrentDirectoryResultIsNotDirectoryRoot.BoolValue
});
}
}
catch (Exception e)
{
Debug.Console(2, this, "Error: {0}", e);
}
//PrintDirectory(result);
}
/// <summary>
/// Builds the cameras List by using the Zoom Room zStatus.Cameras data. Could later be modified to build from config data
/// </summary>
private void SetUpCameras()
{
SelectedCameraFeedback = new StringFeedback(() => Configuration.Video.Camera.SelectedId);
ControllingFarEndCameraFeedback = new BoolFeedback(() => SelectedCamera is IAmFarEndCamera);
foreach (var cam in Status.Cameras)
{
// Known Issue:
// Crestron UC engine systems seem to report an item in the cameras list that represnts the USB bridge device.
// If we know the name and it's reliably consistent, we could ignore it here...
if (cam.Name.IndexOf("HD-CONV-USB") > -1)
{
// Skip this as it's the Crestron USB box, not a real camera
continue;
}
var camera = new ZoomRoomCamera(cam.id, cam.Name, this);
Cameras.Add(camera);
if (cam.Selected)
{
SelectedCamera = camera;
}
}
if (IsInCall)
{
UpdateFarEndCameras();
}
_syncState.CamerasSetUp();
}
/// <summary>
/// Dynamically creates far end cameras for call participants who have far end control enabled.
/// </summary>
private void UpdateFarEndCameras()
{
// TODO: set up far end cameras for the current call
}
#region Implementation of IHasParticipants
public CodecParticipants Participants { get; private set; }
public void RemoveParticipant(int userId)
{
SendText(string.Format("zCommand Call Expel Id: {0}", userId));
}
public void SetParticipantAsHost(int userId)
{
SendText(string.Format("zCommand Call HostChange Id: {0}", userId));
}
#endregion
#region IHasParticipantAudioMute Members
public void MuteAudioForAllParticipants()
{
SendText(string.Format("zCommand Call MuteAll Mute: on"));
}
public void MuteAudioForParticipant(int userId)
{
SendText(string.Format("zCommand Call MuteParticipant Mute: on Id: {0}", userId));
}
public void UnmuteAudioForParticipant(int userId)
{
SendText(string.Format("zCommand Call MuteParticipant Mute: off Id: {0}", userId));
}
public void ToggleAudioForParticipant(int userId)
{
var user = Participants.CurrentParticipants.FirstOrDefault(p => p.UserId.Equals(userId));
if (user == null)
{
Debug.Console(2, this, "Unable to find user with id: {0}", userId);
return;
}
if (user.AudioMuteFb)
{
UnmuteAudioForParticipant(userId);
}
else
{
MuteAudioForParticipant(userId);
}
}
#endregion
#region IHasParticipantVideoMute Members
public void MuteVideoForParticipant(int userId)
{
SendText(string.Format("zCommand Call MuteParticipantVideo Mute: on Id: {0}", userId));
}
public void UnmuteVideoForParticipant(int userId)
{
SendText(string.Format("zCommand Call MuteParticipantVideo Mute: off Id: {0}", userId));
}
public void ToggleVideoForParticipant(int userId)
{
var user = Participants.CurrentParticipants.FirstOrDefault(p => p.UserId.Equals(userId));
if (user == null)
{
Debug.Console(2, this, "Unable to find user with id: {0}", userId);
return;
}
if (user.VideoMuteFb)
{
UnmuteVideoForParticipant(userId);
}
else
{
MuteVideoForParticipant(userId);
}
}
#endregion
#region IHasParticipantPinUnpin Members
private Func<int> NumberOfScreensFeedbackFunc
{
get { return () => Status.NumberOfScreens.NumOfScreens; }
}
public IntFeedback NumberOfScreensFeedback { get; private set; }
public int ScreenIndexToPinUserTo { get; private set; }
public void PinParticipant(int userId, int screenIndex)
{
SendText(string.Format("zCommand Call Pin Id: {0} Enable: on Screen: {1}", userId, screenIndex));
}
public void UnPinParticipant(int userId)
{
SendText(string.Format("zCommand Call Pin Id: {0} Enable: off", userId));
}
public void ToggleParticipantPinState(int userId, int screenIndex)
{
var user = Participants.CurrentParticipants.FirstOrDefault(p => p.UserId.Equals(userId));
if (user == null)
{
Debug.Console(2, this, "Unable to find user with id: {0}", userId);
return;
}
if (user.IsPinnedFb)
{
UnPinParticipant(userId);
}
else
{
PinParticipant(userId, screenIndex);
}
}
#endregion
#region Implementation of IHasCameraOff
public BoolFeedback CameraIsOffFeedback { get; private set; }
public void CameraOff()
{
CameraMuteOn();
}
#endregion
public BoolFeedback CameraIsMutedFeedback { get; private set; }
public void CameraMuteOn()
{
SendText("zConfiguration Call Camera Mute: On");
}
public void CameraMuteOff()
{
SendText("zConfiguration Call Camera Mute: Off");
}
public void CameraMuteToggle()
{
if (CameraIsMutedFeedback.BoolValue)
CameraMuteOff();
else
CameraMuteOn();
}
#region Implementation of IHasCameraAutoMode
//Zoom doesn't support camera auto modes. Setting this to just unmute video
public void CameraAutoModeOn()
{
CameraMuteOff();
throw new NotImplementedException("Zoom Room Doesn't support camera auto mode");
}
//Zoom doesn't support camera auto modes. Setting this to just unmute video
public void CameraAutoModeOff()
{
SendText("zConfiguration Call Camera Mute: Off");
}
public void CameraAutoModeToggle()
{
throw new NotImplementedException("Zoom Room doesn't support camera auto mode");
}
public BoolFeedback CameraAutoModeIsOnFeedback { get; private set; }
#endregion
#region Implementation of IHasFarEndContentStatus
public BoolFeedback ReceivingContent { get; private set; }
#endregion
#region Implementation of IHasSelfviewPosition
private CodecCommandWithLabel _currentSelfviewPipPosition;
public StringFeedback SelfviewPipPositionFeedback { get; private set; }
public void SelfviewPipPositionSet(CodecCommandWithLabel position)
{
SendText(String.Format("zConfiguration Call Layout Position: {0}", position.Command));
}
public void SelfviewPipPositionToggle()
{
if (_currentSelfviewPipPosition != null)
{
var nextPipPositionIndex = SelfviewPipPositions.IndexOf(_currentSelfviewPipPosition) + 1;
if (nextPipPositionIndex >= SelfviewPipPositions.Count)
// Check if we need to loop back to the first item in the list
nextPipPositionIndex = 0;
SelfviewPipPositionSet(SelfviewPipPositions[nextPipPositionIndex]);
}
}
public List<CodecCommandWithLabel> SelfviewPipPositions = new List<CodecCommandWithLabel>()
{
new CodecCommandWithLabel("UpLeft", "Center Left"),
new CodecCommandWithLabel("UpRight", "Center Right"),
new CodecCommandWithLabel("DownRight", "Lower Right"),
new CodecCommandWithLabel("DownLeft", "Lower Left")
};
private void ComputeSelfviewPipPositionStatus()
{
_currentSelfviewPipPosition =
SelfviewPipPositions.FirstOrDefault(
p => p.Command.ToLower().Equals(Configuration.Call.Layout.Position.ToString().ToLower()));
}
#endregion
// TODO: #714 [ ] Implementation of IHasSelfviewPipSize
#region Implementation of IHasSelfviewPipSize
private CodecCommandWithLabel _currentSelfviewPipSize;
public StringFeedback SelfviewPipSizeFeedback { get; private set; }
public void SelfviewPipSizeSet(CodecCommandWithLabel size)
{
SendText(String.Format("zConfiguration Call Layout Size: {0}", size.Command));
}
public void SelfviewPipSizeToggle()
{
if (_currentSelfviewPipSize != null)
{
var nextPipSizeIndex = SelfviewPipSizes.IndexOf(_currentSelfviewPipSize) + 1;
if (nextPipSizeIndex >= SelfviewPipSizes.Count)
// Check if we need to loop back to the first item in the list
nextPipSizeIndex = 0;
SelfviewPipSizeSet(SelfviewPipSizes[nextPipSizeIndex]);
}
}
public List<CodecCommandWithLabel> SelfviewPipSizes = new List<CodecCommandWithLabel>()
{
new CodecCommandWithLabel("Off", "Off"),
new CodecCommandWithLabel("Size1", "Size 1"),
new CodecCommandWithLabel("Size2", "Size 2"),
new CodecCommandWithLabel("Size3", "Size 3"),
new CodecCommandWithLabel("Strip", "Strip")
};
private void ComputeSelfviewPipSizeStatus()
{
_currentSelfviewPipSize =
SelfviewPipSizes.FirstOrDefault(
p => p.Command.ToLower().Equals(Configuration.Call.Layout.Size.ToString().ToLower()));
}
#endregion
#region Implementation of IHasPhoneDialing
private Func<bool> PhoneOffHookFeedbackFunc
{
get { return () => Status.PhoneCall.OffHook; }
}
private Func<string> CallerIdNameFeedbackFunc
{
get { return () => Status.PhoneCall.PeerDisplayName; }
}
private Func<string> CallerIdNumberFeedbackFunc
{
get { return () => Status.PhoneCall.PeerNumber; }
}
public BoolFeedback PhoneOffHookFeedback { get; private set; }
public StringFeedback CallerIdNameFeedback { get; private set; }
public StringFeedback CallerIdNumberFeedback { get; private set; }
public void DialPhoneCall(string number)
{
SendText(String.Format("zCommand Dial PhoneCallOut Number: {0}", number));
}
public void EndPhoneCall()
{
SendText(String.Format("zCommand Dial PhoneHangUp CallId: {0}", Status.PhoneCall.CallId));
}
public void SendDtmfToPhone(string digit)
{
SendText(String.Format("zCommand SendSipDTMF CallId: {0} Key: {1}", Status.PhoneCall.CallId, digit));
}
#endregion
#region IHasZoomRoomLayouts Members
public event EventHandler<LayoutInfoChangedEventArgs> LayoutInfoChanged;
private Func<bool> LayoutViewIsOnFirstPageFeedbackFunc
{
get { return () => Status.Layout.is_In_First_Page; }
}
private Func<bool> LayoutViewIsOnLastPageFeedbackFunc
{
get { return () => Status.Layout.is_In_Last_Page; }
}
private Func<bool> CanSwapContentWithThumbnailFeedbackFunc
{
get { return () => Status.Layout.can_Switch_Floating_Share_Content; }
}
private Func<bool> ContentSwappedWithThumbnailFeedbackFunc
{
get { return () => Configuration.Call.Layout.ShareThumb; }
}
public BoolFeedback LayoutViewIsOnFirstPageFeedback { get; private set; }
public BoolFeedback LayoutViewIsOnLastPageFeedback { get; private set; }
public BoolFeedback CanSwapContentWithThumbnailFeedback { get; private set; }
public BoolFeedback ContentSwappedWithThumbnailFeedback { get; private set; }
public zConfiguration.eLayoutStyle LastSelectedLayout { get; private set; }
public zConfiguration.eLayoutStyle AvailableLayouts { get; private set; }
/// <summary>
/// Reads individual properties to determine if which layouts are avalailable
/// </summary>
private void ComputeAvailableLayouts()
{
Debug.Console(1, this, "Computing available layouts...");
zConfiguration.eLayoutStyle availableLayouts = zConfiguration.eLayoutStyle.None;
if (Status.Layout.can_Switch_Wall_View)
{
availableLayouts |= zConfiguration.eLayoutStyle.Gallery;
}
if (Status.Layout.can_Switch_Speaker_View)
{
availableLayouts |= zConfiguration.eLayoutStyle.Speaker;
}
if (Status.Layout.can_Switch_Share_On_All_Screens)
{
availableLayouts |= zConfiguration.eLayoutStyle.ShareAll;
}
// 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)
{
availableLayouts |= zConfiguration.eLayoutStyle.Strip;
}
Debug.Console(1, this, "availablelayouts: {0}", availableLayouts);
AvailableLayouts = availableLayouts;
}
private void OnLayoutInfoChanged()
{
var handler = LayoutInfoChanged;
if (handler != null)
{
handler(this, new LayoutInfoChangedEventArgs()
{
AvailableLayouts = AvailableLayouts,
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,
ContentSwappedWithThumbnail = ContentSwappedWithThumbnailFeedback.BoolValue,
});
}
}
public void GetAvailableLayouts()
{
SendText("zStatus Call Layout");
}
public void SetLayout(zConfiguration.eLayoutStyle layoutStyle)
{
LastSelectedLayout = layoutStyle;
SendText(String.Format("zConfiguration Call Layout Style: {0}", layoutStyle.ToString()));
}
public void SwapContentWithThumbnail()
{
if (CanSwapContentWithThumbnailFeedback.BoolValue)
{
var oppositeValue = ContentSwappedWithThumbnailFeedback.BoolValue ? "on" : "off";
// Get the value based on the opposite of the current state
// TODO: #697 [*] Need to verify the ternary above and make sure that the correct on/off value is being send based on the true/false value of the feedback
// to toggle the state
SendText(String.Format("zConfiguration Call Layout ShareThumb: {0}", oppositeValue));
}
}
public void LayoutTurnNextPage()
{
SendText("zCommand Call Layout TurnPage Forward: On");
}
public void LayoutTurnPreviousPage()
{
SendText("zCommand Call Layout TurnPage Forward: Off");
}
#endregion
#region IHasCodecLayouts Members
private Func<string> LocalLayoutFeedbackFunc
{
get
{
return () =>
{
if (Configuration.Call.Layout.Style != zConfiguration.eLayoutStyle.None)
return Configuration.Call.Layout.Style.ToString();
else
return Configuration.Client.Call.Layout.Style.ToString();
};
}
}
public StringFeedback LocalLayoutFeedback { get; private set; }
public void LocalLayoutToggle()
{
var currentLayout = LocalLayoutFeedback.StringValue;
var eCurrentLayout = (int) Enum.Parse(typeof (zConfiguration.eLayoutStyle), currentLayout, true);
var nextLayout = GetNextLayout(eCurrentLayout);
if (nextLayout != zConfiguration.eLayoutStyle.None)
{
SetLayout(nextLayout);
}
}
/// <summary>
/// Tries to get the next available layout
/// </summary>
/// <param name="currentLayout"></param>
/// <returns></returns>
private zConfiguration.eLayoutStyle GetNextLayout(int currentLayout)
{
if (AvailableLayouts == zConfiguration.eLayoutStyle.None)
{
return zConfiguration.eLayoutStyle.None;
}
zConfiguration.eLayoutStyle nextLayout;
if (((zConfiguration.eLayoutStyle) currentLayout & zConfiguration.eLayoutStyle.ShareAll) ==
zConfiguration.eLayoutStyle.ShareAll)
{
nextLayout = zConfiguration.eLayoutStyle.Gallery;
}
else
{
nextLayout = (zConfiguration.eLayoutStyle) (currentLayout << 1);
}
if ((AvailableLayouts & nextLayout) == nextLayout)
{
return nextLayout;
}
else
{
return GetNextLayout((int) nextLayout);
}
}
public void LocalLayoutToggleSingleProminent()
{
throw new NotImplementedException();
}
public void MinMaxLayoutToggle()
{
throw new NotImplementedException();
}
#endregion
#region IPasswordPrompt Members
public event EventHandler<PasswordPromptEventArgs> PasswordRequired;
public void SubmitPassword(string password)
{
Debug.Console(2, this, "Password Submitted: {0}", password);
Dial(_lastDialedMeetingNumber, password);
}
void OnPasswordRequired(bool lastAttemptIncorrect, bool loginFailed, bool loginCancelled, string message)
{
var handler = PasswordRequired;
if (handler != null)
{
handler(this, new PasswordPromptEventArgs(lastAttemptIncorrect, loginFailed, loginCancelled, message));
}
}
#endregion
#region IHasMeetingInfo Members
public event EventHandler<MeetingInfoEventArgs> MeetingInfoChanged;
private MeetingInfo _meetingInfo;
public MeetingInfo MeetingInfo
{
get { return _meetingInfo; }
private set
{
if (value != _meetingInfo)
{
_meetingInfo = value;
var handler = MeetingInfoChanged;
if (handler != null)
{
handler(this, new MeetingInfoEventArgs(_meetingInfo));
}
}
}
}
#endregion
#region Implementation of IHasPresentationOnlyMeeting
public void StartSharingOnlyMeeting()
{
StartSharingOnlyMeeting(eSharingMeetingMode.None, 30, String.Empty);
}
public void StartSharingOnlyMeeting(eSharingMeetingMode mode)
{
StartSharingOnlyMeeting(mode, 30, String.Empty);
}
public void StartSharingOnlyMeeting(eSharingMeetingMode mode, ushort duration)
{
StartSharingOnlyMeeting(mode, duration, String.Empty);
}
public void StartSharingOnlyMeeting(eSharingMeetingMode mode, ushort duration, string password)
{
SendText(String.Format("zCommand Dial Sharing Duration: {0} DisplayState: {1} Password: {2}", duration, mode, password));
}
public void StartNormalMeetingFromSharingOnlyMeeting()
{
Debug.Console(2, this, "Converting Sharing Meeting to Normal Meeting");
SendText("zCommand call sharing ToNormal");
}
#endregion
#region IHasMeetingLock Members
public BoolFeedback MeetingIsLockedFeedback { get; private set; }
public void LockMeeting()
{
SendText(string.Format("zConfiguration Call Lock Enable: on"));
}
public void UnLockMeeting()
{
SendText(string.Format("zConfiguration Call Lock Enable: off"));
}
public void ToggleMeetingLock()
{
if (MeetingIsLockedFeedback.BoolValue)
{
UnLockMeeting();
}
else
{
LockMeeting();
}
}
#endregion
#region IHasMeetingRecording Members
public BoolFeedback MeetingIsRecordingFeedback { get; private set; }
public void StartRecording()
{
SendText(string.Format("Command Call Record Enable: on"));
}
public void StopRecording()
{
SendText(string.Format("Command Call Record Enable: off"));
}
public void ToggleRecording()
{
if (MeetingIsRecordingFeedback.BoolValue)
{
StopRecording();
}
else
{
StartRecording();
}
}
#endregion
}
/// <summary>
/// Zoom Room specific info object
/// </summary>
public class ZoomRoomInfo : VideoCodecInfo
{
public ZoomRoomInfo(ZoomRoomStatus status, ZoomRoomConfiguration configuration)
{
Status = status;
Configuration = configuration;
}
[JsonIgnore]
public ZoomRoomStatus Status { get; private set; }
[JsonIgnore]
public ZoomRoomConfiguration Configuration { get; private set; }
public override bool AutoAnswerEnabled
{
get { return Status.SystemUnit.RoomInfo.AutoAnswerIsEnabled; }
}
public override string E164Alias
{
get
{
if (!string.IsNullOrEmpty(Status.SystemUnit.MeetingNumber))
{
return Status.SystemUnit.MeetingNumber;
}
return string.Empty;
}
}
public override string H323Id
{
get
{
if (!string.IsNullOrEmpty(Status.Call.Info.meeting_list_item.third_party.h323_address))
{
return Status.Call.Info.meeting_list_item.third_party.h323_address;
}
return string.Empty;
}
}
public override string IpAddress
{
get
{
if (!string.IsNullOrEmpty(Status.SystemUnit.RoomInfo.AccountEmail))
{
return Status.SystemUnit.RoomInfo.AccountEmail;
}
return string.Empty;
}
}
public override bool MultiSiteOptionIsEnabled
{
get { return true; }
}
public override string SipPhoneNumber
{
get
{
if (!string.IsNullOrEmpty(Status.Call.Info.dialIn))
{
return Status.Call.Info.dialIn;
}
return string.Empty;
}
}
public override string SipUri
{
get
{
if (!string.IsNullOrEmpty(Status.Call.Info.meeting_list_item.third_party.sip_address))
{
return Status.Call.Info.meeting_list_item.third_party.sip_address;
}
return string.Empty;
}
}
}
/// <summary>
/// Tracks the initial sycnronization state when establishing a new connection
/// </summary>
public class ZoomRoomSyncState : IKeyed
{
private readonly ZoomRoom _parent;
private readonly CrestronQueue<string> _syncQueries;
private bool _initialSyncComplete;
public ZoomRoomSyncState(string key, ZoomRoom parent)
{
_parent = parent;
Key = key;
_syncQueries = new CrestronQueue<string>(50);
CodecDisconnected();
}
public bool InitialSyncComplete
{
get { return _initialSyncComplete; }
private set
{
if (value)
{
var handler = InitialSyncCompleted;
if (handler != null)
{
handler(this, new EventArgs());
}
}
_initialSyncComplete = value;
}
}
public bool LoginResponseWasReceived { get; private set; }
public bool FirstJsonResponseWasReceived { get; private set; }
public bool InitialQueryMessagesWereSent { get; private set; }
public bool LastQueryResponseWasReceived { get; private set; }
public bool CamerasHaveBeenSetUp { get; private set; }
#region IKeyed Members
public string Key { get; private set; }
#endregion
public event EventHandler<EventArgs> InitialSyncCompleted;
public event EventHandler FirstJsonResponseReceived;
public void StartSync()
{
DequeueQueries();
}
private void DequeueQueries()
{
while (!_syncQueries.IsEmpty)
{
var query = _syncQueries.Dequeue();
_parent.SendText(query);
}
InitialQueryMessagesSent();
}
public void AddQueryToQueue(string query)
{
_syncQueries.Enqueue(query);
}
public void LoginResponseReceived()
{
LoginResponseWasReceived = true;
Debug.Console(1, this, "Login Rsponse Received.");
CheckSyncStatus();
}
public void ReceivedFirstJsonResponse()
{
FirstJsonResponseWasReceived = true;
Debug.Console(1, this, "First JSON Response Received.");
var handler = FirstJsonResponseReceived;
if (handler != null)
{
handler(this, null);
}
CheckSyncStatus();
}
public void InitialQueryMessagesSent()
{
InitialQueryMessagesWereSent = true;
Debug.Console(1, this, "Query Messages Sent.");
CheckSyncStatus();
}
public void LastQueryResponseReceived()
{
LastQueryResponseWasReceived = true;
Debug.Console(1, this, "Last Query Response Received.");
CheckSyncStatus();
}
public void CamerasSetUp()
{
CamerasHaveBeenSetUp = true;
Debug.Console(1, this, "Cameras Set Up.");
CheckSyncStatus();
}
public void CodecDisconnected()
{
_syncQueries.Clear();
LoginResponseWasReceived = false;
FirstJsonResponseWasReceived = false;
InitialQueryMessagesWereSent = false;
LastQueryResponseWasReceived = false;
CamerasHaveBeenSetUp = false;
InitialSyncComplete = false;
}
private void CheckSyncStatus()
{
if (LoginResponseWasReceived && FirstJsonResponseWasReceived && InitialQueryMessagesWereSent && LastQueryResponseWasReceived &&
CamerasHaveBeenSetUp)
{
InitialSyncComplete = true;
Debug.Console(1, this, "Initial Codec Sync Complete!");
}
else
{
InitialSyncComplete = false;
}
}
}
public class ZoomRoomFactory : EssentialsDeviceFactory<ZoomRoom>
{
public ZoomRoomFactory()
{
TypeNames = new List<string> {"zoomroom"};
}
public override EssentialsDevice BuildDevice(DeviceConfig dc)
{
Debug.Console(1, "Factory Attempting to create new ZoomRoom Device");
var comm = CommFactory.CreateCommForDevice(dc);
return new ZoomRoom(dc, comm);
}
}
}