Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions docs/operator-validation-runbook.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,12 +156,19 @@ Troubleshooting:

1. Ensure sign-in from step 8 (required for external-account meetings).
2. Enter meeting URL (and passcode if needed) in the join UI.
3. After join, verify:
3. Wait for **Zoom Live**, then turn **Capture** on. Joining establishes the
Meeting SDK session; Capture is the separate control that subscribes and
forwards raw participant video/audio into CoreVideo Pro.
4. Verify Capture is genuinely running before evaluating the show:
- `engineOn` is `true` in control state / diagnostics.
- Participant `video_frame_received` counters advance for every expected feed.
- Expected participant audio callbacks and meters advance when each guest speaks.
5. After join and Capture On, verify:
- **Participants** roster populates (not empty / not stale stub).
- **Program** tile shows video (GPU/full-res if D3D11 interop works, else CPU/BGRA preview).
- **Breakout room** name updates when you move rooms.
- Diagnostics show live Zoom capabilities when the dev SDK path is active (`zoom-raw-video`, `zoom-raw-audio`).
4. **Leave** meeting; roster clears, meeting state returns to idle.
6. **Leave** meeting; roster clears, meeting state returns to idle.

## 10. Headless live Zoom harness (optional)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ public void CoreActions_ArePresent()
foreach (var id in new[]
{
"zoom.join", "zoom.leave", "transport.take", "transport.record.toggle", "transport.stream.set", "scene.select",
"scene.dynamicGallery.create",
"input.assign", "input.name", "graphics.lowerThird.toggle", "audio.zoomMode.set", "audio.monitor.set",
"multiview.layout.set", "automation.autoAssignInputs.set"
})
Expand Down
2 changes: 2 additions & 0 deletions native-shell/CoreVideoPro.Control/ControlActionRegistry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,8 @@ private static IReadOnlyList<ControlAction> BuildActions()
// ---- Scenes / view ----------------------------------------------------------
new("scene.select", "Select scene", "Cue a scene to Preview by id.",
new[] { new ControlParam("sceneId", s, true) }),
new("scene.dynamicGallery.create", "Create CoreVideo Tiles",
"Create a dynamic CoreVideo Tiles scene and cue it to Preview."),
new("view.setMode", "Set view mode", "Set the operator view (program/preview/programPreview/multiview).",
new[] { new ControlParam("mode", s, true) }),

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,15 @@ public void MergeNormalizesMeetingStateAndParticipants()
[
new ZoomMediaSpineSubscription
{
ParticipantId = "operator-1",
Kind = "participant-video",
Status = "subscribed",
FramesReceived = 3
LastResultCode = "ok",
DeliveredWidth = 1280,
DeliveredHeight = 720,
DeliveredFps = 30,
FramesReceived = 3,
FrameFresh = true
}
]
};
Expand All @@ -41,6 +47,10 @@ public void MergeNormalizesMeetingStateAndParticipants()
Assert.Equal("operator-1", merged.Participants[0].UserId);
Assert.Equal("operator-1", merged.ActiveSpeakerId);
Assert.True(merged.SourceSnapshot.SubscribedSourceCount >= 1);
var subscription = Assert.Single(merged.ZoomSubscriptions);
Assert.Equal("operator-1", subscription.ParticipantId);
Assert.Equal(1280, subscription.DeliveredWidth);
Assert.True(subscription.FrameFresh);
}

[Fact]
Expand All @@ -49,4 +59,4 @@ public void NormalizeMeetingStateMapsHyphenatedValues()
Assert.Equal("in_meeting", ZoomMediaSpineSnapshotMerger.NormalizeMeetingState("in-meeting"));
Assert.Equal("idle", ZoomMediaSpineSnapshotMerger.NormalizeMeetingState("leaving"));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -818,6 +818,10 @@ public sealed record NativeMediaCoreStateSnapshot
/// <summary>Live Zoom roster from media-core sync when the engine is connected.</summary>
public string? ActiveSpeakerId { get; init; }
public IReadOnlyList<RawParticipantEvent> Participants { get; init; } = [];
/// <summary>Latest per-source Zoom SDK subscription evidence, retained for
/// operator-facing source diagnostics instead of being collapsed into only
/// aggregate frame counters.</summary>
public IReadOnlyList<ZoomMediaSpineSubscription> ZoomSubscriptions { get; init; } = [];
}

