Files
Neil Dorin 1ae6069ac2 Adds USB routing to DmChassisController and DmChassisControllerBridge.
Major update to remove eRoutingSignalType.AudioVideo in favor of bitmasked values and adding UsbOutput and UsbInput types.  Updated all affected routing ports and ExecuteSwitch method calls.
Need to review and test routing to ensure bitwise operators are all correct.
2019-08-15 14:07:10 -06:00

1679 lines
61 KiB
C#

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.Devices.Common.Occupancy;
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<string> 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;
/// <summary>
/// Gets and returns the scaled volume of the codec
/// </summary>
protected override Func<int> VolumeLevelFeedbackFunc
{
get
{
return () => CrestronEnvironment.ScaleWithLimits(Configuration.Audio.Output.Volume, 100, 0, 65535, 0);
}
}
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 () => false;
}
}
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<string> SelfviewPipPositionFeedbackFunc
{
get
{
return () => "";
}
}
protected Func<string> LocalLayoutFeedbackFunc
{
get
{
return () => "";
}
}
protected Func<bool> 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<ZoomRoomPropertiesConfig>(config.Properties.ToString());
// The queue that will collect the repsonses in the order they are received
ReceiveQueue = new CrestronQueue<string>(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<EventArgs>(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<CameraBase>();
SetUpDirectory();
}
void SyncState_InitialSyncCompleted(object sender, EventArgs e)
{
SetUpRouting();
SetIsReady();
}
/// <summary>
/// Subscribes to the PropertyChanged events on the state objects and fires the corresponding feedbacks.
/// </summary>
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<CodecDirectory>();
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);
}
/// <summary>
/// Creates the fake OSD source, and connects it's AudioVideo output to the CodecOsdIn input
/// to enable routing
/// </summary>
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);
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<GenericSocketStatusChageEventArgs>(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);
}
/// <summary>
/// Gathers responses and enqueues them.
/// </summary>
/// <param name="dev"></param>
/// <param name="args"></param>
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();
}
/// <summary>
/// Runs in it's own thread to dequeue messages in the order they were received to be processed
/// </summary>
/// <returns></returns>
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;
}
/// <summary>
/// Queues the initial queries to be sent upon connection
/// </summary>
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();
}
/// <summary>
/// Processes messages as they are dequeued
/// </summary>
/// <param name="message"></param>
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;
}
}
}
}
/// <summary>
/// Deserializes a JSON formatted response
/// </summary>
/// <param name="response"></param>
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<string>(), true);
var topKey = message["topKey"].Value<string>();
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<List<zCommand.ListParticipant>>(responseObj.ToString());
}
else if (responseObj.Type == JTokenType.Object)
{
// this is a single participant event notification
var participant = JsonConvert.DeserializeObject<zCommand.ListParticipant>(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<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 newContact = JsonConvert.DeserializeObject<zStatus.Contact>(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<zCommand.BookingsListResult>();
codecBookings = JsonConvert.DeserializeObject < List<zCommand.BookingsListResult>>(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<zEvent.IncomingCallIndication>(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<zEvent.IncomingCallIndication>(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<zEvent.CallDisconnect>(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<zEvent.NeedWaitForHost>(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, "************************************************************************");
}
}
/// <summary>
/// Retrieves bookings list
/// </summary>
void GetBookings()
{
SendText("zCommand Bookings List");
}
/// <summary>
/// Updates the current call status
/// </summary>
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();
}
/// <summary>
/// Stops sharing the current presentation
/// </summary>
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();
}
/// <summary>
/// Increments the voluem
/// </summary>
/// <param name="pressRelease"></param>
public override void VolumeUp(bool pressRelease)
{
// TODO: Implment volume increment 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 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));
}
/// <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 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<DirectoryEventArgs> DirectoryResultReturned;
/// Call when directory results are updated
/// </summary>
/// <param name="result"></param>
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<CodecDirectory> DirectoryBrowseHistory { get; private set; }
#endregion
#region IHasScheduleAwareness Members
public CodecScheduleAwareness CodecSchedule { get; private set; }
public void GetSchedule()
{
GetBookings();
}
#endregion
/// <summary>
/// Builds the cameras List by using the Zoom Room zStatus.Cameras data. Could later be modified to build from config data
/// </summary>
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();
}
/// <summary>
/// Dynamically creates far end cameras for call participants who have far end control enabled.
/// </summary>
void UpdateFarEndCameras()
{
// TODO: set up far end cameras for the current call
}
#region IHasCameras Members
public event EventHandler<CameraSelectedEventArgs> CameraSelected;
public List<CameraBase> 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
}
/// <summary>
/// Zoom Room specific info object
/// </summary>
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;
}
}
/// <summary>
/// Tracks the initial sycnronization state when establishing a new connection
/// </summary>
public class ZoomRoomSyncState : IKeyed
{
bool _InitialSyncComplete;
public event EventHandler<EventArgs> InitialSyncCompleted;
private CrestronQueue<string> 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<string>(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;
}
}
}