TASK 4 ship step 5.5: social bar bug fixes + bar on the live output — direction-snap drag (SocialBarSnap.Decide + ClearValue, local Canvas.Top was overriding the binding), fediverse nodeinfo subdomain probing + load-time heal (settable FediverseSoftware), SocialBarRenderer strip blitted above the flash via a re-read FramePump socialBar seam — 155 tests passing, 0 warnings

This commit is contained in:
2026-08-13 09:29:58 -07:00
parent e72ba71165
commit ac60a26d82
16 changed files with 666 additions and 52 deletions
+18 -9
View File
@@ -18,13 +18,16 @@ public sealed class SceneCompositor
/// Composite <paramref name="scene"/> into the tier's output frame. Layer order (back →
/// front): live backdrop (the scene's <c>IsBackdrop</c> source) → background image →
/// visible elements (z-order = <c>Elements</c> order, mirroring the XAML DataTemplate) →
/// branding flash. Transparent regions read opaque black.
/// branding flash → social bar (a global overlay; spans the master width at
/// <paramref name="socialBarTop"/>). Transparent regions read opaque black.
/// </summary>
public VideoFrame Render(
Scene scene,
Func<SceneElement, VideoFrame?> frameFor,
VideoFrame? flashFrame,
CompositorOptions options)
CompositorOptions options,
VideoFrame? socialBarFrame = null,
int socialBarTop = 0)
{
if (scene == null) throw new ArgumentNullException(nameof(scene));
if (frameFor == null) throw new ArgumentNullException(nameof(frameFor));
@@ -79,7 +82,10 @@ public sealed class SceneCompositor
}
if (flashFrame != null)
BlitFlash(buffer, cropW, cropH, options, flashFrame);
BlitOverlay(buffer, cropW, cropH, options, flashFrame, 0, 0);
if (socialBarFrame != null && socialBarFrame.Width > 0 && socialBarFrame.Height > 0)
BlitOverlay(buffer, cropW, cropH, options, socialBarFrame, 0, socialBarTop);
return StretchMath.BilinearScale(
new VideoFrame(cropW, cropH, buffer), options.OutputWidth, options.OutputHeight);
@@ -166,17 +172,20 @@ public sealed class SceneCompositor
}
}
/// <summary>1:1 copy of the master-sized branding flash, cropped to the active source rect.</summary>
private static void BlitFlash(byte[] dst, int dstW, int dstH, CompositorOptions options, VideoFrame flash)
/// <summary>1:1 copy of a master-sized overlay (flash, social bar) cropped to the
/// active source rect. The overlay is positioned in master space by (sx0, sy0).</summary>
private static void BlitOverlay(
byte[] dst, int dstW, int dstH, CompositorOptions options,
VideoFrame overlay, int sx0, int sy0)
{
for (var y = 0; y < dstH; y++)
{
for (var x = 0; x < dstW; x++)
{
var sx = x + options.SourceRectX;
var sy = y + options.SourceRectY;
if (sx >= flash.Width || sy >= flash.Height) continue;
var sample = StretchMath.SampleBgra(flash.BgraPixels, flash.Width, flash.Height, sx, sy);
var sx = x + options.SourceRectX - sx0;
var sy = y + options.SourceRectY - sy0;
if (sx < 0 || sy < 0 || sx >= overlay.Width || sy >= overlay.Height) continue;
var sample = StretchMath.SampleBgra(overlay.BgraPixels, overlay.Width, overlay.Height, sx, sy);
BlendPixel(dst, (y * dstW + x) * 4, sample, 1f);
}
}
+107
View File
@@ -0,0 +1,107 @@
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Effects;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
using ytLive.Models;
namespace ytLive.Services.Compositor;
/// <summary>
/// Rasterizes the global social bar into a transparent BGRA8 strip the output
/// compositor overlays onto the master frame (top or bottom edge). Replicates the
/// preview's bar DataTemplate in code — one Path (logo) + one TextBlock (handle)
/// per entry, white on transparent, green glow — then software-renders it via
/// <c>RenderTargetBitmap</c> and converts Pbgra32 (premultiplied) to straight-alpha
/// BGRA for the compositor. WPF glue like <see cref="StaticPixelCache"/>: the
/// compositor core itself stays pure byte-math. Renders on the UI thread only;
/// the returned frame is immutable afterwards, so the frame pump may read it from
/// any thread.
/// </summary>
public static class SocialBarRenderer
{
/// <summary>Icon size + the preview's 8px top/bottom margins.</summary>
private const int ContentHeight = 40;
/// <summary>Room for the green DropShadowEffect blur to bleed past the content.</summary>
private const int GlowPad = 24;
/// <summary>Master-frame width — the bar spans the full 1920px output.</summary>
public const int Width = 1920;
/// <summary>Returns the strip frame, or null when there is nothing to render.</summary>
public static VideoFrame? Render(IEnumerable<SocialEntry>? entries)
{
var list = entries?.Where(e => e != null).ToList();
if (list == null || list.Count == 0) return null;
var height = ContentHeight + GlowPad * 2;
var row = new StackPanel { Orientation = Orientation.Horizontal };
foreach (var entry in list)
{
var item = new StackPanel
{
Orientation = Orientation.Horizontal,
Margin = new Thickness(0, 0, 20, 0),
};
item.Children.Add(new Path
{
Data = Geometry.Parse(entry.LogoData),
Fill = Brushes.White,
Width = 24,
Height = 24,
Stretch = Stretch.Uniform,
VerticalAlignment = VerticalAlignment.Center,
});
item.Children.Add(new TextBlock
{
Text = entry.Handle,
Foreground = Brushes.White,
FontSize = 14,
VerticalAlignment = VerticalAlignment.Center,
Margin = new Thickness(8, 0, 0, 0),
MaxWidth = 200,
TextTrimming = TextTrimming.CharacterEllipsis,
});
row.Children.Add(item);
}
// The glow is baked in so the output bar matches the preview's green halo.
var container = new Grid
{
Width = Width,
Height = height,
Background = Brushes.Transparent,
Effect = new DropShadowEffect { Color = Color.FromRgb(0x2e, 0xcc, 0x71), BlurRadius = 18, ShadowDepth = 0, Opacity = 0.9 },
};
var centered = new Grid { HorizontalAlignment = HorizontalAlignment.Center, Margin = new Thickness(0, GlowPad, 0, 0) };
centered.Children.Add(row);
container.Children.Add(centered);
container.Measure(new Size(Width, height));
container.Arrange(new Rect(0, 0, Width, height));
var bitmap = new RenderTargetBitmap(Width, height, 96, 96, PixelFormats.Pbgra32);
bitmap.Render(container);
var pixels = new byte[Width * height * 4];
bitmap.CopyPixels(pixels, Width * 4, 0);
// Pbgra32 is premultiplied — unpremultiply so the compositor's straight-alpha
// source-over blend doesn't darken the logo edges with a halo.
for (var i = 0; i < pixels.Length; i += 4)
{
var a = pixels[i + 3];
if (a == 0 || a == 255) continue;
pixels[i] = (byte)(pixels[i] * 255 / a);
pixels[i + 1] = (byte)(pixels[i + 1] * 255 / a);
pixels[i + 2] = (byte)(pixels[i + 2] * 255 / a);
}
return new VideoFrame(Width, height, pixels);
}
}