Files
LlamaCasty/Services/VideoFrame.cs
T
gramps 938c5b3de4 alert ticker: draw it inside the Stream Alerts box, not as a top-edge bar
Creator 2026-09-26: "the ticker should appear over the stream alerts video, not over
the entire preview window". It was a GLOBAL 1920x48 bar blitted last at a hardcoded
(0, 0) -- it covered the whole preview width, not the alert video.

The strip is now rendered at the alert box's own size and the box's origin travels on
the frame in a new VideoFrame.Placement ((int X, int Y)?). OriginX/OriginY default to
0 when Placement is null, so every full-canvas overlay (the branding flash) is
unaffected. Every ticker blit site now reads tickerFrame.OriginX/OriginY instead of a
literal 0, 0 -- SceneCompositor x2 and FramePump's static-bake Overlay path -- and
PreviewPane.xaml's AlertTickerElement binds the same rect (AlertTickerLeft/Top/
Width/Height), so the preview cannot drift from the recording the creator is judging.

Why the position rides on the frame rather than in a full-canvas frame: a 1920x1080
overlay is 8.3MB of large-object-heap garbage per tick, ~2.5GB churned over one 10s
alert at 30fps. A box-sized strip is ~370KB. The branding flash does render
full-canvas but RECYCLES one 8MB buffer and bumps Epoch, so it never allocates per
frame; the ticker allocates per call, so it has to stay small.

Also fixed: CopyStrip now clips ROWS to the target height. The pill rasterises at its
natural 48px, so an alert box shorter than that would have written past the end of the
target buffer once the strip became box-sized.

No alert box in the scene now means no ticker at all -- there is no global position
left for it, and this is the guard against the old bar quietly coming back.

Tests (3 new facts, 27 in the file):
  ComposedOutput_PutsTheTickerInsideTheAlertBox_NotAtTheTopEdge -- renders through
    the real SceneCompositor and asserts the pixels land at (620, 430) and NOT at the
    top-left corner. The creator is judging compositing from local recordings, so the
    OUTPUT side is the side that needed proving.
  ASceneWithNoAlertBox_PublishesNoTicker
  ABoxShorterThanTheStrip_ClipsRowsInsteadOfOverrunning
Updated AlertLayer_PublishesARealTickerFrameToThePreviewSink, which asserted the old
1920 width from a box with no geometry (defaulted to 1px); it now uses the product's
own default box (680x200 at 620,430, MainViewModel.Sources.cs:54-57) and pins the
frame size and origin.

Full suite 367/367.
2026-09-27 09:43:26 -07:00

60 lines
3.1 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
namespace ytLive.Services;
/// <summary>
/// A normalized CPU frame (32bpp BGRA, tightly packed). The capture path hands
/// these to the UI thread, which copies them into the shared WriteableBitmap.
/// Deliberately the only pixel type the rest of the app knows about — every
/// future capture source (screen, background-removed webcam) feeds the same seam.
/// </summary>
public sealed class VideoFrame
{
public int Width { get; }
public int Height { get; }
public byte[] BgraPixels { get; }
public int Stride => Width * 4;
/// <summary>Producer frame epoch — a monotonically increasing number stamped by
/// sources that RECYCLE their pixel buffers (the screen-capture ring, take-11 spike
/// fix). Consumers that key caches on buffer identity (the compositor's paste cache)
/// MUST include it, or a recycled array false-hits with stale content. Producers
/// that hand out fresh arrays per frame leave it 0 — identity alone is then enough.</summary>
public long Epoch { get; init; }
/// <summary>Producer contract: every alpha byte in <see cref="BgraPixels"/> is 255
/// (DWM capture surfaces and MediaCapture video carry no alpha — the OS fills 255).
/// Lets the compositor take a straight-copy fast path for a full-canvas opaque
/// layer instead of 2M per-pixel blends. NEVER set it for paths whose pixels can
/// be transparent (static art, web overlays, chat, the social-bar strip).</summary>
public bool IsOpaque { get; init; }
/// <summary>WebSource crop metadata: the alpha bounding box of the widget content
/// within the full canvas (X/Y/W/H in pixels). When set, the compositor uses these
/// bounds for Fill-style scaling (stretch to cover, no aspect preservation) instead of
/// UniformToFill. The transparent margins of the full canvas are preserved so they
/// reveal layers beneath — the crop defines the actual content region.</summary>
public (int X, int Y, int W, int H)? CropBounds { get; init; }
/// <summary>Where a SMALLER-than-canvas overlay composites, in master pixels
/// (creator ruling 2026-09-26: the alert ticker draws INSIDE the Stream Alerts video
/// box, not as a full-width strip pinned to the top edge). <c>null</c> — the default
/// — means the canvas origin, which is what every full-canvas overlay wants.
/// <para>This is deliberately NOT a full-canvas frame: at 30–60fps a 1920×1080
/// overlay is 8.3MB of large-object-heap garbage per tick, and a 10s alert would
/// churn ~2.5GB. A box-sized strip is ~370KB, so the position travels on the frame
/// instead of in a 1920-wide transparent margin.</para></summary>
public (int X, int Y)? Placement { get; init; }
/// <summary>Composite origin X: <see cref="Placement"/> if set, else 0.</summary>
public int OriginX => Placement?.X ?? 0;
/// <summary>Composite origin Y: <see cref="Placement"/> if set, else 0.</summary>
public int OriginY => Placement?.Y ?? 0;
public VideoFrame(int width, int height, byte[] bgraPixels)
{
Width = width;
Height = height;
BgraPixels = bgraPixels;
}
}