diff --git a/src/TeamsISO.App/MainWindow.xaml b/src/TeamsISO.App/MainWindow.xaml
index 825c15f..aa16a8a 100644
--- a/src/TeamsISO.App/MainWindow.xaml
+++ b/src/TeamsISO.App/MainWindow.xaml
@@ -865,6 +865,9 @@
+
diff --git a/src/TeamsISO.App/ViewModels/MainViewModel.cs b/src/TeamsISO.App/ViewModels/MainViewModel.cs
index 6bd0b2e..fff6b22 100644
--- a/src/TeamsISO.App/ViewModels/MainViewModel.cs
+++ b/src/TeamsISO.App/ViewModels/MainViewModel.cs
@@ -813,7 +813,7 @@ public sealed class MainViewModel : ObservableObject, IDisposable
}
else
{
- vm = new ParticipantViewModel(_controller, p);
+ vm = new ParticipantViewModel(_controller, p, Toast);
_byId[p.Id] = vm;
Participants.Add(vm);
}
diff --git a/src/TeamsISO.App/ViewModels/ParticipantViewModel.cs b/src/TeamsISO.App/ViewModels/ParticipantViewModel.cs
index ce70d30..21d0833 100644
--- a/src/TeamsISO.App/ViewModels/ParticipantViewModel.cs
+++ b/src/TeamsISO.App/ViewModels/ParticipantViewModel.cs
@@ -15,6 +15,7 @@ namespace TeamsISO.App.ViewModels;
public sealed class ParticipantViewModel : ObservableObject
{
private readonly IIsoController _controller;
+ private readonly ToastViewModel? _toast;
private Participant _participant;
private bool _isEnabled;
private bool _isProcessing;
@@ -43,9 +44,10 @@ public sealed class ParticipantViewModel : ObservableObject
/// True when is non-null. Bound to Visibility in XAML.
public bool HasThumbnail => _thumbnail is not null;
- public ParticipantViewModel(IIsoController controller, Participant participant)
+ public ParticipantViewModel(IIsoController controller, Participant participant, ToastViewModel? toast = null)
{
_controller = controller;
+ _toast = toast;
_participant = participant;
_customName = string.Empty;
ToggleIsoCommand = new AsyncRelayCommand(ToggleIsoAsync, () => !_isProcessing);
@@ -75,6 +77,62 @@ public sealed class ParticipantViewModel : ObservableObject
RestartIsoCommand = new AsyncRelayCommand(RestartIsoAsync,
() => _isEnabled && !_isProcessing);
+
+ SaveSnapshotCommand = new RelayCommand(SaveSnapshot, () => _isEnabled);
+ }
+
+ ///
+ /// Encode the most recent
+ /// as a PNG and write it to %USERPROFILE%\Pictures\TeamsISO\. Used
+ /// by the participants' context menu for grabbing a stillframe — useful
+ /// for highlight reels, social posts, bug reports. Best-effort: a no-op
+ /// + warn-toast if no frame is currently available (pipeline just spun
+ /// up, or recording isn't enabled). Filename includes participant name
+ /// + timestamp so back-to-back snapshots don't collide.
+ ///
+ private void SaveSnapshot()
+ {
+ try
+ {
+ var frame = _controller.GetLatestProcessedFrame(Id);
+ if (frame is null || frame.Pixels.IsEmpty)
+ {
+ _toast?.Warn("No frame available yet — try again in a few seconds");
+ return;
+ }
+
+ var dir = System.IO.Path.Combine(
+ Environment.GetFolderPath(Environment.SpecialFolder.MyPictures),
+ "TeamsISO");
+ System.IO.Directory.CreateDirectory(dir);
+
+ var safeName = string.Join("_", DisplayName.Split(System.IO.Path.GetInvalidFileNameChars()));
+ if (string.IsNullOrWhiteSpace(safeName)) safeName = "participant";
+ var path = System.IO.Path.Combine(dir,
+ $"{safeName}_{DateTimeOffset.Now:yyyyMMdd_HHmmss}.png");
+
+ // ProcessedFrame is BGRA32, top-down. WriteableBitmap with
+ // Bgra32 pixel format takes the bytes verbatim.
+ 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);
+
+ _toast?.Show($"Saved snapshot: {System.IO.Path.GetFileName(path)}");
+ }
+ catch (Exception ex)
+ {
+ _toast?.Warn($"Snapshot failed: {ex.Message}");
+ }
}
///
@@ -358,6 +416,7 @@ public sealed class ParticipantViewModel : ObservableObject
/// Open a non-modal floating preview window for this participant. Multi-monitor friendly.
public RelayCommand OpenPreviewCommand { get; }
+ public RelayCommand SaveSnapshotCommand { get; }
///
/// Restart the pipeline for this participant: disable, brief pause, re-enable.