public sealed class NativeMediaCoreValidation
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,17 @@ public sealed class ZoomMediaSpineSubscription
public string LastResultCode { get; init; } = "ok";
public int FramesReceived { get; init; }
public int AudioPacketsReceived { get; init; }
public int DeliveredWidth { get; init; }
public int DeliveredHeight { get; init; }
public int DeliveredFps { get; init; }
public double FirstFrameAtMs { get; init; } = -1;
public double FirstFrameDelayMs { get; init; } = -1;
public double LastFrameAtMs { get; init; } = -1;
public double LastFrameAgeMs { get; init; } = -1;
public int LastFrameId { get; init; }
public bool FrameFresh { get; init; }
public int StaleFrameCount { get; init; }
public int MalformedFrameCount { get; init; }
public string? Warning { get; init; }
}

Expand All @@ -42,4 +50,4 @@ public sealed class ZoomMediaSpineNativeSnapshot
public IReadOnlyList<ZoomMediaSpineSubscription> Subscriptions { get; init; } = [];
public IReadOnlyList<string> Warnings { get; init; } = [];
public IReadOnlyList<string> Events { get; init; } = [];
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ public static NativeMediaCoreStateSnapshot Merge(
return merged with
{
SourceSnapshot = sourceSnapshot,
Diagnostics = merged.Diagnostics with { SourceSnapshot = sourceSnapshot }
Diagnostics = merged.Diagnostics with { SourceSnapshot = sourceSnapshot },
ZoomSubscriptions = spine.Subscriptions
};
}

Expand Down Expand Up @@ -80,4 +81,4 @@ public static RawCaptureSnapshot ToCaptureSnapshot(
null or "" => "idle",
_ => meetingState
};
}
}
32 changes: 32 additions & 0 deletions native-shell/CoreVideoPro.WinUI.Tests/AudioMeterScaleTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,36 @@ public void ToLevel_UsesCalibratedMinus60ToZeroDbfsScale(double dbfs, int expect
[Fact]
public void ToLevel_MutedAlwaysReportsSilence() =>
Assert.Equal(0, AudioMeterScale.ToLevel(-3, muted: true));

[Theory]
[InlineData(324)]
[InlineData(576)]
[InlineData(900)]
public void FitVerticalSegments_FillsAvailableWindowHeight(double availableHeight)
{
var layout = AudioMeterScale.FitVerticalSegments(availableHeight, 36);

Assert.Equal(36, layout.SegmentCount);
Assert.Equal(availableHeight, layout.OccupiedSize, precision: 6);
}

[Fact]
public void FitVerticalSegments_GrowsSegmentsInTallWindow()
{
var compact = AudioMeterScale.FitVerticalSegments(324, 36);
var tall = AudioMeterScale.FitVerticalSegments(576, 36);

Assert.True(tall.SegmentSize > compact.SegmentSize);
Assert.True(tall.SegmentSize > 7);
}

[Fact]
public void FitVerticalSegments_ReducesResolutionRatherThanOverflowingShortWindow()
{
var layout = AudioMeterScale.FitVerticalSegments(72, 36);

Assert.True(layout.SegmentCount < 36);
Assert.True(layout.SegmentSize >= 2);
Assert.Equal(72, layout.OccupiedSize, precision: 6);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
using CoreVideoPro.WinUI.Services;
using Xunit;

namespace CoreVideoPro.WinUI.Tests;

public sealed class DynamicGalleryLayoutServiceTests
{
[Theory]
[InlineData(1)]
[InlineData(2)]
[InlineData(5)]
[InlineData(8)]
[InlineData(16)]
public void LayoutIsBoundedUniformAndUsesRequestedAspect(int count)
{
var rects = DynamicGalleryLayoutService.BuildRects(count, tileAspectPreset: "4:3");

Assert.Equal(count, rects.Count);
var first = rects[0];
foreach (var rect in rects)
{
Assert.InRange(rect.X, 0, 1);
Assert.InRange(rect.Y, 0, 1);
Assert.InRange(rect.X + rect.Width, 0, 1.000001);
Assert.InRange(rect.Y + rect.Height, 0, 1.000001);
Assert.Equal(first.Width, rect.Width, 6);
Assert.Equal(first.Height, rect.Height, 6);
Assert.Equal(4.0 / 3.0, rect.Width * (16.0 / 9.0) / rect.Height, 5);
}
}

[Fact]
public void ShortFinalRowIsCentered()
{
var rects = DynamicGalleryLayoutService.BuildRects(5);
var lastRowY = rects.Max(rect => rect.Y);
var lastRow = rects.Where(rect => Math.Abs(rect.Y - lastRowY) < 0.000001).ToList();

Assert.Equal(2, lastRow.Count);
var leftMargin = lastRow[0].X;
var rightMargin = 1 - (lastRow[^1].X + lastRow[^1].Width);
Assert.Equal(leftMargin, rightMargin, 6);
}

[Fact]
public void EmptyGalleryProducesNoRects()
{
Assert.Empty(DynamicGalleryLayoutService.BuildRects(0));
}
}
37 changes: 37 additions & 0 deletions native-shell/CoreVideoPro.WinUI.Tests/ProductionRoleTests.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using CoreVideoPro.WinUI.Models;
using CoreVideoPro.WinUI.Services;
using CoreVideoPro.MediaCore.Models;
using Xunit;

namespace CoreVideoPro.WinUI.Tests;
Expand Down Expand Up @@ -67,4 +68,40 @@ public void CloneCopiesTheProductionRole()
var route = SceneRoutingService.BuildAddedSourceRoute("scene-1", 0, "role:guest-2");
Assert.Equal("guest-2", route.Clone().ProductionRoleId);
}

[Fact]
public void FeedHealthCarriesPerParticipantZoomSubscriptionEvidence()
{
var rows = ProductionStateHelper.BuildFeedHealthRows(
[new Participant { Id = "42", Name = "Guest", Health = FeedHealth.Live }],
subscriptions:
[
new ZoomMediaSpineSubscription
{
ParticipantId = "42",
Kind = "participant-video",
Status = "subscribed",
LastResultCode = "ok",
DeliveredWidth = 1280,
DeliveredHeight = 720,
DeliveredFps = 30,
FramesReceived = 900,
FrameFresh = true
},
new ZoomMediaSpineSubscription
{
ParticipantId = "42",
Kind = "participant-audio",
Status = "subscribed",
AudioPacketsReceived = 1500
}
]);

var row = Assert.Single(rows);
Assert.Equal("1280x720 @ 30fps", row.DeliveredVideo);
Assert.Equal(900, row.VideoFramesReceived);
Assert.Equal(1500, row.AudioPacketsReceived);
Assert.Contains("Video subscribed", row.DiagnosticSummary);
Assert.False(row.HasRecommendedAction);
}
}
41 changes: 41 additions & 0 deletions native-shell/CoreVideoPro.WinUI.Tests/SceneCanvasIaTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -635,6 +635,47 @@ public void Layer_SourceChange_DoesNotIndependentlyDriveAudioRole()
Assert.Equal(SourceAudioRole.Mix, route.AudioRole);
}

[Fact]
public void Layer_SyncFromRoute_RebindsEditsToTheNewTemplateRoute()
{
var first = new SourceRoute
{
Id = "first-1",
Mode = SourceRouteMode.Fixed,
ParticipantId = "p1",
BorderStyle = "solid",
BorderThickness = 1
};
var second = new SourceRoute
{
Id = "second-1",
Mode = SourceRouteMode.Fixed,
ParticipantId = "p2",
BorderStyle = "solid",
BorderThickness = 4
};
var participants = new List<Participant>
{
new() { Id = "p1", Name = "Host" },
new() { Id = "p2", Name = "Guest" }
};
var layer = new SceneCanvasLayerViewModel(
0,
first,
participants,
captureDevices: [],
showInputs: [],
mediaAssets: [],
_ => { });

layer.SyncFromRoute(second, participants, [], [], []);
layer.BorderThickness = 7;

Assert.Equal(1, first.BorderThickness);
Assert.Equal(7, second.BorderThickness);
Assert.Equal("p2", layer.ParticipantId);
}

[Fact]
public void Layer_SourcePickerListsMediaAssetsAsCanvasSources()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,21 @@ public sealed class ScenePersistenceServiceTests
[Fact]
public void SceneRoundTripsThroughPersistedDtoAndJson()
{
var scene = new Scene { Id = "custom-abc12345", Name = "Interview wide", Layout = "two-up" };
var scene = new Scene
{
Id = "custom-abc12345",
Name = "Interview wide",
Layout = "dynamic-gallery",
DynamicGallery = new DynamicGallerySettings
{
MaxTiles = 8,
TileAspect = "4:3",
BorderShape = "rounded",
BorderColor = "#FF8800",
BorderThickness = 4,
GlowSize = 12
}
};
var routes = new List<SourceRoute>
{
new()
Expand Down Expand Up @@ -49,6 +63,12 @@ public void SceneRoundTripsThroughPersistedDtoAndJson()
var restoredScene = Assert.Single(reloaded!.CustomScenes);
Assert.Equal("custom-abc12345", restoredScene.Id);
Assert.Equal("Interview wide", restoredScene.Name);
var restoredGallery = ScenePersistenceService.SceneFromPersisted(restoredScene).DynamicGallery;
Assert.NotNull(restoredGallery);
Assert.Equal(8, restoredGallery!.MaxTiles);
Assert.Equal("4:3", restoredGallery.TileAspect);
Assert.Equal("rounded", restoredGallery.BorderShape);
Assert.Equal(12, restoredGallery.GlowSize, 3);

var restored = restoredScene.Routes.Select(ScenePersistenceService.FromPersisted).ToList();
Assert.Equal(2, restored.Count);
Expand Down
Loading
Loading