diff --git a/ViewModels/MainViewModel.Webcam.cs b/ViewModels/MainViewModel.Webcam.cs
new file mode 100644
index 0000000..07d7847
--- /dev/null
+++ b/ViewModels/MainViewModel.Webcam.cs
@@ -0,0 +1,269 @@
+using System.Windows;
+using System.Windows.Input;
+using System.Windows.Media.Imaging;
+using ytLive;
+using ytLive.Models;
+using ytLive.Services;
+
+namespace ytLive.ViewModels;
+
+public partial class MainViewModel
+{
+ // Webcam: one camera input app-wide. The identity (DeviceId) lives on the
+ // Webcam entity; each scene that shows the webcam has a WebcamSceneConfig
+ // (webcam.{scene}.config). CameraManager still owns the single session.
+ private readonly ICameraEnumerator _cameraEnumerator;
+ private readonly CameraManager _cameraManager;
+ private Webcam? _webcam;
+ private string? _webcamError;
+
+ /// The Add → Webcam menu item: enabled only while no webcam exists anywhere —
+ /// one camera identity app-wide; re-adding always opens the picker.
+ public bool CanAddWebcam => StagedScene != null && _webcam == null;
+
+ ///
+ /// Right-click-on-preview → "Show Webcam": offered when the active scene has no
+ /// webcam config (add one) or hides it (unhide — keeps the config row).
+ ///
+ public bool CanShowWebcamInStagedScene
+ => StagedScene is { } scene && (scene.WebcamConfig == null || !scene.WebcamConfig.IsVisible);
+
+ /// Swap-the-device item: enabled once a camera has been picked at all.
+ public bool CanChangeWebcam => _webcam != null;
+
+ /// Shows the mirror / clip-shape row in the source chip when a webcam is selected.
+ public bool IsWebcamSelected => SelectedElement is WebcamSceneConfig;
+
+ ///
+ /// The reason the webcam feed is down (device in use, offline, locked, no frames),
+ /// or null when it's alive. Surfaced as a red chip so a dead camera is never a
+ /// silent empty box. Cleared the moment a real frame arrives.
+ ///
+ public string? WebcamError
+ {
+ get => _webcamError;
+ private set => SetProperty(ref _webcamError, value);
+ }
+
+ // Webcam size safeguard: no more than half the frame in any dimension
+ // (960x540 over the 1920x1080 master), and no less than 10% of it
+ // (192x108). Enforced at resize and on layout load. The Chat scene is
+ // exempt from the per-dimension half — its webcam may take half the
+ // screen AREA (~1358x764 @16:9) so the viewer sees the creator better,
+ // matched by canonical name (SceneCatalog.IsChat).
+ public const double WebcamMaxWidth = 960;
+ public const double WebcamMaxHeight = 540;
+ public const double WebcamChatMaxWidth = 1358;
+ public const double WebcamChatMaxHeight = 764;
+ public const double WebcamMinWidth = MasterFrameWidth * 0.1;
+ public const double WebcamMinHeight = MasterFrameHeight * 0.1;
+
+ public static double MaxWebcamWidthFor(string? sceneName)
+ => SceneCatalog.IsChat(sceneName) ? WebcamChatMaxWidth : WebcamMaxWidth;
+
+ public static double MaxWebcamHeightFor(string? sceneName)
+ => SceneCatalog.IsChat(sceneName) ? WebcamChatMaxHeight : WebcamMaxHeight;
+
+ internal static void ClampWebcamToBounds(WebcamSceneConfig config, string? sceneName)
+ {
+ var maxWidth = MaxWebcamWidthFor(sceneName);
+ var maxHeight = MaxWebcamHeightFor(sceneName);
+ var scale = Math.Min(maxWidth / config.Width, maxHeight / config.Height);
+ if (scale < 1)
+ {
+ config.Width = Math.Round(config.Width * scale);
+ config.Height = Math.Round(config.Height * scale);
+ return;
+ }
+
+ var minScale = Math.Max(WebcamMinWidth / config.Width, WebcamMinHeight / config.Height);
+ if (minScale > 1)
+ {
+ config.Width = Math.Round(config.Width * minScale);
+ config.Height = Math.Round(config.Height * minScale);
+ }
+ }
+
+ // One-time heal for layouts saved before the rect dims were persisted (v3→v4):
+ // a Traditional webcam that ended up square (Round resize then reload lost the
+ // pre-Round rect) gets widened to 16:9, keeping the height. Round is skipped —
+ // a square bounding box is correct there — and an explicit pre-Round rect wins.
+ internal static void HealLegacySquareRect(WebcamSceneConfig config)
+ {
+ if (config.ClipShape != ClipShape.Traditional) return;
+ if (config.RectWidth != null || config.RectHeight != null) return;
+ if (Math.Abs(config.Width - config.Height) >= 1) return;
+ config.Width = Math.Round(config.Height * 16.0 / 9.0);
+ }
+
+ public ICommand AddWebcamCommand { get; }
+ public ICommand ChangeWebcamCommand { get; }
+ public ICommand ShowWebcamCommand { get; }
+
+ // After a layout load / file open: re-point the webcam identity, stop the
+ // previous device if it changed, and acquire the current one once per scene
+ // that uses it (CameraManager refcounts by DeviceId — one camera session).
+ private void ReacquireWebcam()
+ {
+ var previousDevice = _webcam?.DeviceId;
+ _webcam = _layoutStore.Webcam;
+ var newDevice = _webcam?.DeviceId;
+
+ if (!string.IsNullOrWhiteSpace(previousDevice) && previousDevice != newDevice)
+ _ = _cameraManager.ReleaseAllAsync(previousDevice);
+
+ OnPropertyChanged(nameof(CanChangeWebcam));
+ OnPropertyChanged(nameof(CanAddWebcam));
+
+ if (_webcam == null || string.IsNullOrWhiteSpace(newDevice)) return;
+ if (previousDevice != newDevice)
+ foreach (var unused in Scenes.SelectMany(s => s.Elements.OfType()))
+ _ = _cameraManager.AcquireAsync(newDevice);
+
+ // A session already running (same device, or one that produced a first
+ // frame) has its shared bitmap; freshly loaded configs must adopt it here,
+ // because PreviewBitmapChanged never re-fires for an existing bitmap.
+ if (_cameraManager.GetPreviewBitmap(newDevice) is { } running)
+ foreach (var config in Scenes.SelectMany(s => s.Elements.OfType()))
+ config.VideoImageSource = running;
+ }
+
+ // CameraManager creates the shared WriteableBitmap on the UI thread at the
+ // device's frame size; every scene's webcam config picks it up from here. A
+ // bitmap means a real frame arrived — the camera is provably alive.
+ private void OnCameraPreviewBitmapChanged(string deviceId, WriteableBitmap bitmap)
+ {
+ if (_webcam?.DeviceId != deviceId) return;
+ WebcamError = null;
+ foreach (var scene in Scenes)
+ foreach (var config in scene.Elements.OfType())
+ config.VideoImageSource = bitmap;
+ }
+
+ // The camera failed to start or died asynchronously (in use, offline, locked,
+ // no frames within the proof timeout) — surface it instead of a silent box.
+ private void OnCameraFailed(string deviceId, string message)
+ {
+ if (_webcam?.DeviceId != deviceId) return;
+ WebcamError = $"Webcam offline: {message}";
+ }
+
+ // Adds the webcam to the active scene. The creator ALWAYS picks from the
+ // cameras Windows has registered — never silently resurrects the previous
+ // camera (which is what happened after deleting one scene's webcam while
+ // another scene still used it). Picking a different camera than the current
+ // app-wide one swaps it everywhere, so the single-identity model stays honest;
+ // each scene's placement config is independent (webcam.{scene}.config).
+ private async Task AddWebcamToStagedSceneAsync()
+ {
+ var scene = StagedScene;
+ if (scene == null || scene.WebcamConfig != null) return;
+
+ var dialog = new CameraPickerDialog(new CameraPickerViewModel(_cameraEnumerator))
+ {
+ Owner = Application.Current.MainWindow
+ };
+ if (dialog.ShowDialog() != true || dialog.PickedDevice == null) return;
+ var device = dialog.PickedDevice;
+
+ if (_webcam == null)
+ {
+ _webcam = new Webcam { DeviceId = device.Id, Name = device.DisplayName };
+ OnPropertyChanged(nameof(CanChangeWebcam));
+ OnPropertyChanged(nameof(CanAddWebcam));
+ }
+ else if (_webcam.DeviceId != device.Id)
+ {
+ await SwapWebcamIdentityAsync(device);
+ }
+
+ var config = new WebcamSceneConfig
+ {
+ WebcamId = _webcam.Id,
+ Name = _webcam.Name,
+ Width = 480,
+ Height = 270,
+ X = MasterFrameWidth - 480 - 32,
+ Y = MasterFrameHeight - 270 - 32,
+ };
+
+ scene.Elements.Add(config);
+ SelectedElement = config;
+ OnPropertyChanged(nameof(ShowEmptySceneHint));
+ OnPropertyChanged(nameof(ShowSourcesEmptyHint));
+ UpdateActiveBackground();
+
+ // PreviewBitmapChanged fires only on the camera's first frame, so a config
+ // added while the session is already running must pick up the shared bitmap
+ // directly (it's written in place from then on).
+ if (_cameraManager.GetPreviewBitmap(_webcam.DeviceId) is { } running)
+ config.VideoImageSource = running;
+
+ var started = await _cameraManager.AcquireAsync(_webcam.DeviceId);
+ if (!started)
+ {
+ _notifications.Warning("Camera unavailable",
+ WebcamError ?? "Couldn't start that camera. It may be in use by another app, or Windows camera access may be turned off.");
+ }
+ }
+
+ // Swaps the device on the app-wide webcam identity. The old device is stopped
+ // unconditionally; each scene that uses the webcam re-acquires the new one so
+ // the per-config refcount stays honest. Only called when the device differs.
+ private async Task SwapWebcamIdentityAsync(CameraDeviceInfo device)
+ {
+ var oldDevice = _webcam!.DeviceId;
+ _webcam.DeviceId = device.Id;
+ _webcam.Name = device.DisplayName;
+ ScheduleSave();
+
+ if (!string.IsNullOrWhiteSpace(oldDevice) && oldDevice != device.Id)
+ await _cameraManager.ReleaseAllAsync(oldDevice);
+
+ foreach (var unused in Scenes.SelectMany(s => s.Elements.OfType()))
+ {
+ var started = await _cameraManager.AcquireAsync(device.Id);
+ if (!started)
+ {
+ _notifications.Warning("Camera unavailable",
+ WebcamError ?? "Couldn't start that camera. It may be in use by another app, or Windows camera access may be turned off.");
+ break;
+ }
+ }
+ }
+
+ // "Change Webcam…" from the webcam's context menu: picker, then swap the
+ // app-wide identity if a different device was chosen.
+ private async Task ChangeWebcamAsync()
+ {
+ if (_webcam == null) return;
+
+ var dialog = new CameraPickerDialog(new CameraPickerViewModel(_cameraEnumerator))
+ {
+ Owner = Application.Current.MainWindow
+ };
+ if (dialog.ShowDialog() != true || dialog.PickedDevice == null) return;
+ if (dialog.PickedDevice.Id == _webcam.DeviceId) return;
+
+ await SwapWebcamIdentityAsync(dialog.PickedDevice);
+ }
+
+ // "Show Webcam" from a right-click on the empty preview. Unhides this scene's
+ // existing (hidden) config — the config row survives Hide in this scene — or
+ // adds the webcam here for the first time (which opens the camera picker).
+ private void ShowWebcamInStagedScene()
+ {
+ var scene = StagedScene;
+ if (scene == null) return;
+
+ var config = scene.WebcamConfig;
+ if (config != null)
+ {
+ config.IsVisible = true;
+ SelectedElement = config;
+ return;
+ }
+
+ _ = AddWebcamToStagedSceneAsync();
+ }
+}
\ No newline at end of file
diff --git a/ViewModels/MainViewModel.cs b/ViewModels/MainViewModel.cs
index 345202d..8debead 100644
--- a/ViewModels/MainViewModel.cs
+++ b/ViewModels/MainViewModel.cs
@@ -106,13 +106,6 @@ public partial class MainViewModel : ViewModelBase
// news up the real service; the VM is built from XAML so no DI seam exists.
private readonly INotificationService _notifications = new NotificationService();
- // Webcam: one camera input app-wide. The identity (DeviceId) lives on the
- // Webcam entity; each scene that shows the webcam has a WebcamSceneConfig
- // (webcam.{scene}.config). CameraManager still owns the single session.
- private readonly ICameraEnumerator _cameraEnumerator;
- private readonly CameraManager _cameraManager;
- private Webcam? _webcam;
- private string? _webcamError;
private SocialsConfig? _socials;
private VideoFrame? _socialBarFrame;
private readonly IMicrophoneEnumerator _microphoneEnumerator;
@@ -202,26 +195,12 @@ public partial class MainViewModel : ViewModelBase
}
}
- /// The Add → Webcam menu item: enabled only while no webcam exists anywhere —
- /// one camera identity app-wide; re-adding always opens the picker.
- public bool CanAddWebcam => StagedScene != null && _webcam == null;
-
/// The Add → YouTube Chat menu item: enabled only while no chat layer exists
/// anywhere — one chat overlay per layout.
public bool CanAddYouTubeChat
=> StagedScene != null
&& !Scenes.Any(s => s.Elements.OfType().Any(e => e.Type == SourceType.ChatBox));
- ///
- /// Right-click-on-preview → "Show Webcam": offered when the active scene has no
- /// webcam config (add one) or hides it (unhide — keeps the config row).
- ///
- public bool CanShowWebcamInStagedScene
- => StagedScene is { } scene && (scene.WebcamConfig == null || !scene.WebcamConfig.IsVisible);
-
- /// Swap-the-device item: enabled once a camera has been picked at all.
- public bool CanChangeWebcam => _webcam != null;
-
// ─── Social bar (global resource, shown on every scene) ───
private static readonly SolidColorBrush BarOnBrush = CreateBrush("#2ecc71");
@@ -249,25 +228,11 @@ public partial class MainViewModel : ViewModelBase
private readonly ISocialValidator _socialValidator = new HttpSocialValidator();
- /// Shows the mirror / clip-shape row in the source chip when a webcam is selected.
- public bool IsWebcamSelected => SelectedElement is WebcamSceneConfig;
-
/// True when the Elements panel should show the "No configurable elements" hint.
public bool IsElementsPanelEmpty => SelectedElement is { } el
&& !el.IsDraggable && !el.IsWebcam
&& el is not Source { Type: SourceType.Countdown or SourceType.WebSource or SourceType.ChatBox };
- ///
- /// The reason the webcam feed is down (device in use, offline, locked, no frames),
- /// or null when it's alive. Surfaced as a red chip so a dead camera is never a
- /// silent empty box. Cleared the moment a real frame arrives.
- ///
- public string? WebcamError
- {
- get => _webcamError;
- private set => SetProperty(ref _webcamError, value);
- }
-
public StreamStatus StreamStatus
{
get => _streamStatus;
@@ -973,57 +938,6 @@ public partial class MainViewModel : ViewModelBase
private const double MasterFrameWidth = 1920;
private const double MasterFrameHeight = 1080;
- // Webcam size safeguard: no more than half the frame in any dimension
- // (960x540 over the 1920x1080 master), and no less than 10% of it
- // (192x108). Enforced at resize and on layout load. The Chat scene is
- // exempt from the per-dimension half — its webcam may take half the
- // screen AREA (~1358x764 @16:9) so the viewer sees the creator better,
- // matched by canonical name (SceneCatalog.IsChat).
- public const double WebcamMaxWidth = 960;
- public const double WebcamMaxHeight = 540;
- public const double WebcamChatMaxWidth = 1358;
- public const double WebcamChatMaxHeight = 764;
- public const double WebcamMinWidth = MasterFrameWidth * 0.1;
- public const double WebcamMinHeight = MasterFrameHeight * 0.1;
-
- public static double MaxWebcamWidthFor(string? sceneName)
- => SceneCatalog.IsChat(sceneName) ? WebcamChatMaxWidth : WebcamMaxWidth;
-
- public static double MaxWebcamHeightFor(string? sceneName)
- => SceneCatalog.IsChat(sceneName) ? WebcamChatMaxHeight : WebcamMaxHeight;
-
- internal static void ClampWebcamToBounds(WebcamSceneConfig config, string? sceneName)
- {
- var maxWidth = MaxWebcamWidthFor(sceneName);
- var maxHeight = MaxWebcamHeightFor(sceneName);
- var scale = Math.Min(maxWidth / config.Width, maxHeight / config.Height);
- if (scale < 1)
- {
- config.Width = Math.Round(config.Width * scale);
- config.Height = Math.Round(config.Height * scale);
- return;
- }
-
- var minScale = Math.Max(WebcamMinWidth / config.Width, WebcamMinHeight / config.Height);
- if (minScale > 1)
- {
- config.Width = Math.Round(config.Width * minScale);
- config.Height = Math.Round(config.Height * minScale);
- }
- }
-
- // One-time heal for layouts saved before the rect dims were persisted (v3→v4):
- // a Traditional webcam that ended up square (Round resize then reload lost the
- // pre-Round rect) gets widened to 16:9, keeping the height. Round is skipped —
- // a square bounding box is correct there — and an explicit pre-Round rect wins.
- internal static void HealLegacySquareRect(WebcamSceneConfig config)
- {
- if (config.ClipShape != ClipShape.Traditional) return;
- if (config.RectWidth != null || config.RectHeight != null) return;
- if (Math.Abs(config.Width - config.Height) >= 1) return;
- config.Width = Math.Round(config.Height * 16.0 / 9.0);
- }
-
public QualityOption[] QualityOptions { get; } =
{
new("1080p60", 60, 8.0, 1920, 1080),
@@ -1123,13 +1037,10 @@ public partial class MainViewModel : ViewModelBase
// Commands
public ICommand AddSceneCommand { get; }
public ICommand AddSourceCommand { get; }
- public ICommand AddWebcamCommand { get; }
public ICommand AddImageCommand { get; }
public ICommand RemoveSourceCommand { get; }
public ICommand EditElementCommand { get; }
public ICommand ToggleElementVisibilityCommand { get; }
- public ICommand ChangeWebcamCommand { get; }
- public ICommand ShowWebcamCommand { get; }
public ICommand ToggleMicMuteCommand { get; }
public ICommand ToggleGameMuteCommand { get; }
public ICommand OpenMicPickerCommand { get; }
@@ -1399,54 +1310,6 @@ public partial class MainViewModel : ViewModelBase
AppLog.Write("LoadLayout end");
}
- // After a layout load / file open: re-point the webcam identity, stop the
- // previous device if it changed, and acquire the current one once per scene
- // that uses it (CameraManager refcounts by DeviceId — one camera session).
- private void ReacquireWebcam()
- {
- var previousDevice = _webcam?.DeviceId;
- _webcam = _layoutStore.Webcam;
- var newDevice = _webcam?.DeviceId;
-
- if (!string.IsNullOrWhiteSpace(previousDevice) && previousDevice != newDevice)
- _ = _cameraManager.ReleaseAllAsync(previousDevice);
-
- OnPropertyChanged(nameof(CanChangeWebcam));
- OnPropertyChanged(nameof(CanAddWebcam));
-
- if (_webcam == null || string.IsNullOrWhiteSpace(newDevice)) return;
- if (previousDevice != newDevice)
- foreach (var unused in Scenes.SelectMany(s => s.Elements.OfType()))
- _ = _cameraManager.AcquireAsync(newDevice);
-
- // A session already running (same device, or one that produced a first
- // frame) has its shared bitmap; freshly loaded configs must adopt it here,
- // because PreviewBitmapChanged never re-fires for an existing bitmap.
- if (_cameraManager.GetPreviewBitmap(newDevice) is { } running)
- foreach (var config in Scenes.SelectMany(s => s.Elements.OfType()))
- config.VideoImageSource = running;
- }
-
- // CameraManager creates the shared WriteableBitmap on the UI thread at the
- // device's frame size; every scene's webcam config picks it up from here. A
- // bitmap means a real frame arrived — the camera is provably alive.
- private void OnCameraPreviewBitmapChanged(string deviceId, WriteableBitmap bitmap)
- {
- if (_webcam?.DeviceId != deviceId) return;
- WebcamError = null;
- foreach (var scene in Scenes)
- foreach (var config in scene.Elements.OfType())
- config.VideoImageSource = bitmap;
- }
-
- // The camera failed to start or died asynchronously (in use, offline, locked,
- // no frames within the proof timeout) — surface it instead of a silent box.
- private void OnCameraFailed(string deviceId, string message)
- {
- if (_webcam?.DeviceId != deviceId) return;
- WebcamError = $"Webcam offline: {message}";
- }
-
private void RegisterLoadedWebSources()
{
if (_webView2Manager == null) return;
@@ -1616,125 +1479,6 @@ public partial class MainViewModel : ViewModelBase
return $"{baseName}{i}";
}
- // Adds the webcam to the active scene. The creator ALWAYS picks from the
- // cameras Windows has registered — never silently resurrects the previous
- // camera (which is what happened after deleting one scene's webcam while
- // another scene still used it). Picking a different camera than the current
- // app-wide one swaps it everywhere, so the single-identity model stays honest;
- // each scene's placement config is independent (webcam.{scene}.config).
- private async Task AddWebcamToStagedSceneAsync()
- {
- var scene = StagedScene;
- if (scene == null || scene.WebcamConfig != null) return;
-
- var dialog = new CameraPickerDialog(new CameraPickerViewModel(_cameraEnumerator))
- {
- Owner = Application.Current.MainWindow
- };
- if (dialog.ShowDialog() != true || dialog.PickedDevice == null) return;
- var device = dialog.PickedDevice;
-
- if (_webcam == null)
- {
- _webcam = new Webcam { DeviceId = device.Id, Name = device.DisplayName };
- OnPropertyChanged(nameof(CanChangeWebcam));
- OnPropertyChanged(nameof(CanAddWebcam));
- }
- else if (_webcam.DeviceId != device.Id)
- {
- await SwapWebcamIdentityAsync(device);
- }
-
- var config = new WebcamSceneConfig
- {
- WebcamId = _webcam.Id,
- Name = _webcam.Name,
- Width = 480,
- Height = 270,
- X = MasterFrameWidth - 480 - 32,
- Y = MasterFrameHeight - 270 - 32,
- };
-
- scene.Elements.Add(config);
- SelectedElement = config;
- OnPropertyChanged(nameof(ShowEmptySceneHint));
- OnPropertyChanged(nameof(ShowSourcesEmptyHint));
- UpdateActiveBackground();
-
- // PreviewBitmapChanged fires only on the camera's first frame, so a config
- // added while the session is already running must pick up the shared bitmap
- // directly (it's written in place from then on).
- if (_cameraManager.GetPreviewBitmap(_webcam.DeviceId) is { } running)
- config.VideoImageSource = running;
-
- var started = await _cameraManager.AcquireAsync(_webcam.DeviceId);
- if (!started)
- {
- _notifications.Warning("Camera unavailable",
- WebcamError ?? "Couldn't start that camera. It may be in use by another app, or Windows camera access may be turned off.");
- }
- }
-
- // Swaps the device on the app-wide webcam identity. The old device is stopped
- // unconditionally; each scene that uses the webcam re-acquires the new one so
- // the per-config refcount stays honest. Only called when the device differs.
- private async Task SwapWebcamIdentityAsync(CameraDeviceInfo device)
- {
- var oldDevice = _webcam!.DeviceId;
- _webcam.DeviceId = device.Id;
- _webcam.Name = device.DisplayName;
- ScheduleSave();
-
- if (!string.IsNullOrWhiteSpace(oldDevice) && oldDevice != device.Id)
- await _cameraManager.ReleaseAllAsync(oldDevice);
-
- foreach (var unused in Scenes.SelectMany(s => s.Elements.OfType()))
- {
- var started = await _cameraManager.AcquireAsync(device.Id);
- if (!started)
- {
- _notifications.Warning("Camera unavailable",
- WebcamError ?? "Couldn't start that camera. It may be in use by another app, or Windows camera access may be turned off.");
- break;
- }
- }
- }
-
- // "Change Webcam…" from the webcam's context menu: picker, then swap the
- // app-wide identity if a different device was chosen.
- private async Task ChangeWebcamAsync()
- {
- if (_webcam == null) return;
-
- var dialog = new CameraPickerDialog(new CameraPickerViewModel(_cameraEnumerator))
- {
- Owner = Application.Current.MainWindow
- };
- if (dialog.ShowDialog() != true || dialog.PickedDevice == null) return;
- if (dialog.PickedDevice.Id == _webcam.DeviceId) return;
-
- await SwapWebcamIdentityAsync(dialog.PickedDevice);
- }
-
- // "Show Webcam" from a right-click on the empty preview. Unhides this scene's
- // existing (hidden) config — the config row survives Hide in this scene — or
- // adds the webcam here for the first time (which opens the camera picker).
- private void ShowWebcamInStagedScene()
- {
- var scene = StagedScene;
- if (scene == null) return;
-
- var config = scene.WebcamConfig;
- if (config != null)
- {
- config.IsVisible = true;
- SelectedElement = config;
- return;
- }
-
- _ = AddWebcamToStagedSceneAsync();
- }
-
private void AddImage()
{
var scene = StagedScene;
diff --git a/ViewModels/index.md b/ViewModels/index.md
index 0c03f39..0c3de11 100644
--- a/ViewModels/index.md
+++ b/ViewModels/index.md
@@ -50,12 +50,19 @@ Progress (Commit N/12 · tag):
`ChangeCaptureCommand`/`RefreshCaptureCommand`/`SetBackgroundDisplayCommand`/
`ToggleShowDesktopCommand`/`BrowseBackgroundCommand`). `ShowPreviewPlaceholder`
stays in core.
+- **Commit 5/12 · `refactor-commit-5`: Webcam landed** → `MainViewModel.Webcam.cs`
+ (webcam identity `_webcam`/`ReacquireWebcam`/`SwapWebcamIdentityAsync`, per-scene
+ configs `AddWebcamToStagedSceneAsync`/`ShowWebcamInStagedScene`, capture lifetime
+ `OnCameraPreviewBitmapChanged`/`OnCameraFailed`, gating `CanAddWebcam`/
+ `CanChangeWebcam`/`CanShowWebcamInStagedScene`/`IsWebcamSelected`/`WebcamError`,
+ size safeguard `WebcamMax*`/`ClampWebcamToBounds`/`HealLegacySquareRect`,
+ `AddWebcamCommand`/`ChangeWebcamCommand`/`ShowWebcamCommand`).
- Core file keeps: fields, ctor, `Shutdown`/`SaveLayoutNow`/`ScheduleSave`/`LoadLayout`,
`SetPanel`/`ShowOverlay`/`ShowLicenses`, `CreateBrush`, `OpenUrl`/`ComposeEmail`,
`SubmitBug`/`SubmitFeature`, window-geometry pass-through.
- Partials: `Account` ✔ (Commit 1), `License` ✔ (Commit 2), `Scenes` ✔ (Commit 3), `Background` ✔ (Commit 4),
- `Webcam` (webcam identity + per-scene configs + captures),
+ `Webcam` ✔ (Commit 5),
`Audio` (mic + game bars + TRAX), `Trax` (music player), `Socials` (social bar + dialog),
`Streaming` (go-live/record + health + quality tiers + media source pipeline),
`Chat` (YouTube chat + mock preview + chatbox render), `Overlays` (settings/bug/feature/about