mirror of
https://github.com/PepperDash/Essentials.git
synced 2026-08-31 19:08:29 +00:00
feat: enhance routing graph handling for tile-sink children and add RoutingGraphHelpers utility class
This commit is contained in:
parent
7d1e0432cb
commit
079f2beb8b
4 changed files with 335 additions and 44 deletions
|
|
@ -5,6 +5,7 @@ using Crestron.SimplSharp.WebScripting;
|
|||
using Newtonsoft.Json;
|
||||
using PepperDash.Core;
|
||||
using PepperDash.Core.Web.RequestHandlers;
|
||||
using PepperDash.Essentials.Core.Web;
|
||||
|
||||
namespace PepperDash.Essentials.Core.Web.RequestHandlers
|
||||
{
|
||||
|
|
@ -27,9 +28,17 @@ namespace PepperDash.Essentials.Core.Web.RequestHandlers
|
|||
{
|
||||
var devices = new List<RoutingDeviceInfo>();
|
||||
|
||||
// Tile-sink children of an IRoutingSinkWithLayouts device (e.g. a multiview decoder's
|
||||
// per-window sinks) are rendered as synthesized input ports on the parent's node instead
|
||||
// of as their own separate nodes - see BuildRoutingDeviceInfo below.
|
||||
var tileChildren = RoutingGraphHelpers.BuildTileChildMap();
|
||||
|
||||
// Get all devices from DeviceManager
|
||||
foreach (var device in DeviceManager.AllDevices)
|
||||
{
|
||||
if (tileChildren.ContainsKey(device.Key))
|
||||
continue;
|
||||
|
||||
var deviceInfo = new RoutingDeviceInfo
|
||||
{
|
||||
Key = device.Key,
|
||||
|
|
@ -68,6 +77,27 @@ namespace PepperDash.Essentials.Core.Web.RequestHandlers
|
|||
deviceInfo.HasInputsAndOutputs = true;
|
||||
}
|
||||
|
||||
// Devices implementing IRoutingSinkWithLayouts (e.g. a multiview decoder) don't
|
||||
// implement IRoutingInputs themselves - their tiles do. Synthesize one input port per
|
||||
// tile (qualified so multiple tiles don't collide) so the graph can render a single
|
||||
// node with one edge-target input per tile.
|
||||
if (device is IRoutingSinkWithLayouts layoutDevice)
|
||||
{
|
||||
deviceInfo.HasInputs = true;
|
||||
|
||||
var tilePorts = layoutDevice.WindowTileSinks
|
||||
.OrderBy(kvp => kvp.Key)
|
||||
.SelectMany(kvp => (kvp.Value as IRoutingInputs)?.InputPorts.Select(p => new PortInfo
|
||||
{
|
||||
Key = RoutingGraphHelpers.QualifyTilePortKey(kvp.Key, p.Key),
|
||||
SignalType = p.Type.ToString(),
|
||||
ConnectionType = p.ConnectionType.ToString(),
|
||||
IsInternal = p.IsInternal
|
||||
}) ?? []);
|
||||
|
||||
deviceInfo.InputPorts = (deviceInfo.InputPorts ?? []).Concat(tilePorts).ToList();
|
||||
}
|
||||
|
||||
// Only include devices that have routing capabilities
|
||||
if (deviceInfo.HasInputs || deviceInfo.HasOutputs)
|
||||
{
|
||||
|
|
@ -75,15 +105,28 @@ namespace PepperDash.Essentials.Core.Web.RequestHandlers
|
|||
}
|
||||
}
|
||||
|
||||
// Get all tielines
|
||||
var tielines = TieLineCollection.Default.Select(tl => new TieLineInfo
|
||||
// Get all tielines, remapping any that target a tile-sink child so they point at its
|
||||
// IRoutingSinkWithLayouts parent's node/qualified port instead.
|
||||
var tielines = TieLineCollection.Default.Select(tl =>
|
||||
{
|
||||
SourceDeviceKey = tl.SourcePort.ParentDevice.Key,
|
||||
SourcePortKey = tl.SourcePort.Key,
|
||||
DestinationDeviceKey = tl.DestinationPort.ParentDevice.Key,
|
||||
DestinationPortKey = tl.DestinationPort.Key,
|
||||
SignalType = tl.Type.ToString(),
|
||||
IsInternal = tl.IsInternal
|
||||
var destinationDeviceKey = tl.DestinationPort.ParentDevice.Key;
|
||||
var destinationPortKey = tl.DestinationPort.Key;
|
||||
|
||||
if (tileChildren.TryGetValue(destinationDeviceKey, out var tileInfo))
|
||||
{
|
||||
destinationDeviceKey = tileInfo.Parent.Key;
|
||||
destinationPortKey = RoutingGraphHelpers.QualifyTilePortKey(tileInfo.TileNumber, destinationPortKey);
|
||||
}
|
||||
|
||||
return new TieLineInfo
|
||||
{
|
||||
SourceDeviceKey = tl.SourcePort.ParentDevice.Key,
|
||||
SourcePortKey = tl.SourcePort.Key,
|
||||
DestinationDeviceKey = destinationDeviceKey,
|
||||
DestinationPortKey = destinationPortKey,
|
||||
SignalType = tl.Type.ToString(),
|
||||
IsInternal = tl.IsInternal
|
||||
};
|
||||
}).ToList();
|
||||
|
||||
// Get current active routes from DefaultCollection, grouped by signal type
|
||||
|
|
@ -92,17 +135,43 @@ namespace PepperDash.Essentials.Core.Web.RequestHandlers
|
|||
.Select(g => new CurrentRouteGroupInfo
|
||||
{
|
||||
SignalType = g.Key,
|
||||
Routes = [.. g.Select(d => new ActiveRouteInfo
|
||||
Routes = [.. g.Select(d =>
|
||||
{
|
||||
SourceDeviceKey = d.Source.Key,
|
||||
DestinationDeviceKey = d.Destination.Key,
|
||||
DestinationInputPortKey = d.InputPort?.Key,
|
||||
Steps = [.. d.Routes.Select(r => new RouteSwitchStepInfo
|
||||
var destinationDeviceKey = d.Destination.Key;
|
||||
var destinationInputPortKey = d.InputPort?.Key;
|
||||
|
||||
if (tileChildren.TryGetValue(destinationDeviceKey, out var tileInfo))
|
||||
{
|
||||
SwitchingDeviceKey = r.SwitchingDevice?.Key,
|
||||
InputPortKey = r.InputPort?.Key,
|
||||
OutputPortKey = r.OutputPort?.Key
|
||||
})]
|
||||
destinationDeviceKey = tileInfo.Parent.Key;
|
||||
if (destinationInputPortKey != null)
|
||||
destinationInputPortKey = RoutingGraphHelpers.QualifyTilePortKey(tileInfo.TileNumber, destinationInputPortKey);
|
||||
}
|
||||
|
||||
return new ActiveRouteInfo
|
||||
{
|
||||
SourceDeviceKey = d.Source.Key,
|
||||
DestinationDeviceKey = destinationDeviceKey,
|
||||
DestinationInputPortKey = destinationInputPortKey,
|
||||
Steps = [.. d.Routes.Select(r =>
|
||||
{
|
||||
var switchingDeviceKey = r.SwitchingDevice?.Key;
|
||||
var inputPortKey = r.InputPort?.Key;
|
||||
|
||||
if (switchingDeviceKey != null && tileChildren.TryGetValue(switchingDeviceKey, out var stepTileInfo))
|
||||
{
|
||||
switchingDeviceKey = stepTileInfo.Parent.Key;
|
||||
if (inputPortKey != null)
|
||||
inputPortKey = RoutingGraphHelpers.QualifyTilePortKey(stepTileInfo.TileNumber, inputPortKey);
|
||||
}
|
||||
|
||||
return new RouteSwitchStepInfo
|
||||
{
|
||||
SwitchingDeviceKey = switchingDeviceKey,
|
||||
InputPortKey = inputPortKey,
|
||||
OutputPortKey = r.OutputPort?.Key
|
||||
};
|
||||
})]
|
||||
};
|
||||
})]
|
||||
}).ToList();
|
||||
|
||||
|
|
@ -110,7 +179,8 @@ namespace PepperDash.Essentials.Core.Web.RequestHandlers
|
|||
{
|
||||
Devices = devices,
|
||||
TieLines = tielines,
|
||||
CurrentRoutes = currentRoutes
|
||||
CurrentRoutes = currentRoutes,
|
||||
SinkCurrentSources = BuildSinkCurrentSources(tileChildren)
|
||||
};
|
||||
|
||||
var jsonResponse = JsonConvert.SerializeObject(response, Formatting.Indented);
|
||||
|
|
@ -122,6 +192,50 @@ namespace PepperDash.Essentials.Core.Web.RequestHandlers
|
|||
context.Response.Write(jsonResponse, false);
|
||||
context.Response.End();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds current-source info for every sink device, read directly from each sink's own
|
||||
/// <see cref="ICurrentSources"/> bookkeeping (see <see cref="RoutingGraphHelpers.GetCurrentSourceKey"/>).
|
||||
/// Unlike <see cref="RouteDescriptorCollection"/>-based <see cref="CurrentRouteGroupInfo"/>, this
|
||||
/// also reflects routes made via a device-specific bulk API (e.g.
|
||||
/// <c>IHasDynamicMultiviewLayout.ApplyDynamicLayout</c>) that never creates a
|
||||
/// <see cref="RouteDescriptor"/> or <see cref="TieLine"/> at all, so it's what the dev tools app
|
||||
/// should use to seed initial sink-routing state on page load.
|
||||
/// </summary>
|
||||
private static List<SinkCurrentSourceInfo> BuildSinkCurrentSources(
|
||||
Dictionary<string, RoutingGraphHelpers.TileChildInfo> tileChildren)
|
||||
{
|
||||
var result = new List<SinkCurrentSourceInfo>();
|
||||
|
||||
foreach (var device in DeviceManager.AllDevices.OfType<IRoutingSinkWithFeedback>())
|
||||
{
|
||||
if (device.CurrentInputPort == null)
|
||||
continue;
|
||||
|
||||
var sourceKey = RoutingGraphHelpers.GetCurrentSourceKey(device);
|
||||
if (string.IsNullOrEmpty(sourceKey))
|
||||
continue;
|
||||
|
||||
var deviceKey = device.Key;
|
||||
var inputPortKey = device.CurrentInputPort.Key;
|
||||
|
||||
if (tileChildren.TryGetValue(deviceKey, out var tileInfo))
|
||||
{
|
||||
deviceKey = tileInfo.Parent.Key;
|
||||
inputPortKey = RoutingGraphHelpers.QualifyTilePortKey(tileInfo.TileNumber, inputPortKey);
|
||||
}
|
||||
|
||||
result.Add(new SinkCurrentSourceInfo
|
||||
{
|
||||
DeviceKey = deviceKey,
|
||||
InputPortKey = inputPortKey,
|
||||
SourceDeviceKey = sourceKey,
|
||||
SignalType = device.CurrentInputPort.Type.ToString()
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -148,6 +262,44 @@ namespace PepperDash.Essentials.Core.Web.RequestHandlers
|
|||
/// </summary>
|
||||
[JsonProperty("currentRoutes")]
|
||||
public List<CurrentRouteGroupInfo> CurrentRoutes { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the current source feeding each sink device, read directly from each sink's own
|
||||
/// current-source bookkeeping. Covers routes made via device-specific bulk APIs (e.g.
|
||||
/// dynamic multiview layouts) that <see cref="CurrentRoutes"/> does not.
|
||||
/// </summary>
|
||||
[JsonProperty("sinkCurrentSources")]
|
||||
public List<SinkCurrentSourceInfo> SinkCurrentSources { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents the source currently feeding a single sink device's input port
|
||||
/// </summary>
|
||||
public class SinkCurrentSourceInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the key of the sink device (or its IRoutingSinkWithLayouts parent, if this is a tile)
|
||||
/// </summary>
|
||||
[JsonProperty("deviceKey")]
|
||||
public string DeviceKey { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the key of the input port currently receiving the source
|
||||
/// </summary>
|
||||
[JsonProperty("inputPortKey")]
|
||||
public string InputPortKey { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the key of the device currently feeding this input
|
||||
/// </summary>
|
||||
[JsonProperty("sourceDeviceKey")]
|
||||
public string SourceDeviceKey { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the signal type of the input port (e.g., AudioVideo, Audio, Video, etc.)
|
||||
/// </summary>
|
||||
[JsonProperty("signalType")]
|
||||
public string SignalType { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -33,6 +33,11 @@ public class RoutingFeedbackWebsocket : IKeyed
|
|||
|
||||
private readonly Dictionary<string, Timer> _debounceTimers = new Dictionary<string, Timer>();
|
||||
|
||||
// Tile-sink children of an IRoutingSinkWithLayouts device (e.g. a multiview decoder's per-window
|
||||
// sinks) are reported to clients as synthesized inputs on the parent's node, rather than as their
|
||||
// own separate nodes - see RoutingGraphHelpers. Rebuilt whenever the server (re)starts.
|
||||
private Dictionary<string, RoutingGraphHelpers.TileChildInfo> _tileChildren = new Dictionary<string, RoutingGraphHelpers.TileChildInfo>();
|
||||
|
||||
private static readonly JsonSerializerSettings JsonSettings = new JsonSerializerSettings
|
||||
{
|
||||
ContractResolver = new CamelCasePropertyNamesContractResolver(),
|
||||
|
|
@ -157,7 +162,7 @@ public class RoutingFeedbackWebsocket : IKeyed
|
|||
internal string GetSnapshotMessage()
|
||||
{
|
||||
var midpointRoutes = new Dictionary<string, List<MidpointRouteDto>>();
|
||||
var sinkRoutes = new Dictionary<string, SinkRouteDto>();
|
||||
var sinkRoutes = new Dictionary<string, List<SinkRouteDto>>();
|
||||
|
||||
// Collect midpoint current routes
|
||||
var midpointDevices = DeviceManager.AllDevices.OfType<IRoutingMidpointWithFeedback>();
|
||||
|
|
@ -177,27 +182,42 @@ public class RoutingFeedbackWebsocket : IKeyed
|
|||
.ToList();
|
||||
}
|
||||
|
||||
// Collect sink current sources
|
||||
// Collect sink current sources directly from each sink's own current-source bookkeeping
|
||||
// (ICurrentSources, part of IRoutingSinkWithFeedback), which is authoritative regardless of
|
||||
// whether the route was made via a tie line (ReleaseAndMakeRoute) or a device-specific bulk
|
||||
// API (e.g. IHasDynamicMultiviewLayout.ApplyDynamicLayout) that never touches
|
||||
// TieLineCollection/RouteDescriptorCollection at all.
|
||||
var sinkDevices = DeviceManager.AllDevices.OfType<IRoutingSinkWithFeedback>();
|
||||
foreach (var device in sinkDevices)
|
||||
{
|
||||
if (device.CurrentInputPort == null)
|
||||
continue;
|
||||
|
||||
// Trace back to find source
|
||||
var tieLine = TieLineCollection.Default.FirstOrDefault(tl =>
|
||||
tl.DestinationPort.Key == device.CurrentInputPort.Key &&
|
||||
tl.DestinationPort.ParentDevice.Key == device.CurrentInputPort.ParentDevice.Key);
|
||||
var sourceKey = RoutingGraphHelpers.GetCurrentSourceKey(device);
|
||||
if (string.IsNullOrEmpty(sourceKey))
|
||||
continue;
|
||||
|
||||
if (tieLine != null)
|
||||
var deviceKey = device.Key;
|
||||
var inputPortKey = device.CurrentInputPort.Key;
|
||||
|
||||
if (_tileChildren.TryGetValue(deviceKey, out var tileInfo))
|
||||
{
|
||||
sinkRoutes[device.Key] = new SinkRouteDto
|
||||
{
|
||||
InputPortKey = device.CurrentInputPort.Key,
|
||||
SourceDeviceKey = tieLine.SourcePort.ParentDevice.Key,
|
||||
SignalType = device.CurrentInputPort.Type.ToString()
|
||||
};
|
||||
deviceKey = tileInfo.Parent.Key;
|
||||
inputPortKey = RoutingGraphHelpers.QualifyTilePortKey(tileInfo.TileNumber, inputPortKey);
|
||||
}
|
||||
|
||||
if (!sinkRoutes.TryGetValue(deviceKey, out var routes))
|
||||
{
|
||||
routes = new List<SinkRouteDto>();
|
||||
sinkRoutes[deviceKey] = routes;
|
||||
}
|
||||
|
||||
routes.Add(new SinkRouteDto
|
||||
{
|
||||
InputPortKey = inputPortKey,
|
||||
SourceDeviceKey = sourceKey,
|
||||
SignalType = device.CurrentInputPort.Type.ToString()
|
||||
});
|
||||
}
|
||||
|
||||
var snapshot = new RoutingSnapshotDto
|
||||
|
|
@ -212,6 +232,8 @@ public class RoutingFeedbackWebsocket : IKeyed
|
|||
|
||||
private void SubscribeToRoutingEvents()
|
||||
{
|
||||
_tileChildren = RoutingGraphHelpers.BuildTileChildMap();
|
||||
|
||||
var midpointDevices = DeviceManager.AllDevices.OfType<IRoutingMidpointWithFeedback>();
|
||||
foreach (var device in midpointDevices)
|
||||
{
|
||||
|
|
@ -267,22 +289,31 @@ public class RoutingFeedbackWebsocket : IKeyed
|
|||
|
||||
private void HandleSinkInputChanged(IRoutingSinkWithFeedback sender, RoutingInputPort currentInputPort)
|
||||
{
|
||||
// Tile-sink children are reported under their IRoutingSinkWithLayouts parent's key, with a
|
||||
// qualified port key, so clients see this as an input change on the parent's node rather than
|
||||
// on a device that isn't otherwise represented in the graph.
|
||||
var deviceKey = sender.Key;
|
||||
var inputPortKey = currentInputPort?.Key ?? "";
|
||||
|
||||
if (_tileChildren.TryGetValue(deviceKey, out var tileInfo))
|
||||
{
|
||||
deviceKey = tileInfo.Parent.Key;
|
||||
if (!string.IsNullOrEmpty(inputPortKey))
|
||||
inputPortKey = RoutingGraphHelpers.QualifyTilePortKey(tileInfo.TileNumber, inputPortKey);
|
||||
}
|
||||
|
||||
DebounceBroadcast($"sink-{sender.Key}", () =>
|
||||
{
|
||||
var sourceDeviceKey = "";
|
||||
if (currentInputPort != null)
|
||||
{
|
||||
var tieLine = TieLineCollection.Default.FirstOrDefault(tl =>
|
||||
tl.DestinationPort.Key == currentInputPort.Key &&
|
||||
tl.DestinationPort.ParentDevice.Key == currentInputPort.ParentDevice.Key);
|
||||
sourceDeviceKey = tieLine?.SourcePort.ParentDevice.Key ?? "";
|
||||
}
|
||||
// Read the source directly from the sink's own current-source bookkeeping (see
|
||||
// GetCurrentSourceKey) rather than tracing a tie line - a route made via a
|
||||
// device-specific bulk API (e.g. ApplyDynamicLayout) never creates a tie line at all.
|
||||
var sourceDeviceKey = currentInputPort != null ? (RoutingGraphHelpers.GetCurrentSourceKey(sender) ?? "") : "";
|
||||
|
||||
var msg = new SinkInputChangedDto
|
||||
{
|
||||
Type = "sinkInputChanged",
|
||||
DeviceKey = sender.Key,
|
||||
InputPortKey = currentInputPort?.Key ?? "",
|
||||
DeviceKey = deviceKey,
|
||||
InputPortKey = inputPortKey,
|
||||
SourceDeviceKey = sourceDeviceKey,
|
||||
SignalType = currentInputPort?.Type.ToString() ?? ""
|
||||
};
|
||||
|
|
@ -360,7 +391,7 @@ public class RoutingFeedbackWebsocket : IKeyed
|
|||
{
|
||||
public string Type { get; set; }
|
||||
public Dictionary<string, List<MidpointRouteDto>> MidpointRoutes { get; set; }
|
||||
public Dictionary<string, SinkRouteDto> SinkRoutes { get; set; }
|
||||
public Dictionary<string, List<SinkRouteDto>> SinkRoutes { get; set; }
|
||||
}
|
||||
|
||||
private class MidpointRouteChangedDto
|
||||
|
|
|
|||
89
src/PepperDash.Essentials.Core/Web/RoutingGraphHelpers.cs
Normal file
89
src/PepperDash.Essentials.Core/Web/RoutingGraphHelpers.cs
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using PepperDash.Core;
|
||||
|
||||
namespace PepperDash.Essentials.Core.Web;
|
||||
|
||||
/// <summary>
|
||||
/// Shared helpers for building routing-graph data for consumers of the routing dev tools
|
||||
/// (<see cref="RequestHandlers.GetRoutingDevicesAndTieLinesHandler"/> and
|
||||
/// <see cref="RoutingFeedbackWebsocket"/>).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Devices that implement <see cref="IRoutingSinkWithLayouts"/> (e.g. a multiview decoder) expose a
|
||||
/// set of per-tile child sink devices (<see cref="IRoutingSinkWithLayouts.WindowTileSinks"/>), each of
|
||||
/// which is independently registered with <see cref="DeviceManager"/> and independently routable.
|
||||
/// Rendered naively, each tile shows up as its own top-level node in the routing graph, which is
|
||||
/// confusing - a multiview decoder with N tiles doesn't look like N separate devices to a user.
|
||||
/// These helpers let routing-graph consumers instead: skip tile-sink children when enumerating
|
||||
/// top-level devices, and represent each tile as a distinctly-keyed input "edge target" on the
|
||||
/// parent's own node (see <see cref="QualifyTilePortKey"/>), remapping any tie line / active route
|
||||
/// that targets a tile so it points at the parent device instead.
|
||||
/// </remarks>
|
||||
public static class RoutingGraphHelpers
|
||||
{
|
||||
/// <summary>
|
||||
/// Describes where a tile-sink child device (<see cref="IRoutingSinkWithLayouts.WindowTileSinks"/>)
|
||||
/// belongs, for remapping it back to its parent's node in routing-graph output.
|
||||
/// </summary>
|
||||
public readonly struct TileChildInfo
|
||||
{
|
||||
/// <summary>The parent device this tile belongs to.</summary>
|
||||
public IRoutingSinkWithLayouts Parent { get; }
|
||||
|
||||
/// <summary>The tile's 1-based window number within the parent's layout.</summary>
|
||||
public int TileNumber { get; }
|
||||
|
||||
/// <summary>Initializes a new instance of the <see cref="TileChildInfo"/> struct.</summary>
|
||||
public TileChildInfo(IRoutingSinkWithLayouts parent, int tileNumber)
|
||||
{
|
||||
Parent = parent;
|
||||
TileNumber = tileNumber;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds a map of every tile-sink child device key (across all <see cref="IRoutingSinkWithLayouts"/>
|
||||
/// devices currently in <see cref="DeviceManager"/>) to its parent device and tile number.
|
||||
/// </summary>
|
||||
public static Dictionary<string, TileChildInfo> BuildTileChildMap()
|
||||
{
|
||||
var map = new Dictionary<string, TileChildInfo>();
|
||||
|
||||
foreach (var parent in DeviceManager.AllDevices.OfType<IRoutingSinkWithLayouts>())
|
||||
{
|
||||
foreach (var kvp in parent.WindowTileSinks)
|
||||
{
|
||||
if (kvp.Value is not IKeyed tile || string.IsNullOrEmpty(tile.Key))
|
||||
continue;
|
||||
|
||||
map[tile.Key] = new TileChildInfo(parent, kvp.Key);
|
||||
}
|
||||
}
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds a graph-unique port key for a tile's port, so multiple tiles synthesized onto the same
|
||||
/// parent node don't collide (every tile's own <c>InputPorts</c> collection typically contains a
|
||||
/// single, identically-keyed port, e.g. "tileInput").
|
||||
/// </summary>
|
||||
public static string QualifyTilePortKey(int tileNumber, string portKey) => $"tile{tileNumber}:{portKey}";
|
||||
|
||||
/// <summary>
|
||||
/// Gets the device key of whatever source is currently feeding a sink's Video signal (falling back
|
||||
/// to Audio), read directly from its own <see cref="ICurrentSources"/> bookkeeping (part of
|
||||
/// <see cref="IRoutingSinkWithFeedback"/>). This is authoritative regardless of whether the route
|
||||
/// was made via a tie line (<c>ReleaseAndMakeRoute</c>) or a device-specific bulk API (e.g.
|
||||
/// <c>IHasDynamicMultiviewLayout.ApplyDynamicLayout</c>) that never touches
|
||||
/// <c>TieLineCollection</c>/<c>RouteDescriptorCollection</c> at all.
|
||||
/// </summary>
|
||||
public static string GetCurrentSourceKey(IRoutingSinkWithFeedback device)
|
||||
{
|
||||
if (device.CurrentSourceKeys.TryGetValue(eRoutingSignalType.Video, out var videoKey) && !string.IsNullOrEmpty(videoKey))
|
||||
return videoKey;
|
||||
|
||||
return device.CurrentSourceKeys.TryGetValue(eRoutingSignalType.Audio, out var audioKey) ? audioKey : null;
|
||||
}
|
||||
}
|
||||
|
|
@ -1305,7 +1305,16 @@ namespace PepperDash.Essentials.WebSocketServer
|
|||
}
|
||||
byte[] contents = File.ReadAllBytes(filePath);
|
||||
res.ContentLength64 = contents.LongLength;
|
||||
res.Close(contents, true);
|
||||
try
|
||||
{
|
||||
res.Close(contents, true);
|
||||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
// Client disconnected (e.g. panel refreshed/navigated away) before the response could be
|
||||
// fully written. This is a benign, expected condition, not an application error.
|
||||
this.LogVerbose("Client disconnected before image response could be sent: {message}", ex.Message);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -1427,7 +1436,17 @@ namespace PepperDash.Essentials.WebSocketServer
|
|||
}
|
||||
|
||||
res.ContentLength64 = contents.LongLength;
|
||||
res.Close(contents, true);
|
||||
try
|
||||
{
|
||||
res.Close(contents, true);
|
||||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
// Client disconnected (e.g. panel refreshed/navigated away, or made a duplicate request)
|
||||
// before the response could be fully written. This is a benign, expected condition
|
||||
// (e.g. a "Broken pipe" IOException), not an application error worth logging at Error level.
|
||||
this.LogVerbose("Client disconnected before user app response could be sent: {message}", ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue