37791086ae7802ec358e0d4cb8e1d3e0483bc129
11
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
71d0e87cac |
feat(blur): a privacy blur layer for the Live scene — boxes and circles over the desktop
Creator request 2026-11-01: a layer that hides part of the desktop from viewers. Box or circle, drag/resize like any layer, one strength slider, several allowed. **Z-order is policy, not preference.** `AddSource` inserts a blur directly above the background and therefore below the brand logo, and `OnSceneElementsReordered` pushes any blur that ends up above the brand back down. Nothing else in the app can move it into the brand's way. **Overlap = the strongest single blur, never a stack.** `SceneCompositor.ApplyBlurLayers` collects every visible blur the first time one is reached in the z-order, sorts them strongest-first, and shares one coverage map: a pixel already covered by a stronger blur is skipped. Blurring back-to-front would have produced something stronger than any slider setting allows, which is exactly what was ruled out. **One blur curve, shared with the webcam.** `RegionBlur` carries the constants the webcam blur already used — downscale 4, radius 1–6 downscaled px, `RadiusFor(strength)` — and `BackgroundEffectProcessor` now delegates to it instead of holding its own copy. The layer's slider is the same 0–1 scale with the same 0.5 default, so "half strength" means the same blur in both features. Cost is proportional to the blur's own rect, not the frame. **Live scene only**, enforced in `AddSource` as well as the (+) menu (`CanAddBlur`), so a stale menu item or a scripted call cannot plant one on BRB. `ElementKind.Dynamic`, so it is never baked into the static base and every render path gets it — pump, preview, thumbnails, scene dump. Persistence: `Source.BlurAmount` column + migration; shape rides the existing `ClipShape`. Properties panel: Box | Circle plus the strength slider, gated on `IsBlurSelected`. 4 tests: outside the mask is byte-identical, inside loses its hard edges, a circle leaves its box corners alone, and two identical stacked blurs equal one (the coverage map). Release builds at 0 warnings. |
||
|
|
3239808a73 |
Layers panel and scene strip: scene layers header with icons, per-type layer icons, PROGRAM badge and selected/live orange borders, scene name and hotkey overlays, Starting as default scene
- LeftPanel: SCENE LAYERS header with layer count; per-type blue icons; properties header gear; layers list min/max height; properties placeholder; list and properties indent - SceneThumbnailStrip: names/hotkeys over the tile; PROGRAM badge with blinking red dot on the selected tile; orange border on selected and live tiles; fixed the element border override that hid the style border; reflag tiles on load - LiveScene is now set on go-live, record start, and live transitions (it was never assigned, so IsLive never fired) - Startup stages the scene named Starting - DESIGN.md: color roles (red = YouTube/on-air/record, orange #ff8c3a = attention, amber = game audio/Super Chat) - Staged live tile uses background art instead of the green LIVE placeholder Not pushed. |
||
|
|
3a77662a95 |
vv webcam Unit 2: the Chat scene in the vertical tier - rect only, tile or one whole 607x540 area
The creator's locked table (2026-10-17) gives the Chat scene its own rules in the 9:16 tier: RECT ONLY - the circle option is not offered - and a size that is either the normal small tile or ENLARGED to fill one whole 607x540 area (top or bottom). Unit 1 shipped the per-scene placement; this unit is the Chat scene's own variant of it. The rule lives on the MODEL, not on the button: - Models/SceneElement.cs: ClipShape and ToggleClipShape() are now virtual. - Models/WebcamSceneConfig.cs: VerticalShapeLocked (set per scene by the VM) OVERRIDES the ClipShape property, so no writer can slip a circle in - the toggle, the Shape button and the placement swap all go through it. The lock is cleared BEFORE the 16:9 layout comes back, so the desktop shape returns. - ViewModels/MainViewModel.Webcam.cs: CanToggleClipShape (the circle is NOT offered in the vertical Chat layout - the Shape button disappears, it is not merely greyed out), IsChatSceneCamera, ResizeWebcamInVerticalTier. - Models/VerticalCameraLayout.cs: FillsArea (the "enlarged" state, derived - no new column) and SizeFor - the Chat camera's TWO sizes, the tile or the whole area, whichever is nearer the requested width, so the resize handle has two detents instead of any size in between. - An enlarged camera FILLS the area its centre is in (top or bottom), so dragging it up/down swaps the areas. - The Chat camera is auto-tiled like the Live one when the vertical tier is picked - otherwise the vertical chat stream has no camera at all. - Controls/PreviewPane.xaml.cs: the resize routes through the VM; the drag assigns, lets the model clamp into the band, then snaps (so the enlarged camera can be dragged from the top area to the bottom one). Test: ytLive.Tests/VerticalChatCameraLayoutTests.cs - the ONE Good Dog test for the unit: the vertical tier is picked -> the Chat camera comes into the band as a small RECT tile -> the circle option is not offered and cannot be set even on the model -> dragged up and enlarged it fills the TOP area exactly -> back to 16:9 the desktop layout is restored exactly and the circle option is offered again. 430/430 tests pass, 0 warnings. Research (the creator's table is the established practice, not our invention): - Streamlabs' vertical guide: "splitting the screen in half horizontally, placing the camera feed in the top half and the main content on the bottom (or vice versa) may be the bet", and a just-chatting vertical wants the camera "to take up most of the screen" (streamlabs.com/content-hub/post/overlays-vertical-and-horizontal-streaming) - Bloopbot's vertical overlay docs describe the same per-layer model we use at scene granularity: "move or resize a layer on Vertical and it stops being Auto-placed. It keeps the place you gave it, and the horizontal layout is not touched" (bloopbot.com/docs/vertical-overlays-twitch-dual-format) - our swap-at-tier-change, stored per scene. - Landmine recorded: "about the bottom 560 px of the vertical video can be covered by Twitch's chat and player controls on phones" (Bloopbot) - the Chat camera's enlarged BOTTOM area may be half-hidden by the platform chrome; TASK 53's safe-zone work decides that. Memory in the same commit: ai.md (the Chat rules, the shape lock, the tests), TASKS.md + TASKS/task-57-vertical-camera-layout.md (TASK 57 complete), HANDOFF.md, MyMistakes.md (3 recipes: lock the model property not the UI button; two detents for a discrete size set; the edit tool cannot match an oldString with an em dash), Models/ViewModels/Controls index rows. |
||
|
|
6be062c45f |
vv webcam Unit 1: the vertical (9:16) tier keeps its OWN camera placement per scene
The vertical tier is a centred band crop of the SAME 1920x1080 master (band x
656.25, w 607.5, h 1080), so the 16:9 camera placement (x~1408) falls outside the
cropped window and the creator is simply not in the vertical stream. Each scene
now keeps its own vertical position, size and shape; switching back to 16:9
restores the desktop layout exactly (the creator's locked table, 2026-10-17).
Geometry only - NO render-path change. The preview and the compositor both read
the element's own X/Y/W/H + ClipShape, so the tier switch swaps the LIVE geometry:
- Models/VerticalCameraLayout.cs (new): the pure math - the band, its two 607x540
areas, the six spots (top/bottom x left/centre/right), the 32px inset and the
270x152 default tile (480x270 in the 1080x1920 output) in the bottom-right spot,
the corner the 16:9 default webcam uses.
- Models/WebcamSceneConfig.cs: the vertical placement lives in seven additive
columns (VerticalX/Y/Width/Height/ClipShape/RectWidth/RectHeight; NULL = "this
scene has no vertical placement yet"). The 16:9 layout stays in the old columns
and is parked in the transient Landscape* fields while the vertical tier is
live; Begin/EndVerticalLayout + CommitLiveLayoutToVertical do the swap. The
X/Y/W/H overrides clamp every write into the band - outside it the camera is
invisible in the vertical output.
- Models/SceneElement.cs: X/Y/Width/Height became virtual so the derived element
constrains every writer (drag, resize handle, typed fields) in ONE choke point
instead of three writers each re-implementing the rule.
- ViewModels/MainViewModel.Webcam.cs + .Streaming.cs: IsVerticalTier/VerticalBand,
ResolveVerticalCameraLayout(s) (the tier swap), MoveWebcamInVerticalTier /
SnapWebcamInVerticalTier (the six-spot snap, by the tile's centre),
EnsureVerticalCameraLayout; ApplyStreamQuality is the tier-change hook. The
Live scene gets its small tile automatically on the first vertical switch (the
vertical stream would otherwise have no camera at all); the other scenes keep
their 16:9 placement until the creator drags them into the band.
- Controls/PreviewPane.xaml.cs: the drag/resize route through the VM snap under
the vertical tier instead of free-floating over the master.
Controls/LeftPanel.xaml: the webcam properties gained a Geometry section
(X/Y/W/H) for the manual entry the creator asked for, bound to the LIVE
placement, gated on the VM's IsWebcamSelected (NOT SelectedElement.IsWebcam -
the TASK 51 test finds that panel by binding text and would match twice).
- The store: seven guarded ADD COLUMNs, the INSERT/SELECT extended, and the
X/Y/W/H/ClipShape columns stay the 16:9 layout - while the vertical tier is
live the save writes the parked values there and the live ones into Vertical*.
Test: ytLive.Tests/VerticalWebcamLayoutTests.cs - the ONE Good Dog test. The
vertical tier brings the Live camera INTO the band as a small tile; dragging it to
the top-right lands it top-right inside the band; back to 16:9 the desktop layout
is restored exactly (1408/778/480x270); both placements survive a save + reload.
Full suite 429/429, 0 warnings. LayerReorderPersistenceTests.RealMouseDrag fails
ALONE on a clean tree in this environment - pre-existing flake, proven by stashing
this work, not a regression.
Derivative work: OBS has no built-in per-aspect-ratio layout storage - the
maintainers' answer is "create a second scene and add the scene you want to send
as source" (obsproject.com/forum/threads/161307) - and Streamyard's dual-aspect
guide says the same ("no built-in way to store separate layouts per aspect ratio",
so you rely on manual workarounds and plugins):
https://streamyard.com/blog/dual-aspect-ratio-streaming-tool
Our per-scene, per-tier stored placement, resolved on the tier change, is the
step past both.
Memory in the same commit: ai.md (new section), TASKS.md + TASKS/task-57-*.md
(Unit 1 shipped, Unit 2 = the Chat scene still open), HANDOFF.md, MyMistakes.md
(the per-tier geometry-swap recipe, the binding-text panel trap, the store's
webcam-identity rule) and the Models/Services/ViewModels/Controls index rows.
|
||
|
|
352f117cb2 | TASK 51 slice 5: webcam Background section in the Layers properties — Remove/Blur PillToggles (radio-exclusive) + Strength slider; strength changes never blank the webcam (new variant inherits the same-effect frame + bitmap). TASK 51 built (Unit: bg-effects UI) | ||
|
|
de22983b36 | TASK 51 slice 4: camera-path hookup — CameraManager renders per-scene effect variants (one segmentation per raw frame on a latest-frame-drop worker, raw path untouched), VM declares/adopts variants per config; separable upsampling cuts Remove 46->3 ms, Blur 80->18 ms (Debug, 720p) (Unit: bg-effects hookup) | ||
|
|
75723acc1b |
fix(webcam): offer the Web Cam row only when a camera is attainable (live lock), not merely selected
Creator refinement: 'offered iff there's not one already configured & attainable'. CanAddWebcam now requires IsWebcamAttainable = identity present AND a RUNNING session (CameraManager.IsRunning) — an identity whose camera was unplugged or whose lock keeps failing leaves the row greyed with reason 'No webcam is currently available…', and it un-greys the moment a session is live. Gate re-raised at every attainability flip: staging, removal, startup lock success, first frame, camera failure, identity swap. Root cause the old test surfaced: the startup pass skipped acquiring when the loaded identity's configs already held the session, so there was no independent app base ref — removing the last placement dropped RefCount to 0 and killed the session. The single-camera branch now ALWAYS acquires (a running session just bumps), laying the app-wide base hold so the default outlives the scenes. Good Dog: WebcamMenuGateTests second fact — identity loaded, session can't start → row NOT offered + 'No webcam is currently available…' tooltip. Positive fact waits for WebcamStartupValidationTask to make the IsRunning read deterministic. Docs same commit (ai.md gate + base-lock, TASKS.md, HANDOFF.md incl. proven pre-existing audio flake). 305 tests (304 pass + known flake), 0 warnings. |
||
|
|
9d00955004 |
feat(webcam): app-default gate slice — per-scene offer, Add places default directly, identity survives removal
Chat's camera no longer greys Web Cam in Live (per-scene max, not app-wide); TASK 26 superseded by creator directive (app-level resource model). Add Webcam now places the existing app default without the picker; the picker runs only for the initial selection. Removing the last placement keeps the identity (_webcam never nulled) so Add stays offered. Startup pass adopts a solo camera as the app default, so a clean layout offers the layer in Live/Chat at once. Dynamic WebcamAddToolTip names the why (incl. the 'graduated to OBS' line). Good Dog: WebcamMenuGateTests rewritten (real app + temp DB + camera seams) — identity in Chat does not gray Live; Add in Live places same wc-1 no picker; scene-with-placement stays gray; identity survives both removals (DB row 1). WebcamStartupResourceTests single-lock fact asserts CanAddWebcam after adoption. Docs same commit (ai.md supersession, TASKS.md, HANDOFF.md). 304/304, 0 warnings. |
||
|
|
1e4017df03 |
feat(webcam): resource lifecycle startup slice — poll-on-start, single-cam lock, persistent Layers alert
The creator couldn't add a webcam to Live (grayed app-wide) and nothing in the app explained why. Ground truth from the live DB: one Webcam identity AND one WebcamSceneConfig in the Chat scene — so the gray was the single-identity rule working, but the reason was unobservable. This slice makes the webcam a resource the app validates and locks, mirroring how OBS reserves its devices. At startup we enumerate the OS once (ValidateWebcamResourceStartupAsync, fired after LoadLayout, stored as WebcamStartupValidationTask for tests to await): - 0 webcams -> app runs on, layer inactive, no alarm - exactly 1 -> attempt CameraManager.AcquireAsync as an app-wide lock; on failure show a persistent red alert at the bottom of the Layers panel (WebcamLockAlert + Retry) that re-polls every 5s and clears itself the moment the camera locks, or on any first real frame - >=2 -> deliberately no auto-lock; camera selection belongs to the App Settings dialog (gear) — next slice CameraManager.IsRunning(deviceId) tells the pass a session already exists (started OR still starting) so loaded identity configs count as the lock and the pass never double-acquires. Test seams mirror LayoutPathOverride: CameraEnumeratorOverride / CameraFrameSourceFactoryOverride so the startup probe never touches real hardware under test. Good Dog test: WebcamStartupResourceTests x3 — single-cam locked + app runs on, zero-cams no-alarm/no-lock, lock-fails -> red alert -> Retry -> clears. Full suite 304/304, build 0 warnings, scope-check passed. Docs in-commit: TASKS Open items + ai.md Webcam section + HANDOFF rewrite. Also corrects the record: 'NVIDIA Broadcast opens the webcam exclusively' was a suspect-list claim (CameraConflictProbe reads process names only, no handles) — not restated as fact. |
||
|
|
670fe3a10a |
TASK 31: SceneGraph component + baked-crust compositor optimization
- Add ElementKind (Static/Dynamic) to SceneElement base; Source/WebcamSceneConfig classify - Services/SceneGraph.cs: owns the Scenes collection (ViewModel's Scenes delegates to it), the element mutation surface (Add/Insert/Remove/Move, each invalidating the bake), and the queries that were scattered LINQ (GetBackground/GetWebcam/GetChatBoxes/GetSplitPoint/IsStatic) - SceneCompositor: split-aware BakeStaticBase + CompositeLayers + Render(.., staticBase, split); builds/caches the static base below the split point in source-rect space - FramePump: optional SceneGraph -> optimized bake+composite path; falls back to full render - MainViewModel: routes element mutations through the graph; invalidates the bake on static layout/opacity/visibility/useDefaultBackground changes and after background heal/Ensure - Integration test SceneGraphTests.BakedStaticBase_WithDynamicLayer_CompositesCorrectly Derivative survey (mandated): tried before writing — OBS does per-source opacity/visibility caching and static-scene baking; this mirrors OBS's 'cached static source' optimization. 3 documented defensive deviations from the spec (ChatOverlayLayer stays decoupled; background helpers stay VM-static for direct testability; full facade peel deferred post-1.0) in TASKS.md + ai.md. Scope check passed; clean build 0 warnings. |
||
|
|
e2867198b9 | refactor: split Webcam out of MainViewModel — Commit 5/12 (tag refactor-commit-5) |