Commit Graph

73 Commits

Author SHA1 Message Date
gramps 716a77f61a fix(pump+compositor): take-3 starvation — deadline pacing + row-blit fast paths
Two defects made the producer 17x slow (37s record -> 2.1s/127-frame file,
rawvideo stamps by arrival): FramePump slept the FULL interval after each
render (period = render+submit+interval) and SceneCompositor did per-pixel
float sampling + Math.Round blends over all 2.07M master pixels, scanning the
whole destination per overlay (258ms avg render vs 1.5ms submit).

Both solutions are established, not invented — researched before coding per
the derivative-work rule:
- deadline pacing: OBS libobs/media-io/video-io.c video_thread (nextTick +=
  intervalTicks, sleep only the remainder, rebase on overrun, never burst)
- row blits: libyuv pattern (BSD-3, chromium.googlesource.com/libyuv/libyuv)
  — 1:1 aligned identity fast path, per-pixel alpha branch, integer
  fixed-point blend, overlay clipped to the intersection rect, skip the dead
  black pre-fill when the backdrop covers

ONE integration test: Pump_Paces_To_The_Deadline_Compensating_Render_Cost
(lands after a fake-seam lesson: pacing fakes must await, not complete
synchronously, or the pump loop runs inline on StartAsync and hangs vstest).
Clean build 0 warnings; FramePumpTests 10/10, SceneCompositor/SceneGraph/
SocialBar/StretchMath 20/20. Docs same commit: ai.md pipeline section,
TASKS.md TASK 18 (webcam-in-output + rename modal verified from take 3),
MyMistakes recipe, HANDOFF rewritten. Take 4 pending on the user's machine.
2026-09-03 09:02:45 -07:00
gramps 89fee6ca4a fix: webcam identity->device key for output frames; top bar always has a Start; light-pill grouping
Take two (2026-09-01) three confirmed defects, all from creator feedback + the
new resolver logging:

1. NO WEBCAM IN OUTPUT: WebcamSceneConfig.WebcamId carries the identity GUID;
   CameraManager keys sessions by DEVICE id — GetLatestFrame(guid) returned null
   forever, the compositor silently dropped the layer while the preview (bitmap
   path) looked fine. The resolver logging added in 8dcaee0 caught it red-handed.
   DeviceKeyForWebcam maps identity->device through the _webcam singleton (identity
   mismatch / unknown ids pass through). Test: WebcamOutputKeyTests.

2. START BUTTON VANISHED AFTER STOP: my pills-clear ruling met the old
   ShowPrimaryStartButton gate (needed IsConnected or a lit REC pill) — signed-out,
   pills-off = blank top bar, no session reachable. Start is now ALWAYS the idle
   face; unarmed Start records (local needs no account — no dead-end no-op); the
   Sign In button folds into Start's context menu ('Sign in to YouTube', shown while
   disconnected) with a dedicated SignInCommand. SessionTeardownTests extended:
   stopped session must leave a reachable Start.

3. TOP BAR ORDER (creator spec): [sign light] REC [pill]  [sign light] ON-AIR [pill]
   — each reality lamp now sits in front of its own intent switch (was: two pills,
   then two orphaned dots). Sign labels keep the shared StatusSignText style.

Tests: WebcamOutputKey, SessionTeardown, FramePump, WebcamMenuGate, GlobalHotkey,
BroadcastPullOut — 14/14 across the six classes, clean build 0 warnings.
2026-09-01 22:25:20 -07:00
gramps 7f2bda8ed3 fix(audio): game meter scales by GameAudioVolume — meter/stream/headphones all follow the one knob
Creator report 2026-09-01: with the desktop slider dragged to 20%, the game bar's
meter stayed pegged yellow/red. Root cause: the mic meter's formula (level x volume)
was cloned to the game bar WITHOUT the x-volume factor, and the clone inherited a
false premise — that WASAPI loopback capture tracks the endpoint volume. Tonight's
observation disproves it (the tap is pre-endpoint-volume), so both the meter (display)
and the mix (AudioGainProvider.LoopbackGain, fixed in 5ead064) must scale it themselves.
ai.md's two 'loopback scales with the slider' sentences corrected in the same pass.

Test seam: VolumePushOverride (internal static, mirrors LayoutPathOverride pattern) so
the test never hijacks the machine's real volume. GameMeterHonestyTests (RealApp +
temp DB): unity -> hot/red, 20% -> ~1/5 width/green, push observed.
2026-09-01 21:22:57 -07:00
gramps 5a1a3c566a fix(18): stop ends everything — pills clear, pump/prep failures roll the session back
Creator ruling 2026-09-01 (stuck REC pill after the failed first recording):
- StopStream clears RecordPillOn + OnAirPillOn — intent resets with reality.
- OnFramePumpFailed: dispatcher-marshalled FULL rollback via StopStream (was:
  toast + Error-status only when live — record-only sessions zombied with
  IsRecording=true over a dead encoder; the 20:34 attempt proved it). Toast copy
  says 'Recording stopped' vs 'stream pipeline stopped'.
- Same zombie class in the three go-live prep failure branches: StreamStatus.Error
  limbo replaced by StopStream() (audio loop + recording + pills unwind; a created
  broadcast still gets the close-out).
- AudioMixer.StopLive made explicitly idempotent (Stop() twice, rollback paths that
  never reached StartLive).
- Seams: OnFramePumpFailed + IsRecording setter internal (InternalsVisibleTo; test
  pattern mirrors LayoutPathOverride).
ONE integration test: SessionTeardownTests (real window + temp DB: pump death with
lit pill -> no zombie, no End button, Offline). AudioPipelineTests confirmed to hang
STANDALONE (pre-existing, the declared-known audio class) — ai.md test-count
paragraph corrected to stop claiming a suite total that cannot currently be measured.
2026-09-01 21:15:57 -07:00
gramps 688682d5b5 feat(9): real broadcast close-out — transition(complete) in StopStream after RTMP EOF
The specced 'End stream -> transition(complete)' call never existed: stopping relied
entirely on enableAutoStop (viewers sat on a frozen stream-offline for ~a minute, VOD
finalized late). Found during the 2026-09-01 recording-verification pass while the
creator asked 'if there's proper close-out info yt needs, we'll provide it?'

EndBroadcastAsync POSTs liveBroadcasts/transition?broadcastStatus=complete&id=..&part=status,
called after the pump stops (RTMP EOF first) and only when a live session had a broadcast —
record-only stops stay offline. invalidTransition/410 (autoStop already ended it) is logged
and returned as an error string, never thrown: a stop must never fail over close-out.
ONE integration test (URL shape + never-throws on 403). ai.md/TASKS.md design lines marked
SHIPPED with the map-lie note.

