mirror of
https://github.com/PepperDash/Essentials.git
synced 2026-08-31 19:08:29 +00:00
feat: add multiview layout support with new interface and state classes
This commit is contained in:
parent
f05a25c3e2
commit
6852f47ce2
5 changed files with 206 additions and 2 deletions
|
|
@ -0,0 +1,46 @@
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace PepperDash.Essentials.Core;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Extends <see cref="IRoutingSinkWithLayouts"/> 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 <see cref="IRoutingSinkWithLayouts.WindowTileSinks"/> -
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
public interface IRoutingSinkWithLayoutState : IRoutingSinkWithLayouts
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 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).
|
||||||
|
/// </summary>
|
||||||
|
MultiviewLayoutState CurrentLayout { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Raised whenever the canvas shape, a tile's geometry/stacking order, or a tile's routed source
|
||||||
|
/// changes.
|
||||||
|
/// </summary>
|
||||||
|
event EventHandler<MultiviewLayoutStateEventArgs> LayoutChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Event arguments for <see cref="IRoutingSinkWithLayoutState.LayoutChanged"/>.
|
||||||
|
/// </summary>
|
||||||
|
public class MultiviewLayoutStateEventArgs : EventArgs
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The current multiview layout state at the time this event was raised, or null if no layout
|
||||||
|
/// is currently active.
|
||||||
|
/// </summary>
|
||||||
|
public MultiviewLayoutState CurrentLayout { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="MultiviewLayoutStateEventArgs"/> class.
|
||||||
|
/// </summary>
|
||||||
|
public MultiviewLayoutStateEventArgs(MultiviewLayoutState currentLayout)
|
||||||
|
{
|
||||||
|
CurrentLayout = currentLayout;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,85 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace PepperDash.Essentials.Core;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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 (<see cref="Web.RoutingFeedbackWebsocket"/>) and the routing devices/tie-lines
|
||||||
|
/// HTTP snapshot (<see cref="Web.RequestHandlers.GetRoutingDevicesAndTieLinesHandler"/>), for
|
||||||
|
/// rendering a visual mock-up of what is actually displayed on the fed monitor.
|
||||||
|
/// </summary>
|
||||||
|
public class MultiviewLayoutState
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Width, in pixels, of the canvas that <see cref="Tiles"/> positions/sizes are expressed
|
||||||
|
/// against (typically the decoder's current output resolution width).
|
||||||
|
/// </summary>
|
||||||
|
[JsonProperty("canvasWidth")]
|
||||||
|
public int CanvasWidth { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Height, in pixels, of the canvas that <see cref="Tiles"/> positions/sizes are expressed
|
||||||
|
/// against (typically the decoder's current output resolution height).
|
||||||
|
/// </summary>
|
||||||
|
[JsonProperty("canvasHeight")]
|
||||||
|
public int CanvasHeight { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Position, size, stacking order and routed source for every visible tile in the layout.
|
||||||
|
/// </summary>
|
||||||
|
[JsonProperty("tiles")]
|
||||||
|
public List<MultiviewTileState> Tiles { get; set; } = new List<MultiviewTileState>();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Describes a single tile/window within a <see cref="MultiviewLayoutState"/>.
|
||||||
|
/// </summary>
|
||||||
|
public class MultiviewTileState
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 1-based tile/window number, matching the key in
|
||||||
|
/// <see cref="IRoutingSinkWithLayouts.WindowTileSinks"/>.
|
||||||
|
/// </summary>
|
||||||
|
[JsonProperty("tileNumber")]
|
||||||
|
public int TileNumber { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Device key of this tile's <see cref="IRoutingSinkWithFeedback"/> 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.
|
||||||
|
/// </summary>
|
||||||
|
[JsonProperty("tileSinkKey")]
|
||||||
|
public string TileSinkKey { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Left edge of the tile, in pixels, within the canvas.</summary>
|
||||||
|
[JsonProperty("x")]
|
||||||
|
public int X { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Top edge of the tile, in pixels, within the canvas.</summary>
|
||||||
|
[JsonProperty("y")]
|
||||||
|
public int Y { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Width of the tile, in pixels.</summary>
|
||||||
|
[JsonProperty("width")]
|
||||||
|
public int Width { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Height of the tile, in pixels.</summary>
|
||||||
|
[JsonProperty("height")]
|
||||||
|
public int Height { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
[JsonProperty("zOrder")]
|
||||||
|
public int ZOrder { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Device key of the source currently routed to this tile, or null if the tile is empty.
|
||||||
|
/// </summary>
|
||||||
|
[JsonProperty("sourceDeviceKey")]
|
||||||
|
public string SourceDeviceKey { get; set; }
|
||||||
|
}
|
||||||
|
|
@ -180,7 +180,8 @@ namespace PepperDash.Essentials.Core.Web.RequestHandlers
|
||||||
Devices = devices,
|
Devices = devices,
|
||||||
TieLines = tielines,
|
TieLines = tielines,
|
||||||
CurrentRoutes = currentRoutes,
|
CurrentRoutes = currentRoutes,
|
||||||
SinkCurrentSources = BuildSinkCurrentSources(tileChildren)
|
SinkCurrentSources = BuildSinkCurrentSources(tileChildren),
|
||||||
|
MultiviewLayouts = RoutingGraphHelpers.BuildMultiviewLayoutSnapshot()
|
||||||
};
|
};
|
||||||
|
|
||||||
var jsonResponse = JsonConvert.SerializeObject(response, Formatting.Indented);
|
var jsonResponse = JsonConvert.SerializeObject(response, Formatting.Indented);
|
||||||
|
|
@ -270,6 +271,16 @@ namespace PepperDash.Essentials.Core.Web.RequestHandlers
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[JsonProperty("sinkCurrentSources")]
|
[JsonProperty("sinkCurrentSources")]
|
||||||
public List<SinkCurrentSourceInfo> SinkCurrentSources { get; set; }
|
public List<SinkCurrentSourceInfo> SinkCurrentSources { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the current multiview canvas/tile layout for every device implementing
|
||||||
|
/// <see cref="IRoutingSinkWithLayoutState"/>, 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 <see cref="RoutingGraphHelpers.BuildMultiviewLayoutSnapshot"/>.
|
||||||
|
/// </summary>
|
||||||
|
[JsonProperty("multiviewLayouts")]
|
||||||
|
public Dictionary<string, MultiviewLayoutState> MultiviewLayouts { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
||||||
|
|
@ -224,7 +224,8 @@ public class RoutingFeedbackWebsocket : IKeyed
|
||||||
{
|
{
|
||||||
Type = "snapshot",
|
Type = "snapshot",
|
||||||
MidpointRoutes = midpointRoutes,
|
MidpointRoutes = midpointRoutes,
|
||||||
SinkRoutes = sinkRoutes
|
SinkRoutes = sinkRoutes,
|
||||||
|
Layouts = RoutingGraphHelpers.BuildMultiviewLayoutSnapshot()
|
||||||
};
|
};
|
||||||
|
|
||||||
return JsonConvert.SerializeObject(snapshot, JsonSettings);
|
return JsonConvert.SerializeObject(snapshot, JsonSettings);
|
||||||
|
|
@ -245,6 +246,12 @@ public class RoutingFeedbackWebsocket : IKeyed
|
||||||
{
|
{
|
||||||
device.InputChanged += HandleSinkInputChanged;
|
device.InputChanged += HandleSinkInputChanged;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var layoutDevices = DeviceManager.AllDevices.OfType<IRoutingSinkWithLayoutState>();
|
||||||
|
foreach (var device in layoutDevices)
|
||||||
|
{
|
||||||
|
device.LayoutChanged += HandleLayoutChanged;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void UnsubscribeFromRoutingEvents()
|
private void UnsubscribeFromRoutingEvents()
|
||||||
|
|
@ -260,6 +267,12 @@ public class RoutingFeedbackWebsocket : IKeyed
|
||||||
{
|
{
|
||||||
device.InputChanged -= HandleSinkInputChanged;
|
device.InputChanged -= HandleSinkInputChanged;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var layoutDevices = DeviceManager.AllDevices.OfType<IRoutingSinkWithLayoutState>();
|
||||||
|
foreach (var device in layoutDevices)
|
||||||
|
{
|
||||||
|
device.LayoutChanged -= HandleLayoutChanged;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void HandleMidpointRouteChanged(IRoutingMidpointWithFeedback midpoint, RouteSwitchDescriptor newRoute)
|
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)
|
private void HandleSinkInputChanged(IRoutingSinkWithFeedback sender, RoutingInputPort currentInputPort)
|
||||||
{
|
{
|
||||||
// Tile-sink children are reported under their IRoutingSinkWithLayouts parent's key, with a
|
// 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 string Type { get; set; }
|
||||||
public Dictionary<string, List<MidpointRouteDto>> MidpointRoutes { get; set; }
|
public Dictionary<string, List<MidpointRouteDto>> MidpointRoutes { get; set; }
|
||||||
public Dictionary<string, List<SinkRouteDto>> SinkRoutes { get; set; }
|
public Dictionary<string, List<SinkRouteDto>> SinkRoutes { get; set; }
|
||||||
|
public Dictionary<string, MultiviewLayoutState> Layouts { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private class LayoutChangedDto
|
||||||
|
{
|
||||||
|
public string Type { get; set; }
|
||||||
|
public string DeviceKey { get; set; }
|
||||||
|
public MultiviewLayoutState Layout { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
private class MidpointRouteChangedDto
|
private class MidpointRouteChangedDto
|
||||||
|
|
|
||||||
|
|
@ -86,4 +86,27 @@ public static class RoutingGraphHelpers
|
||||||
|
|
||||||
return device.CurrentSourceKeys.TryGetValue(eRoutingSignalType.Audio, out var audioKey) ? audioKey : null;
|
return device.CurrentSourceKeys.TryGetValue(eRoutingSignalType.Audio, out var audioKey) ? audioKey : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Builds a snapshot of the current <see cref="MultiviewLayoutState"/> for every device in
|
||||||
|
/// <see cref="DeviceManager"/> that implements <see cref="IRoutingSinkWithLayoutState"/>, keyed
|
||||||
|
/// by device key. Devices with no currently active layout (<c>CurrentLayout == null</c>) are
|
||||||
|
/// omitted. Shared by <see cref="RequestHandlers.GetRoutingDevicesAndTieLinesHandler"/> (initial
|
||||||
|
/// HTTP snapshot) and <see cref="RoutingFeedbackWebsocket"/> (WebSocket snapshot on connect).
|
||||||
|
/// </summary>
|
||||||
|
public static Dictionary<string, MultiviewLayoutState> BuildMultiviewLayoutSnapshot()
|
||||||
|
{
|
||||||
|
var result = new Dictionary<string, MultiviewLayoutState>();
|
||||||
|
|
||||||
|
foreach (var device in DeviceManager.AllDevices.OfType<IRoutingSinkWithLayoutState>())
|
||||||
|
{
|
||||||
|
var layout = device.CurrentLayout;
|
||||||
|
if (layout == null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
result[device.Key] = layout;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue