Files
2020-07-01 19:30:45 -05:00

1695 lines
62 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.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<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;
}
}
public class ZoomRoomFactory : EssentialsDeviceFactory<ZoomRoom>
{
public ZoomRoomFactory()
{
TypeNames = new List<string>() { "zoomroom" };
}
public override EssentialsDevice BuildDevice(DeviceConfig dc)
{
Debug.Console(1, "Factory Attempting to create new ZoomRoom Device");
var comm = CommFactory.CreateCommForDevice(dc);
return new VideoCodec.ZoomRoom.ZoomRoom(dc, comm);
}
}
}