Ref: https://developers.google.com/youtube/v3/live/docs/liveBroadcasts/transition
2026-09-01 20:55:06 -07:00
gramps 22b780e075 fix(ffmpeg): re-pin dead BtbN tag (first real recording 404'd), wrap download failures in actionable IOException
The 2026-08-09 pin was a DAILY build; BtbN retention aged it out and the cold-cache
download 404'd on the creator's first native recording attempt (2026-09-01,
startup.log). Re-pinned to the MONTH-END build autobuild-2026-08-31-13-27
(N-126342, lgpl-shared win64 — 2-year retention; tag+variant recorded in TASKS.md
per the licensing rule). Verified alive: HEAD 200 + zip contents (bin/ffmpeg.exe,
bin/ffprobe.exe, 7 libav DLLs) match the name-agnostic extractor.
Also closes the wrap-gap: HttpRequestException escaped the locator untouched,
surfacing a raw 'Response status code... 404' from the frame pump; now wrapped in
IOException with the refresh-pin-or-install-ffmpeg message. FfmpegLocatorTests 7/7
(one updated, one added for the 404 case).
2026-09-01 20:52:23 -07:00
gramps 6799c40175 fix: first native launch since refactor crashed — 3 stacked faults, all closed; RoundClip 'known failure' root-caused and green
User launch (2026-09-01 19:07) NRE'd in MainViewModel ctor:
1. SceneGraph (TASK 31) was 'null!'-declared, assigned mid-ctor, but Scenes is touched
   ~120 lines earlier — field-initialized now.
2. LeftPanel extraction (c9fd1bd) moved StaticResource users (EyeButton/EyeIconStyle)
   into a UserControl while the styles stayed window-scope — invisible at parse time;
   moved to Themes/Controls.xaml (app scope, the existing rule). Full audit: these were
   the only two offenders (grep of Controls/*.xaml StaticResource keys vs app dictionary).
3. RoundClipInteractionTests — the second 'known failure' the map never explained: it
   was two stale-test layers (window.FindName across the new UserControl namescope +
   VisualTreeHelper.HitTest, which returned the IsHitTestVisible=False WebViewHostPanel
   overlay for EVERY point; UIElement.InputHitTest — the real input pipeline — shows the
   corner IS grabbable in both Traditional and Round). Test fixed, no product bug.
Verified: clean rebuild 0 warnings; app boots (log shows full MainWindow loaded; user
clicked + closed, zero new exceptions; real DB Webcam row = the genuine C920, untouched);
RoundClip + 6 RealApp classes pass natively per-class.
Docs: ai.md known-failure note → 246/247 (audio only); TASK 31 verification paragraph
corrected ('cannot run headless' overstated — per-class Windows-host vstest runs them);
MyMistakes: InputHitTest-vs-VTH recipe + namescope/app-style + shared-log facts.
Spin-guard citations: WPF Visual Tree Overview (InputHitTest vs VisualTreeHelper hit
semantics) + XAML namescope docs, learn.microsoft.com.
2026-09-01 19:33:31 -07:00
gramps 881addb5b4 TASK 21 slice 3: loop control in MediaVideoSource (process factory + Loop flag)
- IMediaFrameSource gains bool Looping.
- MediaVideoSource ctor takes Func<IDecodeProcess> processFactory instead of a
  single IDecodeProcess: a System.Diagnostics.Process can't be re-Start()ed, so
  each loop pass creates a fresh decoder. Decode wrapped in do-while(Looping):
  restart on natural EOF instead of raising Completed.
- Production wiring (MainViewModel media factory): passes the process factory
  AND FfmpegFrameRateProbe -- closes the slice-2b gap where production had no
  probe and therefore no pacing.
- Tests: loop test (single frame re-emits across passes, Completed only when
  loop cleared); fakes updated for the new interface member. Media tests 12/12,
  build 0 warnings.

Wiring Source.MediaIsLooping into the flag needs a manager-level per-path loop
provider -> lands with the UI-picker (acquisition) slice.

Derivative reference: looping media by restarting decode on EOF, standard in
playback/overlay tooling (OBS media source repeat).
2026-08-31 19:52:10 -07:00
gramps a2219be198 TASK 21 slice 2b: native-FPS pacing in MediaVideoSource (probe -> delay seam)
- MediaVideoSource takes optional IFrameRateProbe? + Func<TimeSpan,CancellationToken,Task>? delay
  seams (default Task.Delay); probes FPS once in RunAsync, delays by 1/fps after each
  emitted frame. No probe/unknown fps -> no pacing (ffmpeg pipe backpressure throttles).
- Test: MediaVideoSource_PacesFramesByProbedFps (fake probe returns 1000fps + recording
  delay; one delay per frame ~= 1ms). Media tests 5/5, build 0 warnings.

Derivative reference: per-frame delay pacing of decoded output, standard in media playback.
2026-08-31 19:47:38 -07:00
gramps 8fa78423e6 TASK 21 slice 2a: native-FPS probe seam (ffprobe parse + derive sibling)
- FfmpegFrameRateParser (pure): prefers avg_frame_rate= then r_frame_rate=,
  rational N/N/M, unknown/0 -> null.
- IFrameRateProbe + FfmpegFrameRateProbe: derives sibling ffprobe.exe from the
  located ffmpeg dir, reuses the IDecodeProcess seam for the ffprobe subprocess
  text; null if ffprobe absent.
- FfmpegLocator now also extracts ffprobe.exe (ProbeFileName) from the pinned
  archive, conditional so old caches without it degrade to no pacing.
- Tests: 6 pure parser units + 1 probe integration via fake locator/process;
  FfmpegLocatorTests still green. 15/15, 0 warnings.

Pacing (probe->delay) is slice 2b. Derivative reference: standard ffprobe
avg_frame_rate probing used across OBS/media tooling.
2026-08-31 19:45:48 -07:00
gramps 071ec8be0c TASK 21 slice 1 step 4: wire media into resolver + preview routing
Wire the MediaVideoSourceManager into the live pipeline (new partial
ViewModels/MainViewModel.Media.cs mirrors Webcam/Background partials):
readonly _mediaManager assigned in the core ctor (decode at 1920x1080 master,
dispatcher-coalesced preview), OnMediaPreviewBitmapChanged adopts the shared
bitmap onto every IsMediaSource with a matching MediaPath, a
Source { IsMediaSource, MediaPath } case in ResolveOutputFrame feeds
GetLatestFrame, and the manager is disposed on shutdown.

Source.DisplaySource now routes VideoImageSource for MediaSource (Source.cs),
so media previews/canvas show decoded video.

Compositor needs no change: BlitContent UniformToFill-scales any frame to the
element rect. Decoder/manager/seam derived-work mirrors ScreenCaptureManager
(citation in commit for slice-step-3).

Test (one unit for this change, mirroring the live-capture case):
Source_DisplaySource_IsVideoImageSource_WhenMediaSource. 14/14 media +
DisplaySource tests pass, build 0 warnings. Docs (TASKS/HANDOFF/ai.md) updated.
2026-08-31 19:32:41 -07:00
gramps 480ead0fc7 TASK 21 slice 1 step 3: IMediaFrameSource + MediaVideoSourceManager
Refactor MediaVideoSource onto IMediaFrameSource (Key/FrameAvailable/Completed/
StartAsync/StopAsync, renaming Start->StartAsync, FrameReady->FrameAvailable),
behavior preserved. Add MediaVideoSourceManager: app-wide decode-session owner
refcounted by MediaPath with Func<string,IMediaFrameSource?> factory seam,
Acquire/Release/ReleaseAll/GetLatestFrame, coalescing each file's frames onto
the UI dispatcher onto one shared WriteableBitmap; MediaFailed + PreviewBitmapChanged.

Mirrors ScreenCaptureManager (screen-capture session ownership + dispatcher
coalescing), per the codebase precedent and derivative-work rule.

Tests: MediaVideoSourceManagerTests (5 unit + 1 integration: single shared
bitmap, coalesce-to-latest); MediaVideoSourceTests updated for renames.
10/10 media tests pass, build 0 warnings. Docs (TASKS/HANDOFF/ai.md) updated.
2026-08-31 19:22:02 -07:00
gramps 8f490102ee TASK 21 Increment B: FFmpeg rawvideo video decoder -> VideoFrame
Adds the codec-agnostic decoder half of the media source. Spawns ffmpeg
with -f rawvideo -pix_fmt bgra (reusing the already-shipped ffmpeg via
IFfmpegLocator) and drains the raw BGRA stdout pipe into VideoFrames.

- Services/RawVideoFrameReader.cs: pure rawvideo BGRA stream -> frames
  (partial reads kept across Feed; no ffmpeg needed to test)
- Services/IDecodeProcess.cs + FfmpegDecodeProcess.cs: binary-stdout
  subprocess seam, mirror of the encoder's IEncoderProcess
- Services/MediaVideoSource.cs: owns the decode, raises FrameReady/Completed
- MediaVideoSourceTests: 3 pure reader + 1 integration (fake decode
  process through the real source loop, frames in order)

Reference (external scan): ffmpeg rawvideo pipe decode is the canonical
codec-agnostic frame feeds pattern (ffmpeg docs -f rawvideo; how OBS/media
pipelines push frames to a compositor). Verified: 4/4 tests, build 0 warnings.
Native-FPS pacing + resolver/compositor wiring are the next slice.
2026-08-31 18:57:29 -07:00
gramps e8ff4df05f TASK 22: global audio sync offset (positive delay at the mixer out)
Adds AudioSyncOffsetMs (0..500ms, default 0) that delays the whole
interleaved-stereo mix so audio lands on the video when it runs ahead —
OBS's documented lip-sync fix. Positive-only: advancing audio needs a
video-side delay and is out of the audio layer's scope.

- Services/Audio/AudioSyncDelay.cs: pure delay line, flushed on Configure
- AudioMixer: Func<int> syncOffsetMs seam + delay applied post-limiter
- LayoutStore.Settings + MainViewModel.Audio/VM: load/save + binding
- PreviewPane mic bar: SYNC slider + status dot (IntToSyncBrushConverter)
- AudioSyncDelayTests: identity, negative/beyond-500 clamps, 10ms→960 samples

Reference (external scan): https://obs-versions.com/blog/how-to-fix-audio-delay-on-obs
(audio ahead => positive delay). Verified: build 0 warnings; 3/3 delay tests pass.
2026-08-31 18:30:03 -07:00
gramps 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.
2026-08-31 17:53:34 -07:00
gramps 6031a5682d Gear cleanup + Default Location for Recordings + single Start + socials hover glow
KISS: removed Gear-menu Save/SaveAs/Open Layout (redundant with auto-save; multi-layout = OBS).
App Settings: first real setting — Default Location for Recordings (Browse + Use Downloads reset,
fallback: configured → Downloads → MyVideos). Single Start button (pills = intent, button = trigger).
Socials button moved to preview bottom row, left of TRAX; hover pulses the SocialBarElement
DropShadowEffect (sine wave BlurRadius 24→48, Opacity 0.7→1.0 at 30ms). TextBox OneWay binding
fix for get-only RecordFolderDisplay. ONE fallback test. Build 0 warnings, 247 tests (2 known).
2026-08-29 10:30:40 -07:00
gramps eba6af5b9b TASK 21 Increment A: media-source model + persistence (schema migration + round-trip test)
SourceType.MediaSource added; MediaSourceType (Video/Audio) and MediaPlaybackState
(Stopped/Playing/Paused) enums. Source gains MediaPath, MediaIsLooping, MediaVolume
(0-1 clamped), MediaPlaybackState. LayoutStore: MigrateSourceTable adds 4 guarded
ALTERs (MediaPath, MediaIsLooping, MediaVolume, MediaPlaybackState); SELECT appends
indices 26-29, INSERT adds 4 columns + params. Round-trip test
MediaSource_Config_Survives_Save_And_Reload passes. Build 0 warnings, 245/247
tests (2 known pre-existing). Gated on creator run for later integration (ffmpeg).
2026-08-29 09:43:29 -07:00
gramps a9eb360288 TASK 18: local (record-only) recording pipeline + top-bar pills/light/Start-End button UX
Encoder dual-output: StreamEnabled/RecordEnabled/RecordPath options; per-output
ffmpeg block (stream ->flv, record ->mp4); throw unless at least one output.

RecordingFile: auto-name ty-<yyyymmdd>-<start hhmm>-0000.mp4, rename-on-stop to
ty-...-<len hhmmss>.mp4 with numeric-suffix on collision. LayoutStore persists
record folder. MainViewModel: REC/ON-AIR pills, status light, single Start<->End
button (replaces Start/Stop variants), StopStream no longer signs out, explicit
session timer. About overlay widened. Sign In label + spacing before Start.

Tests: 244 passed / 246 (2 pre-existing: AudioPipeline gains-mute, RoundClip).
Build 0 warnings. Scope check clean.

Memory-map correction (derived-solution rule, 2026-08-29): image-shrink recipe was
never recorded when first done, so it was re-derived. Added Derived-solution/recipes
rule to AGENTS.md; MyMistakes.md now a permanent recipes registry (records the
verified WPF-imaging shrink recipe); schema.md rows MyMistakes as registry; ai.md
points to the rule. README hero image replaced: 3.2MB screenshot -> 1400x794 ->
188KB docs/ytLlive-preview.jpg (JPEG q82).
2026-08-29 09:02:55 -07:00
gramps 9b56e88800 Revert ab29ec8: container==content auto-fit made it worse — back to alpha-crop with the container at element size
The container tracks the element's own Width/Height again; the crop stays in the bitmap.
2026-08-28 12:09:13 -07:00
gramps ab29ec801d Web source: container == content — element grid + selection box sized to the cropped frame's canvas extent
- FindContentBounds already trimmed the bitmap; the grid/box still rendered at the uncropped user
  size, so Fill stretched the tight widget into a bigger frame = dead-between-content-and-border.
- ContentCanvasSize maps the crop rect to canvas units (1920/pixW per axis, DPI-correct) and the
  preview event now carries (bitmap, canvasW, canvasH).
- OnWebView2PreviewBitmapChanged sets the element Width/Height to that size; the grid and the
  SelectionOverlay (both bound to the element's Width/Height) now equal the widget exactly.
- IsPreviewDragging (set in MainWindow drag/resize) suppresses auto-fit mid-gesture.
- Tests 10/10 (alpha bounds, canvas-size mapping incl. DPI, control size); 0 warnings.
2026-08-28 12:08:04 -07:00
gramps bccdb4892c Web source: crop to the frame's ALPHA bounding box — the widget's true rendered extent, no DOM timing, can't break the render
- FindContentBounds scans the Bgra32 capture for the bounding box of non-transparent pixels
  (background is injected transparent, so alpha = widget extent). Crop to that rect + Stretch=Fill
  maps the widget flush under the selection box: full-canvas widgets fall through to the full frame
  (identical to the confirmed-good image); floating widgets get tight-fitted with no dead space.
- Removes QueryContentBoundsAsync + the JS script entirely — the DOM-union attempt (a5b9952) broke
  rendering by measuring an unsettled layout on NavigationCompleted; alpha measurement is immune.
- 3 new unit tests: tight box, no-alpha no-crop, full-alpha no-crop. 7/7 pass, 0 warnings.
2026-08-28 11:53:36 -07:00
gramps 70fe2b80b2 Web source: render at canvas size 1920x1080 — full-bleed widgets no longer truncated to upper-left 2026-08-28 11:16:46 -07:00
gramps 23874c2dd6 Web source: stable viewport (snapshot at register) + Stretch=Fill + overflow:hidden — no reflow, no scrollbars, no dead space 2026-08-28 10:56:23 -07:00
gramps 8d793af91a Web source: geometry-agnostic viewport==box model — viewport locked to element box, Stretch=Fill, no dead space 2026-08-28 10:48:53 -07:00
gramps e917c41a48 Web source: fix rendering — fixed 1920×1080 viewport (OBS pattern), Stretch=Uniform, startup registration 2026-08-28 10:23:59 -07:00
gramps fd6e02b687 Web source: off-screen WebView2 capture + layout persistence + transparent bg
Bundle exception (2026-08-27): this lands prior-session work plus today's
transparency fix as one commit per creator instruction.

WebView2Manager (new): off-screen WebView2 control parked at -5000/-5000 in
the window's visual tree, captures via CapturePreviewAsync every 100 ms into
a BGRA8 VideoFrame. Source Width/Height sync to the control via
PropertyChanged. JS transparent-background injection runs on every
NavigationStarting/NavigationCompleted so the page's own CSS does not paint
white over the capture.

Transparency fix: DefaultBackgroundColor = System.Drawing.Color.Transparent
set on the WPF control before EnsureCoreWebView2Async. The WPF SDK doc says
the value is forwarded to the controller on init; the IDL (ICoreWebView2-
Controller2) confirms alpha=0 makes the capture alpha-preserving. The
prior-session 'CoreWebView2.BackgroundColor' path was the wrong property
name and has been replaced.

LayoutStore: WebUri column added (idempotent ALTER TABLE) and round-tripped
on save/load (schema v11).

MainViewModel: WebView2Manager constructed on Loaded; loaded web sources
re-registered; Source.WebUri PropertyChanged forwards to manager.

MainWindow: off-screen host Canvas added to visual tree.

Tests: 3 scale-sync + WebView2_DefaultBackgroundColor_Is_Transparent +
LayoutStorePersistenceTests.WebSource_WebUri_Persists. 234/236 pass
(2 pre-existing failures unchanged).

Scale bug (DPI/physical-pixel mismatch) still open.
2026-08-27 08:52:39 -07:00
gramps 3a1484b3c5 TASK 20 step 2: hotkey config UI + modifier chord persistence + conflict detection 2026-08-26 06:48:14 -07:00
gramps 89ab83be2e Update Broadcast button: diff before push, offline=local only, live=API; Go Live empty-field prompt 2026-08-25 17:25:25 -07:00
gramps d9e352f996 Text tab toggles the drawer; click-away collapses it 2026-08-24 14:49:15 -07:00
gramps e6baa55867 Fix logo-click crash (Run is not a Visual); Text tab always visible; avatar initial fallback 2026-08-24 14:44:16 -07:00
gramps b988e6d72e Broadcast Text pull-out (Live screen) + launch geometry 1920x1040/1366x768 with restore 2026-08-24 11:04:41 -07:00
gramps 7357836f9b ON-AIR sign right of REC: gray offline, red public, green private 2026-08-24 10:22:18 -07:00
gramps b0bace7432 TASK 20 step 1: global hotkeys F1-F9 (scenes, stream, mutes, TRAX) 2026-08-24 09:32:54 -07:00
gramps 708b336da7 YouTube Chat layer: one-per-layout gate + legacy label heal (TASK 27) 2026-08-24 09:08:03 -07:00
gramps ef710c5537 Webcam menu gates on the app-wide identity (TASK 26) 2026-08-24 08:56:17 -07:00
gramps 7dd140a4c0 Backgrounds: one locked Background per screen + mini rule (TASK 25) 2026-08-23 13:33:33 -07:00
gramps b400ab59c5 Toasts: non-blocking notifications replace MessageBoxes + log-only failures (TASK 24) 2026-08-23 12:25:04 -07:00
gramps 809c066184 Fix: derive IsBackground from Type on layout load (legacy rows healed) 2026-08-23 07:25:54 -07:00
gramps d2114c7e1c the ai is an overthinking fucking moron who needs it's plug pulled until such time it can repeat a similar action with its head and ass 2026-08-23 06:25:38 -07:00
gramps b74f15ff14 Fix: IsBackground derived from SourceType + per-scene background property tests
- Source.Type setter auto-sets IsBackground = true when Type is Background
- SceneElement.IsBackground: expression-bodied → virtual auto-property
- Source.IsBackground: new → override (WPF binding fix)
- Three new tests: IsBackground per-scene, derived from type, UseDefaultBackground default
2026-08-22 18:16:11 -07:00
gramps 4b9a8e515f Control Surface UX: pill toggle, background rename, per-scene properties
- Sliding pill toggle (green ON / red OFF) replaces checkbox
- Geometry mismatch error for custom backgrounds
- Backdrop -> Background: C# rename, SQL column rename with migration
- Source.IsBackground: new -> override (fixes WPF binding)
- Legacy 'Backdrop' display name migrated to 'Background'
- Per-scene UseDefaultBackground + CustomBackgroundPath
- Asset files renamed backdrop -> background
- StagedScene/LiveScene split, thumbnail strip, edit gating
- TransitionService (Cut/Fade/Move)
- Context menu: Capture Desktop, Refresh Desktop
- Resolution badge removed, ON-AIR status light added
2026-08-22 18:10:59 -07:00
gramps 0bd17ab428 feat(ui): Show Desktop toggle, avatar login, chat preview, backdrop seeding, border animations
- Source.ShowDesktop bool persisted in DB (schema migration + LayoutStore)
- ClearBackdropCaptureAsync clears CaptureKey + VideoImageSource for static fallback
- Top bar: red/green/pulsing status light, avatar via BitmapImage, Log In button
- ShowStartStream now requires IsConnected
- RunMockChatPreviewAsync: simple async loop, ChatPreviewEnabled property on Source
- EnsureDefaultBackdrop called on every scene switch, seeds all 5 scenes
- SeedEndingBackdrop replaces existing, SeedChatBackdrop added
- SceneCatalog: Chat scene display name corrected (layer = 'YouTube Chat')
- Left panel spacing consistency, context menu reordered
- Border animation rendering via code-behind Storyboard approach
- 3 new backdrop assets (ending-backdrop.jpg, chat-backdrop.jpg, live-backdrop.jpg)
- Build 0 warnings, 178 tests passing
2026-08-20 18:02:41 -07:00
gramps e97406d649 v1 task queue: add TASK 19-23 (transitions, hotkeys, media source, audio sync, studio mode)
- TASKS.md: added 5 new tasks for v1 feature completeness, execution order, updated backlog
- MONETIZATION.md: added v1 feature completeness section (competitive gap analysis)
- ai.md: added v1 task queue note in Current limitations/TODOs
- HANDOFF.md: updated session state and next steps
- ViewModels/SocialsDialogViewModel.cs: removed isPremium parameter (social slot locks)
- ViewModels/MainViewModel.cs: removed isPremium argument from SocialsDialogViewModel constructor
- ytLive.Tests/SocialsDialogViewModelTests.cs: updated 5 tests for unlocked slots
2026-08-19 11:51:32 -07:00
gramps a3dd0791db fix: creator feedback batch Branch 1 — TRAX volume, tooltip, meter boost, slider click, backdrop
- TRAX: LocalGain now applies 0.2× factor so music is quieter relative to desktop audio slider
- TRAX tooltip: switched to ToolTipService with Placement=Top to fix z-order in Viewbox overlay
- Mic meter: MeterLevel multiplied by 1.2× before clamping for ~20% higher visual readout
- Volume sliders: click anywhere on track to jump thumb to that position before dragging
- Backdrop: Image Visibility now binds to BackdropVisible; compositor checks IsVisible before blit
- Renamed 'Backdrop' to 'Game Capture' in EnsureBackdrop + test assertions
2026-08-17 07:45:29 -07:00
gramps 0fac1e6a5a TASK 5 items 1-3: reusable stream, health banner, docs refresh
- Reusable stream: GetOrCreateReusableStreamAsync, cached via LayoutStore, bound at broadcast insert
- Health monitoring: GetStreamHealthAsync polls liveStreams.status, report-by-exception banner
- Go-live order: ensure stream → create broadcast → then start pump (URL must exist first)
- Full broadcast form scoped (TASK 5 items 6-7) in TASKS.md + ai.md
- TASK 23 trimmed to technical scope, TASK 26 added (marcom/launch kit)
- Confidential files: MONETIZATION.md + MARCOM.md (gitignored, not committed)
- .gitignore updated for confidential business files
- HANDOFF.md updated with session state and next steps
2026-08-16 17:48:37 -07:00
gramps 92f1471ab4 TASK 25: master limiter on the live mix (2026-08-15, queued from the TRAX discussion) — the review confirmed two of the creator's three instincts were already satisfied (MediaFoundationReader streams from disk so file size needs no guard; the MusicPlayer 0.20 bed rides the same loopback gain as the game, so music is always exactly 20% of the desktop volume by construction) and found one real gap: AudioMixer.FillAndMix summed mic + loopback with no output ceiling, so hot gains could pass 0 dBFS and clip the AAC encode. New pure Services/Audio/MasterLimiter.cs: -1 dBFS ceiling (Ceiling = 0.891), instant attack per frame (a hot frame is scaled exactly to the ceiling — no overshoot), smoothed release toward unity so loud passages don't pump, gain never exceeds 1. Applied at the end of FillAndMix right before the pipe write. 3 unit tests (trim-to-ceiling, never-boost, recovery) + ONE integration test (MasterLimiter_CapsTheLiveMix_OnThePipe: a 0.95 loopback bed capped to exactly 0.891 on the wire while staying audible). Docs in the same commit: ai.md go-live audio section, Services/index.md new row, TASKS.md TASK 25 record, HANDOFF.md shipped state. Build 0 warnings, 193 tests passing (189 + 4 new) 2026-08-15 16:10:16 -07:00
gramps 24f16b2cd9 Game audio bar is now ALWAYS visible (TASK 4 gating removed, 2026-08-15): the show/hide machinery kept hiding the desktop/game meter whenever no full-screen game with sound was up or no TRAX music played, which the creator read as the meter being lost. Deleted IGameAudioDetector/GameAudioDetector/GameAudioHysteresis (Services/), the VM's 250ms DispatcherTimer poll + OnGameAudioActiveChanged/OnGameAudioPollTick + IsGameAudioBarVisible + _gameAudioBarActive/_gameAudioTimer/_gameAudioDetector, and the two detector test files (GameAudioDetectorTests, GameAudioHysteresisTests); MainWindow.xaml drops the Visibility binding so the bar renders unconditionally. Desktop audio is just automatic WASAPI loopback now. Docs in the same commit: ai.md (audio section + Services inventory), Services/index.md, ViewModels/index.md, TASKS.md TASK 9 item 7 follow-up, HANDOFF.md shipped state + stale poll-timer landmine removed. Build 0 warnings, 189 tests passing (8 detector tests removed) 2026-08-15 15:53:51 -07:00
gramps a0a8331569 TASK 24 post-pause polish batch (creator's 8 review issues, 2026-08-15): (1) desktop/game audio volume slider + (2) mute were no-ops — the AudioMixer's gain Func seams defaulted to unity because the VM never passed them; new AudioGainProvider (Services/Audio/) wires MicMuted/MicVolume/GameMuted/GameAudioVolume into the mixer at construction, read live each mix tick, so sliders and mutes are stream-honest; the game bar also scales TRAX locally via new MusicPlayer.LocalGain (UpdateTraxLocalGain on every game volume/mute change + on load) so the creator HEARS the controls work on the music in the headphones. (3) TRAX played once then stopped — OnPlaybackStopped only looped on Position>=Length (unreliable for MediaFoundationReader); now any clean stop rewinds + replays, errors/explicit stops surface via PlaybackEnded; unused using dropped. (4+6) TRAX moved right of Socials in the footer's left cluster (centered under scenes/sources); the mic cluster is now MIC + meter + mute + volume. (5) mic source persists — LayoutStore gains a Settings key/value table (SaveMicSourceName/LoadMicSourceName); the VM saves on pick and restores before the mixer's first Start, so a restart reconnects the same vetted device (green) or reports it missing (yellow). (7) Backdrop's missing trashcan shifted the edit/eye icons right — new HiddenBoolToVisibilityConverter keeps the trash column reserved (Hidden, not Collapsed) so every source row's icons stay in fixed columns. (8) a 1px hairline with top/bottom padding separates scenes from sources in the left panel. Docs in the same commit: ai.md (gains honest + drive TRAX locally, TRAX loops, mic persistence, footer layout), TASKS.md TASK 24, HANDOFF.md shipped state, ViewModels/index.md. ONE integration test: AudioPipelineTests.Mix_HonorsProviderGains_AndGameMute_KillsTheLoopback (real mixer + provider gains through the pipe harness: scaled loopback audible, silence after mute). Build 0 warnings, 197 tests passing 2026-08-15 10:23:49 -07:00
gramps 86fcd868a8 Monetization policy locked + dev pause recorded (docs changeset, 2026-08-14): the one paid line becomes an annual subscription — early access $49.99/yr → $99/yr list at GA, grandfather-while-subscribed with lapse → list on renewal (no escalation matrix); free tier unchanged (branding flash is the free tier's billboard + no Alerts, which is the one paid feature); billing NOT locked to itch.io (hunted fact: itch.io has no native subscription billing — candidates Gumroad, whose native affiliate program is the tiebreaker, and Lemon Squeezy); support = in-app bug-report → git issues with a when-I-get-around-to-it cadence + emergency patches. ai.md Monetization section rewritten + stale paid-unlock seams fixed (L115 subscription-entitlement wording, the About hub's tabled billing PremiumUrl seam); TASKS.md gains TASK 23 (Monetization — billing provider decision, unlock mechanism with lapse→flash-returns enforcement, in-app bug-report loop, Unlock Premium seam lighting, Alerts gating, and line-item pointers for the early-access strategy, affiliate/ads phase 2, and the channel revival); HANDOFF.md records the decisions + the development pause (resume streaming = pair-programming dev streams via OBS + The Division 2 under the gramps/GOAT persona, 67 and gaming since 1981, 3D-printing content retired; dogfood switch to ytLlive once TASK 5 lands; resume point = TASK 5). Stale itch.io assertions corrected across comments and indexes (MainViewModel.cs PremiumUrl/IsPremium seams, MainWindow.xaml tooltip, AboutHubTests comment, ViewModels/index.md, TASK 9 out-of-scope note). Build 0 warnings, 196 tests passing 2026-08-14 18:48:42 -07:00
gramps 6d71acef8c TASK 9 audio milestone: real stream audio + voice filters + auto-duck + free TRAX music — the mixer now feeds the encoder real audio (ffmpeg reads a Windows named pipe, -f f32le -ar 48000 -ac 2 -i \.\pipe\ytllive_audio, replacing anullsrc silence; explicit -map 0:v -map 1:a via EncoderOptions.AudioPipeName), with honest gains reaching the stream (MicVolume scales mic, GameAudioVolume + mute scale loopback, Func<double> seams on AudioMixer), an always-on pure-C# voice chain on the mic BEFORE the meter and mix (LowShelfFilter 120Hz +4dB → HighShelfFilter 8kHz +3dB → NoiseGate 0.005/hysteresis 0.5 → Compressor 0.5 4:1, all TDF2), AutoDucker (mic RMS>0.02 → loopback ×0.25, attack 0.05/release 0.005), and TRAX free background music (MusicPlayer = MediaFoundationReader → VolumeWaveProvider16 at fixed 0.20 → WaveOutEvent via the new sibling NAudio.WinMM 2.2.1 package; plays to the default device so the existing loopback carries it, ducked with game; footer TRAX button with red/yellow/green status dot, left-click toggles/picks, right-click opens the OpenFileDialog picker, tooltip shows the track name; persisted via schema v9 single-row Music). Build-time deviations from the plan (recorded in ai.md + TASKS.md): WasapiCapture in NAudio 2.2.1 exposes no overridable GetDefaultMixFormat so sources run device mix format and the mixer's TinyResampler normalizes any rate to 48kHz (resampler IS the design); FfmpegEncoder.cs untouched — MainViewModel.StopStream calls _audioMixer.StopLive() (pipe EOF) before the frame pump stops; sound-bar relabelled 'Desktop Audio', IsGameAudioBarVisible = game sound OR music playing. Tests: new AudioPipelineTests (DSP/ring-buffer/ducker/resampler units + the ONE integration test Mix_WithFiltersDuckAndGain_Lands_On_AudioPipe reading real pipe bytes; ring-buffer overwrite bug found + fixed), FfmpegEncoderTests/LayoutStorePersistenceTests updated — 196 tests passing, 0 warnings 2026-08-14 18:09:26 -07:00