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
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
using CoreVideoPro.MediaCore.Services;
using Xunit;

namespace CoreVideoPro.MediaCore.Tests;

// #470 / T3.7. Live 2026-09-10: 7 Zoom ISOs armed with Engine off recorded
// nothing, reported nothing, and the operator lost the head of every stream.
public class IsoCapturePreflightTests
{
[Fact]
public void ArmedZoomIsosWithCaptureOffAreLoud()
{
var warning = IsoCapturePreflight.Describe(isoEnabled: true, zoomIsoSourceCount: 7, rawMediaActive: false);
Assert.NotNull(warning);
Assert.Contains("7 Zoom ISO sources", warning);
// It must say what to do, not just that something is wrong.
Assert.Contains("Turn Engine on", warning);
}

[Fact]
public void OneSourceReadsAsOneSource()
{
var warning = IsoCapturePreflight.Describe(true, 1, false);
Assert.NotNull(warning);
Assert.Contains("1 Zoom ISO source ", warning);
}

[Theory]
[InlineData(true, 3, true)] // capture on: nothing to say
[InlineData(false, 3, false)] // ISOs off: the switch is off, not a fault
[InlineData(true, 0, false)] // no Zoom ISO sources selected
public void NothingToSayStaysSilent(bool isoEnabled, int count, bool rawMediaActive)
{
Assert.Null(IsoCapturePreflight.Describe(isoEnabled, count, rawMediaActive));
}

// UNKNOWN is not a problem. Before the first snapshot we cannot tell
// capture-off from not-yet-reported, and warning on an unobserved state is
// how an indicator gets trained into background noise.
[Fact]
public void AnUnobservedCaptureStateIsNotAWarning()
{
Assert.Null(IsoCapturePreflight.Describe(isoEnabled: true, zoomIsoSourceCount: 7, rawMediaActive: null));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
namespace CoreVideoPro.MediaCore.Services;

/// <summary>
/// #470 / T3.7. ISO streams armed while Zoom capture is OFF write nothing, and
/// said nothing.
///
/// Seen live 2026-09-10: recording started with ISOs enabled and 7 Zoom guests
/// selected while Engine was off. Program recorded normally; all 7 ISO streams
/// sat at framesWritten 0 with no file, recording.warning null and no per-stream
/// warning. Turning Engine on mid-recording opened the writers lazily at the
/// first frame, so the operator silently lost the head of every ISO.
///
/// It cannot be caught downstream: an ISO writer opens lazily at its first
/// frame, so a source that never produces one has no stream to carry a warning.
/// The only place that knows is the arming decision itself.
///
/// Breaks ISO-1's "Loud, never silent" (a video-only-broken ISO must be as loud
/// as #286 made a video-only program).
/// </summary>
public static class IsoCapturePreflight
{
/// <param name="isoEnabled">The "Program + ISOs" switch.</param>
/// <param name="zoomIsoSourceCount">Selected ISO sources that come from Zoom.</param>
/// <param name="rawMediaActive">The ENGINE-reported capture state (null when
/// no snapshot has arrived yet), not the button we last pressed.</param>
/// <returns>An operator-facing warning, or null when there is nothing to say.</returns>
public static string? Describe(bool isoEnabled, int zoomIsoSourceCount, bool? rawMediaActive)
{
if (!isoEnabled || zoomIsoSourceCount <= 0)
{
return null;
}

// UNKNOWN is not a problem. Before the first snapshot arrives we cannot
// tell capture-off from not-yet-reported, and warning on a state we have
// not observed is how an indicator gets ignored.
if (rawMediaActive is not false)
{
return null;
}

var sources = zoomIsoSourceCount == 1 ? "1 Zoom ISO source" : $"{zoomIsoSourceCount} Zoom ISO sources";
return $"{sources} will record NOTHING while Zoom capture is off. " +
"Turn Engine on before recording — an ISO writer opens at its first frame, " +
"so turning it on later loses everything before that point.";
}
}
13 changes: 13 additions & 0 deletions native-shell/CoreVideoPro.WinUI/ViewModels/StudioViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,19 @@
{
get
{
// #470 / T3.7 FIRST, because it is the case with no stream to warn
// on: an ISO writer opens lazily at its first frame, so sources that
// never produce one leave nothing behind to carry a warning. Armed
// with capture off, all 7 ISOs recorded nothing and said nothing.
var capture = IsoCapturePreflight.Describe(
IsoRecordingEnabled,
BuildIsoSourceTargets().SourceIds.Count(id => id.StartsWith("zoom:", StringComparison.Ordinal)),
Settings.RawMediaActive);
if (capture is not null)
{
return capture;
}

var streams = _bridge.LastSnapshot?.Recording?.Streams;
if (streams is null)
{
Expand Down Expand Up @@ -3592,7 +3605,7 @@
}
}

partial void OnActiveSceneIdChanged(string oldValue, string value)

Check warning on line 3608 in native-shell/CoreVideoPro.WinUI/ViewModels/StudioViewModel.cs

View workflow job for this annotation

GitHub Actions / native-shell-windows

Nullability of reference types in type of parameter 'oldValue' doesn't match partial method declaration.

Check warning on line 3608 in native-shell/CoreVideoPro.WinUI/ViewModels/StudioViewModel.cs

View workflow job for this annotation

GitHub Actions / native-shell-windows

Partial method declarations 'void StudioViewModel.OnActiveSceneIdChanged(string? oldValue, string newValue)' and 'void StudioViewModel.OnActiveSceneIdChanged(string oldValue, string value)' have signature differences.

Check warning on line 3608 in native-shell/CoreVideoPro.WinUI/ViewModels/StudioViewModel.cs

View workflow job for this annotation

GitHub Actions / native-shell-windows

Nullability of reference types in type of parameter 'oldValue' doesn't match partial method declaration.

Check warning on line 3608 in native-shell/CoreVideoPro.WinUI/ViewModels/StudioViewModel.cs

View workflow job for this annotation

GitHub Actions / native-shell-windows

Partial method declarations 'void StudioViewModel.OnActiveSceneIdChanged(string? oldValue, string newValue)' and 'void StudioViewModel.OnActiveSceneIdChanged(string oldValue, string value)' have signature differences.

Check warning on line 3608 in native-shell/CoreVideoPro.WinUI/ViewModels/StudioViewModel.cs

View workflow job for this annotation

GitHub Actions / native-shell-windows

Nullability of reference types in type of parameter 'oldValue' doesn't match partial method declaration.
{
if (!_scenes.Any(scene => string.Equals(scene.Id, value, StringComparison.Ordinal)))
{
Expand Down
Loading