Files
ytLlive/HANDOFF.md
T

143 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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`](schema.md). Rewrite this file at session
> end, compaction, or any interruption.
## Session state (last updated: 2026-08-14)
- **Branch:** `main` (audio milestone work is uncommitted, see "Uncommitted").
- **AUDIO MILESTONE (TASK 9) — SHIPPED this session.** Real stream audio + voice filters + auto-duck
+ free TRAX background music. Build **0 warnings**, full suite **196 passing** (ONE integration
test: `AudioPipelineTests.Mix_WithFiltersDuckAndGain_Lands_On_AudioPipe`). Everything below in the
milestone section is DONE. **NOT yet committed** — next step is review + commit (with `ai.md`/
`TASKS.md`/this file already updated in the same changeset).
- **Two build-time deviations from the settled plan (both recorded in ai.md + TASKS.md):**
1. `MusicPlayer` uses **`WaveOutEvent`** (new sibling package `NAudio.WinMM` 2.2.1) — not `WasapiOut`
as planned; `WaveOutEvent` isn't in `NAudio.Wasapi`/`NAudio.Core`.
2. No forced 48 kHz capture — NAudio 2.2.1's `WasapiCapture` has no overridable
`GetDefaultMixFormat`, so both sources run the device mix format and the mixer's
`TinyResampler` normalizes any rate to 48 kHz (resampler IS the design, not a fallback).
3. `FfmpegEncoder.cs` is **untouched** — the VM closes the audio pipe (`_audioMixer.StopLive()`) in
`StopStream` BEFORE the frame pump stops (audio EOF then video EOF, in order).
- **SERVER RECOVERY (llamachile.tube / YunoHost / DO droplet) — done earlier session.** Side task
complete; **rotate the DO API token** (control-panel only) and any password no longer trusted.
- **Shipped, all pushed before this session:** TASK 21 (creator-hub About, `3d92bd0`), TASK 8 (meter
+10 dB), TASK 4 ship step 7 (one-click go-live + private-only). **174 tests passing, 0 warnings.**
- **Uncommitted (whole TASK 9 milestone):** `Services/Audio/` (new `VoiceFilterChain.cs`,
`AudioRingBuffer.cs`, `AutoDucker.cs`, `TinyResampler.cs`, `MusicPlayer.cs`, `NamedPipeAudioWriter.cs`,
rewritten `AudioMixer.cs`, simplified `WasapiMicAudioSource.cs`), `Models/Music.cs` (new),
`FfmpegArgs.cs`, `EncoderOptions.cs`, `LayoutStore.cs` (v9), `MainViewModel.cs`, `MainWindow.xaml`,
`MainWindow.xaml.cs`, `ytLive.csproj` (NAudio.WinMM), `ytLive.Tests/` (`AudioPipelineTests.cs` new,
`FfmpegEncoderTests.cs` + `LayoutStorePersistenceTests.cs` updated), and docs `ai.md`, `TASKS.md`,
`HANDOFF.md`.
- **Next step:** commit the milestone (docs already staged in the same changeset), push, then start
**TASK 5** (reusable stream → `_rtmpUrlProvider` — the last blocker before go-live actually encodes).
Then **itch.io product page setup** (premium URL lands in `MainViewModel.PremiumUrl`, lights up
Unlock Premium). Optional, not queued: rewriting the healed entry's `ProfileUrl` to
`https://mastodon.llamachile.tube/@gramps` (user must say the word).
## TASK 9 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 9)
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 22), 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), itch.io `PremiumUrl`.
- **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 game detector is polled on the UI thread via a 250ms `DispatcherTimer`;
`OnGameAudioPollTick` wraps `Poll()` in try/catch + `AppLog`.
- 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`).