mirror of
https://github.com/PepperDash/Essentials.git
synced 2026-02-11 02:35:00 +00:00
2085 lines
73 KiB
C#
2085 lines
73 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Text;
|
|
using Crestron.SimplSharp;
|
|
using Crestron.SimplSharpPro.CrestronThread;
|
|
using Crestron.SimplSharpPro.DeviceSupport;
|
|
using Newtonsoft.Json;
|
|
using Newtonsoft.Json.Linq;
|
|
using PepperDash.Core;
|
|
using PepperDash.Essentials.Core;
|
|
using PepperDash.Essentials.Core.Bridges;
|
|
using PepperDash.Essentials.Core.Config;
|
|
using PepperDash.Essentials.Core.DeviceTypeInterfaces;
|
|
using PepperDash.Essentials.Core.Routing;
|
|
using PepperDash.Essentials.Devices.Common.Cameras;
|
|
using PepperDash.Essentials.Devices.Common.Codec;
|
|
using PepperDash.Essentials.Devices.Common.VideoCodec.Cisco;
|
|
using PepperDash.Essentials.Devices.Common.VideoCodec.Interfaces;
|
|
using PepperDash_Essentials_Core.DeviceTypeInterfaces;
|
|
|
|
namespace PepperDash.Essentials.Devices.Common.VideoCodec.ZoomRoom
|
|
{
|
|
public class ZoomRoom : VideoCodecBase, IHasCodecSelfView, IHasDirectoryHistoryStack, ICommunicationMonitor,
|
|
IRouting,
|
|
IHasScheduleAwareness, IHasCodecCameras, IHasParticipants, IHasCameraOff, IHasCameraAutoMode,
|
|
IHasFarEndContentStatus, IHasSelfviewPosition, IHasPhoneDialing
|
|
{
|
|
private const long MeetingRefreshTimer = 60000;
|
|
private const uint DefaultMeetingDurationMin = 30;
|
|
private const string Delimiter = "\x0D\x0A";
|
|
private readonly CrestronQueue<string> _receiveQueue;
|
|
|
|
|
|
private readonly Thread _receiveThread;
|
|
|
|
private readonly ZoomRoomSyncState _syncState;
|
|
public bool CommDebuggingIsOn;
|
|
private CodecDirectory _currentDirectoryResult;
|
|
private uint _jsonCurlyBraceCounter;
|
|
private bool _jsonFeedbackMessageIsIncoming;
|
|
private StringBuilder _jsonMessage;
|
|
private int _previousVolumeLevel;
|
|
private CameraBase _selectedCamera;
|
|
|
|
private readonly ZoomRoomPropertiesConfig _props;
|
|
|
|
public ZoomRoom(DeviceConfig config, IBasicCommunication comm)
|
|
: base(config)
|
|
{
|
|
_props = JsonConvert.DeserializeObject<ZoomRoomPropertiesConfig>(config.Properties.ToString());
|
|
|
|
// The queue that will collect the repsonses in the order they are received
|
|
_receiveQueue = new CrestronQueue<string>(1024);
|
|
|
|
// The thread responsible for dequeuing and processing the messages
|
|
_receiveThread = new Thread(o => ProcessQueue(), null) {Priority = Thread.eThreadPriority.MediumPriority};
|
|
|
|
Communication = comm;
|
|
|
|
if (_props.CommunicationMonitorProperties != null)
|
|
{
|
|
CommunicationMonitor = new GenericCommunicationMonitor(this, Communication,
|
|
_props.CommunicationMonitorProperties);
|
|
}
|
|
else
|
|
{
|
|
CommunicationMonitor = new GenericCommunicationMonitor(this, Communication, 30000, 120000, 300000,
|
|
"zStatus SystemUnit\r");
|
|
}
|
|
|
|
DeviceManager.AddDevice(CommunicationMonitor);
|
|
|
|
Status = new ZoomRoomStatus();
|
|
|
|
Configuration = new ZoomRoomConfiguration();
|
|
|
|
CodecInfo = new ZoomRoomInfo(Status, Configuration);
|
|
|
|
_syncState = new ZoomRoomSyncState(Key + "--Sync", this);
|
|
|
|
_syncState.InitialSyncCompleted += SyncState_InitialSyncCompleted;
|
|
|
|
PhonebookSyncState = new CodecPhonebookSyncState(Key + "--PhonebookSync");
|
|
|
|
PortGather = new CommunicationGather(Communication, "\x0A") {IncludeDelimiter = true};
|
|
PortGather.LineReceived += Port_LineReceived;
|
|
|
|
CodecOsdIn = new RoutingInputPort(RoutingPortNames.CodecOsd,
|
|
eRoutingSignalType.Audio | eRoutingSignalType.Video,
|
|
eRoutingPortConnectionType.Hdmi, new Action(StopSharing), this);
|
|
|
|
Output1 = new RoutingOutputPort(RoutingPortNames.AnyVideoOut,
|
|
eRoutingSignalType.Audio | eRoutingSignalType.Video,
|
|
eRoutingPortConnectionType.Hdmi, null, this);
|
|
|
|
SelfviewIsOnFeedback = new BoolFeedback(SelfViewIsOnFeedbackFunc);
|
|
|
|
CameraIsOffFeedback = new BoolFeedback(CameraIsOffFeedbackFunc);
|
|
|
|
CameraAutoModeIsOnFeedback = new BoolFeedback(CameraAutoModeIsOnFeedbackFunc);
|
|
|
|
CodecSchedule = new CodecScheduleAwareness(MeetingRefreshTimer);
|
|
|
|
ReceivingContent = new BoolFeedback(FarEndIsSharingContentFeedbackFunc);
|
|
|
|
SelfviewPipPositionFeedback = new StringFeedback(SelfviewPipPositionFeedbackFunc);
|
|
|
|
SetUpFeedbackActions();
|
|
|
|
Cameras = new List<CameraBase>();
|
|
|
|
SetUpDirectory();
|
|
|
|
Participants = new CodecParticipants();
|
|
|
|
SupportsCameraOff = _props.SupportsCameraOff;
|
|
SupportsCameraAutoMode = _props.SupportsCameraAutoMode;
|
|
|
|
PhoneOffHookFeedback = new BoolFeedback(PhoneOffHookFeedbackFunc);
|
|
CallerIdNameFeedback = new StringFeedback(CallerIdNameFeedbackFunc);
|
|
CallerIdNumberFeedback = new StringFeedback(CallerIdNumberFeedbackFunc);
|
|
}
|
|
|
|
public CommunicationGather PortGather { get; private set; }
|
|
|
|
public ZoomRoomStatus Status { get; private set; }
|
|
|
|
public ZoomRoomConfiguration Configuration { get; private set; }
|
|
|
|
//CTimer LoginMessageReceivedTimer;
|
|
//CTimer RetryConnectionTimer;
|
|
|
|
/// <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 () => Status.Call.Sharing.State == zEvent.eSharingState.Receiving; }
|
|
}
|
|
|
|
protected override Func<bool> MuteFeedbackFunc
|
|
{
|
|
get { return () => Configuration.Audio.Output.Volume == 0; }
|
|
}
|
|
|
|
//protected Func<bool> RoomIsOccupiedFeedbackFunc
|
|
//{
|
|
// get
|
|
// {
|
|
// return () => false;
|
|
// }
|
|
//}
|
|
|
|
//protected Func<int> PeopleCountFeedbackFunc
|
|
//{
|
|
// get
|
|
// {
|
|
// return () => 0;
|
|
// }
|
|
//}
|
|
|
|
protected Func<bool> SelfViewIsOnFeedbackFunc
|
|
{
|
|
get { return () => !Configuration.Video.HideConfSelfVideo; }
|
|
}
|
|
|
|
protected Func<bool> CameraIsOffFeedbackFunc
|
|
{
|
|
get { return () => Configuration.Call.Camera.Mute; }
|
|
}
|
|
|
|
protected Func<bool> CameraAutoModeIsOnFeedbackFunc
|
|
{
|
|
get { return () => false; }
|
|
}
|
|
|
|
protected Func<string> SelfviewPipPositionFeedbackFunc
|
|
{
|
|
get { return () => _currentSelfviewPipPosition.Command; }
|
|
}
|
|
|
|
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; }
|
|
|
|
#region ICommunicationMonitor Members
|
|
|
|
public StatusMonitorBase CommunicationMonitor { get; private set; }
|
|
|
|
#endregion
|
|
|
|
#region IHasCodecCameras Members
|
|
|
|
public event EventHandler<CameraSelectedEventArgs> CameraSelected;
|
|
|
|
public List<CameraBase> Cameras { get; private set; }
|
|
|
|
public CameraBase SelectedCamera
|
|
{
|
|
get { return _selectedCamera; }
|
|
private set
|
|
{
|
|
_selectedCamera = value;
|
|
SelectedCameraFeedback.FireUpdate();
|
|
ControllingFarEndCameraFeedback.FireUpdate();
|
|
|
|
var handler = CameraSelected;
|
|
if (handler != null)
|
|
{
|
|
handler(this, new CameraSelectedEventArgs(SelectedCamera));
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
public StringFeedback SelectedCameraFeedback { get; private set; }
|
|
|
|
public void SelectCamera(string key)
|
|
{
|
|
if (Cameras == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var camera = Cameras.FirstOrDefault(c => c.Key.IndexOf(key, StringComparison.OrdinalIgnoreCase) > -1);
|
|
if (camera != null)
|
|
{
|
|
Debug.Console(1, this, "Selected Camera with key: '{0}'", camera.Key);
|
|
SelectedCamera = camera;
|
|
}
|
|
else
|
|
{
|
|
Debug.Console(1, this, "Unable to select camera with key: '{0}'", key);
|
|
}
|
|
}
|
|
|
|
public CameraBase FarEndCamera { get; private set; }
|
|
|
|
public BoolFeedback ControllingFarEndCameraFeedback { get; private set; }
|
|
|
|
#endregion
|
|
|
|
#region IHasCodecSelfView Members
|
|
|
|
public BoolFeedback SelfviewIsOnFeedback { get; private set; }
|
|
|
|
public void SelfViewModeOn()
|
|
{
|
|
SendText("zConfiguration Video hide_conf_self_video: off");
|
|
}
|
|
|
|
public void SelfViewModeOff()
|
|
{
|
|
SendText("zConfiguration Video hide_conf_self_video: on");
|
|
}
|
|
|
|
public void SelfViewModeToggle()
|
|
{
|
|
if (SelfviewIsOnFeedback.BoolValue)
|
|
{
|
|
SelfViewModeOff();
|
|
}
|
|
else
|
|
{
|
|
SelfViewModeOn();
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region IHasDirectoryHistoryStack Members
|
|
|
|
public event EventHandler<DirectoryEventArgs> DirectoryResultReturned;
|
|
public CodecDirectory DirectoryRoot { get; private set; }
|
|
|
|
public CodecDirectory CurrentDirectoryResult
|
|
{
|
|
get { return _currentDirectoryResult; }
|
|
}
|
|
|
|
public CodecPhonebookSyncState PhonebookSyncState { get; private set; }
|
|
|
|
public void SearchDirectory(string searchString)
|
|
{
|
|
var directoryResults = new CodecDirectory();
|
|
|
|
directoryResults.AddContactsToDirectory(
|
|
DirectoryRoot.CurrentDirectoryResults.FindAll(
|
|
c => c.Name.IndexOf(searchString, 0, StringComparison.OrdinalIgnoreCase) > -1));
|
|
|
|
DirectoryBrowseHistoryStack.Clear();
|
|
_currentDirectoryResult = directoryResults;
|
|
|
|
OnDirectoryResultReturned(directoryResults);
|
|
}
|
|
|
|
public void GetDirectoryFolderContents(string folderId)
|
|
{
|
|
var directoryResults = new CodecDirectory {ResultsFolderId = folderId};
|
|
|
|
directoryResults.AddContactsToDirectory(
|
|
DirectoryRoot.CurrentDirectoryResults.FindAll(c => c.ParentFolderId.Equals(folderId)));
|
|
|
|
DirectoryBrowseHistoryStack.Push(_currentDirectoryResult);
|
|
|
|
_currentDirectoryResult = directoryResults;
|
|
|
|
OnDirectoryResultReturned(directoryResults);
|
|
}
|
|
|
|
public void SetCurrentDirectoryToRoot()
|
|
{
|
|
DirectoryBrowseHistoryStack.Clear();
|
|
|
|
_currentDirectoryResult = DirectoryRoot;
|
|
|
|
OnDirectoryResultReturned(DirectoryRoot);
|
|
}
|
|
|
|
public void GetDirectoryParentFolderContents()
|
|
{
|
|
if (DirectoryBrowseHistoryStack.Count == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var currentDirectory = DirectoryBrowseHistoryStack.Pop();
|
|
|
|
_currentDirectoryResult = currentDirectory;
|
|
|
|
OnDirectoryResultReturned(currentDirectory);
|
|
}
|
|
|
|
public BoolFeedback CurrentDirectoryResultIsNotDirectoryRoot { get; private set; }
|
|
|
|
public List<CodecDirectory> DirectoryBrowseHistory { get; private set; }
|
|
|
|
public Stack<CodecDirectory> DirectoryBrowseHistoryStack { get; private set; }
|
|
|
|
#endregion
|
|
|
|
#region IHasScheduleAwareness Members
|
|
|
|
public CodecScheduleAwareness CodecSchedule { get; private set; }
|
|
|
|
public void GetSchedule()
|
|
{
|
|
GetBookings();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region IRouting Members
|
|
|
|
public void ExecuteSwitch(object inputSelector, object outputSelector, eRoutingSignalType signalType)
|
|
{
|
|
ExecuteSwitch(inputSelector);
|
|
}
|
|
|
|
#endregion
|
|
|
|
private void SyncState_InitialSyncCompleted(object sender, EventArgs e)
|
|
{
|
|
SetUpRouting();
|
|
|
|
SetIsReady();
|
|
}
|
|
|
|
private void SetUpCallFeedbackActions()
|
|
{
|
|
Status.Call.Sharing.PropertyChanged += (o, a) =>
|
|
{
|
|
if (a.PropertyName == "State")
|
|
{
|
|
SharingContentIsOnFeedback.FireUpdate();
|
|
ReceivingContent.FireUpdate();
|
|
}
|
|
};
|
|
|
|
Status.Call.PropertyChanged += (o, a) =>
|
|
{
|
|
if (a.PropertyName == "Info")
|
|
{
|
|
Debug.Console(1, this, "Updating Call Status");
|
|
UpdateCallStatus();
|
|
}
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Subscribes to the PropertyChanged events on the state objects and fires the corresponding feedbacks.
|
|
/// </summary>
|
|
private void SetUpFeedbackActions()
|
|
{
|
|
Configuration.Audio.Output.PropertyChanged += (o, a) =>
|
|
{
|
|
if (a.PropertyName == "Volume")
|
|
{
|
|
VolumeLevelFeedback.FireUpdate();
|
|
MuteFeedback.FireUpdate();
|
|
}
|
|
};
|
|
|
|
Configuration.Call.Microphone.PropertyChanged += (o, a) =>
|
|
{
|
|
if (a.PropertyName == "Mute")
|
|
{
|
|
PrivacyModeIsOnFeedback.FireUpdate();
|
|
}
|
|
};
|
|
|
|
Configuration.Video.PropertyChanged += (o, a) =>
|
|
{
|
|
if (a.PropertyName == "HideConfSelfVideo")
|
|
{
|
|
SelfviewIsOnFeedback.FireUpdate();
|
|
}
|
|
};
|
|
Configuration.Video.Camera.PropertyChanged += (o, a) =>
|
|
{
|
|
if (a.PropertyName == "SelectedId")
|
|
{
|
|
SelectCamera(Configuration.Video.Camera.SelectedId);
|
|
// this will in turn fire the affected feedbacks
|
|
}
|
|
};
|
|
|
|
Configuration.Call.Camera.PropertyChanged += (o, a) =>
|
|
{
|
|
Debug.Console(1, this, "Configuration.Call.Camera.PropertyChanged: {0}", a.PropertyName);
|
|
|
|
if (a.PropertyName != "Mute") return;
|
|
|
|
CameraIsOffFeedback.FireUpdate();
|
|
CameraAutoModeIsOnFeedback.FireUpdate();
|
|
};
|
|
|
|
Configuration.Call.Layout.PropertyChanged += (o, a) =>
|
|
{
|
|
if (a.PropertyName != "Position") return;
|
|
|
|
ComputeSelfviewPipStatus();
|
|
|
|
SelfviewPipPositionFeedback.FireUpdate();
|
|
};
|
|
|
|
Status.Call.Sharing.PropertyChanged += (o, a) =>
|
|
{
|
|
if (a.PropertyName == "State")
|
|
{
|
|
SharingContentIsOnFeedback.FireUpdate();
|
|
ReceivingContent.FireUpdate();
|
|
}
|
|
};
|
|
|
|
Status.Call.PropertyChanged += (o, a) =>
|
|
{
|
|
if (a.PropertyName == "Info")
|
|
{
|
|
Debug.Console(1, this, "Updating Call Status");
|
|
UpdateCallStatus();
|
|
}
|
|
};
|
|
|
|
Status.Sharing.PropertyChanged += (o, a) =>
|
|
{
|
|
switch (a.PropertyName)
|
|
{
|
|
case "dispState":
|
|
SharingSourceFeedback.FireUpdate();
|
|
break;
|
|
case "password":
|
|
break;
|
|
}
|
|
};
|
|
|
|
Status.PhoneCall.PropertyChanged += (o, a) =>
|
|
{
|
|
switch (a.PropertyName)
|
|
{
|
|
case "IsIncomingCall":
|
|
Debug.Console(1, this, "Incoming Phone Call: {0}", Status.PhoneCall.IsIncomingCall);
|
|
break;
|
|
case "PeerDisplayName":
|
|
Debug.Console(1, this, "Peer Display Name: {0}", Status.PhoneCall.PeerDisplayName);
|
|
CallerIdNameFeedback.FireUpdate();
|
|
break;
|
|
case "PeerNumber":
|
|
Debug.Console(1, this, "Peer Number: {0}", Status.PhoneCall.PeerNumber);
|
|
CallerIdNumberFeedback.FireUpdate();
|
|
break;
|
|
case "OffHook":
|
|
Debug.Console(1, this, "Phone is OffHook: {0}", Status.PhoneCall.OffHook);
|
|
PhoneOffHookFeedback.FireUpdate();
|
|
break;
|
|
}
|
|
};
|
|
}
|
|
|
|
private void SetUpDirectory()
|
|
{
|
|
DirectoryRoot = new CodecDirectory();
|
|
|
|
DirectoryBrowseHistory = new List<CodecDirectory>();
|
|
DirectoryBrowseHistoryStack = new Stack<CodecDirectory>();
|
|
|
|
CurrentDirectoryResultIsNotDirectoryRoot = new BoolFeedback(() => _currentDirectoryResult != DirectoryRoot);
|
|
|
|
CurrentDirectoryResultIsNotDirectoryRoot.FireUpdate();
|
|
}
|
|
|
|
private void SetUpRouting()
|
|
{
|
|
// Set up input ports
|
|
CreateOsdSource();
|
|
InputPorts.Add(CodecOsdIn);
|
|
|
|
// Set up output ports
|
|
OutputPorts.Add(Output1);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates the fake OSD source, and connects it's AudioVideo output to the CodecOsdIn input
|
|
/// to enable routing
|
|
/// </summary>
|
|
private void CreateOsdSource()
|
|
{
|
|
OsdSource = new DummyRoutingInputsDevice(Key + "[osd]");
|
|
DeviceManager.AddDevice(OsdSource);
|
|
var tl = new TieLine(OsdSource.AudioVideoOutputPort, CodecOsdIn);
|
|
TieLineCollection.Default.Add(tl);
|
|
|
|
//foreach(var input in Status.Video.
|
|
}
|
|
|
|
/// <summary>
|
|
/// Starts the HTTP feedback server and syncronizes state of codec
|
|
/// </summary>
|
|
/// <returns></returns>
|
|
public override bool CustomActivate()
|
|
{
|
|
CrestronConsole.AddNewConsoleCommand(SetCommDebug, "SetCodecCommDebug", "0 for Off, 1 for on",
|
|
ConsoleAccessLevelEnum.AccessOperator);
|
|
if (!_props.DisablePhonebookAutoDownload)
|
|
{
|
|
CrestronConsole.AddNewConsoleCommand(s => SendText("zCommand Phonebook List Offset: 0 Limit: 512"),
|
|
"GetZoomRoomContacts", "Triggers a refresh of the codec phonebook",
|
|
ConsoleAccessLevelEnum.AccessOperator);
|
|
}
|
|
|
|
CrestronConsole.AddNewConsoleCommand(s => GetBookings(), "GetZoomRoomBookings",
|
|
"Triggers a refresh of the booking data for today", ConsoleAccessLevelEnum.AccessOperator);
|
|
|
|
var socket = Communication as ISocketStatus;
|
|
if (socket != null)
|
|
{
|
|
socket.ConnectionChange += socket_ConnectionChange;
|
|
}
|
|
|
|
CommDebuggingIsOn = false;
|
|
|
|
Communication.Connect();
|
|
|
|
CommunicationMonitor.Start();
|
|
|
|
return base.CustomActivate();
|
|
}
|
|
|
|
public void SetCommDebug(string s)
|
|
{
|
|
if (s == "1")
|
|
{
|
|
CommDebuggingIsOn = true;
|
|
Debug.Console(0, this, "Comm Debug Enabled.");
|
|
}
|
|
else
|
|
{
|
|
CommDebuggingIsOn = false;
|
|
Debug.Console(0, this, "Comm Debug Disabled.");
|
|
}
|
|
}
|
|
|
|
private void socket_ConnectionChange(object sender, GenericSocketStatusChageEventArgs e)
|
|
{
|
|
Debug.Console(1, this, "Socket status change {0}", e.Client.ClientStatus);
|
|
if (e.Client.IsConnected)
|
|
{
|
|
}
|
|
else
|
|
{
|
|
_syncState.CodecDisconnected();
|
|
PhonebookSyncState.CodecDisconnected();
|
|
}
|
|
}
|
|
|
|
public void SendText(string command)
|
|
{
|
|
if (CommDebuggingIsOn)
|
|
{
|
|
Debug.Console(1, this, "Sending: '{0}'", command);
|
|
}
|
|
|
|
Communication.SendText(command + Delimiter);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gathers responses and enqueues them.
|
|
/// </summary>
|
|
/// <param name="dev"></param>
|
|
/// <param name="args"></param>
|
|
private void Port_LineReceived(object dev, GenericCommMethodReceiveTextArgs args)
|
|
{
|
|
//if (CommDebuggingIsOn)
|
|
// Debug.Console(1, this, "Gathered: '{0}'", args.Text);
|
|
|
|
_receiveQueue.Enqueue(args.Text);
|
|
|
|
// If the receive thread has for some reason stopped, this will restart it
|
|
if (_receiveThread.ThreadState != Thread.eThreadStates.ThreadRunning)
|
|
{
|
|
_receiveThread.Start();
|
|
}
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Runs in it's own thread to dequeue messages in the order they were received to be processed
|
|
/// </summary>
|
|
/// <returns></returns>
|
|
private object ProcessQueue()
|
|
{
|
|
try
|
|
{
|
|
while (true)
|
|
{
|
|
var message = _receiveQueue.Dequeue();
|
|
|
|
ProcessMessage(message);
|
|
}
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Debug.Console(1, this, "Error Processing Queue: {0}", e);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Queues the initial queries to be sent upon connection
|
|
/// </summary>
|
|
private void SetUpSyncQueries()
|
|
{
|
|
// zStatus
|
|
_syncState.AddQueryToQueue("zStatus Call Status");
|
|
_syncState.AddQueryToQueue("zStatus Audio Input Line");
|
|
_syncState.AddQueryToQueue("zStatus Audio Output Line");
|
|
_syncState.AddQueryToQueue("zStatus Video Camera Line");
|
|
_syncState.AddQueryToQueue("zStatus Video Optimizable");
|
|
_syncState.AddQueryToQueue("zStatus Capabilities");
|
|
_syncState.AddQueryToQueue("zStatus Sharing");
|
|
_syncState.AddQueryToQueue("zStatus CameraShare");
|
|
_syncState.AddQueryToQueue("zStatus Call Layout");
|
|
_syncState.AddQueryToQueue("zStatus Call ClosedCaption Available");
|
|
_syncState.AddQueryToQueue("zStatus NumberOfScreens");
|
|
|
|
// zConfiguration
|
|
|
|
_syncState.AddQueryToQueue("zConfiguration Call Sharing optimize_video_sharing");
|
|
_syncState.AddQueryToQueue("zConfiguration Call Microphone Mute");
|
|
_syncState.AddQueryToQueue("zConfiguration Call Camera Mute");
|
|
_syncState.AddQueryToQueue("zConfiguration Audio Input SelectedId");
|
|
_syncState.AddQueryToQueue("zConfiguration Audio Input is_sap_disabled");
|
|
_syncState.AddQueryToQueue("zConfiguration Audio Input reduce_reverb");
|
|
_syncState.AddQueryToQueue("zConfiguration Audio Input volume");
|
|
_syncState.AddQueryToQueue("zConfiguration Audio Output selectedId");
|
|
_syncState.AddQueryToQueue("zConfiguration Audio Output volume");
|
|
_syncState.AddQueryToQueue("zConfiguration Video hide_conf_self_video");
|
|
_syncState.AddQueryToQueue("zConfiguration Video Camera selectedId");
|
|
_syncState.AddQueryToQueue("zConfiguration Video Camera Mirror");
|
|
_syncState.AddQueryToQueue("zConfiguration Client appVersion");
|
|
_syncState.AddQueryToQueue("zConfiguration Client deviceSystem");
|
|
_syncState.AddQueryToQueue("zConfiguration Call Layout ShareThumb");
|
|
_syncState.AddQueryToQueue("zConfiguration Call Layout Style");
|
|
_syncState.AddQueryToQueue("zConfiguration Call Layout Size");
|
|
_syncState.AddQueryToQueue("zConfiguration Call Layout Position");
|
|
_syncState.AddQueryToQueue("zConfiguration Call Lock Enable");
|
|
_syncState.AddQueryToQueue("zConfiguration Call MuteUserOnEntry Enable");
|
|
_syncState.AddQueryToQueue("zConfiguration Call ClosedCaption FontSize ");
|
|
_syncState.AddQueryToQueue("zConfiguration Call ClosedCaption Visible");
|
|
|
|
// zCommand
|
|
|
|
if (!_props.DisablePhonebookAutoDownload)
|
|
{
|
|
_syncState.AddQueryToQueue("zCommand Phonebook List Offset: 0 Limit: 512");
|
|
}
|
|
|
|
_syncState.AddQueryToQueue("zCommand Bookings List");
|
|
_syncState.AddQueryToQueue("zCommand Call ListParticipants");
|
|
_syncState.AddQueryToQueue("zCommand Call Info");
|
|
|
|
|
|
_syncState.StartSync();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Processes messages as they are dequeued
|
|
/// </summary>
|
|
/// <param name="message"></param>
|
|
private void ProcessMessage(string message)
|
|
{
|
|
// Counts the curly braces
|
|
if (message.Contains("client_loop: send disconnect: Broken pipe"))
|
|
{
|
|
Debug.Console(0, this, Debug.ErrorLogLevel.Error,
|
|
"Zoom Room Controller or App connected. Essentials will NOT control the Zoom Room until it is disconnected.");
|
|
|
|
return;
|
|
}
|
|
|
|
if (message.Contains('{'))
|
|
{
|
|
_jsonCurlyBraceCounter++;
|
|
}
|
|
|
|
if (message.Contains('}'))
|
|
{
|
|
_jsonCurlyBraceCounter--;
|
|
}
|
|
|
|
Debug.Console(2, this, "JSON Curly Brace Count: {0}", _jsonCurlyBraceCounter);
|
|
|
|
if (!_jsonFeedbackMessageIsIncoming && message.Trim('\x20') == "{" + Delimiter)
|
|
// Check for the beginning of a new JSON message
|
|
{
|
|
_jsonFeedbackMessageIsIncoming = true;
|
|
_jsonCurlyBraceCounter = 1; // reset the counter for each new message
|
|
|
|
_jsonMessage = new StringBuilder();
|
|
|
|
_jsonMessage.Append(message);
|
|
|
|
if (CommDebuggingIsOn)
|
|
{
|
|
Debug.Console(2, this, "Incoming JSON message...");
|
|
}
|
|
|
|
return;
|
|
}
|
|
if (_jsonFeedbackMessageIsIncoming && message.Trim('\x20') == "}" + Delimiter)
|
|
// Check for the end of a JSON message
|
|
{
|
|
_jsonMessage.Append(message);
|
|
|
|
if (_jsonCurlyBraceCounter == 0)
|
|
{
|
|
_jsonFeedbackMessageIsIncoming = false;
|
|
|
|
if (CommDebuggingIsOn)
|
|
{
|
|
Debug.Console(2, this, "Complete JSON Received:\n{0}", _jsonMessage.ToString());
|
|
}
|
|
|
|
// Forward the complete message to be deserialized
|
|
DeserializeResponse(_jsonMessage.ToString());
|
|
}
|
|
|
|
//JsonMessage = new StringBuilder();
|
|
return;
|
|
}
|
|
|
|
// NOTE: This must happen after the above conditions have been checked
|
|
// Append subsequent partial JSON fragments to the string builder
|
|
if (_jsonFeedbackMessageIsIncoming)
|
|
{
|
|
_jsonMessage.Append(message);
|
|
|
|
//Debug.Console(1, this, "Building JSON:\n{0}", JsonMessage.ToString());
|
|
return;
|
|
}
|
|
|
|
if (CommDebuggingIsOn)
|
|
{
|
|
Debug.Console(1, this, "Non-JSON response: '{0}'", message);
|
|
}
|
|
|
|
_jsonCurlyBraceCounter = 0; // reset on non-JSON response
|
|
|
|
if (!_syncState.InitialSyncComplete)
|
|
{
|
|
switch (message.Trim().ToLower()) // remove the whitespace
|
|
{
|
|
case "*r login successful":
|
|
{
|
|
_syncState.LoginMessageReceived();
|
|
|
|
// Fire up a thread to send the intial commands.
|
|
CrestronInvoke.BeginInvoke(o =>
|
|
{
|
|
Thread.Sleep(100);
|
|
// disable echo of commands
|
|
SendText("echo off");
|
|
Thread.Sleep(100);
|
|
// set feedback exclusions
|
|
SendText("zFeedback Register Op: ex Path: /Event/InfoResult/info/callin_country_list");
|
|
Thread.Sleep(100);
|
|
SendText("zFeedback Register Op: ex Path: /Event/InfoResult/info/callout_country_list");
|
|
Thread.Sleep(100);
|
|
|
|
if (!_props.DisablePhonebookAutoDownload)
|
|
{
|
|
SendText("zFeedback Register Op: ex Path: /Event/Phonebook/AddedContact");
|
|
}
|
|
// switch to json format
|
|
SendText("format json");
|
|
});
|
|
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Deserializes a JSON formatted response
|
|
/// </summary>
|
|
/// <param name="response"></param>
|
|
private void DeserializeResponse(string response)
|
|
{
|
|
try
|
|
{
|
|
var trimmedResponse = response.Trim();
|
|
|
|
if (trimmedResponse.Length <= 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var message = JObject.Parse(trimmedResponse);
|
|
|
|
var eType =
|
|
(eZoomRoomResponseType)
|
|
Enum.Parse(typeof (eZoomRoomResponseType), message["type"].Value<string>(), true);
|
|
|
|
var topKey = message["topKey"].Value<string>();
|
|
|
|
var responseObj = message[topKey];
|
|
|
|
Debug.Console(1, "{0} Response Received. topKey: '{1}'\n{2}", eType, topKey, responseObj.ToString());
|
|
|
|
switch (eType)
|
|
{
|
|
case eZoomRoomResponseType.zConfiguration:
|
|
{
|
|
switch (topKey.ToLower())
|
|
{
|
|
case "call":
|
|
{
|
|
JsonConvert.PopulateObject(responseObj.ToString(), Configuration.Call);
|
|
|
|
break;
|
|
}
|
|
case "audio":
|
|
{
|
|
JsonConvert.PopulateObject(responseObj.ToString(), Configuration.Audio);
|
|
|
|
break;
|
|
}
|
|
case "video":
|
|
{
|
|
JsonConvert.PopulateObject(responseObj.ToString(), Configuration.Video);
|
|
|
|
break;
|
|
}
|
|
case "client":
|
|
{
|
|
JsonConvert.PopulateObject(responseObj.ToString(), Configuration.Client);
|
|
|
|
break;
|
|
}
|
|
default:
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case eZoomRoomResponseType.zCommand:
|
|
{
|
|
switch (topKey.ToLower())
|
|
{
|
|
case "inforesult":
|
|
{
|
|
JsonConvert.PopulateObject(responseObj.ToString(), Status.Call.Info);
|
|
break;
|
|
}
|
|
case "phonebooklistresult":
|
|
{
|
|
JsonConvert.PopulateObject(responseObj.ToString(), Status.Phonebook);
|
|
|
|
if (!PhonebookSyncState.InitialSyncComplete)
|
|
{
|
|
PhonebookSyncState.InitialPhonebookFoldersReceived();
|
|
PhonebookSyncState.PhonebookRootEntriesReceived();
|
|
PhonebookSyncState.SetPhonebookHasFolders(false);
|
|
PhonebookSyncState.SetNumberOfContacts(Status.Phonebook.Contacts.Count);
|
|
}
|
|
|
|
var directoryResults =
|
|
zStatus.Phonebook.ConvertZoomContactsToGeneric(Status.Phonebook.Contacts);
|
|
|
|
DirectoryRoot = directoryResults;
|
|
|
|
_currentDirectoryResult = DirectoryRoot;
|
|
|
|
OnDirectoryResultReturned(directoryResults);
|
|
|
|
break;
|
|
}
|
|
case "listparticipantsresult":
|
|
{
|
|
Debug.Console(1, this, "JTokenType: {0}", responseObj.Type);
|
|
|
|
switch (responseObj.Type)
|
|
{
|
|
case JTokenType.Array:
|
|
Status.Call.Participants =
|
|
JsonConvert.DeserializeObject<List<zCommand.ListParticipant>>(
|
|
responseObj.ToString());
|
|
break;
|
|
case JTokenType.Object:
|
|
{
|
|
// this is a single participant event notification
|
|
|
|
var participant =
|
|
JsonConvert.DeserializeObject<zCommand.ListParticipant>(
|
|
responseObj.ToString());
|
|
|
|
if (participant != null)
|
|
{
|
|
switch (participant.Event)
|
|
{
|
|
case "ZRCUserChangedEventUserInfoUpdated":
|
|
case "ZRCUserChangedEventLeftMeeting":
|
|
{
|
|
var existingParticipant =
|
|
Status.Call.Participants.FirstOrDefault(
|
|
p => p.UserId.Equals(participant.UserId));
|
|
|
|
if (existingParticipant != null)
|
|
{
|
|
switch (participant.Event)
|
|
{
|
|
case "ZRCUserChangedEventLeftMeeting":
|
|
Status.Call.Participants.Remove(existingParticipant);
|
|
break;
|
|
case "ZRCUserChangedEventUserInfoUpdated":
|
|
JsonConvert.PopulateObject(responseObj.ToString(),
|
|
existingParticipant);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
case "ZRCUserChangedEventJoinedMeeting":
|
|
Status.Call.Participants.Add(participant);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
|
|
var participants =
|
|
zCommand.ListParticipant.GetGenericParticipantListFromParticipantsResult(
|
|
Status.Call.Participants);
|
|
|
|
Participants.CurrentParticipants = participants;
|
|
|
|
PrintCurrentCallParticipants();
|
|
|
|
break;
|
|
}
|
|
default:
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case eZoomRoomResponseType.zEvent:
|
|
{
|
|
switch (topKey.ToLower())
|
|
{
|
|
case "phonebook":
|
|
{
|
|
if (responseObj["Updated Contact"] != null)
|
|
{
|
|
var updatedContact =
|
|
JsonConvert.DeserializeObject<zStatus.Contact>(
|
|
responseObj["Updated Contact"].ToString());
|
|
|
|
var existingContact =
|
|
Status.Phonebook.Contacts.FirstOrDefault(c => c.Jid.Equals(updatedContact.Jid));
|
|
|
|
if (existingContact != null)
|
|
{
|
|
// Update existing contact
|
|
JsonConvert.PopulateObject(responseObj["Updated Contact"].ToString(),
|
|
existingContact);
|
|
}
|
|
}
|
|
else if (responseObj["Added Contact"] != null)
|
|
{
|
|
var jToken = responseObj["Updated Contact"];
|
|
if (jToken != null)
|
|
{
|
|
var newContact =
|
|
JsonConvert.DeserializeObject<zStatus.Contact>(
|
|
jToken.ToString());
|
|
|
|
// Add a new contact
|
|
Status.Phonebook.Contacts.Add(newContact);
|
|
}
|
|
}
|
|
|
|
break;
|
|
}
|
|
case "bookingslistresult":
|
|
{
|
|
if (!_syncState.InitialSyncComplete)
|
|
{
|
|
_syncState.LastQueryResponseReceived();
|
|
}
|
|
|
|
var codecBookings = JsonConvert.DeserializeObject<List<zCommand.BookingsListResult>>(
|
|
responseObj.ToString());
|
|
|
|
if (codecBookings != null && codecBookings.Count > 0)
|
|
{
|
|
CodecSchedule.Meetings = zCommand.GetGenericMeetingsFromBookingResult(
|
|
codecBookings, CodecSchedule.MeetingWarningMinutes);
|
|
}
|
|
|
|
break;
|
|
}
|
|
case "bookings updated":
|
|
{
|
|
GetBookings();
|
|
|
|
break;
|
|
}
|
|
case "sharingstate":
|
|
{
|
|
JsonConvert.PopulateObject(responseObj.ToString(), Status.Call.Sharing);
|
|
|
|
SetLayout();
|
|
|
|
break;
|
|
}
|
|
case "incomingcallindication":
|
|
{
|
|
var incomingCall =
|
|
JsonConvert.DeserializeObject<zEvent.IncomingCallIndication>(responseObj.ToString());
|
|
|
|
if (incomingCall != null)
|
|
{
|
|
var newCall = new CodecActiveCallItem
|
|
{
|
|
Direction = eCodecCallDirection.Incoming,
|
|
Status = eCodecCallStatus.Ringing,
|
|
Type = eCodecCallType.Unknown,
|
|
Name = incomingCall.callerName,
|
|
Id = incomingCall.callerJID
|
|
};
|
|
|
|
ActiveCalls.Add(newCall);
|
|
|
|
OnCallStatusChange(newCall);
|
|
}
|
|
|
|
break;
|
|
}
|
|
case "treatedincomingcallindication":
|
|
{
|
|
var incomingCall =
|
|
JsonConvert.DeserializeObject<zEvent.IncomingCallIndication>(responseObj.ToString());
|
|
|
|
if (incomingCall != null)
|
|
{
|
|
var existingCall =
|
|
ActiveCalls.FirstOrDefault(c => c.Id.Equals(incomingCall.callerJID));
|
|
|
|
if (existingCall != null)
|
|
{
|
|
existingCall.Status = !incomingCall.accepted
|
|
? eCodecCallStatus.Disconnected
|
|
: eCodecCallStatus.Connecting;
|
|
|
|
OnCallStatusChange(existingCall);
|
|
}
|
|
|
|
UpdateCallStatus();
|
|
}
|
|
|
|
break;
|
|
}
|
|
case "calldisconnect":
|
|
{
|
|
var disconnectEvent =
|
|
JsonConvert.DeserializeObject<zEvent.CallDisconnect>(responseObj.ToString());
|
|
|
|
if (disconnectEvent.Successful)
|
|
{
|
|
if (ActiveCalls.Count > 0)
|
|
{
|
|
var activeCall = ActiveCalls.FirstOrDefault(c => c.IsActiveCall);
|
|
|
|
if (activeCall != null)
|
|
{
|
|
activeCall.Status = eCodecCallStatus.Disconnected;
|
|
|
|
OnCallStatusChange(activeCall);
|
|
}
|
|
}
|
|
var emptyList = new List<Participant>();
|
|
Participants.CurrentParticipants = emptyList;
|
|
}
|
|
|
|
UpdateCallStatus();
|
|
break;
|
|
}
|
|
case "callconnecterror":
|
|
{
|
|
UpdateCallStatus();
|
|
break;
|
|
}
|
|
case "videounmuterequest":
|
|
{
|
|
// TODO: notify room of a request to unmute video
|
|
break;
|
|
}
|
|
case "meetingneedspassword":
|
|
{
|
|
// TODO: notify user to enter a password
|
|
break;
|
|
}
|
|
case "needwaitforhost":
|
|
{
|
|
var needWait =
|
|
JsonConvert.DeserializeObject<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;
|
|
}
|
|
case "phonecallstatus":
|
|
{
|
|
JsonConvert.PopulateObject(responseObj.ToString(), Status.PhoneCall);
|
|
break;
|
|
}
|
|
default:
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case eZoomRoomResponseType.zStatus:
|
|
{
|
|
switch (topKey.ToLower())
|
|
{
|
|
case "login":
|
|
{
|
|
_syncState.LoginMessageReceived();
|
|
|
|
if (!_syncState.InitialQueryMessagesWereSent)
|
|
{
|
|
SetUpSyncQueries();
|
|
}
|
|
|
|
JsonConvert.PopulateObject(responseObj.ToString(), Status.Login);
|
|
|
|
break;
|
|
}
|
|
case "systemunit":
|
|
{
|
|
JsonConvert.PopulateObject(responseObj.ToString(), Status.SystemUnit);
|
|
|
|
break;
|
|
}
|
|
case "call":
|
|
{
|
|
JsonConvert.PopulateObject(responseObj.ToString(), Status.Call);
|
|
|
|
UpdateCallStatus();
|
|
|
|
break;
|
|
}
|
|
case "capabilities":
|
|
{
|
|
JsonConvert.PopulateObject(responseObj.ToString(), Status.Capabilities);
|
|
break;
|
|
}
|
|
case "sharing":
|
|
{
|
|
JsonConvert.PopulateObject(responseObj.ToString(), Status.Sharing);
|
|
|
|
break;
|
|
}
|
|
case "numberofscreens":
|
|
{
|
|
JsonConvert.PopulateObject(responseObj.ToString(), Status.NumberOfScreens);
|
|
break;
|
|
}
|
|
case "video":
|
|
{
|
|
JsonConvert.PopulateObject(responseObj.ToString(), Status.Video);
|
|
break;
|
|
}
|
|
case "camerashare":
|
|
{
|
|
JsonConvert.PopulateObject(responseObj.ToString(), Status.CameraShare);
|
|
break;
|
|
}
|
|
case "layout":
|
|
{
|
|
JsonConvert.PopulateObject(responseObj.ToString(), Status.Layout);
|
|
break;
|
|
}
|
|
case "audio input line":
|
|
{
|
|
JsonConvert.PopulateObject(responseObj.ToString(), Status.AudioInputs);
|
|
break;
|
|
}
|
|
case "audio output line":
|
|
{
|
|
JsonConvert.PopulateObject(responseObj.ToString(), Status.AudioOuputs);
|
|
break;
|
|
}
|
|
case "video camera line":
|
|
{
|
|
JsonConvert.PopulateObject(responseObj.ToString(), Status.Cameras);
|
|
|
|
if (!_syncState.CamerasHaveBeenSetUp)
|
|
{
|
|
SetUpCameras();
|
|
}
|
|
|
|
break;
|
|
}
|
|
default:
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
|
|
break;
|
|
}
|
|
default:
|
|
{
|
|
Debug.Console(1, "Unknown Response Type:");
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Debug.Console(1, this, "Error Deserializing feedback: {0}", ex);
|
|
}
|
|
}
|
|
|
|
private void SetLayout()
|
|
{
|
|
if (!_props.AutoDefaultLayouts) return;
|
|
|
|
if (
|
|
(Status.Call.Sharing.State == zEvent.eSharingState.Receiving ||
|
|
Status.Call.Sharing.State == zEvent.eSharingState.Sending))
|
|
{
|
|
SendText(String.Format("zconfiguration call layout style: {0}",
|
|
_props.DefaultSharingLayout));
|
|
}
|
|
else
|
|
{
|
|
SendText(String.Format("zconfiguration call layout style: {0}",
|
|
_props.DefaultCallLayout));
|
|
}
|
|
}
|
|
|
|
public void PrintCurrentCallParticipants()
|
|
{
|
|
if (Debug.Level <= 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
Debug.Console(1, this, "****************************Call Participants***************************");
|
|
foreach (var participant in Participants.CurrentParticipants)
|
|
{
|
|
Debug.Console(1, this, "Name: {0} Audio: {1} IsHost: {2}", participant.Name,
|
|
participant.AudioMuteFb, participant.IsHost);
|
|
}
|
|
Debug.Console(1, this, "************************************************************************");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Retrieves bookings list
|
|
/// </summary>
|
|
private void GetBookings()
|
|
{
|
|
SendText("zCommand Bookings List");
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Updates the current call status
|
|
/// </summary>
|
|
private void UpdateCallStatus()
|
|
{
|
|
if (Status.Call != null)
|
|
{
|
|
var callStatus = Status.Call.Status;
|
|
|
|
// If not currently in a meeting, intialize the call object
|
|
if (callStatus != zStatus.eCallStatus.IN_MEETING || callStatus != zStatus.eCallStatus.CONNECTING_MEETING)
|
|
{
|
|
Status.Call = new zStatus.Call {Status = callStatus};
|
|
|
|
SetUpCallFeedbackActions();
|
|
}
|
|
|
|
if (ActiveCalls.Count == 0)
|
|
{
|
|
if (callStatus == zStatus.eCallStatus.CONNECTING_MEETING ||
|
|
callStatus == zStatus.eCallStatus.IN_MEETING)
|
|
{
|
|
var newStatus = eCodecCallStatus.Unknown;
|
|
|
|
switch (callStatus)
|
|
{
|
|
case zStatus.eCallStatus.CONNECTING_MEETING:
|
|
newStatus = eCodecCallStatus.Connecting;
|
|
break;
|
|
case zStatus.eCallStatus.IN_MEETING:
|
|
newStatus = eCodecCallStatus.Connected;
|
|
break;
|
|
}
|
|
|
|
var newCall = new CodecActiveCallItem {Status = newStatus};
|
|
|
|
ActiveCalls.Add(newCall);
|
|
|
|
OnCallStatusChange(newCall);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
var existingCall = ActiveCalls.FirstOrDefault(c => !c.Status.Equals(eCodecCallStatus.Ringing));
|
|
|
|
switch (callStatus)
|
|
{
|
|
case zStatus.eCallStatus.IN_MEETING:
|
|
existingCall.Status = eCodecCallStatus.Connected;
|
|
break;
|
|
case zStatus.eCallStatus.NOT_IN_MEETING:
|
|
existingCall.Status = eCodecCallStatus.Disconnected;
|
|
break;
|
|
}
|
|
|
|
OnCallStatusChange(existingCall);
|
|
}
|
|
}
|
|
|
|
Debug.Console(1, this, "****************************Active Calls*********************************");
|
|
|
|
// Clean up any disconnected calls left in the list
|
|
for (int i = 0; i < ActiveCalls.Count; i++)
|
|
{
|
|
var call = ActiveCalls[i];
|
|
|
|
Debug.Console(1, this,
|
|
@"Name: {0}
|
|
ID: {1}
|
|
IsActive: {2}
|
|
Status: {3}
|
|
Direction: {4}", call.Name, call.Id, call.IsActiveCall, call.Status, call.Direction);
|
|
|
|
if (!call.IsActiveCall)
|
|
{
|
|
Debug.Console(1, this, "******Removing Inactive Call: {0}******", call.Name);
|
|
ActiveCalls.Remove(call);
|
|
}
|
|
}
|
|
Debug.Console(1, this, "**************************************************************************");
|
|
|
|
//clear participants list after call cleanup
|
|
if (ActiveCalls.Count == 0)
|
|
{
|
|
Participants.CurrentParticipants = new List<Participant>();
|
|
}
|
|
}
|
|
|
|
protected override void OnCallStatusChange(CodecActiveCallItem item)
|
|
{
|
|
base.OnCallStatusChange(item);
|
|
|
|
if (_props.AutoDefaultLayouts)
|
|
{
|
|
SetLayout();
|
|
}
|
|
}
|
|
|
|
public override void StartSharing()
|
|
{
|
|
SendText("zCommand Call Sharing HDMI Start");
|
|
}
|
|
|
|
/// <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 decrement that calls SetVolume()
|
|
}
|
|
|
|
/// <summary>
|
|
/// Decrements the volume
|
|
/// </summary>
|
|
/// <param name="pressRelease"></param>
|
|
public override void VolumeDown(bool pressRelease)
|
|
{
|
|
// TODO: Implment volume decrement that calls SetVolume()
|
|
}
|
|
|
|
/// <summary>
|
|
/// Scales the level and sets the codec to the specified level within its range
|
|
/// </summary>
|
|
/// <param name="level">level from slider (0-65535 range)</param>
|
|
public override void SetVolume(ushort level)
|
|
{
|
|
var scaledLevel = CrestronEnvironment.ScaleWithLimits(level, 65535, 0, 100, 0);
|
|
SendText(string.Format("zConfiguration Audio Output volume: {0}", scaledLevel));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Recalls the default volume on the codec
|
|
/// </summary>
|
|
public void VolumeSetToDefault()
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
///
|
|
/// </summary>
|
|
public override void StandbyActivate()
|
|
{
|
|
// No corresponding function on device
|
|
}
|
|
|
|
/// <summary>
|
|
///
|
|
/// </summary>
|
|
public override void StandbyDeactivate()
|
|
{
|
|
// No corresponding function on device
|
|
}
|
|
|
|
public override void LinkToApi(BasicTriList trilist, uint joinStart, string joinMapKey, EiscApiAdvanced bridge)
|
|
{
|
|
LinkVideoCodecToApi(this, trilist, joinStart, joinMapKey, bridge);
|
|
}
|
|
|
|
public override void ExecuteSwitch(object selector)
|
|
{
|
|
var action = selector as Action;
|
|
if (action == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
action();
|
|
}
|
|
|
|
public void AcceptCall()
|
|
{
|
|
var incomingCall =
|
|
ActiveCalls.FirstOrDefault(
|
|
c => c.Status.Equals(eCodecCallStatus.Ringing) && c.Direction.Equals(eCodecCallDirection.Incoming));
|
|
|
|
AcceptCall(incomingCall);
|
|
}
|
|
|
|
public override void AcceptCall(CodecActiveCallItem call)
|
|
{
|
|
SendText(string.Format("zCommand Call Accept callerJID: {0}", call.Id));
|
|
|
|
call.Status = eCodecCallStatus.Connected;
|
|
|
|
OnCallStatusChange(call);
|
|
|
|
UpdateCallStatus();
|
|
}
|
|
|
|
public void RejectCall()
|
|
{
|
|
var incomingCall =
|
|
ActiveCalls.FirstOrDefault(
|
|
c => c.Status.Equals(eCodecCallStatus.Ringing) && c.Direction.Equals(eCodecCallDirection.Incoming));
|
|
|
|
RejectCall(incomingCall);
|
|
}
|
|
|
|
public override void RejectCall(CodecActiveCallItem call)
|
|
{
|
|
SendText(string.Format("zCommand Call Reject callerJID: {0}", call.Id));
|
|
|
|
call.Status = eCodecCallStatus.Disconnected;
|
|
|
|
OnCallStatusChange(call);
|
|
|
|
UpdateCallStatus();
|
|
}
|
|
|
|
public override void Dial(Meeting meeting)
|
|
{
|
|
SendText(string.Format("zCommand Dial Start meetingNumber: {0}", meeting.Id));
|
|
}
|
|
|
|
public override void Dial(string number)
|
|
{
|
|
SendText(string.Format("zCommand Dial Join meetingNumber: {0}", number));
|
|
}
|
|
|
|
/// <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)
|
|
{
|
|
SendDtmfToPhone(s);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Call when directory results are updated
|
|
/// </summary>
|
|
/// <param name="result"></param>
|
|
private void OnDirectoryResultReturned(CodecDirectory result)
|
|
{
|
|
CurrentDirectoryResultIsNotDirectoryRoot.FireUpdate();
|
|
|
|
// This will return the latest results to all UIs. Multiple indendent UI Directory browsing will require a different methodology
|
|
var handler = DirectoryResultReturned;
|
|
if (handler != null)
|
|
{
|
|
handler(this, new DirectoryEventArgs
|
|
{
|
|
Directory = result,
|
|
DirectoryIsOnRoot = !CurrentDirectoryResultIsNotDirectoryRoot.BoolValue
|
|
});
|
|
}
|
|
|
|
//PrintDirectory(result);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Builds the cameras List by using the Zoom Room zStatus.Cameras data. Could later be modified to build from config data
|
|
/// </summary>
|
|
private void SetUpCameras()
|
|
{
|
|
SelectedCameraFeedback = new StringFeedback(() => Configuration.Video.Camera.SelectedId);
|
|
|
|
ControllingFarEndCameraFeedback = new BoolFeedback(() => SelectedCamera is IAmFarEndCamera);
|
|
|
|
foreach (var cam in Status.Cameras)
|
|
{
|
|
var camera = new ZoomRoomCamera(cam.id, cam.Name, this);
|
|
|
|
Cameras.Add(camera);
|
|
|
|
if (cam.Selected)
|
|
{
|
|
SelectedCamera = camera;
|
|
}
|
|
}
|
|
|
|
if (IsInCall)
|
|
{
|
|
UpdateFarEndCameras();
|
|
}
|
|
|
|
_syncState.CamerasSetUp();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Dynamically creates far end cameras for call participants who have far end control enabled.
|
|
/// </summary>
|
|
private void UpdateFarEndCameras()
|
|
{
|
|
// TODO: set up far end cameras for the current call
|
|
}
|
|
|
|
#region Implementation of IHasParticipants
|
|
|
|
public CodecParticipants Participants { get; private set; }
|
|
|
|
#endregion
|
|
|
|
#region Implementation of IHasCameraOff
|
|
|
|
public BoolFeedback CameraIsOffFeedback { get; private set; }
|
|
|
|
public void CameraOff()
|
|
{
|
|
SendText("zConfiguration Call Camera Mute: On");
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Implementation of IHasCameraAutoMode
|
|
|
|
//Zoom doesn't support camera auto modes. Setting this to just unmute video
|
|
public void CameraAutoModeOn()
|
|
{
|
|
throw new NotImplementedException("Zoom Room Doesn't support camera auto mode");
|
|
}
|
|
|
|
//Zoom doesn't support camera auto modes. Setting this to just unmute video
|
|
public void CameraAutoModeOff()
|
|
{
|
|
SendText("zConfiguration Call Camera Mute: Off");
|
|
}
|
|
|
|
public void CameraAutoModeToggle()
|
|
{
|
|
throw new NotImplementedException("Zoom Room doesn't support camera auto mode");
|
|
}
|
|
|
|
public BoolFeedback CameraAutoModeIsOnFeedback { get; private set; }
|
|
|
|
#endregion
|
|
|
|
#region Implementation of IHasFarEndContentStatus
|
|
|
|
public BoolFeedback ReceivingContent { get; private set; }
|
|
|
|
#endregion
|
|
|
|
#region Implementation of IHasSelfviewPosition
|
|
|
|
private CodecCommandWithLabel _currentSelfviewPipPosition;
|
|
|
|
public StringFeedback SelfviewPipPositionFeedback { get; private set; }
|
|
|
|
public void SelfviewPipPositionSet(CodecCommandWithLabel position)
|
|
{
|
|
SendText(String.Format("zConfiguration Call Layout Position: {0}", position.Command));
|
|
}
|
|
|
|
public void SelfviewPipPositionToggle()
|
|
{
|
|
if (_currentSelfviewPipPosition != null)
|
|
{
|
|
var nextPipPositionIndex = SelfviewPipPositions.IndexOf(_currentSelfviewPipPosition) + 1;
|
|
|
|
if (nextPipPositionIndex >= SelfviewPipPositions.Count)
|
|
// Check if we need to loop back to the first item in the list
|
|
nextPipPositionIndex = 0;
|
|
|
|
SelfviewPipPositionSet(SelfviewPipPositions[nextPipPositionIndex]);
|
|
}
|
|
}
|
|
|
|
public List<CodecCommandWithLabel> SelfviewPipPositions = new List<CodecCommandWithLabel>()
|
|
{
|
|
new CodecCommandWithLabel("UpLeft", "Center Left"),
|
|
new CodecCommandWithLabel("UpRight", "Center Right"),
|
|
new CodecCommandWithLabel("DownRight", "Lower Right"),
|
|
new CodecCommandWithLabel("DownLeft", "Lower Left")
|
|
};
|
|
|
|
private void ComputeSelfviewPipStatus()
|
|
{
|
|
_currentSelfviewPipPosition =
|
|
SelfviewPipPositions.FirstOrDefault(
|
|
p => p.Command.ToLower().Equals(Configuration.Call.Layout.Position.ToString().ToLower()));
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Implementation of IHasPhoneDialing
|
|
|
|
private Func<bool> PhoneOffHookFeedbackFunc {get {return () => Status.PhoneCall.OffHook; }}
|
|
private Func<string> CallerIdNameFeedbackFunc { get { return () => Status.PhoneCall.PeerDisplayName; } }
|
|
private Func<string> CallerIdNumberFeedbackFunc { get { return () => Status.PhoneCall.PeerNumber; } }
|
|
|
|
public BoolFeedback PhoneOffHookFeedback { get; private set; }
|
|
public StringFeedback CallerIdNameFeedback { get; private set; }
|
|
public StringFeedback CallerIdNumberFeedback { get; private set; }
|
|
|
|
public void DialPhoneCall(string number)
|
|
{
|
|
SendText(String.Format("zCommand Dial PhoneCallOut Number: {0}", number));
|
|
}
|
|
|
|
public void EndPhoneCall()
|
|
{
|
|
SendText(String.Format("zCommand Dial PhoneHangUp CallId: {0}", Status.PhoneCall.CallId));
|
|
}
|
|
|
|
public void SendDtmfToPhone(string digit)
|
|
{
|
|
SendText(String.Format("zCommand SendSipDTMF CallId: {0} Key: {1}", Status.PhoneCall.CallId, digit));
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
|
|
/// <summary>
|
|
/// Zoom Room specific info object
|
|
/// </summary>
|
|
public class ZoomRoomInfo : VideoCodecInfo
|
|
{
|
|
public ZoomRoomInfo(ZoomRoomStatus status, ZoomRoomConfiguration configuration)
|
|
{
|
|
Status = status;
|
|
Configuration = configuration;
|
|
}
|
|
|
|
public ZoomRoomStatus Status { get; private set; }
|
|
public ZoomRoomConfiguration Configuration { get; private set; }
|
|
|
|
public override bool AutoAnswerEnabled
|
|
{
|
|
get { return Status.SystemUnit.RoomInfo.AutoAnswerIsEnabled; }
|
|
}
|
|
|
|
public override string E164Alias
|
|
{
|
|
get
|
|
{
|
|
if (!string.IsNullOrEmpty(Status.SystemUnit.MeetingNumber))
|
|
{
|
|
return Status.SystemUnit.MeetingNumber;
|
|
}
|
|
return string.Empty;
|
|
}
|
|
}
|
|
|
|
public override string H323Id
|
|
{
|
|
get
|
|
{
|
|
if (!string.IsNullOrEmpty(Status.Call.Info.meeting_list_item.third_party.h323_address))
|
|
{
|
|
return Status.Call.Info.meeting_list_item.third_party.h323_address;
|
|
}
|
|
return string.Empty;
|
|
}
|
|
}
|
|
|
|
public override string IpAddress
|
|
{
|
|
get
|
|
{
|
|
if (!string.IsNullOrEmpty(Status.SystemUnit.RoomInfo.AccountEmail))
|
|
{
|
|
return Status.SystemUnit.RoomInfo.AccountEmail;
|
|
}
|
|
return string.Empty;
|
|
}
|
|
}
|
|
|
|
public override bool MultiSiteOptionIsEnabled
|
|
{
|
|
get { return true; }
|
|
}
|
|
|
|
public override string SipPhoneNumber
|
|
{
|
|
get
|
|
{
|
|
if (!string.IsNullOrEmpty(Status.Call.Info.dialIn))
|
|
{
|
|
return Status.Call.Info.dialIn;
|
|
}
|
|
return string.Empty;
|
|
}
|
|
}
|
|
|
|
public override string SipUri
|
|
{
|
|
get
|
|
{
|
|
if (!string.IsNullOrEmpty(Status.Call.Info.meeting_list_item.third_party.sip_address))
|
|
{
|
|
return Status.Call.Info.meeting_list_item.third_party.sip_address;
|
|
}
|
|
return string.Empty;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tracks the initial sycnronization state when establishing a new connection
|
|
/// </summary>
|
|
public class ZoomRoomSyncState : IKeyed
|
|
{
|
|
private readonly ZoomRoom _parent;
|
|
private readonly CrestronQueue<string> _syncQueries;
|
|
private bool _initialSyncComplete;
|
|
|
|
public ZoomRoomSyncState(string key, ZoomRoom parent)
|
|
{
|
|
_parent = parent;
|
|
Key = key;
|
|
_syncQueries = new CrestronQueue<string>(50);
|
|
CodecDisconnected();
|
|
}
|
|
|
|
public bool InitialSyncComplete
|
|
{
|
|
get { return _initialSyncComplete; }
|
|
private set
|
|
{
|
|
if (value)
|
|
{
|
|
var handler = InitialSyncCompleted;
|
|
if (handler != null)
|
|
{
|
|
handler(this, new EventArgs());
|
|
}
|
|
}
|
|
_initialSyncComplete = value;
|
|
}
|
|
}
|
|
|
|
public bool LoginMessageWasReceived { get; private set; }
|
|
|
|
public bool InitialQueryMessagesWereSent { get; private set; }
|
|
|
|
public bool LastQueryResponseWasReceived { get; private set; }
|
|
|
|
public bool CamerasHaveBeenSetUp { get; private set; }
|
|
|
|
#region IKeyed Members
|
|
|
|
public string Key { get; private set; }
|
|
|
|
#endregion
|
|
|
|
public event EventHandler<EventArgs> InitialSyncCompleted;
|
|
|
|
public void StartSync()
|
|
{
|
|
DequeueQueries();
|
|
}
|
|
|
|
private void DequeueQueries()
|
|
{
|
|
while (!_syncQueries.IsEmpty)
|
|
{
|
|
var query = _syncQueries.Dequeue();
|
|
|
|
_parent.SendText(query);
|
|
}
|
|
|
|
InitialQueryMessagesSent();
|
|
}
|
|
|
|
public void AddQueryToQueue(string query)
|
|
{
|
|
_syncQueries.Enqueue(query);
|
|
}
|
|
|
|
public void LoginMessageReceived()
|
|
{
|
|
LoginMessageWasReceived = true;
|
|
Debug.Console(1, this, "Login Message Received.");
|
|
CheckSyncStatus();
|
|
}
|
|
|
|
public void InitialQueryMessagesSent()
|
|
{
|
|
InitialQueryMessagesWereSent = true;
|
|
Debug.Console(1, this, "Query Messages Sent.");
|
|
CheckSyncStatus();
|
|
}
|
|
|
|
public void LastQueryResponseReceived()
|
|
{
|
|
LastQueryResponseWasReceived = true;
|
|
Debug.Console(1, this, "Last Query Response Received.");
|
|
CheckSyncStatus();
|
|
}
|
|
|
|
public void CamerasSetUp()
|
|
{
|
|
CamerasHaveBeenSetUp = true;
|
|
Debug.Console(1, this, "Cameras Set Up.");
|
|
CheckSyncStatus();
|
|
}
|
|
|
|
public void CodecDisconnected()
|
|
{
|
|
_syncQueries.Clear();
|
|
LoginMessageWasReceived = false;
|
|
InitialQueryMessagesWereSent = false;
|
|
LastQueryResponseWasReceived = false;
|
|
CamerasHaveBeenSetUp = false;
|
|
InitialSyncComplete = false;
|
|
}
|
|
|
|
private void CheckSyncStatus()
|
|
{
|
|
if (LoginMessageWasReceived && InitialQueryMessagesWereSent && LastQueryResponseWasReceived &&
|
|
CamerasHaveBeenSetUp)
|
|
{
|
|
InitialSyncComplete = true;
|
|
Debug.Console(1, this, "Initial Codec Sync Complete!");
|
|
}
|
|
else
|
|
{
|
|
InitialSyncComplete = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
public class ZoomRoomFactory : EssentialsDeviceFactory<ZoomRoom>
|
|
{
|
|
public ZoomRoomFactory()
|
|
{
|
|
TypeNames = new List<string> {"zoomroom"};
|
|
}
|
|
|
|
public override EssentialsDevice BuildDevice(DeviceConfig dc)
|
|
{
|
|
Debug.Console(1, "Factory Attempting to create new ZoomRoom Device");
|
|
var comm = CommFactory.CreateCommForDevice(dc);
|
|
return new ZoomRoom(dc, comm);
|
|
}
|
|
}
|
|
} |