Three reports from the creator, plus two bugs the real clip exposed. 1. A video layer was INVISIBLE in the preview. The pane's per-layer Image is Collapsed by default and switched on by a whitelist of DataTriggers; MediaSource matched none, so the clip decoded into a bitmap that nothing displayed. Same defect the alert box hit on 2026-09-26 and the scoreboard after it — now recorded at the top of docs/ai/ui-surface.md with a test that asserts the bound Image is Visible (`MediaPreviewDisplayTests`). 2. Play is no longer tied to Loop: the button is always enabled (creator ruling — Loop must not toggle Play). Loop still restarts a finished clip, which is the behaviour that was broken. 3. Geometry is now the LAST section of the properties list for every draggable element, with opacity at the top; the web source's duplicate geometry block is gone (it now uses the shared one, which had been sitting above the media panel). 4. The physical mouse-drag test is deleted. It injected real cursor movement, hijacked whatever machine ran it, and failed whenever the desktop was busy. The arithmetic it covered now lives in `Helpers/DragReorderMath.cs` — pure, and covered by `DragReorderMathTests` (threshold, up/down moves, no-op and out-of-range). The two persistence tests were already synthetic. 5. Real-clip check (creator's `test_greenscreen_moving_subject.mp4`, 1920×1080): the detector REJECTED it — a fully saturated green sits at value 1.0 and the detector's 0.98 brightness ceiling threw it away. White highlights are excluded by the saturation floor, which is the property that actually separates them, so the ceiling is gone. With it fixed, the clip is detected as green (high confidence), and keyed through our own pipe 97.8% of pixels go transparent. Measured: keying with the canonical #00B140 keys 0.0% of this clip — which is why the key colour comes from the measured screen. Tests: 567 passed, 0 failed, clean build 0 warnings. The gate's scope check flags `Assets/Videos/test_greenscreen_moving_subject.mp4` — the creator's working copy, deliberately not committed; the fixture lives under ytLive.Tests/Fixtures.
128 lines
5.1 KiB
C#
128 lines
5.1 KiB
C#
using System;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using System.Windows;
|
|
using System.Windows.Controls;
|
|
using System.Windows.Media;
|
|
using Microsoft.Data.Sqlite;
|
|
using Xunit;
|
|
using ytLive.Models;
|
|
using ytLive.Services;
|
|
using ytLive.ViewModels;
|
|
|
|
namespace ytLive.Tests;
|
|
|
|
/// <summary>
|
|
/// 2026-11-05 report: a video layer added and playing showed NOTHING in the preview pane. The decode
|
|
/// was healthy — the preview's <c>Image</c> is switched on by a whitelist of DataTriggers (image,
|
|
/// chat, alert, ticker, scoreboard, text, web, webcam) and a video layer matched none of them, so its
|
|
/// Image stayed Collapsed. Same class of defect as the invisible alert box (2026-09-26).
|
|
/// <para>These assert the two ends: the layer receives frames, and the pane actually shows it.</para>
|
|
/// </summary>
|
|
[Collection("RealApp")]
|
|
public sealed class MediaPreviewDisplayTests
|
|
{
|
|
private readonly RealAppHost _app;
|
|
|
|
public MediaPreviewDisplayTests(RealAppHost app) => _app = app;
|
|
|
|
[Fact]
|
|
public void VideoLayer_IsDrawnByThePreviewPane()
|
|
{
|
|
_app.Run(() =>
|
|
{
|
|
var tempDb = Path.Combine(Path.GetTempPath(), $"ytLlive-test-{Guid.NewGuid():N}.db");
|
|
var videoPath = Path.Combine(Path.GetTempPath(), "preview-clip.mp4");
|
|
var decoders = new System.Collections.Generic.List<FakeMediaSource>();
|
|
MainViewModel.LayoutPathOverride = tempDb;
|
|
MainViewModel.MediaFrameSourceFactoryOverride = key =>
|
|
{
|
|
var decoder = new FakeMediaSource(key);
|
|
decoders.Add(decoder);
|
|
return decoder;
|
|
};
|
|
MainViewModel.ChromaProbeOverride = new NoProbe();
|
|
|
|
var window = new MainWindow();
|
|
var vm = (MainViewModel)window.DataContext;
|
|
try
|
|
{
|
|
window.Show();
|
|
window.UpdateLayout();
|
|
|
|
vm.AddMediaSource(videoPath);
|
|
var layer = vm.StagedScene!.Elements.OfType<Source>().Single(s => s.IsMediaSource);
|
|
|
|
// The layer receives a decoded frame…
|
|
var frame = SolidFrame(64, 36);
|
|
decoders.Single().Emit(frame);
|
|
Assert.NotNull(layer.DisplaySource);
|
|
|
|
window.UpdateLayout();
|
|
|
|
// …and the preview pane actually renders it. Without the IsMediaSource trigger the
|
|
// Image is Collapsed and the clip is invisible while everything else looks healthy.
|
|
var pane = (UserControl)window.FindName("PreviewPane")!;
|
|
var image = FindImageBoundTo(pane, layer.DisplaySource!);
|
|
Assert.True(image != null,
|
|
"the preview pane has no Image bound to the video layer's frame — the layer is invisible");
|
|
Assert.Equal(Visibility.Visible, image!.Visibility);
|
|
Assert.True(image.ActualWidth > 0 && image.ActualHeight > 0,
|
|
$"the video layer's preview Image has no size ({image.ActualWidth}x{image.ActualHeight})");
|
|
}
|
|
finally
|
|
{
|
|
window.Close();
|
|
MainViewModel.ChromaProbeOverride = null;
|
|
MainViewModel.MediaFrameSourceFactoryOverride = null;
|
|
MainViewModel.LayoutPathOverride = null;
|
|
SqliteConnection.ClearAllPools();
|
|
try { File.Delete(tempDb); } catch { /* best-effort cleanup */ }
|
|
}
|
|
});
|
|
}
|
|
|
|
private static Image? FindImageBoundTo(DependencyObject parent, ImageSource source)
|
|
{
|
|
for (var i = 0; i < VisualTreeHelper.GetChildrenCount(parent); i++)
|
|
{
|
|
var child = VisualTreeHelper.GetChild(parent, i);
|
|
if (child is Image image && ReferenceEquals(image.Source, source)) return image;
|
|
if (FindImageBoundTo(child, source) is { } found) return found;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
private static VideoFrame SolidFrame(int w, int h)
|
|
{
|
|
var bytes = new byte[w * h * 4];
|
|
for (var i = 0; i < bytes.Length; i += 4)
|
|
{
|
|
bytes[i] = 40; bytes[i + 1] = 200; bytes[i + 2] = 40; bytes[i + 3] = 255;
|
|
}
|
|
return new VideoFrame(w, h, bytes);
|
|
}
|
|
|
|
private sealed class FakeMediaSource : IMediaFrameSource
|
|
{
|
|
public FakeMediaSource(string key) => Key = key;
|
|
public string Key { get; }
|
|
public bool Looping { get; set; }
|
|
public event Action<VideoFrame>? FrameAvailable;
|
|
public event Action<ytLive.Services.Audio.AudioSample>? AudioAvailable { add { } remove { } }
|
|
public event Action? Completed { add { } remove { } }
|
|
public void Emit(VideoFrame frame) => FrameAvailable?.Invoke(frame);
|
|
public Task StartAsync(CancellationToken cancellationToken = default) => Task.CompletedTask;
|
|
public Task StopAsync() => Task.CompletedTask;
|
|
public void Dispose() { }
|
|
}
|
|
|
|
private sealed class NoProbe : IChromaProbe
|
|
{
|
|
public Task<VideoFrame?> ProbeAsync(string path, CancellationToken cancellationToken = default)
|
|
=> Task.FromResult<VideoFrame?>(null);
|
|
}
|
|
}
|