diff --git a/src/PepperDash.Essentials.Core/Routing/IRoutingSinkWithLayoutState.cs b/src/PepperDash.Essentials.Core/Routing/IRoutingSinkWithLayoutState.cs new file mode 100644 index 00000000..a0ac9bec --- /dev/null +++ b/src/PepperDash.Essentials.Core/Routing/IRoutingSinkWithLayoutState.cs @@ -0,0 +1,46 @@ +using System; + +namespace PepperDash.Essentials.Core; + +/// +/// Extends for devices that can report the current shape of +/// their multiview canvas, along with the position/size, stacking order and routed source of every +/// tile, in a generic, product-agnostic form. This is intentionally decoupled from the routing +/// graph (nodes/edges/tie-lines) built from - +/// it exists to support a separate visualization concern: a mock-up of what is actually displayed +/// on the monitor fed by the decoder, suitable for a React UI or the developer tools Routing page. +/// +public interface IRoutingSinkWithLayoutState : IRoutingSinkWithLayouts +{ + /// + /// Gets the current multiview canvas/tile layout, or null if no layout is currently active or + /// applicable (e.g. the device is not currently in a multiview mode). + /// + MultiviewLayoutState CurrentLayout { get; } + + /// + /// Raised whenever the canvas shape, a tile's geometry/stacking order, or a tile's routed source + /// changes. + /// + event EventHandler LayoutChanged; +} + +/// +/// Event arguments for . +/// +public class MultiviewLayoutStateEventArgs : EventArgs +{ + /// + /// The current multiview layout state at the time this event was raised, or null if no layout + /// is currently active. + /// + public MultiviewLayoutState CurrentLayout { get; } + + /// + /// Initializes a new instance of the class. + /// + public MultiviewLayoutStateEventArgs(MultiviewLayoutState currentLayout) + { + CurrentLayout = currentLayout; + } +} diff --git a/src/PepperDash.Essentials.Core/Routing/MultiviewLayoutState.cs b/src/PepperDash.Essentials.Core/Routing/MultiviewLayoutState.cs new file mode 100644 index 00000000..382adebe --- /dev/null +++ b/src/PepperDash.Essentials.Core/Routing/MultiviewLayoutState.cs @@ -0,0 +1,85 @@ +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace PepperDash.Essentials.Core; + +/// +/// Describes the current shape of a multiview canvas and the position, size, stacking order and +/// routed source of every visible tile within it. Fully product-agnostic (no dependency on any +/// particular decoder/hardware type) and JSON-serializable so it can be sent as-is over the routing +/// feedback WebSocket () and the routing devices/tie-lines +/// HTTP snapshot (), for +/// rendering a visual mock-up of what is actually displayed on the fed monitor. +/// +public class MultiviewLayoutState +{ + /// + /// Width, in pixels, of the canvas that positions/sizes are expressed + /// against (typically the decoder's current output resolution width). + /// + [JsonProperty("canvasWidth")] + public int CanvasWidth { get; set; } + + /// + /// Height, in pixels, of the canvas that positions/sizes are expressed + /// against (typically the decoder's current output resolution height). + /// + [JsonProperty("canvasHeight")] + public int CanvasHeight { get; set; } + + /// + /// Position, size, stacking order and routed source for every visible tile in the layout. + /// + [JsonProperty("tiles")] + public List Tiles { get; set; } = new List(); +} + +/// +/// Describes a single tile/window within a . +/// +public class MultiviewTileState +{ + /// + /// 1-based tile/window number, matching the key in + /// . + /// + [JsonProperty("tileNumber")] + public int TileNumber { get; set; } + + /// + /// Device key of this tile's child sink, so a client can + /// cross-reference existing routing-feedback data (e.g. sink current-source state) for this tile + /// without re-deriving it. + /// + [JsonProperty("tileSinkKey")] + public string TileSinkKey { get; set; } + + /// Left edge of the tile, in pixels, within the canvas. + [JsonProperty("x")] + public int X { get; set; } + + /// Top edge of the tile, in pixels, within the canvas. + [JsonProperty("y")] + public int Y { get; set; } + + /// Width of the tile, in pixels. + [JsonProperty("width")] + public int Width { get; set; } + + /// Height of the tile, in pixels. + [JsonProperty("height")] + public int Height { get; set; } + + /// + /// Stacking order for overlapping tiles (e.g. picture-in-picture/overlay layouts). Tiles with + /// higher values are drawn on top of tiles with lower values. + /// + [JsonProperty("zOrder")] + public int ZOrder { get; set; } + + /// + /// Device key of the source currently routed to this tile, or null if the tile is empty. + /// + [JsonProperty("sourceDeviceKey")] + public string SourceDeviceKey { get; set; } +} diff --git a/src/PepperDash.Essentials.Core/Web/RequestHandlers/GetRoutingDevicesAndTieLinesHandler.cs b/src/PepperDash.Essentials.Core/Web/RequestHandlers/GetRoutingDevicesAndTieLinesHandler.cs index 0c5a3fae..0c3e17e6 100644 --- a/src/PepperDash.Essentials.Core/Web/RequestHandlers/GetRoutingDevicesAndTieLinesHandler.cs +++ b/src/PepperDash.Essentials.Core/Web/RequestHandlers/GetRoutingDevicesAndTieLinesHandler.cs @@ -180,7 +180,8 @@ namespace PepperDash.Essentials.Core.Web.RequestHandlers Devices = devices, TieLines = tielines, CurrentRoutes = currentRoutes, - SinkCurrentSources = BuildSinkCurrentSources(tileChildren) + SinkCurrentSources = BuildSinkCurrentSources(tileChildren), + MultiviewLayouts = RoutingGraphHelpers.BuildMultiviewLayoutSnapshot() }; var jsonResponse = JsonConvert.SerializeObject(response, Formatting.Indented); @@ -270,6 +271,16 @@ namespace PepperDash.Essentials.Core.Web.RequestHandlers /// [JsonProperty("sinkCurrentSources")] public List SinkCurrentSources { get; set; } + + /// + /// Gets or sets the current multiview canvas/tile layout for every device implementing + /// , keyed by device key. Devices with no currently + /// active layout are omitted. Lets a client render an initial visual mock-up of each + /// multiview decoder's monitor output without waiting for a routing feedback WebSocket + /// connection - see . + /// + [JsonProperty("multiviewLayouts")] + public Dictionary MultiviewLayouts { get; set; } } /// diff --git a/src/PepperDash.Essentials.Core/Web/RoutingFeedbackWebsocket.cs b/src/PepperDash.Essentials.Core/Web/RoutingFeedbackWebsocket.cs index 4e53e261..6cdf1e8e 100644 --- a/src/PepperDash.Essentials.Core/Web/RoutingFeedbackWebsocket.cs +++ b/src/PepperDash.Essentials.Core/Web/RoutingFeedbackWebsocket.cs @@ -224,7 +224,8 @@ public class RoutingFeedbackWebsocket : IKeyed { Type = "snapshot", MidpointRoutes = midpointRoutes, - SinkRoutes = sinkRoutes + SinkRoutes = sinkRoutes, + Layouts = RoutingGraphHelpers.BuildMultiviewLayoutSnapshot() }; return JsonConvert.SerializeObject(snapshot, JsonSettings); @@ -245,6 +246,12 @@ public class RoutingFeedbackWebsocket : IKeyed { device.InputChanged += HandleSinkInputChanged; } + + var layoutDevices = DeviceManager.AllDevices.OfType(); + foreach (var device in layoutDevices) + { + device.LayoutChanged += HandleLayoutChanged; + } } private void UnsubscribeFromRoutingEvents() @@ -260,6 +267,12 @@ public class RoutingFeedbackWebsocket : IKeyed { device.InputChanged -= HandleSinkInputChanged; } + + var layoutDevices = DeviceManager.AllDevices.OfType(); + foreach (var device in layoutDevices) + { + device.LayoutChanged -= HandleLayoutChanged; + } } private void HandleMidpointRouteChanged(IRoutingMidpointWithFeedback midpoint, RouteSwitchDescriptor newRoute) @@ -287,6 +300,24 @@ public class RoutingFeedbackWebsocket : IKeyed }); } + private void HandleLayoutChanged(object sender, MultiviewLayoutStateEventArgs e) + { + if (sender is not IKeyed device) + return; + + DebounceBroadcast($"layout-{device.Key}", () => + { + var msg = new LayoutChangedDto + { + Type = "layoutChanged", + DeviceKey = device.Key, + Layout = e.CurrentLayout + }; + + Broadcast(JsonConvert.SerializeObject(msg, JsonSettings)); + }); + } + private void HandleSinkInputChanged(IRoutingSinkWithFeedback sender, RoutingInputPort currentInputPort) { // Tile-sink children are reported under their IRoutingSinkWithLayouts parent's key, with a @@ -392,6 +423,14 @@ public class RoutingFeedbackWebsocket : IKeyed public string Type { get; set; } public Dictionary> MidpointRoutes { get; set; } public Dictionary> SinkRoutes { get; set; } + public Dictionary Layouts { get; set; } + } + + private class LayoutChangedDto + { + public string Type { get; set; } + public string DeviceKey { get; set; } + public MultiviewLayoutState Layout { get; set; } } private class MidpointRouteChangedDto diff --git a/src/PepperDash.Essentials.Core/Web/RoutingGraphHelpers.cs b/src/PepperDash.Essentials.Core/Web/RoutingGraphHelpers.cs index 714823ec..387c8824 100644 --- a/src/PepperDash.Essentials.Core/Web/RoutingGraphHelpers.cs +++ b/src/PepperDash.Essentials.Core/Web/RoutingGraphHelpers.cs @@ -86,4 +86,27 @@ public static class RoutingGraphHelpers return device.CurrentSourceKeys.TryGetValue(eRoutingSignalType.Audio, out var audioKey) ? audioKey : null; } + + /// + /// Builds a snapshot of the current for every device in + /// that implements , keyed + /// by device key. Devices with no currently active layout (CurrentLayout == null) are + /// omitted. Shared by (initial + /// HTTP snapshot) and (WebSocket snapshot on connect). + /// + public static Dictionary BuildMultiviewLayoutSnapshot() + { + var result = new Dictionary(); + + foreach (var device in DeviceManager.AllDevices.OfType()) + { + var layout = device.CurrentLayout; + if (layout == null) + continue; + + result[device.Key] = layout; + } + + return result; + } }