Snapshot all enabled participants in one click
Some checks failed
CI / build-and-test (push) Failing after 27s

Header button 'Snapshot all' fires SnapshotAllCommand which iterates every enabled participant, grabs the latest ProcessedFrame, encodes as PNG into a fresh timestamped subfolder under %USERPROFILE%\\Pictures\\TeamsISO\\snapshots-yyyyMMdd_HHmmss\\. One folder per click so back-to-back snapshot sessions don't comingle.

Reuses the per-participant snapshot path established earlier — same WriteableBitmap(Bgra32) → PngBitmapEncoder pipeline. Reports saved + failed counts in the toast so the operator knows if anything was missed (typical failure: pipeline still warming up, no frame yet).
This commit is contained in:
Zac Gaetano 2026-05-10 21:23:49 -04:00
parent d6793d8d9c
commit 10a0826fb3
2 changed files with 91 additions and 0 deletions

View file

@ -776,6 +776,26 @@
VerticalAlignment="Center"/> VerticalAlignment="Center"/>
</StackPanel> </StackPanel>
</Button> </Button>
<Button Style="{StaticResource Wd.Button.Ghost}"
Command="{Binding SnapshotAllCommand}"
VerticalAlignment="Center"
Margin="0,0,8,0"
ToolTip="Save a PNG of every enabled participant's current frame. One folder per click under %USERPROFILE%\Pictures\TeamsISO\snapshots-&lt;timestamp&gt;.">
<StackPanel Orientation="Horizontal">
<Path Data="M 1,3 L 4,3 L 5,1 L 11,1 L 12,3 L 15,3 L 15,13 L 1,13 Z M 8,5 A 3,3 0 1 1 8,11 A 3,3 0 1 1 8,5"
Stroke="{DynamicResource Wd.Text.Primary}"
StrokeThickness="1.2"
Fill="Transparent"
Width="16" Height="14"
Stretch="None"
VerticalAlignment="Center"
Margin="0,0,8,0"/>
<TextBlock Text="Snapshot all"
Style="{StaticResource Wd.Text.Body}"
FontSize="12"
VerticalAlignment="Center"/>
</StackPanel>
</Button>
<Button Style="{StaticResource Wd.Button.Ghost}" <Button Style="{StaticResource Wd.Button.Ghost}"
Click="OnPresetsClick" Click="OnPresetsClick"
VerticalAlignment="Center" VerticalAlignment="Center"

View file

@ -178,6 +178,9 @@ public sealed class MainViewModel : ObservableObject, IDisposable
/// <summary>Join a Teams meeting from a pasted URL — see <see cref="JoinMeetingUrl"/>.</summary> /// <summary>Join a Teams meeting from a pasted URL — see <see cref="JoinMeetingUrl"/>.</summary>
public RelayCommand JoinMeetingCommand { get; } public RelayCommand JoinMeetingCommand { get; }
/// <summary>Save a PNG snapshot of every enabled participant's current frame.</summary>
public RelayCommand SnapshotAllCommand { get; }
/// <summary> /// <summary>
/// Two-way bound to the quick-join input. Whatever the operator pastes /// Two-way bound to the quick-join input. Whatever the operator pastes
/// gets handed to <see cref="TeamsLauncher.TryJoinMeeting"/> when the /// gets handed to <see cref="TeamsLauncher.TryJoinMeeting"/> when the
@ -407,6 +410,8 @@ public sealed class MainViewModel : ObservableObject, IDisposable
notes.Show(); // non-modal so operators can stamp + read alongside the show notes.Show(); // non-modal so operators can stamp + read alongside the show
}); });
SnapshotAllCommand = new RelayCommand(SnapshotAll, () => Participants.Any(p => p.IsEnabled));
JoinMeetingCommand = new RelayCommand(() => JoinMeetingCommand = new RelayCommand(() =>
{ {
// Trim + handle the operator pasting whitespace around the URL. // Trim + handle the operator pasting whitespace around the URL.
@ -587,6 +592,72 @@ public sealed class MainViewModel : ObservableObject, IDisposable
Toast.Show($"Stopped {enabled.Length} ISO(s)"); Toast.Show($"Stopped {enabled.Length} ISO(s)");
} }
/// <summary>
/// Save a PNG of every currently-enabled participant's latest processed
/// frame to a timestamped subdirectory under
/// %USERPROFILE%\Pictures\TeamsISO\. One folder per Snapshot All click
/// so back-to-back clicks don't comingle. Useful for end-of-meeting
/// archives, recapping who showed up, etc.
/// </summary>
private void SnapshotAll()
{
var enabled = Participants.Where(p => p.IsEnabled).ToArray();
if (enabled.Length == 0)
{
Toast.Warn("No enabled participants to snapshot");
return;
}
var rootDir = System.IO.Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.MyPictures),
"TeamsISO",
$"snapshots-{DateTimeOffset.Now:yyyyMMdd_HHmmss}");
try
{
System.IO.Directory.CreateDirectory(rootDir);
}
catch (Exception ex)
{
Toast.Warn($"Couldn't create snapshot dir: {ex.Message}");
return;
}
var saved = 0;
var failed = 0;
foreach (var p in enabled)
{
try
{
var frame = _controller.GetLatestProcessedFrame(p.Id);
if (frame is null || frame.Pixels.IsEmpty) { failed++; continue; }
var safeName = string.Join("_", p.DisplayName.Split(System.IO.Path.GetInvalidFileNameChars()));
if (string.IsNullOrWhiteSpace(safeName)) safeName = "participant";
var path = System.IO.Path.Combine(rootDir, $"{safeName}.png");
var stride = frame.Width * 4;
var bmp = new System.Windows.Media.Imaging.WriteableBitmap(
frame.Width, frame.Height, 96, 96,
System.Windows.Media.PixelFormats.Bgra32, null);
bmp.WritePixels(
new System.Windows.Int32Rect(0, 0, frame.Width, frame.Height),
frame.Pixels.ToArray(), stride, 0);
using var fs = new System.IO.FileStream(path, System.IO.FileMode.Create);
var encoder = new System.Windows.Media.Imaging.PngBitmapEncoder();
encoder.Frames.Add(System.Windows.Media.Imaging.BitmapFrame.Create(bmp));
encoder.Save(fs);
saved++;
}
catch { failed++; }
}
Toast.Show(failed > 0
? $"Saved {saved} snapshot(s) ({failed} failed) to Pictures\\TeamsISO\\{System.IO.Path.GetFileName(rootDir)}"
: $"Saved {saved} snapshot(s) to Pictures\\TeamsISO\\{System.IO.Path.GetFileName(rootDir)}");
}
/// <summary> /// <summary>
/// Format a byte count as a human-readable string with 0-1 decimal /// Format a byte count as a human-readable string with 0-1 decimal
/// places — e.g. "8.4 GB", "245 GB", "1.2 TB". Optimized for footer /// places — e.g. "8.4 GB", "245 GB", "1.2 TB". Optimized for footer