From 2d7ad8ba2a08b53f6d7b4811a1c99de78f767493 Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Fri, 7 May 2021 18:07:25 -0600 Subject: [PATCH] #698 Updates to add participant hand raised and pin/unpin --- .../VideoCodec/Interfaces/IHasParticipants.cs | 47 +++- .../VideoCodec/VideoCodecBase.cs | 20 +- .../VideoCodec/ZoomRoom/ResponseObjects.cs | 30 ++- .../VideoCodec/ZoomRoom/ZoomRoom.cs | 200 ++++++++++++++++-- .../VideoCodec/ZoomRoom/ZoomRoomJoinMap.cs | 20 ++ 5 files changed, 284 insertions(+), 33 deletions(-) diff --git a/essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/Interfaces/IHasParticipants.cs b/essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/Interfaces/IHasParticipants.cs index e0f1d1a3..f03c1984 100644 --- a/essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/Interfaces/IHasParticipants.cs +++ b/essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/Interfaces/IHasParticipants.cs @@ -1,13 +1,20 @@ using System; using System.Collections.Generic; +using PepperDash.Essentials.Core; namespace PepperDash.Essentials.Devices.Common.VideoCodec.Interfaces { + /// + /// Describes a device that has call participants + /// public interface IHasParticipants { CodecParticipants Participants { get; } } + /// + /// Describes the ability to mute and unmute a participant's video in a meeting + /// public interface IHasParticipantVideoMute:IHasParticipants { void MuteVideoForParticipant(int userId); @@ -15,13 +22,29 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec.Interfaces void ToggleVideoForParticipant(int userId); } - public interface IHasParticipantAudioMute:IHasParticipantVideoMute + /// + /// Describes the ability to mute and unmute a participant's audio in a meeting + /// + public interface IHasParticipantAudioMute : IHasParticipantVideoMute { void MuteAudioForParticipant(int userId); void UnmuteAudioForParticipant(int userId); void ToggleAudioForParticipant(int userId); } + /// + /// Describes the ability to pin and unpin a participant in a meeting + /// + public interface IHasParticipantPinUnpin : IHasParticipants + { + IntFeedback NumberOfScreensFeedback { get; } + int ScreenIndexToPinUserTo { get; } + + void PinParticipant(int userId, int screenIndex); + void UnPinParticipant(int userId); + void ToggleParticipantPinState(int userId, int screenIndex); + } + public class CodecParticipants { private List _currentParticipants; @@ -31,11 +54,7 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec.Interfaces set { _currentParticipants = value; - var handler = ParticipantsListHasChanged; - - if(handler == null) return; - - handler(this, new EventArgs()); + OnParticipantsChanged(); } } @@ -45,15 +64,31 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec.Interfaces { _currentParticipants = new List(); } + + public void OnParticipantsChanged() + { + var handler = ParticipantsListHasChanged; + + if (handler == null) return; + + handler(this, new EventArgs()); + } } + /// + /// Represents a call participant + /// public class Participant { + public int UserId { get; set; } public bool IsHost { get; set; } public string Name { get; set; } public bool CanMuteVideo { get; set; } public bool CanUnmuteVideo { get; set; } public bool VideoMuteFb { get; set; } public bool AudioMuteFb { get; set; } + public bool HandIsRaisedFb { get; set; } + public bool IsPinnedFb { get; set; } + public int ScreenIndexIsPinnedToFb { get; set; } } } \ No newline at end of file diff --git a/essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/VideoCodecBase.cs b/essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/VideoCodecBase.cs index 7b2f7f42..1cdb6bc7 100644 --- a/essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/VideoCodecBase.cs +++ b/essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/VideoCodecBase.cs @@ -547,16 +547,21 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec trilist.SetUshort(joinMap.ParticipantCount.JoinNumber, (ushort)codec.Participants.CurrentParticipants.Count); }; + + // TODO: #698 Figure out how to decode xsig data and trigger actions based on values from SIMPL + // trilist.SetStringSigAction(joinMap.CurrentParticipants.JoinNumber, // add method here to decode the xsig info and trigger actions } private string UpdateParticipantsXSig(List currentParticipants) { const int maxParticipants = 50; - const int maxDigitals = 5; + const int maxDigitals = 7; const int maxStrings = 1; + const int maxAnalogs = 1; const int offset = maxDigitals + maxStrings; var digitalIndex = maxStrings * maxParticipants; //15 var stringIndex = 0; + var analogIndex = 0; var meetingIndex = 0; var tokenArray = new XSigToken[maxParticipants * offset]; @@ -571,29 +576,42 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec tokenArray[digitalIndex + 2] = new XSigDigitalToken(digitalIndex + 3, participant.CanMuteVideo); tokenArray[digitalIndex + 3] = new XSigDigitalToken(digitalIndex + 4, participant.CanUnmuteVideo); tokenArray[digitalIndex + 4] = new XSigDigitalToken(digitalIndex + 5, participant.IsHost); + tokenArray[digitalIndex + 5] = new XSigDigitalToken(digitalIndex + 6, participant.HandIsRaisedFb); + tokenArray[digitalIndex + 6] = new XSigDigitalToken(digitalIndex + 6, participant.IsPinnedFb); //serials tokenArray[stringIndex] = new XSigSerialToken(stringIndex + 1, participant.Name); + //analogs + tokenArray[analogIndex] = new XSigAnalogToken(analogIndex + 1, (ushort)participant.ScreenIndexIsPinnedToFb); + digitalIndex += maxDigitals; meetingIndex += offset; stringIndex += maxStrings; + analogIndex += maxAnalogs; } while (meetingIndex < maxParticipants * offset) { + //digitals tokenArray[digitalIndex] = new XSigDigitalToken(digitalIndex + 1, false); tokenArray[digitalIndex + 1] = new XSigDigitalToken(digitalIndex + 2, false); tokenArray[digitalIndex + 2] = new XSigDigitalToken(digitalIndex + 3, false); tokenArray[digitalIndex + 3] = new XSigDigitalToken(digitalIndex + 4, false); tokenArray[digitalIndex + 4] = new XSigDigitalToken(digitalIndex + 5, false); + tokenArray[digitalIndex + 5] = new XSigDigitalToken(digitalIndex + 6, false); + tokenArray[digitalIndex + 6] = new XSigDigitalToken(digitalIndex + 7, false); //serials tokenArray[stringIndex] = new XSigSerialToken(stringIndex + 1, String.Empty); + //analogs + tokenArray[analogIndex] = new XSigAnalogToken(analogIndex + 1, 0); + digitalIndex += maxDigitals; meetingIndex += offset; stringIndex += maxStrings; + analogIndex += maxAnalogs; } return GetXSigString(tokenArray); diff --git a/essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/ZoomRoom/ResponseObjects.cs b/essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/ZoomRoom/ResponseObjects.cs index 6720d4fc..c8bfd5bf 100644 --- a/essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/ZoomRoom/ResponseObjects.cs +++ b/essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/ZoomRoom/ResponseObjects.cs @@ -480,12 +480,28 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec.ZoomRoom public string wifiName { get; set; } } - public class NumberOfScreens + public class NumberOfScreens : NotifiableObject { + private int _numOfScreens; + [JsonProperty("NumberOfCECScreens")] public int NumOfCECScreens { get; set; } [JsonProperty("NumberOfScreens")] - public int NumOfScreens { get; set; } + public int NumOfScreens + { + get + { + return _numOfScreens; + } + set + { + if (value != _numOfScreens) + { + _numOfScreens = value; + NotifyPropertyChanged("NumberOfScreens"); + } + } + } } /// @@ -803,6 +819,8 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec.ZoomRoom public class PinStatusOfScreenNotification { + + [JsonProperty("can_be_pinned")] public bool CanBePinned { get; set; } [JsonProperty("can_pin_share")] @@ -1301,8 +1319,8 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec.ZoomRoom { [JsonProperty("is_raise_hand")] public bool IsRaiseHand { get; set; } - [JsonProperty("optimize_vis_validideo_sharing")] - public string IsValid { get; set; } + [JsonProperty("is_valid")] + public bool IsValid { get; set; } [JsonProperty("time_stamp")] public string TimeStamp { get; set; } } @@ -1377,12 +1395,14 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec.ZoomRoom p => new Participant { + UserId = p.UserId, Name = p.UserName, IsHost = p.IsHost, CanMuteVideo = p.IsVideoCanMuteByHost, CanUnmuteVideo = p.IsVideoCanUnmuteByHost, AudioMuteFb = p.AudioStatusState == "AUDIO_MUTED", - VideoMuteFb = p.VideoStatusIsSending + VideoMuteFb = p.VideoStatusIsSending, + HandIsRaisedFb = p.HandStatus.IsValid && p.HandStatus.IsRaiseHand, }).ToList(); } } diff --git a/essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/ZoomRoom/ZoomRoom.cs b/essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/ZoomRoom/ZoomRoom.cs index da8f0d67..a0a8158c 100644 --- a/essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/ZoomRoom/ZoomRoom.cs +++ b/essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/ZoomRoom/ZoomRoom.cs @@ -23,7 +23,7 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec.ZoomRoom public class ZoomRoom : VideoCodecBase, IHasCodecSelfView, IHasDirectoryHistoryStack, ICommunicationMonitor, IRouting, IHasScheduleAwareness, IHasCodecCameras, IHasParticipants, IHasCameraOff, IHasCameraMute, IHasCameraAutoMode, - IHasFarEndContentStatus, IHasSelfviewPosition, IHasPhoneDialing, IHasZoomRoomLayouts + IHasFarEndContentStatus, IHasSelfviewPosition, IHasPhoneDialing, IHasZoomRoomLayouts, IHasParticipantPinUnpin, IHasParticipantAudioMute { private const long MeetingRefreshTimer = 60000; private const uint DefaultMeetingDurationMin = 30; @@ -125,13 +125,12 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec.ZoomRoom LocalLayoutFeedback = new StringFeedback(LocalLayoutFeedbackFunc); LayoutViewIsOnFirstPageFeedback = new BoolFeedback(LayoutViewIsOnFirstPageFeedbackFunc); - LayoutViewIsOnLastPageFeedback = new BoolFeedback(LayoutViewIsOnLastPageFeedbackFunc); - CanSwapContentWithThumbnailFeedback = new BoolFeedback(CanSwapContentWithThumbnailFeedbackFunc); - ContentSwappedWithThumbnailFeedback = new BoolFeedback(ContentSwappedWithThumbnailFeedbackFunc); + NumberOfScreensFeedback = new IntFeedback(NumberOfScreensFeedbackFunc); + } public CommunicationGather PortGather { get; private set; } @@ -574,30 +573,39 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec.ZoomRoom case "can_Switch_Wall_View": case "can_Switch_Share_On_All_Screens": { - // #697 TODO: Calls the method to compute the available layouts and set the value of AvailableLayouts enum ComputeAvailableLayouts(); break; } case "is_In_First_Page": { - // TODO: #697 Fires appropriate feedback LayoutViewIsOnFirstPageFeedback.FireUpdate(); break; } case "is_In_Last_Page": { - // TODO: #697 Fires appropriate feedback LayoutViewIsOnLastPageFeedback.FireUpdate(); break; } //case "video_type": // { - // // TODO: #697 It appears as though the actual value we want to watch is Configuration.Call.Layout.Style + // It appears as though the actual value we want to watch is Configuration.Call.Layout.Style // LocalLayoutFeedback.FireUpdate(); // break; // } } }; + + Status.NumberOfScreens.PropertyChanged += (o, a) => + { + switch (a.PropertyName) + { + case "NumberOfScreens": + { + NumberOfScreensFeedback.FireUpdate(); + break; + } + } + }; } private void SetUpDirectory() @@ -1278,6 +1286,38 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec.ZoomRoom JsonConvert.PopulateObject(responseObj.ToString(), Status.PhoneCall); break; } + case "pinstatusofscreennotification": + { + var status = responseObj.ToObject(); + + var participant = Participants.CurrentParticipants.FirstOrDefault(p => p.UserId.Equals(status.PinnedUserId)); + + if (participant != null) + { + participant.IsPinnedFb = true; + participant.ScreenIndexIsPinnedToFb = status.ScreenIndex; + } + else + { + participant = Participants.CurrentParticipants.FirstOrDefault(p => p.ScreenIndexIsPinnedToFb.Equals(status.ScreenIndex)); + + if (participant == null) + { + Debug.Console(2, this, "no matching participant found by pinned_user_id: {0} or screen_index: {1}", status.PinnedUserId, status.ScreenIndex); + return; + } + else + { + participant.IsPinnedFb = false; + participant.ScreenIndexIsPinnedToFb = -1; + } + } + + // fire the event as we've modified the participants list + Participants.OnParticipantsChanged(); + + break; + } default: { break; @@ -1682,11 +1722,11 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec.ZoomRoom /// public void LinkZoomRoomToApi(BasicTriList trilist, ZoomRoomJoinMap joinMap) { - var codec = this as IHasZoomRoomLayouts; + var layoutsCodec = this as IHasZoomRoomLayouts; - if (codec != null) + if (layoutsCodec != null) { - codec.AvailableLayoutsChanged += (o, a) => + layoutsCodec.AvailableLayoutsChanged += (o, a) => { trilist.SetBool(joinMap.LayoutGalleryIsAvailable.JoinNumber, a.AvailableLayouts == (a.AvailableLayouts & zConfiguration.eLayoutStyle.Gallery)); @@ -1698,14 +1738,14 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec.ZoomRoom == (a.AvailableLayouts & zConfiguration.eLayoutStyle.ShareAll)); }; - codec.CanSwapContentWithThumbnailFeedback.LinkInputSig(trilist.BooleanInput[joinMap.CanSwapContentWithThumbnail.JoinNumber]); - trilist.SetSigFalseAction(joinMap.SwapContentWithThumbnail.JoinNumber, () => codec.SwapContentWithThumbnail()); - codec.ContentSwappedWithThumbnailFeedback.LinkInputSig(trilist.BooleanInput[joinMap.SwapContentWithThumbnail.JoinNumber]); + layoutsCodec.CanSwapContentWithThumbnailFeedback.LinkInputSig(trilist.BooleanInput[joinMap.CanSwapContentWithThumbnail.JoinNumber]); + trilist.SetSigFalseAction(joinMap.SwapContentWithThumbnail.JoinNumber, () => layoutsCodec.SwapContentWithThumbnail()); + layoutsCodec.ContentSwappedWithThumbnailFeedback.LinkInputSig(trilist.BooleanInput[joinMap.SwapContentWithThumbnail.JoinNumber]); - codec.LayoutViewIsOnFirstPageFeedback.LinkInputSig(trilist.BooleanInput[joinMap.LayoutIsOnFirstPage.JoinNumber]); - codec.LayoutViewIsOnLastPageFeedback.LinkInputSig(trilist.BooleanInput[joinMap.LayoutIsOnLastPage.JoinNumber]); - trilist.SetSigFalseAction(joinMap.LayoutTurnToNextPage.JoinNumber, () => codec.LayoutTurnNextPage() ); - trilist.SetSigFalseAction(joinMap.LayoutTurnToPreviousPage.JoinNumber, () => codec.LayoutTurnPreviousPage()); + layoutsCodec.LayoutViewIsOnFirstPageFeedback.LinkInputSig(trilist.BooleanInput[joinMap.LayoutIsOnFirstPage.JoinNumber]); + layoutsCodec.LayoutViewIsOnLastPageFeedback.LinkInputSig(trilist.BooleanInput[joinMap.LayoutIsOnLastPage.JoinNumber]); + trilist.SetSigFalseAction(joinMap.LayoutTurnToNextPage.JoinNumber, () => layoutsCodec.LayoutTurnNextPage()); + trilist.SetSigFalseAction(joinMap.LayoutTurnToPreviousPage.JoinNumber, () => layoutsCodec.LayoutTurnPreviousPage()); trilist.SetStringSigAction(joinMap.GetSetCurrentLayout.JoinNumber, (s) => @@ -1721,9 +1761,17 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec.ZoomRoom } }); - codec.LocalLayoutFeedback.LinkInputSig(trilist.StringInput[joinMap.GetSetCurrentLayout.JoinNumber]); + layoutsCodec.LocalLayoutFeedback.LinkInputSig(trilist.StringInput[joinMap.GetSetCurrentLayout.JoinNumber]); + } - + var pinCodec = this as IHasParticipantPinUnpin; + + if (pinCodec != null) + { + pinCodec.NumberOfScreensFeedback.LinkInputSig(trilist.UShortInput[joinMap.NumberOfScreens.JoinNumber]); + + // Set the value of the local property to be used when pinning a participant + trilist.SetUShortSigAction(joinMap.ScreenIndexToPinUserTo.JoinNumber, (u) => ScreenIndexToPinUserTo = u); } } @@ -1894,6 +1942,114 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec.ZoomRoom #endregion + #region IHasParticipantAudioMute Members + + public void MuteAudioForParticipant(int userId) + { + SendText(string.Format("zCommand Call MuteParticipant Mute: on Id: {0}", userId)); + } + + public void UnmuteAudioForParticipant(int userId) + { + SendText(string.Format("zCommand Call MuteParticipant Mute: off Id: {0}", userId)); + } + + public void ToggleAudioForParticipant(int userId) + { + var user = Participants.CurrentParticipants.FirstOrDefault(p => p.UserId.Equals(userId)); + + if (user == null) + { + Debug.Console(2, this, "Unable to find user with id: {0}", userId); + return; + } + + if (user.AudioMuteFb) + { + UnmuteAudioForParticipant(userId); + } + else + { + MuteAudioForParticipant(userId); + } + } + + #endregion + + #region IHasParticipantVideoMute Members + + public void MuteVideoForParticipant(int userId) + { + SendText(string.Format("zCommand Call MuteParticipantVideo Mute: on Id: {0}", userId)); + } + + public void UnmuteVideoForParticipant(int userId) + { + SendText(string.Format("zCommand Call MuteParticipantVideo Mute: off Id: {0}", userId)); + } + + public void ToggleVideoForParticipant(int userId) + { + var user = Participants.CurrentParticipants.FirstOrDefault(p => p.UserId.Equals(userId)); + + if (user == null) + { + Debug.Console(2, this, "Unable to find user with id: {0}", userId); + return; + } + + if (user.VideoMuteFb) + { + UnmuteVideoForParticipant(userId); + } + else + { + MuteVideoForParticipant(userId); + } + } + + #endregion + + #region IHasParticipantPinUnpin Members + + private Func NumberOfScreensFeedbackFunc { get { return () => Status.NumberOfScreens.NumOfScreens; } } + + public IntFeedback NumberOfScreensFeedback { get; private set; } + + public int ScreenIndexToPinUserTo { get; private set; } + + public void PinParticipant(int userId, int screenIndex) + { + SendText(string.Format("zCommand Call Pin Id: {0} Enable: on Screen: {1}", userId, screenIndex)); + } + + public void UnPinParticipant(int userId) + { + SendText(string.Format("zCommand Call Pin Id: {0} Enable: off", userId)); + } + + public void ToggleParticipantPinState(int userId, int screenIndex) + { + var user = Participants.CurrentParticipants.FirstOrDefault(p => p.UserId.Equals(userId)); + + if(user == null) + { + Debug.Console(2, this, "Unable to find user with id: {0}", userId); + return; + } + + if (user.IsPinnedFb) + { + UnPinParticipant(userId); + } + else + { + PinParticipant(userId, screenIndex); + } + } + + #endregion + #region Implementation of IHasCameraOff public BoolFeedback CameraIsOffFeedback { get; private set; } @@ -2052,7 +2208,8 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec.ZoomRoom private void ComputeAvailableLayouts() { zConfiguration.eLayoutStyle availableLayouts = zConfiguration.eLayoutStyle.None; - //TODO: #697 Compute the avaialble layouts and set the value of AvailableLayouts + // TODO: #697 Compute the avaialble layouts and set the value of AvailableLayouts + // Will need to test and confirm that this logic evaluates correctly if (Status.Layout.can_Switch_Wall_View) { availableLayouts |= zConfiguration.eLayoutStyle.Gallery; @@ -2140,6 +2297,7 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec.ZoomRoom } #endregion + } /// diff --git a/essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/ZoomRoom/ZoomRoomJoinMap.cs b/essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/ZoomRoom/ZoomRoomJoinMap.cs index 49b22ae4..f4d11f14 100644 --- a/essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/ZoomRoom/ZoomRoomJoinMap.cs +++ b/essentials-framework/Essentials Devices Common/Essentials Devices Common/VideoCodec/ZoomRoom/ZoomRoomJoinMap.cs @@ -128,6 +128,26 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec.ZoomRoom JoinType = eJoinType.Digital }); + [JoinName("ScreenIndexToPinUserTo")] + public JoinDataComplete ScreenIndexToPinUserTo = + new JoinDataComplete(new JoinData { JoinNumber = 999, JoinSpan = 1 }, + new JoinMetadata + { + Description = "Specifies the screen index a participant should be pinned to", + JoinCapabilities = eJoinCapabilities.FromSIMPL, + JoinType = eJoinType.Analog + }); + + [JoinName("NumberOfScreens")] + public JoinDataComplete NumberOfScreens = + new JoinDataComplete(new JoinData { JoinNumber = 999, JoinSpan = 1 }, + new JoinMetadata + { + Description = "Reports the number of screens connected", + JoinCapabilities = eJoinCapabilities.ToSIMPL, + JoinType = eJoinType.Analog + }); + public ZoomRoomJoinMap(uint joinStart) : base(joinStart, typeof(ZoomRoomJoinMap)) {