using System; using System.Collections.Generic; using System.Linq; using System.Text; using Crestron.SimplSharp; using Crestron.SimplSharpPro.CrestronThread; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using PepperDash.Core; using PepperDash.Essentials.Core; using PepperDash.Essentials.Core.Config; using PepperDash.Essentials.Core.Routing; using PepperDash.Essentials.Devices.Common.Cameras; using PepperDash.Essentials.Devices.Common.Codec; using PepperDash.Essentials.Core; using PepperDash.Essentials.Devices.Common.VideoCodec; namespace PepperDash.Essentials.Devices.Common.VideoCodec.ZoomRoom { public class ZoomRoom : VideoCodecBase, IHasCodecSelfView, IHasDirectory, ICommunicationMonitor, IRouting, IHasScheduleAwareness, IHasCodecCameras { public CommunicationGather PortGather { get; private set; } public StatusMonitorBase CommunicationMonitor { get; private set; } private CrestronQueue ReceiveQueue; private Thread ReceiveThread; string Delimiter = "\x0D\x0A"; private ZoomRoomSyncState SyncState; public ZoomRoomStatus Status { get; private set; } public ZoomRoomConfiguration Configuration { get; private set; } private StringBuilder JsonMessage; private bool JsonFeedbackMessageIsIncoming; private uint JsonCurlyBraceCounter = 0; public bool CommDebuggingIsOn; //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 () => false; } } 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 SelfviewPipPositionFeedbackFunc { get { return () => ""; } } protected Func LocalLayoutFeedbackFunc { get { return () => ""; } } protected Func LocalLayoutIsProminentFeedbackFunc { get { return () => false; } } public RoutingInputPort CodecOsdIn { get; private set; } public RoutingOutputPort Output1 { get; private set; } uint DefaultMeetingDurationMin = 30; int PreviousVolumeLevel = 0; public ZoomRoom(DeviceConfig config, IBasicCommunication comm) : base(config) { var props = JsonConvert.DeserializeObject(config.Properties.ToString()); // The queue that will collect the repsonses in the order they are received ReceiveQueue = new CrestronQueue(25); // The thread responsible for dequeuing and processing the messages ReceiveThread = new Thread((o) => ProcessQueue(), null); 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 += new EventHandler(SyncState_InitialSyncCompleted); PhonebookSyncState = new CodecPhonebookSyncState(Key + "--PhonebookSync"); PortGather = new CommunicationGather(Communication, "\x0A"); PortGather.IncludeDelimiter = true; PortGather.LineReceived += this.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); CodecSchedule = new CodecScheduleAwareness(); SetUpFeedbackActions(); Cameras = new List(); SetUpDirectory(); } void SyncState_InitialSyncCompleted(object sender, EventArgs e) { SetUpRouting(); SetIsReady(); } /// /// Subscribes to the PropertyChanged events on the state objects and fires the corresponding feedbacks. /// void SetUpFeedbackActions() { Configuration.Audio.Output.PropertyChanged += new System.ComponentModel.PropertyChangedEventHandler( (o, a) => { if (a.PropertyName == "Volume") { VolumeLevelFeedback.FireUpdate(); MuteFeedback.FireUpdate(); } }); Configuration.Call.Microphone.PropertyChanged += new System.ComponentModel.PropertyChangedEventHandler( (o, a) => { if (a.PropertyName == "Mute") { PrivacyModeIsOnFeedback.FireUpdate(); } }); Configuration.Video.PropertyChanged += new System.ComponentModel.PropertyChangedEventHandler( (o, a) => { if (a.PropertyName == "HideConfSelfVideo") { SelfviewIsOnFeedback.FireUpdate(); } }); Configuration.Video.Camera.PropertyChanged += new System.ComponentModel.PropertyChangedEventHandler( (o, a) => { if (a.PropertyName == "SelectedId") { SelectCamera(Configuration.Video.Camera.SelectedId); // this will in turn fire the affected feedbacks } }); Status.Call.Sharing.PropertyChanged += new System.ComponentModel.PropertyChangedEventHandler( (o, a) => { if (a.PropertyName == "State") { SharingContentIsOnFeedback.FireUpdate(); } }); Status.Sharing.PropertyChanged += new System.ComponentModel.PropertyChangedEventHandler( (o, a) => { if (a.PropertyName == "dispState") { SharingSourceFeedback.FireUpdate(); } else if (a.PropertyName == "password") { //TODO: Fire Sharing Password Update } }); } void SetUpDirectory() { DirectoryRoot = new CodecDirectory(); DirectoryBrowseHistory = new List(); CurrentDirectoryResultIsNotDirectoryRoot = new BoolFeedback(() => DirectoryBrowseHistory.Count > 0); CurrentDirectoryResultIsNotDirectoryRoot.FireUpdate(); } 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 /// 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); 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 += new EventHandler(socket_ConnectionChange); } // TODO: Turn this off when done initial development CommDebuggingIsOn = true; 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."); } } 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. /// /// /// 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 /// /// 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 /// 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 SyncState.AddQueryToQueue("zCommand Phonebook List Offset: 0 Limit: 512"); SyncState.AddQueryToQueue("zCommand Bookings List"); SyncState.StartSync(); } /// /// Processes messages as they are dequeued /// /// void ProcessMessage(string message) { // Counts the curly braces 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; } else 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); // switch to json format SendText("format json"); }); break; } } } } /// /// Deserializes a JSON formatted response /// /// void DeserializeResponse(string response) { try { var trimmedResponse = response.Trim(); if (trimmedResponse.Length <= 0) return; var message = JObject.Parse(trimmedResponse); eZoomRoomResponseType 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 "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 = new CodecDirectory(); directoryResults = zStatus.Phonebook.ConvertZoomContactsToGeneric(Status.Phonebook.Contacts); DirectoryRoot = directoryResults; OnDirectoryResultReturned(directoryResults); break; } case "listparticipantsresult": { Debug.Console(1, this, "JTokenType: {0}", responseObj.Type); if (responseObj.Type == JTokenType.Array) { // if the type is array this must be the complete list Status.Call.Participants = JsonConvert.DeserializeObject>(responseObj.ToString()); } else if (responseObj.Type == JTokenType.Object) { // this is a single participant event notification var participant = JsonConvert.DeserializeObject(responseObj.ToString()); if (participant != null) { if (participant.Event == "ZRCUserChangedEventLeftMeeting" || participant.Event == "ZRCUserChangedEventUserInfoUpdated") { var existingParticipant = Status.Call.Participants.FirstOrDefault(p => p.UserId.Equals(participant.UserId)); if (existingParticipant != null) { if (participant.Event == "ZRCUserChangedEventLeftMeeting") { // Remove participant Status.Call.Participants.Remove(existingParticipant); } else if (participant.Event == "ZRCUserChangedEventUserInfoUpdated") { // Update participant JsonConvert.PopulateObject(responseObj.ToString(), existingParticipant); } } } else if(participant.Event == "ZRCUserChangedEventJoinedMeeting") { Status.Call.Participants.Add(participant); } } } 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 newContact = JsonConvert.DeserializeObject(responseObj["Updated Contact"].ToString()); // Add a new contact Status.Phonebook.Contacts.Add(newContact); } break; } case "bookingslistresult": { if (!SyncState.InitialSyncComplete) SyncState.LastQueryResponseReceived(); var codecBookings = new List(); codecBookings = JsonConvert.DeserializeObject < List>(responseObj.ToString()); if (codecBookings != null && codecBookings.Count > 0) { CodecSchedule.Meetings = zCommand.GetGenericMeetingsFromBookingResult(codecBookings); } break; } case "bookings": { // Bookings have been updated, trigger a query to retreive the new bookings if (responseObj["Updated"] != null) GetBookings(); break; } case "sharingstate": { JsonConvert.PopulateObject(responseObj.ToString(), Status.Call.Sharing); break; } case "incomingcallindication": { var incomingCall = JsonConvert.DeserializeObject(responseObj.ToString()); if (incomingCall != null) { var newCall = new CodecActiveCallItem(); newCall.Direction = eCodecCallDirection.Incoming; newCall.Status = eCodecCallStatus.Ringing; newCall.Type = eCodecCallType.Unknown; newCall.Name = incomingCall.callerName; newCall.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) { if (!incomingCall.accepted) { existingCall.Status = eCodecCallStatus.Disconnected; } else { existingCall.Status = 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); } } } 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; } 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); } } public void PrintCurrentCallParticipants() { if (Debug.Level > 0) { Debug.Console(1, this, "****************************Call Participants***************************"); foreach (var participant in Status.Call.Participants) { Debug.Console(1, this, "Name: {0} Audio: {1} IsHost: {2}", participant.UserName, participant.AudioStatusState, participant.IsHost); } Debug.Console(1, this, "************************************************************************"); } } /// /// Retrieves bookings list /// void GetBookings() { SendText("zCommand Bookings List"); } /// /// Updates the current call status /// void UpdateCallStatus() { zStatus.eCallStatus callStatus; if (Status.Call != null) { 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.Call.Status = callStatus; // set the status after initializing the object } if (ActiveCalls.Count == 0) { if(callStatus == zStatus.eCallStatus.CONNECTING_MEETING) { var newCall = new CodecActiveCallItem(); newCall.Status = eCodecCallStatus.Connecting; ActiveCalls.Add(newCall); OnCallStatusChange(newCall); } } else { var existingCall = ActiveCalls.FirstOrDefault(c => !c.Status.Equals(eCodecCallStatus.Ringing)); if (callStatus == zStatus.eCallStatus.IN_MEETING) { existingCall.Status = eCodecCallStatus.Connected; } else if (callStatus == zStatus.eCallStatus.NOT_IN_MEETING) { existingCall.Status = eCodecCallStatus.Disconnected; } 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, "**************************************************************************"); } public override void StartSharing() { throw new NotImplementedException(); } /// /// 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 increment 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 ExecuteSwitch(object selector) { (selector as Action)(); } public void ExecuteSwitch(object inputSelector, object outputSelector, eRoutingSignalType signalType) { ExecuteSwitch(inputSelector); } public override void AcceptCall(CodecActiveCallItem call) { var incomingCall = ActiveCalls.FirstOrDefault(c => c.Status.Equals(eCodecCallStatus.Ringing) && c.Direction.Equals(eCodecCallDirection.Incoming)); SendText(string.Format("zCommand Call Accept callerJID: {0}", incomingCall.Id)); } public override void RejectCall(CodecActiveCallItem call) { var incomingCall = ActiveCalls.FirstOrDefault(c => c.Status.Equals(eCodecCallStatus.Ringing) && c.Direction.Equals(eCodecCallDirection.Incoming)); SendText(string.Format("zCommand Call Reject callerJID: {0}", incomingCall.Id)); } 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) { throw new NotImplementedException(); } #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 IHasDirectory Members public event EventHandler DirectoryResultReturned; /// Call when directory results are updated /// /// 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); } public CodecDirectory DirectoryRoot { get; private set; } public CodecDirectory CurrentDirectoryResult { get { if (DirectoryBrowseHistory.Count > 0) return DirectoryBrowseHistory[DirectoryBrowseHistory.Count - 1]; else return DirectoryRoot; } } 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)); DirectoryBrowseHistory.Add(directoryResults); OnDirectoryResultReturned(directoryResults); } public void GetDirectoryFolderContents(string folderId) { var directoryResults = new CodecDirectory(); directoryResults.ResultsFolderId = folderId; directoryResults.AddContactsToDirectory(DirectoryRoot.CurrentDirectoryResults.FindAll(c => c.FolderId.Equals(folderId))); DirectoryBrowseHistory.Add(directoryResults); OnDirectoryResultReturned(directoryResults); } public void SetCurrentDirectoryToRoot() { DirectoryBrowseHistory.Clear(); OnDirectoryResultReturned(DirectoryRoot); } public void GetDirectoryParentFolderContents() { var currentDirectory = new CodecDirectory(); if (DirectoryBrowseHistory.Count > 0) { var lastItemIndex = DirectoryBrowseHistory.Count - 1; var parentDirectoryContents = DirectoryBrowseHistory[lastItemIndex]; DirectoryBrowseHistory.Remove(DirectoryBrowseHistory[lastItemIndex]); currentDirectory = parentDirectoryContents; } else { currentDirectory = DirectoryRoot; } OnDirectoryResultReturned(currentDirectory); } public BoolFeedback CurrentDirectoryResultIsNotDirectoryRoot { get; private set; } public List DirectoryBrowseHistory { get; private set; } #endregion #region IHasScheduleAwareness Members public CodecScheduleAwareness CodecSchedule { get; private set; } public void GetSchedule() { GetBookings(); } #endregion /// /// Builds the cameras List by using the Zoom Room zStatus.Cameras data. Could later be modified to build from config data /// 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. /// void UpdateFarEndCameras() { // TODO: set up far end cameras for the current call } #region IHasCameras Members public event EventHandler CameraSelected; public List Cameras { get; private set; } private CameraBase _selectedCamera; 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) { 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); } } #endregion #region IHasFarEndCameraControl Members public CameraBase FarEndCamera { get; private set; } public BoolFeedback ControllingFarEndCameraFeedback { get; private set; } #endregion } /// /// Zoom Room specific info object /// public class ZoomRoomInfo : VideoCodecInfo { 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; else 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; else return string.Empty; } } public override string IpAddress { get { if (!string.IsNullOrEmpty(Status.SystemUnit.RoomInfo.AccountEmail)) return Status.SystemUnit.RoomInfo.AccountEmail; else 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; else 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; else return string.Empty; } } public ZoomRoomInfo(ZoomRoomStatus status, ZoomRoomConfiguration configuration) { Status = status; Configuration = configuration; } } /// /// Tracks the initial sycnronization state when establishing a new connection /// public class ZoomRoomSyncState : IKeyed { bool _InitialSyncComplete; public event EventHandler InitialSyncCompleted; private CrestronQueue SyncQueries; private ZoomRoom Parent; public string Key { get; private set; } public bool InitialSyncComplete { get { return _InitialSyncComplete; } private set { if (value == true) { 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;} public ZoomRoomSyncState(string key, ZoomRoom parent) { Parent = parent; Key = key; SyncQueries = new CrestronQueue(50); CodecDisconnected(); } public void StartSync() { DequeueQueries(); } 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; } 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 VideoCodec.ZoomRoom.ZoomRoom(dc, comm); } } }