Snapshot all enabled participants in one click
Some checks failed
CI / build-and-test (push) Failing after 27s
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:
parent
d6793d8d9c
commit
10a0826fb3
2 changed files with 91 additions and 0 deletions
|
|
@ -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-<timestamp>.">
|
||||||
|
<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"
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue