diff --git a/Controls/PreviewPane.xaml b/Controls/PreviewPane.xaml
index 7ca04a8..97b092c 100644
--- a/Controls/PreviewPane.xaml
+++ b/Controls/PreviewPane.xaml
@@ -471,6 +471,13 @@
+
+
().Where(s => s.IsBrandLogo && s.IsVisible))
+ {
+ var brandFrame = frameFor(brand);
+ if (brandFrame == null) continue;
+ BlitBakedElement(buffer, cropW, cropH, options, brand, brandFrame, timings, $"brand {brand.Name}");
+ }
+
if (flashFrame != null)
{
var tFlash = System.Diagnostics.Stopwatch.GetTimestamp();
@@ -358,6 +370,9 @@ public sealed class SceneCompositor
case Source { IsBackground: true }:
case Source { Type: SourceType.Background }:
case Source { Type: SourceType.TextOverlay }:
+ // ⛔ The brand logo is drawn AFTER the blur group (creator ruling 2026-11-01):
+ // the blur is universal and must never hide the brand.
+ case Source { IsBrandLogo: true }:
case ZoomSceneElement { IsZoomActive: true }:
continue;
}
diff --git a/ViewModels/MainViewModel.BrandLogo.cs b/ViewModels/MainViewModel.BrandLogo.cs
index b9e86fe..3e42a4f 100644
--- a/ViewModels/MainViewModel.BrandLogo.cs
+++ b/ViewModels/MainViewModel.BrandLogo.cs
@@ -97,6 +97,11 @@ public partial class MainViewModel
Height = BrandLogoHeight,
};
+ /// The staged scene's brand logo, exposed so the preview can draw it ABOVE the universal
+ /// blur (the compositor blits it after the blur group — the logo is never obscured).
+ public Source? StagedBrandLogo =>
+ StagedScene?.Elements.OfType().FirstOrDefault(s => s.IsBrandLogo);
+
/// Exactly one brand logo per scene, placed directly above the background, carrying
/// the current App Settings image.
internal void EnsureBrandLogos()
diff --git a/ViewModels/MainViewModel.Scenes.cs b/ViewModels/MainViewModel.Scenes.cs
index e95bcad..2fcde58 100644
--- a/ViewModels/MainViewModel.Scenes.cs
+++ b/ViewModels/MainViewModel.Scenes.cs
@@ -37,6 +37,7 @@ public partial class MainViewModel
// binding that is never re-notified stays collapsed for the rest of the session
// (build 1082: the option vanished after the first layout load).
OnPropertyChanged(nameof(CanAddBlur));
+ OnPropertyChanged(nameof(StagedBrandLogo));
SelectedElement = null;
if (value != null)
diff --git a/ytLive.Tests/BlurCompositorTests.cs b/ytLive.Tests/BlurCompositorTests.cs
new file mode 100644
index 0000000..71d8f6b
--- /dev/null
+++ b/ytLive.Tests/BlurCompositorTests.cs
@@ -0,0 +1,87 @@
+using Xunit;
+using ytLive.Models;
+using ytLive.Services;
+using ytLive.Services.Compositor;
+
+namespace ytLive.Tests;
+
+///
+/// The universal privacy blur at the compositor level (creator ruling 2026-11-01): it blankets
+/// every layer beneath it, and the brand logo — blitted after the blur — is never swallowed.
+///
+public class BlurCompositorTests
+{
+ private static VideoFrame Solid(int w, int h, byte r, byte g, byte b)
+ {
+ var pixels = new byte[w * h * 4];
+ for (var i = 0; i < pixels.Length; i += 4)
+ {
+ pixels[i] = b; pixels[i + 1] = g; pixels[i + 2] = r; pixels[i + 3] = 255;
+ }
+ return new VideoFrame(w, h, pixels);
+ }
+
+ /// Hard left/right colour split — blurring is unmistakable on it.
+ private static VideoFrame Split(int w, int h, byte lr, byte lg, byte lb, byte rr, byte rg, byte rb)
+ {
+ var pixels = new byte[w * h * 4];
+ var half = w / 2;
+ for (var y = 0; y < h; y++)
+ for (var x = 0; x < w; x++)
+ {
+ var i = (y * w + x) * 4;
+ pixels[i] = x < half ? lb : rb;
+ pixels[i + 1] = x < half ? lg : rg;
+ pixels[i + 2] = x < half ? lr : rr;
+ pixels[i + 3] = 255;
+ }
+ return new VideoFrame(w, h, pixels);
+ }
+
+ private static void AssertPixel(VideoFrame frame, int x, int y, byte r, byte g, byte b)
+ {
+ var i = (y * frame.Width + x) * 4;
+ Assert.True(
+ frame.BgraPixels[i + 2] == r && frame.BgraPixels[i + 1] == g && frame.BgraPixels[i] == b,
+ $"pixel ({x},{y}): expected rgb({r},{g},{b}), got rgb({frame.BgraPixels[i + 2]},{frame.BgraPixels[i + 1]},{frame.BgraPixels[i]})");
+ }
+
+ [Fact]
+ public void The_blur_covers_what_is_beneath_it_and_never_the_brand_logo()
+ {
+ var stripes = Split(1920, 1080, 0, 0, 255, 0, 255, 0); // blue | green, hard edge at x=960
+ var brand = Solid(120, 60, 255, 255, 255); // white logo
+
+ var background = new Source { Type = SourceType.DisplayCapture, IsBackground = true, CaptureKey = "monitor:0" };
+ var logo = new Source { Type = SourceType.Image, IsBrandLogo = true, X = 900, Y = 500, Width = 120, Height = 60 };
+ var blur = new Source { Type = SourceType.Blur, X = 700, Y = 400, Width = 600, Height = 300, BlurAmount = 1.0 };
+
+ var scene = new Scene { Name = "Live" };
+ scene.Elements.Add(background);
+ scene.Elements.Add(logo);
+ scene.Elements.Add(blur);
+
+ VideoFrame? FrameFor(SceneElement e) => e switch
+ {
+ Source { IsBackground: true } => stripes,
+ Source { IsBrandLogo: true } => brand,
+ _ => null,
+ };
+
+ var options = new CompositorOptions
+ {
+ SourceRectX = 0, SourceRectY = 0, SourceRectWidth = 1920, SourceRectHeight = 1080,
+ OutputWidth = 1920, OutputHeight = 1080,
+ };
+
+ var output = new SceneCompositor().Render(scene, FrameFor, null, options);
+
+ // 1. Inside the blur, over the colour edge: blended, not the hard split.
+ var i = (450 * output.Width + 950) * 4;
+ Assert.True(output.BgraPixels[i] > 0 && output.BgraPixels[i + 1] > 0,
+ "a blurred pixel on the colour edge must be a blend of both sides");
+
+ // 2. The logo, sitting inside the blur's area, is untouched.
+ AssertPixel(output, 950, 530, 255, 255, 255);
+ }
+}