feat: enhance MockVC to support participant management and add HDMI output port

This commit is contained in:
Neil Dorin 2026-07-08 14:39:50 -06:00
parent 34c82588f0
commit 122881100a
2 changed files with 114 additions and 4 deletions

View file

@ -14,6 +14,7 @@ using PepperDash.Essentials.Core.Config;
using PepperDash.Essentials.Core.Routing; using PepperDash.Essentials.Core.Routing;
using PepperDash.Essentials.Devices.Common.Cameras; using PepperDash.Essentials.Devices.Common.Cameras;
using PepperDash.Essentials.Devices.Common.Codec; using PepperDash.Essentials.Devices.Common.Codec;
using PepperDash.Essentials.Devices.Common.VideoCodec.Interfaces;
using Serilog.Events; using Serilog.Events;
namespace PepperDash.Essentials.Devices.Common.VideoCodec namespace PepperDash.Essentials.Devices.Common.VideoCodec
@ -21,7 +22,8 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec
/// <summary> /// <summary>
/// Represents a MockVC /// Represents a MockVC
/// </summary> /// </summary>
public class MockVC : VideoCodecBase, IHasCallHistory, IHasScheduleAwareness, IHasCallFavorites, IHasDirectory, IHasCodecCameras, IHasCameraAutoMode, IHasCodecRoomPresets, IRoutingSinkWithFeedback public class MockVC : VideoCodecBase, IHasCallHistory, IHasScheduleAwareness, IHasCallFavorites, IHasDirectory,
IHasCodecCameras, IHasCameraAutoMode, IHasCodecRoomPresets, IRoutingSinkWithFeedback, IHasParticipants
{ {
/// <summary> /// <summary>
/// Gets or sets the PropertiesConfig /// Gets or sets the PropertiesConfig
@ -45,6 +47,9 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec
/// </summary> /// </summary>
public RoutingOutputPort HdmiOut1 { get; private set; } public RoutingOutputPort HdmiOut1 { get; private set; }
/// <summary>
/// Gets or sets the HdmiOut2
/// </summary>
public RoutingOutputPort HdmiOut2 { get; private set; } public RoutingOutputPort HdmiOut2 { get; private set; }
/// <summary> /// <summary>
@ -52,8 +57,10 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec
/// </summary> /// </summary>
public CodecCallFavorites CallFavorites { get; private set; } public CodecCallFavorites CallFavorites { get; private set; }
private readonly CodecParticipants _participants;
/// <summary> /// <summary>
/// /// Initializes a new instance of the <see cref="MockVC"/> class.
/// </summary> /// </summary>
public MockVC(DeviceConfig config) public MockVC(DeviceConfig config)
: base(config) : base(config)
@ -71,6 +78,23 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec
DirectoryBrowseHistory = new List<CodecDirectory>(); DirectoryBrowseHistory = new List<CodecDirectory>();
_participants = new CodecParticipants();
// Populate a fake participants list whenever a call connects, and clear it once all
// calls have ended, so IHasParticipants consumers (e.g. the roster UI) have something
// to display without needing a real far-end connection.
CallStatusChange += (o, a) =>
{
if (a.CallItem.Status == eCodecCallStatus.Connected)
{
PopulateFakeParticipants();
}
else if (a.CallItem.Status == eCodecCallStatus.Disconnected && ActiveCalls.Count == 0)
{
Participants.CurrentParticipants = new List<Participant>();
}
};
// Debug helpers // Debug helpers
MuteFeedback.OutputChange += (o, a) => Debug.LogMessage(LogEventLevel.Debug, this, "Mute={0}", _IsMuted); MuteFeedback.OutputChange += (o, a) => Debug.LogMessage(LogEventLevel.Debug, this, "Mute={0}", _IsMuted);
PrivacyModeIsOnFeedback.OutputChange += (o, a) => Debug.LogMessage(LogEventLevel.Debug, this, "Privacy={0}", _PrivacyModeIsOn); PrivacyModeIsOnFeedback.OutputChange += (o, a) => Debug.LogMessage(LogEventLevel.Debug, this, "Privacy={0}", _PrivacyModeIsOn);
@ -209,6 +233,7 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec
} }
/// <inheritdoc />
public override void Dial(string number, string password) public override void Dial(string number, string password)
{ {
Debug.LogMessage(LogEventLevel.Debug, this, "Dial: {0} with password: {1}", number, password); Debug.LogMessage(LogEventLevel.Debug, this, "Dial: {0} with password: {1}", number, password);
@ -652,6 +677,65 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec
#endregion #endregion
#region IHasParticipants Members
/// <summary>
/// Gets the collection of call participants
/// </summary>
public CodecParticipants Participants
{
get { return _participants; }
}
/// <summary>
/// Builds a fake set of participants and assigns it to <see cref="Participants"/>,
/// simulating a codec that has joined a call with other people already present.
/// </summary>
private void PopulateFakeParticipants()
{
Participants.CurrentParticipants = new List<Participant>
{
new Participant { UserId = 1, Name = "Myself", IsHost = true, IsMyself = true, CanMuteAudio = true, CanUnmuteVideo = true },
new Participant { UserId = 2, Name = "Priya Chandran", CanMuteAudio = true, CanUnmuteVideo = true },
new Participant { UserId = 3, Name = "Miles Anderson", CanMuteAudio = true, CanUnmuteVideo = true, AudioMuteFb = true },
new Participant { UserId = 4, Name = "Owen Bradshaw", CanMuteAudio = true, CanUnmuteVideo = true, HandIsRaisedFb = true },
};
}
/// <inheritdoc />
public void RemoveParticipant(int userId)
{
Debug.LogMessage(LogEventLevel.Debug, this, "RemoveParticipant: {0}", userId);
var remaining = Participants.CurrentParticipants.Where(p => p.UserId != userId).ToList();
Participants.CurrentParticipants = remaining;
}
/// <inheritdoc />
public void SetParticipantAsHost(int userId)
{
Debug.LogMessage(LogEventLevel.Debug, this, "SetParticipantAsHost: {0}", userId);
foreach (var p in Participants.CurrentParticipants)
{
p.IsHost = p.UserId == userId;
}
// Re-assign to trigger ParticipantsListHasChanged, since the mutations above happened
// on the existing list/items in place.
Participants.CurrentParticipants = Participants.CurrentParticipants;
}
/// <inheritdoc />
public void AdmitParticipantFromWaitingRoom(int userId)
{
// No waiting room is simulated for this mock codec.
Debug.LogMessage(LogEventLevel.Debug, this, "AdmitParticipantFromWaitingRoom: {0} (no-op, mock has no waiting room)", userId);
}
#endregion
void SetupCameras() void SetupCameras()
{ {
SupportsCameraAutoMode = true; SupportsCameraAutoMode = true;
@ -884,20 +968,42 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec
#region IRoutingSinkWithFeedback Members #region IRoutingSinkWithFeedback Members
/// <summary>
/// Gets the currently routed input port for this sink
/// </summary>
public RoutingInputPort CurrentInputPort { get; private set; } public RoutingInputPort CurrentInputPort { get; private set; }
/// <summary>
/// Event fired when the currently routed input changes
/// </summary>
public event InputChangedEventHandler InputChanged; public event InputChangedEventHandler InputChanged;
#endregion #endregion
#region ICurrentSources Members #region ICurrentSources Members
/// <summary>
/// Gets the currently routed source devices, keyed by signal type
/// </summary>
public Dictionary<eRoutingSignalType, IRoutingSource> CurrentSources { get; private set; } = new Dictionary<eRoutingSignalType, IRoutingSource>(); public Dictionary<eRoutingSignalType, IRoutingSource> CurrentSources { get; private set; } = new Dictionary<eRoutingSignalType, IRoutingSource>();
/// <summary>
/// Gets the keys of the currently routed source devices, keyed by signal type
/// </summary>
public Dictionary<eRoutingSignalType, string> CurrentSourceKeys { get; private set; } = new Dictionary<eRoutingSignalType, string>(); public Dictionary<eRoutingSignalType, string> CurrentSourceKeys { get; private set; } = new Dictionary<eRoutingSignalType, string>();
/// <summary>
/// Event fired when the currently routed source for a signal type changes
/// </summary>
public event EventHandler<CurrentSourcesChangedEventArgs> CurrentSourcesChanged; public event EventHandler<CurrentSourcesChangedEventArgs> CurrentSourcesChanged;
/// <summary>
/// Sets the currently routed source device for the given signal type, updating
/// <see cref="CurrentSources"/> and <see cref="CurrentSourceKeys"/> and firing
/// <see cref="CurrentSourcesChanged"/>
/// </summary>
/// <param name="signalType">The signal type being routed</param>
/// <param name="sourceDevice">The source device now routed to this signal type, or null if cleared</param>
public void SetCurrentSource(eRoutingSignalType signalType, IRoutingSource sourceDevice) public void SetCurrentSource(eRoutingSignalType signalType, IRoutingSource sourceDevice)
{ {
CurrentSources.TryGetValue(signalType, out var previousSource); CurrentSources.TryGetValue(signalType, out var previousSource);

View file

@ -1273,9 +1273,13 @@ namespace PepperDash.Essentials.WebSocketServer
Debug.LogMessage(LogEventLevel.Verbose, "Requesting Image: {0}", this, path); Debug.LogMessage(LogEventLevel.Verbose, "Requesting Image: {0}", this, path);
var imageBasePath = Global.DirectorySeparator + "html" + Global.DirectorySeparator + "logo" + Global.DirectorySeparator; // Served from the program's own slot-specific folder (e.g. /user/program2/logo/) via
// the Mobile Control direct server, rather than the processor's CWS /html/ webserver.
var imageBasePath = Global.FilePathPrefix + "logo" + Global.DirectorySeparator;
var image = path.Split('/').Last(); // Path.GetFileName strips any directory components (including URL-decoded traversal
// sequences like "..") so only a bare filename within imageBasePath can ever be read.
var image = Path.GetFileName(Uri.UnescapeDataString(path.Split('?')[0]));
var filePath = imageBasePath + image; var filePath = imageBasePath + image;