using System; using System.Collections.Generic; 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.Devices.Common.Cameras; using PepperDash.Essentials.Devices.Common.Codec; using PepperDash.Essentials.Devices.Common.VideoCodec.Cisco; using PepperDash.Essentials.Devices.Common.VideoCodec.Interfaces; using PepperDash_Essentials_Core.DeviceTypeInterfaces; namespace PepperDash.Essentials.Devices.Common.VideoCodec.ZoomRoom { public class ZoomRoom : VideoCodecBase, IHasCodecSelfView, IHasDirectoryHistoryStack, ICommunicationMonitor, IRouting, IHasScheduleAwareness, IHasCodecCameras, IHasParticipants, IHasCameraOff, IHasCameraAutoMode, IHasFarEndContentStatus, IHasSelfviewPosition, IHasPhoneDialing { private const long MeetingRefreshTimer = 60000; private const uint DefaultMeetingDurationMin = 30; private const string Delimiter = "\x0D\x0A"; private readonly CrestronQueue _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 readonly ZoomRoomPropertiesConfig _props; public ZoomRoom(DeviceConfig config, IBasicCommunication comm) : base(config) { _props = JsonConvert.DeserializeObject(config.Properties.ToString()); // The queue that will collect the repsonses in the order they are received _receiveQueue = new CrestronQueue(1024); // The thread responsible for dequeuing and processing the messages _receiveThread = new Thread(o => ProcessQueue(), null) {Priority = Thread.eThreadPriority.MediumPriority}; Communication = comm; if (_props.CommunicationMonitorProperties != null) { CommunicationMonitor = new GenericCommunicationMonitor(this, Communication, _props.CommunicationMonitorProperties); } else { CommunicationMonitor = new GenericCommunicationMonitor(this, Communication, 30000, 120000, 300000, "zStatus SystemUnit\r"); } DeviceManager.AddDevice(CommunicationMonitor); Status = new ZoomRoomStatus(); Configuration = new ZoomRoomConfiguration(); CodecInfo = new ZoomRoomInfo(Status, Configuration); _syncState = new ZoomRoomSyncState(Key + "--Sync", this); _syncState.InitialSyncCompleted += SyncState_InitialSyncCompleted; PhonebookSyncState = new CodecPhonebookSyncState(Key + "--PhonebookSync"); PortGather = new CommunicationGather(Communication, "\x0A") {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); SelfviewIsOnFeedback = new BoolFeedback(SelfViewIsOnFeedbackFunc); CameraIsOffFeedback = new BoolFeedback(CameraIsOffFeedbackFunc); CameraAutoModeIsOnFeedback = new BoolFeedback(CameraAutoModeIsOnFeedbackFunc); CodecSchedule = new CodecScheduleAwareness(MeetingRefreshTimer); ReceivingContent = new BoolFeedback(FarEndIsSharingContentFeedbackFunc); SelfviewPipPositionFeedback = new StringFeedback(SelfviewPipPositionFeedbackFunc); SetUpFeedbackActions(); Cameras = new List(); SetUpDirectory(); Participants = new CodecParticipants(); SupportsCameraOff = _props.SupportsCameraOff; SupportsCameraAutoMode = _props.SupportsCameraAutoMode; PhoneOffHookFeedback = new BoolFeedback(PhoneOffHookFeedbackFunc); CallerIdNameFeedback = new StringFeedback(CallerIdNameFeedbackFunc); CallerIdNumberFeedback = new StringFeedback(CallerIdNumberFeedbackFunc); } public CommunicationGather PortGather { get; private set; } public ZoomRoomStatus Status { get; private set; } public ZoomRoomConfiguration Configuration { get; private set; } //CTimer LoginMessageReceivedTimer; //CTimer RetryConnectionTimer; /// /// Gets and returns the scaled volume of the codec /// protected override Func VolumeLevelFeedbackFunc { get { return () => CrestronEnvironment.ScaleWithLimits(Configuration.Audio.Output.Volume, 100, 0, 65535, 0); } } protected override Func PrivacyModeIsOnFeedbackFunc { get { return () => Configuration.Call.Microphone.Mute; } } protected override Func StandbyIsOnFeedbackFunc { get { return () => false; } } protected override Func SharingSourceFeedbackFunc { get { return () => Status.Sharing.dispState; } } protected override Func SharingContentIsOnFeedbackFunc { get { return () => Status.Call.Sharing.IsSharing; } } protected Func FarEndIsSharingContentFeedbackFunc { get { return () => Status.Call.Sharing.State == zEvent.eSharingState.Receiving; } } protected override Func MuteFeedbackFunc { get { return () => Configuration.Audio.Output.Volume == 0; } } //protected Func RoomIsOccupiedFeedbackFunc //{ // get // { // return () => false; // } //} //protected Func PeopleCountFeedbackFunc //{ // get // { // return () => 0; // } //} protected Func SelfViewIsOnFeedbackFunc { get { return () => !Configuration.Video.HideConfSelfVideo; } } protected Func CameraIsOffFeedbackFunc { get { return () => Configuration.Call.Camera.Mute; } } protected Func CameraAutoModeIsOnFeedbackFunc { get { return () => false; } } protected Func SelfviewPipPositionFeedbackFunc { get { return () => _currentSelfviewPipPosition.Command; } } protected Func LocalLayoutFeedbackFunc { get { return () => ""; } } protected Func LocalLayoutIsProminentFeedbackFunc { get { return () => false; } } public RoutingInputPort CodecOsdIn { get; private set; } public RoutingOutputPort Output1 { get; private set; } #region ICommunicationMonitor Members public StatusMonitorBase CommunicationMonitor { get; private set; } #endregion #region IHasCodecCameras Members public event EventHandler CameraSelected; public List 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 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 DirectoryResultReturned; public CodecDirectory DirectoryRoot { get; private set; } public CodecDirectory CurrentDirectoryResult { get { return _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; OnDirectoryResultReturned(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; OnDirectoryResultReturned(directoryResults); } public void SetCurrentDirectoryToRoot() { DirectoryBrowseHistoryStack.Clear(); _currentDirectoryResult = DirectoryRoot; OnDirectoryResultReturned(DirectoryRoot); } public void GetDirectoryParentFolderContents() { if (DirectoryBrowseHistoryStack.Count == 0) { return; } var currentDirectory = DirectoryBrowseHistoryStack.Pop(); _currentDirectoryResult = currentDirectory; OnDirectoryResultReturned(currentDirectory); } public BoolFeedback CurrentDirectoryResultIsNotDirectoryRoot { get; private set; } public List DirectoryBrowseHistory { get; private set; } public Stack 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 += (o, a) => { if (a.PropertyName == "State") { SharingContentIsOnFeedback.FireUpdate(); ReceivingContent.FireUpdate(); } }; Status.Call.PropertyChanged += (o, a) => { if (a.PropertyName == "Info") { Debug.Console(1, this, "Updating Call Status"); UpdateCallStatus(); } }; } /// /// Subscribes to the PropertyChanged events on the state objects and fires the corresponding feedbacks. /// 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) => { if (a.PropertyName != "Position") return; ComputeSelfviewPipStatus(); SelfviewPipPositionFeedback.FireUpdate(); }; Status.Call.Sharing.PropertyChanged += (o, a) => { if (a.PropertyName == "State") { SharingContentIsOnFeedback.FireUpdate(); ReceivingContent.FireUpdate(); } }; Status.Call.PropertyChanged += (o, a) => { if (a.PropertyName == "Info") { Debug.Console(1, this, "Updating Call Status"); UpdateCallStatus(); } }; Status.Sharing.PropertyChanged += (o, a) => { switch (a.PropertyName) { case "dispState": SharingSourceFeedback.FireUpdate(); break; case "password": 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; } }; } private void SetUpDirectory() { DirectoryRoot = new CodecDirectory(); DirectoryBrowseHistory = new List(); DirectoryBrowseHistoryStack = new Stack(); CurrentDirectoryResultIsNotDirectoryRoot = new BoolFeedback(() => _currentDirectoryResult != DirectoryRoot); CurrentDirectoryResultIsNotDirectoryRoot.FireUpdate(); } private void SetUpRouting() { // Set up input ports CreateOsdSource(); InputPorts.Add(CodecOsdIn); // Set up output ports OutputPorts.Add(Output1); } /// /// Creates the fake OSD source, and connects it's AudioVideo output to the CodecOsdIn input /// to enable routing /// 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. } /// /// Starts the HTTP feedback server and syncronizes state of codec /// /// 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: 512"), "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); var socket = Communication as ISocketStatus; if (socket != null) { socket.ConnectionChange += socket_ConnectionChange; } CommDebuggingIsOn = false; Communication.Connect(); CommunicationMonitor.Start(); return base.CustomActivate(); } public void SetCommDebug(string s) { if (s == "1") { CommDebuggingIsOn = true; Debug.Console(0, this, "Comm Debug Enabled."); } else { CommDebuggingIsOn = false; Debug.Console(0, 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 + Delimiter); } /// /// Gathers responses and enqueues them. /// /// /// private void Port_LineReceived(object dev, GenericCommMethodReceiveTextArgs args) { //if (CommDebuggingIsOn) // Debug.Console(1, this, "Gathered: '{0}'", args.Text); _receiveQueue.Enqueue(args.Text); // If the receive thread has for some reason stopped, this will restart it if (_receiveThread.ThreadState != Thread.eThreadStates.ThreadRunning) { _receiveThread.Start(); } } /// /// Runs in it's own thread to dequeue messages in the order they were received to be processed /// /// private object ProcessQueue() { try { while (true) { var message = _receiveQueue.Dequeue(); ProcessMessage(message); } } catch (Exception e) { Debug.Console(1, this, "Error Processing Queue: {0}", e); } return null; } /// /// Queues the initial queries to be sent upon connection /// 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: 512"); } _syncState.AddQueryToQueue("zCommand Bookings List"); _syncState.AddQueryToQueue("zCommand Call ListParticipants"); _syncState.AddQueryToQueue("zCommand Call Info"); _syncState.StartSync(); } /// /// Processes messages as they are dequeued /// /// private void ProcessMessage(string message) { // Counts the curly braces if (message.Contains("client_loop: send disconnect: Broken pipe")) { Debug.Console(0, 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') == "{" + Delimiter) // 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') == "}" + Delimiter) // 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.LoginMessageReceived(); // Fire up a thread to send the intial commands. CrestronInvoke.BeginInvoke(o => { 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); if (!_props.DisablePhonebookAutoDownload) { SendText("zFeedback Register Op: ex Path: /Event/Phonebook/AddedContact"); } // switch to json format SendText("format json"); }); break; } } } } /// /// Deserializes a JSON formatted response /// /// private void DeserializeResponse(string response) { try { var trimmedResponse = response.Trim(); if (trimmedResponse.Length <= 0) { return; } var message = JObject.Parse(trimmedResponse); var eType = (eZoomRoomResponseType) Enum.Parse(typeof (eZoomRoomResponseType), message["type"].Value(), true); var topKey = message["topKey"].Value(); var responseObj = message[topKey]; Debug.Console(1, "{0} Response Received. topKey: '{1}'\n{2}", eType, topKey, responseObj.ToString()); 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": { JsonConvert.PopulateObject(responseObj.ToString(), Status.Phonebook); if (!PhonebookSyncState.InitialSyncComplete) { PhonebookSyncState.InitialPhonebookFoldersReceived(); PhonebookSyncState.PhonebookRootEntriesReceived(); PhonebookSyncState.SetPhonebookHasFolders(false); PhonebookSyncState.SetNumberOfContacts(Status.Phonebook.Contacts.Count); } var directoryResults = zStatus.Phonebook.ConvertZoomContactsToGeneric(Status.Phonebook.Contacts); DirectoryRoot = directoryResults; _currentDirectoryResult = DirectoryRoot; OnDirectoryResultReturned(directoryResults); break; } case "listparticipantsresult": { Debug.Console(1, this, "JTokenType: {0}", responseObj.Type); switch (responseObj.Type) { case JTokenType.Array: Status.Call.Participants = JsonConvert.DeserializeObject>( responseObj.ToString()); break; case JTokenType.Object: { // this is a single participant event notification var participant = JsonConvert.DeserializeObject( responseObj.ToString()); if (participant != null) { 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": Status.Call.Participants.Add(participant); break; } } } break; } var participants = zCommand.ListParticipant.GetGenericParticipantListFromParticipantsResult( Status.Call.Participants); Participants.CurrentParticipants = participants; PrintCurrentCallParticipants(); break; } default: { break; } } break; } case eZoomRoomResponseType.zEvent: { switch (topKey.ToLower()) { case "phonebook": { if (responseObj["Updated Contact"] != null) { var updatedContact = JsonConvert.DeserializeObject( 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( jToken.ToString()); // Add a new contact Status.Phonebook.Contacts.Add(newContact); } } break; } case "bookingslistresult": { if (!_syncState.InitialSyncComplete) { _syncState.LastQueryResponseReceived(); } var codecBookings = JsonConvert.DeserializeObject>( responseObj.ToString()); if (codecBookings != null && codecBookings.Count > 0) { CodecSchedule.Meetings = zCommand.GetGenericMeetingsFromBookingResult( codecBookings, CodecSchedule.MeetingWarningMinutes); } break; } case "bookings updated": { GetBookings(); break; } case "sharingstate": { JsonConvert.PopulateObject(responseObj.ToString(), Status.Call.Sharing); SetLayout(); break; } case "incomingcallindication": { var incomingCall = JsonConvert.DeserializeObject(responseObj.ToString()); if (incomingCall != null) { var newCall = new CodecActiveCallItem { Direction = eCodecCallDirection.Incoming, Status = eCodecCallStatus.Ringing, Type = eCodecCallType.Unknown, Name = incomingCall.callerName, Id = incomingCall.callerJID }; ActiveCalls.Add(newCall); OnCallStatusChange(newCall); } break; } case "treatedincomingcallindication": { var incomingCall = JsonConvert.DeserializeObject(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(responseObj.ToString()); if (disconnectEvent.Successful) { if (ActiveCalls.Count > 0) { var activeCall = ActiveCalls.FirstOrDefault(c => c.IsActiveCall); if (activeCall != null) { activeCall.Status = eCodecCallStatus.Disconnected; OnCallStatusChange(activeCall); } } var emptyList = new List(); Participants.CurrentParticipants = emptyList; } UpdateCallStatus(); break; } case "callconnecterror": { UpdateCallStatus(); break; } case "videounmuterequest": { // TODO: notify room of a request to unmute video break; } case "meetingneedspassword": { // TODO: notify user to enter a password break; } case "needwaitforhost": { var needWait = JsonConvert.DeserializeObject(responseObj.ToString()); if (needWait.Wait) { // TODO: notify user to wait for host } 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; } default: { break; } } break; } case eZoomRoomResponseType.zStatus: { switch (topKey.ToLower()) { case "login": { _syncState.LoginMessageReceived(); if (!_syncState.InitialQueryMessagesWereSent) { SetUpSyncQueries(); } JsonConvert.PopulateObject(responseObj.ToString(), Status.Login); break; } case "systemunit": { JsonConvert.PopulateObject(responseObj.ToString(), Status.SystemUnit); break; } case "call": { JsonConvert.PopulateObject(responseObj.ToString(), Status.Call); 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); } } 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)); } } public void PrintCurrentCallParticipants() { if (Debug.Level <= 0) { return; } Debug.Console(1, this, "****************************Call Participants***************************"); foreach (var participant in Participants.CurrentParticipants) { Debug.Console(1, this, "Name: {0} Audio: {1} IsHost: {2}", participant.Name, participant.AudioMuteFb, participant.IsHost); } Debug.Console(1, this, "************************************************************************"); } /// /// Retrieves bookings list /// private void GetBookings() { SendText("zCommand Bookings List"); } /// /// Updates the current call status /// private void UpdateCallStatus() { if (Status.Call != null) { var callStatus = Status.Call.Status; // If not currently in a meeting, intialize the call object if (callStatus != zStatus.eCallStatus.IN_MEETING || callStatus != zStatus.eCallStatus.CONNECTING_MEETING) { Status.Call = new zStatus.Call {Status = callStatus}; 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; } var newCall = new CodecActiveCallItem {Status = newStatus}; ActiveCalls.Add(newCall); OnCallStatusChange(newCall); } } else { var existingCall = ActiveCalls.FirstOrDefault(c => !c.Status.Equals(eCodecCallStatus.Ringing)); switch (callStatus) { case zStatus.eCallStatus.IN_MEETING: existingCall.Status = eCodecCallStatus.Connected; break; case zStatus.eCallStatus.NOT_IN_MEETING: existingCall.Status = eCodecCallStatus.Disconnected; break; } OnCallStatusChange(existingCall); } } Debug.Console(1, this, "****************************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, @"Name: {0} ID: {1} IsActive: {2} Status: {3} Direction: {4}", call.Name, call.Id, call.IsActiveCall, call.Status, call.Direction); if (!call.IsActiveCall) { Debug.Console(1, this, "******Removing Inactive Call: {0}******", call.Name); ActiveCalls.Remove(call); } } Debug.Console(1, this, "**************************************************************************"); //clear participants list after call cleanup if (ActiveCalls.Count == 0) { Participants.CurrentParticipants = new List(); } } protected override void OnCallStatusChange(CodecActiveCallItem item) { base.OnCallStatusChange(item); if (_props.AutoDefaultLayouts) { SetLayout(); } } public override void StartSharing() { SendText("zCommand Call Sharing HDMI Start"); } /// /// Stops sharing the current presentation /// public override void StopSharing() { 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() { SetVolume((ushort) _previousVolumeLevel); } public override void MuteOn() { _previousVolumeLevel = Configuration.Audio.Output.Volume; // Store the previous level for recall SetVolume(0); } public override void MuteToggle() { if (MuteFeedback.BoolValue) { MuteOff(); } else { MuteOn(); } } /// /// Increments the voluem /// /// public override void VolumeUp(bool pressRelease) { // TODO: Implment volume decrement that calls SetVolume() } /// /// Decrements the volume /// /// public override void VolumeDown(bool pressRelease) { // TODO: Implment volume decrement that calls SetVolume() } /// /// Scales the level and sets the codec to the specified level within its range /// /// level from slider (0-65535 range) public override void SetVolume(ushort level) { var scaledLevel = CrestronEnvironment.ScaleWithLimits(level, 65535, 0, 100, 0); SendText(string.Format("zConfiguration Audio Output volume: {0}", scaledLevel)); } /// /// Recalls the default volume on the codec /// public void VolumeSetToDefault() { } /// /// /// public override void StandbyActivate() { // No corresponding function on device } /// /// /// public override void StandbyDeactivate() { // No corresponding function on device } public override void LinkToApi(BasicTriList trilist, uint joinStart, string joinMapKey, EiscApiAdvanced bridge) { LinkVideoCodecToApi(this, trilist, joinStart, joinMapKey, bridge); } 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) { SendText(string.Format("zCommand Dial Start meetingNumber: {0}", meeting.Id)); } public override void Dial(string number) { SendText(string.Format("zCommand Dial Join meetingNumber: {0}", number)); } /// /// Invites a contact to either a new meeting (if not already in a meeting) or the current meeting. /// Currently only invites a single user /// /// public override void Dial(IInvitableContact contact) { var ic = contact as zStatus.ZoomDirectoryContact; 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)); } } } 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); } /// /// Call when directory results are updated /// /// private void OnDirectoryResultReturned(CodecDirectory result) { CurrentDirectoryResultIsNotDirectoryRoot.FireUpdate(); // 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 = result, DirectoryIsOnRoot = !CurrentDirectoryResultIsNotDirectoryRoot.BoolValue }); } //PrintDirectory(result); } /// /// Builds the cameras List by using the Zoom Room zStatus.Cameras data. Could later be modified to build from config data /// private void SetUpCameras() { SelectedCameraFeedback = new StringFeedback(() => Configuration.Video.Camera.SelectedId); ControllingFarEndCameraFeedback = new BoolFeedback(() => SelectedCamera is IAmFarEndCamera); foreach (var cam in Status.Cameras) { var camera = new ZoomRoomCamera(cam.id, cam.Name, this); Cameras.Add(camera); if (cam.Selected) { SelectedCamera = camera; } } if (IsInCall) { UpdateFarEndCameras(); } _syncState.CamerasSetUp(); } /// /// Dynamically creates far end cameras for call participants who have far end control enabled. /// private void UpdateFarEndCameras() { // TODO: set up far end cameras for the current call } #region Implementation of IHasParticipants public CodecParticipants Participants { get; private set; } #endregion #region Implementation of IHasCameraOff public BoolFeedback CameraIsOffFeedback { get; private set; } public void CameraOff() { SendText("zConfiguration Call Camera Mute: On"); } #endregion #region Implementation of IHasCameraAutoMode //Zoom doesn't support camera auto modes. Setting this to just unmute video public void CameraAutoModeOn() { 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 SelfviewPipPositions = new List() { new CodecCommandWithLabel("UpLeft", "Center Left"), new CodecCommandWithLabel("UpRight", "Center Right"), new CodecCommandWithLabel("DownRight", "Lower Right"), new CodecCommandWithLabel("DownLeft", "Lower Left") }; private void ComputeSelfviewPipStatus() { _currentSelfviewPipPosition = SelfviewPipPositions.FirstOrDefault( p => p.Command.ToLower().Equals(Configuration.Call.Layout.Position.ToString().ToLower())); } #endregion #region Implementation of IHasPhoneDialing private Func PhoneOffHookFeedbackFunc {get {return () => Status.PhoneCall.OffHook; }} private Func CallerIdNameFeedbackFunc { get { return () => Status.PhoneCall.PeerDisplayName; } } private Func 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 } /// /// Zoom Room specific info object /// public class ZoomRoomInfo : VideoCodecInfo { public ZoomRoomInfo(ZoomRoomStatus status, ZoomRoomConfiguration configuration) { Status = status; Configuration = configuration; } public ZoomRoomStatus Status { get; private set; } 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; } } } /// /// Tracks the initial sycnronization state when establishing a new connection /// public class ZoomRoomSyncState : IKeyed { private readonly ZoomRoom _parent; private readonly CrestronQueue _syncQueries; private bool _initialSyncComplete; public ZoomRoomSyncState(string key, ZoomRoom parent) { _parent = parent; Key = key; _syncQueries = new CrestronQueue(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 LoginMessageWasReceived { 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 InitialSyncCompleted; 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 LoginMessageReceived() { LoginMessageWasReceived = true; Debug.Console(1, this, "Login Message Received."); 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(); LoginMessageWasReceived = false; InitialQueryMessagesWereSent = false; LastQueryResponseWasReceived = false; CamerasHaveBeenSetUp = false; InitialSyncComplete = false; } private void CheckSyncStatus() { if (LoginMessageWasReceived && InitialQueryMessagesWereSent && LastQueryResponseWasReceived && CamerasHaveBeenSetUp) { InitialSyncComplete = true; Debug.Console(1, this, "Initial Codec Sync Complete!"); } else { InitialSyncComplete = false; } } } public class ZoomRoomFactory : EssentialsDeviceFactory { public ZoomRoomFactory() { TypeNames = new List {"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); } } }