Files
LlamaCasty/HANDOFF.md
T
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

24 KiB
Raw Blame History

HANDOFF — session state

Current operational state, read right after TASKS.md. Trust this file as the truth of what is in flight — do not re-derive from git/fs unless it points at a problem. Conventions: schema.md. Rewrite this file at session end, compaction, or any interruption.

Session state (last updated: 2026-08-17, creator feedback batch Branch 1)

  • Branch: main, tracking origin/main. Working tree: TASK 14 Branch 1 (quick fixes) — TRAX volume, tooltip, mic meter boost, click-to-position sliders, backdrop visibility + rename. Build 0 warnings, 207 tests passing. Ready to commit.
  • TASK 14 — CREATOR FEEDBACK BATCH — IN PROGRESS. Branch 1 shipped: TRAX volume 0.2× factor, TRAX tooltip z-order fix, mic meter +20% boost, click-to-position volume sliders, backdrop visibility binding + compositor guard, rename "Backdrop" → "Game Capture". Branch 2 pending: Elements panel, webcam border config, Countdown source (Starting/BRB), Web source.
  • TASK 9 — REUSABLE STREAM + HEALTH BANNER — SHIPPED 2026-08-16 (items 1–3).
    • Items 1–2 (reusable stream): _rtmpUrlProvider now yields a real RTMP URL. YouTubeStreamService gains GetOrCreateReusableStreamAsync (lists liveStreams?mine=true, reuses the existing cdn.isReusable stream, inserts once on first use with resolution=variable/frameRate=variable); CreateBroadcast(..., streamId) binds at insert via boundStreamId + the one-click v3 flags (enableMonitorStream=false, latencyPreference=low). Cached via LayoutStore Settings (SaveReusableStream/LoadReusableStream). Go-live order changed (the pump reads the URL once at start — FramePump.StartAsync): BeginGoLive → PrepareAndStartLiveAsync = ensure stream → create+bind broadcast → THEN start the pump.
    • Item 3 (report-by-exception health): GetStreamHealthAsync(streamId) polls liveStreams?part=status; the pure Services/StreamHealthReporter.BannerFor decides (null text on good/ok/noData/info-only; warning/error issue → its type text, error beats warning). The VM polls every 30s while live (_healthPollTimer, first poll right after go-live, stopped on End/Error via UpdateLiveVisuals; failures log-only). UI = full-width banner strip under the top bar, HealthIssueBanner + HealthIssueBackground (amber #b8860b warning / dark red #8f1f1f error), hidden by NotNullToVis; cleared in ResetHealth.
    • Tests (8 new this branch, 207 total, 0 warnings): 3 service units (parse, good→no issues, no-session→null) + 4 StreamHealthReporterTests + the ONE integration test GetStreamHealthAsync_Report_By_Exception_Banner_Only_On_Warning_Or_Error (real service JSON parse → real reporter: good → no banner, error issue → banner text + error color).
  • CONFIDENTIAL files created (gitignored): MONETIZATION.md (pricing, billing research, unlock mechanics) and MARCOM.md (launch marketing strategy, positioning, platform strategy). These are NOT committed to the public repo. Helpers/OAuthCredentials.cs was already gitignored.
  • TASK 9 items 6-7 scoped (not built): visibility unlock (remove temporary "always Private" enforcement) and full broadcast form (Core + Advanced tabs with all YouTube API-supported fields). These are the next technical tasks after live chat (item 4) and error handling (item 5).
  • TASK 10 trimmed to technical scope only (billing decision, unlock mechanism, bug report, alerts gating). Business details moved to MONETIZATION.md.
  • TASK 13 added — marcom/launch kit. Business details moved to MARCOM.md. Queued after all v1 features.
  • Backlog updated — removed v0.3 (stream scheduling, not needed for casual streamers). Multi-destination restreaming noted as "congrats, you're ready for OBS" moment.
  • Resume point (next branch): TASK 14 Branch 2 — Elements panel beneath Sources (per-element config with live preview, ✕ revert / ✓ commit), webcam border config (color #RRGGBB + picker, thickness 0–10), Countdown source (Starting/BRB scenes, timer 1–60 min), Web source (all scenes, URI input). Then TASK 9 item 4 — live chat.
  • TASK 12 — MASTER LIMITER — COMMITTED + PUSHED 2026-08-15. Queued from the TRAX discussion: the live mix summed mic + loopback with no ceiling, so hot gains could pass 0 dBFS and clip the AAC encode. New pure Services/Audio/MasterLimiter.cs (−1 dBFS ceiling, instant attack per frame, smoothed release) applied at the end of AudioMixer.FillAndMix. ONE integration test (MasterLimiter_CapsTheLiveMix_OnThePipe). The review also confirmed file size needs no guard (MediaFoundationReader streams) and the music 0.20 cap is already relative by construction (music rides the same loopback gain as the game → always exactly 20% of the desktop volume). Build 0 warnings, full suite green.
  • GAME AUDIO BAR ALWAYS VISIBLE — COMMITTED + PUSHED 2026-08-15. The TASK 4 show/hide gating was a UX bug: the desktop/game meter kept vanishing whenever no full-screen game with sound was up (or no TRAX music played). The whole IGameAudioDetector/GameAudioDetector/GameAudioHysteresis stack + the VM's 250ms poll timer + its two test files were deleted; the bar is now permanently overlaid at the bottom of the preview. Build 0 warnings, full suite green.
  • TASK 11 — POST-PAUSE POLISH BATCH — SHIPPED + COMMITTED + PUSHED 2026-08-15. All 8 creator review issues fixed in one branch (details below). Build 0 warnings, full suite 197 passing (ONE integration test for that branch: AudioPipelineTests.Mix_HonorsProviderGains_AndGameMute_KillsTheLoopback).
  • AUDIO MILESTONE (TASK 8) — SHIPPED + COMMITTED + PUSHED. Real stream audio + voice filters + auto-duck + free TRAX background music. Commits 6d71ace (milestone) + f2f6401 (secrets cleanup), force-pushed (725f5a7...f2f6401).
  • MONETIZATION LOCKED (2026-08-14, creator) — committed 86fcd86. One paid line = annual subscription: early access $49.99/yr → $99/yr list at GA, grandfather-while-subscribed, lapse → list on renewal. Free tier unchanged (branding flash = the billboard + no Alerts; Alerts is the paid feature). Billing NOT itch-locked; candidates Gumroad (native affiliates = tiebreaker) / Lemon Squeezy. Support = in-app bug-report → git issues. Full policy in ai.md → Monetization; scoped plan in TASKS.md TASK 10.
  • Secrets scrubbed from git history. The DO token + passwords were purged via git filter-branch
    • git gc --prune=now --aggressive (all-refs scan = 0 hits); values still exist in chat — keep treating as compromised; rotate the DO API token.
  • Shipped, all pushed: TASK 9 items 1–3 (reusable stream + health banner, 2026-08-16), TASK 12 master limiter, TASK 11 polish batch, creator-hub About (3d92bd0), TASK 7 (meter +10 dB), TASK 4 ship step 7 (one-click go-live + private-only), TASK 8 audio milestone (6d71ace), secrets cleanup (f2f6401), monetization docs (86fcd86).

TASK 9 — reusable stream + health banner 2026-08-16 (what changed, items 1–3)

The recorded resume point: the "last blocker" was that go-live ran the visual flow but never pushed — _rtmpUrlProvider returned null, so FramePump.StartAsync skipped the encoder entirely.

  • Services/YouTubeStreamService.cs: new GetOrCreateReusableStreamAsync() lists liveStreams?mine=true and reuses the existing cdn.isReusable stream, inserting once per channel only on first use (cdn.resolution=variable, cdn.frameRate=variable, isReusable=true) and returning a ReusableStream(Id, IngestionAddress, StreamName) record (RtmpUrl = ingestionAddress/streamName). CreateBroadcast takes an optional streamId and binds at insert via contentDetails.boundStreamId (no second bind round-trip) and now always sends the full one-click v3 flag set (enableAutoStart/Stop, enableMonitorStream=false, latencyPreference=low). The old per-broadcast BindStream (throwaway 1080p/60fps stream + contentDetails.streamId) is gone.
  • Services/LayoutStore.cs: SaveReusableStream/LoadReusableStream — the Settings key/value table caches the stream id/address/name so the pump has its RTMP URL at startup, no round-trip.
  • ViewModels/MainViewModel.cs: _rtmpUrlProvider returns _reusableStreamUrl (loaded from the cache in the ctor, set fresh on go-live). BeginGoLive → PrepareAndStartLiveAsync: ensure the stream → cache it → create the broadcast bound to it → then _framePump.StartAsync(). The ordering matters because the pump reads _encoderOptions() once at startup.
  • Tests (6 new, 199 total, 0 warnings): GetOrCreateReusableStreamAsync_Reuses_Existing_Reusable_Stream (list path, no POST), ..._Creates_When_None_Exists (insert path with variable/isReusable), CreateBroadcast_With_StreamId_Binds_Reusable_Stream_At_Insert (boundStreamId + new v3 flags), ..._Without_Session_Returns_Null, ReusableStream_Cache_RoundTrips, and the ONE integration test FramePumpTests.ReusableStream_Url_From_Service_Feeds_Encoder_Startup (real service + real pump + hermetic HTTP: the reusable stream's URL lands in EncoderOptions.RtmpUrl and the encoder starts).

Out of scope this branch (next branches): TASK 9 item 4 live chat, item 5 the YouTube error-code mappings, and the design's bottom-strip YouTube logo/green-red dot (clickable → dialog).

Item 3 — report-by-exception health banner (same session, second branch-worth of scope)

  • Services/YouTubeStreamService.cs: GetStreamHealthAsync(streamId) polls liveStreams?part=status&id={id} → StreamHealth with parsed healthStatus (good|ok|bad|noData)
    • configurationIssues[] (severity info|warning|error + type). The old GetStreamHealth(broadcastId) (wrong endpoint — liveBroadcasts.lifeCycleStatus — zero callers) is gone.
  • Services/StreamHealthReporter.cs (new, pure): BannerFor(issues) → HealthIssueReport(Text?, IsError) — null text on good/ok/noData/info-only; the first warning/error issue produces its type text (comma-joined, blank types dropped); error beats warning for color.
  • ViewModels/MainViewModel.cs: _reusableStream (the record, not just the URL) is stashed by PrepareAndStartLiveAsync + loaded from the cache in the ctor; a 30s DispatcherTimer (_healthPollTimer) polls while live — first poll fires right after the pump starts, the tick handler fire-and-forgets PollHealthAsync() (fully try/caught, failures log-only), and UpdateLiveVisuals' offline/error branch stops the timer. ApplyHealthIssue sets HealthIssueBanner + HealthIssueBackground from the reporter; ResetHealth clears both.
  • MainWindow.xaml: a full-width banner strip in its own window-grid row (below the top bar, above the content) bound via NotNullToVis to HealthIssueBanner, background to HealthIssueBackground (amber #b8860b warning / dark red #8f1f1f error); rows shifted (content → row 2, footer → row 3).
  • Tests (8 new this branch, 207 total, 0 warnings): 3 service units + 4 StreamHealthReporterTests
    • the ONE integration test GetStreamHealthAsync_Report_By_Exception_Banner_Only_On_Warning_Or_Error (real service JSON parse → real reporter: good → no banner, error issue → banner text + error color).

TASK 11 — the 8-issue polish batch — SHIPPED 2026-08-15 (what changed)

The creator's review of the TASK 8 build, all fixed in one branch (full record in TASKS.md TASK 11).

  1. ✅ Desktop/game audio volume slider had no effect — the AudioMixer gain seams defaulted to unity (the VM never passed them): the slider/mute were decorative, stream AND local. Fixed with a new AudioGainProvider (Services/Audio/) wired into the mixer at construction, read live each mix tick.
  2. ✅ Desktop/game mute had no effect — same wiring; GameMuted now zeroes the loopback on the stream AND silences the music locally via new MusicPlayer.LocalGain (scaled by the game bar on every volume/mute change + on TRAX load) — the creator hears the control work in the headphones.
  3. ✅ TRAX played once then stopped — OnPlaybackStopped only looped when Position >= Length (unreliable for MediaFoundationReader); now any clean stop rewinds + replays. Dropped the unused using System.Runtime.InteropServices; too.
  4. ✅ TRAX/MIC buttons swapped — footer mic cluster is now MIC + meter + mute + volume (TRAX is out of the mic cluster entirely).
  5. ✅ Mic source persists across restarts — LayoutStore gained a Settings key/value table (SaveMicSourceName/LoadMicSourceName); the VM saves on pick and restores before the mixer's first Start → same already-vetted device reconnects green on restart, missing → yellow.
  6. ✅ TRAX moved right of Socials — both centered under the scenes/sources listboxes (footer line 1, left cluster).
  7. ✅ Backdrop icons shifted right — new HiddenBoolToVisibilityConverter keeps the trash column reserved (Hidden, not Collapsed) so edit/eye stay in fixed columns for every source row.
  8. ✅ No separation between scenes and sources — a 1px hairline with top/bottom padding now sits between the two left-panel listboxes.

THE ONE integration test: Mix_HonorsProviderGains_AndGameMute_KillsTheLoopback — real mixer + AudioGainProvider gains through the pipe harness: scaled loopback audible at 0.5, silence after mute.

Game audio bar — always visible 2026-08-15 (what changed)

The creator reported the desktop/game sound meter "lost" — it was the TASK 4 show/hide gating (_gameAudioBarActive || IsMusicPlaying): the bar only rendered while a full-screen game produced sound or TRAX played, so it sat hidden during normal use. Fix = the bar is now always visible:

  • Deleted Services/IGameAudioDetector.cs, Services/GameAudioDetector.cs, Services/GameAudioHysteresis.cs, ytLive.Tests/GameAudioDetectorTests.cs, ytLive.Tests/GameAudioHysteresisTests.cs — the stack's only output (_gameAudioBarActive) fed IsGameAudioBarVisible, which no longer exists.
  • MainViewModel.cs: removed _gameAudioTimer (250ms DispatcherTimer), _gameAudioDetector, _gameAudioBarActive, the constructor wiring, OnGameAudioActiveChanged, OnGameAudioPollTick, IsGameAudioBarVisible, and the IsMusicPlaying → visibility notification. Desktop audio is just automatic WASAPI loopback now.
  • MainWindow.xaml: dropped the Visibility binding on the preview-overlay game bar; it renders unconditionally (TRAX button + "Desktop Audio" label + meter + mute + volume unchanged).

TASK 12 — master limiter 2026-08-15 (what changed)

Came out of the TRAX discussion (file-size guard? 20% cap? sound-event balance?). Verdict: two of the three instincts were already satisfied, one real gap existed:

  • No file-size guard needed — MediaFoundationReader streams from disk; memory is flat (~a few MB) whatever the file size.
  • The 0.20 music cap is relative by construction — music rides the same loopback gain as the game, so music:game is always exactly 0.20:1 at any slider position; it cannot rise above 20% of the current desktop volume. (Per-channel hierarchy: voice on top via the ducker −12 dB, then game, then music at 20%.)
  • The gap: FillAndMix summed mic + loopback with no ceiling — hot gains could pass 0 dBFS and clip the AAC encode.
  • Fix: new pure Services/Audio/MasterLimiter.cs — −1 dBFS ceiling (Ceiling = 0.891), instant attack per frame (hot frames scaled exactly to the ceiling, no overshoot), smoothed release toward unity (no pumping); gain never exceeds 1. Applied at the end of AudioMixer.FillAndMix.
  • ONE integration test: MasterLimiter_CapsTheLiveMix_OnThePipe — real mixer + pipe harness, a 0.95 loopback bed capped to exactly 0.891 on the wire while staying audible. Plus 3 unit tests for the pure math.

TASK 8 audio milestone — SHIPPED 2026-08-14 (what changed)

The creator's feature review settled this as the single next branch ("all the audio issues done and tested — a huge milestone"). Final spec and full shipped-state records live in TASKS.md (TASK 8) and ai.md ("Live audio capture"). Highlights:

  • Real audio into the encoder. FfmpegArgs now builds -f f32le -ar 48000 -ac 2 -i \\.\pipe\ytllive_audio
    • explicit -map 0:v -map 1:a (replaces anullsrc silence). MainViewModel.BeginGoLive → _audioMixer.StartLive(EncoderOptions.DefaultAudioPipeName); StopStream → _audioMixer.StopLive() before _framePump.StopAsync().
  • 2-input mix (mic + loopback), honest gains: micGain = MicVolume (mute = 0), loopbackGain = GameMuted ? 0 : GameAudioVolume × duck. No third music channel.
  • Voice chain on the mic (TASK 8), before meter AND mix: bass 120 Hz +4 dB → treble 8 kHz +3 dB → gate (0.005 / hysteresis 0.5) → compressor (0.5, 4:1). Pure TDF2 DSP, per-sample, always on.
  • Auto-duck: mic RMS > 0.02 → loopback ×0.25 (−12 dB), attack 0.05 / release 0.005.
  • TRAX (free BGM): MusicPlayer = MediaFoundationReader → VolumeWaveProvider16 at fixed 0.20 → WaveOutEvent. Plays to the default device → rides the loopback into the stream (ducked with game). Footer TRAX button (dot red/yellow/green + "TRAX"), left-click toggles/picks, right-click opens the picker (OpenFileDialog), tooltip shows the track name. Track persists via schema v9 single-row Music. Sound-bar label "Game Audio Capture" → "Desktop Audio"; IsGameAudioBarVisible = gameDetectorProducingSound || IsMusicPlaying.
  • Tests: new AudioPipelineTests.cs (DSP/ring-buffer/ducker/resampler units + the ONE integration test reading real pipe bytes via NamedPipeClientStream); FfmpegEncoderTests + layout persistence updated for pipe args / Music roundtrip. Ring-buffer overwrite bug found by the unit test and fixed (head must NOT advance on eviction — the write itself advances it). Integration test pre-fills the ring buffers before StartLive so the first pipe tick already carries audio (deterministic — a start-of-stream silence race was seen and eliminated).

Out of scope this branch: IP webcam, chat box, alt-key crop, credits, bg removal, music-off-VOD track (YouTube mutes VODs with copyrighted music — future feature), PremiumUrl (TASK 10 seam).

  • Landmines:

    • Never add another test that constructs new App() — use RealAppHost.Run(...) (shared STA host for the one WPF App per AppDomain; round-clip + source-naming tests).
    • Never set a local Canvas.SetTop on the social bar — a local value permanently overrides {Binding SocialBarTop} (the ClearValue lesson from 5.5).
    • AudioMixer meter Push is unconditional by design now: OnMicSample/ OnLoopbackSample compute the level first, then raise the event — a ?.Invoke(meter.Push(...)) short-circuit skipped the meter update when nothing was subscribed (found by RestartMic_ResetsLevel, fixed).
    • MicConnected comes from the source Started event, raised right after StartRecording() succeeds — tests must MarkStarted() the fake source before asserting connection state.
    • The mic dot is red until a resource connects (see contract below) — green only after Started, yellow on Failed.
    • Zero mic devices at startup = red dot AND the mixer is never started, so loopback + the game bar can't run either (no capture at all) — acceptable.
    • The pump reads the active scene on a background thread while the UI can still edit it — a concurrent-mutation exception is contained (logged + Failed + the pump stops), not a crash.
    • StopAsync must stop the encoder (closes stdin) before awaiting the pump loop — closing stdin unblocks a write stuck on pipe backpressure; the reverse order deadlocks. Same rule for audio: StopLive() (pipe EOF) before the pump stop, so ffmpeg's two inputs end in order.
    • Tests never instantiate MainViewModel directly except via a real MainWindow on the RealAppHost STA thread (round-clip + naming), which never go live.
    • Sandbox can't reach outbound HTTPS — HttpSocialValidator stub-handler tests only, never the real instance.
  • Mic status contract (creator's rule, verified — do NOT "fix"): the status dot is red until a mic resource is actually connected. MicStatus starts NotConnected (red); it goes green ONLY when the mixer's MicConnected fires, which comes strictly from the mic source's Started event raised after StartRecording() succeeds. Zero devices at startup → stays red and the mixer is never started; capture failure → yellow.

  • Secret/DB/port facts live: OAuth client id/secret in Helpers/OAuthCredentials.cs; OAuth session token in Helpers/TokenStore.cs (DPAPI → %APPDATA%\ytLlive\ytLlive.auth); layout DB %APPDATA%\ytLlive\ytLlive.db (schema v9 — single-row Music; the SocialEntry.Software column is a column-presence migration like the others); OAuth callback http://localhost:8765/oauth2/callback; crash log %APPDATA%\ytLlive\startup.log.

Server recovery (llamachile.tube / YunoHost / DO) — 2026-08-14

Facts (hunted this session — do not re-hunt):

  • Droplet llamachile.tube: DO droplet ID 473190301, IP 143.244.176.131, sfo3, 4GB/2vCPU/50GB. SSH port = 2214 (matches git remote ssh://llgit.llamachile.tube:2214/gramps/ytLlive.git); 22/2222/2200/etc all closed. SSH as gramps works with ~/.ssh/id_ed25519 (key auth, no password).
  • Root is YunoHost-locked: PermitRootLogin no in /etc/ssh/sshd_config (there's a conflicting cloud-init override section below it, but DO's "Reset root password" email does NOT work on this box — cloud-init set-passwords only ran once at install). Root is reachable via sudo -i or the DO web Recovery Console (Settings → Recovery console — VNC-based; the droplet page's Console button is SSH-based and fails with "all configured authentication methods failed" when no account has a working password).
  • gramps is a YunoHost LDAP account (local /etc/passwd entry has *, auth via pam_ldap) — its password is changed with sudo yunohost user update gramps -p '<pw>', NOT passwd.
  • fail2ban bans the whole IP for 10-12 min after repeated failed SSH/root logins (all ports refuse; ping still works; droplet API still shows active). Wait it out — do NOT power-cycle.
  • Password reset this session: sudo yunohost tools rootpw -n '<pw>' sets root (rootpw takes -n). Verification: getent shadow root shows a $y$ yescrypt hash + passwd -S root = P.
  • Mastodon services run as systemd units mastodon-web/sidekiq/streaming (all were active after the reboot); gitea/nginx/mariadb/postgres/redis/yunohost-api also systemd units. Disk was 93% full (3.6G free) — keep an eye on it before any big upgrade.
  • The original outage was an interrupted Mastodon upgrade + a DO password_reset reboot; everything came back on its own after boot. No code/schema damage observed.

Secrets (removed 2026-08-14 — see git history if a value is ever needed again): the DO API token, the gramps/root passwords and the DO password-reset email password were recorded in this file and in chat. They are treated as compromised — the DO API token and every listed password have been or should be rotated/revoked, and no new secrets belong in this repo or in chat. Secret paths that stay here are the file locations only (OAuth creds → Helpers/OAuthCredentials.cs, session token → Helpers/TokenStore.cs).