test(probe): 30s Twitch scene-switch probe, zero PC audio#43
Merged
Conversation
Add scripts/probe-twitch-scene-switch.py: a 30s live Twitch broadcast that performs a real mid-course scene change at duration/2 (~t=15s) and carries a silent AAC track (no desktop/mic/process audio). Switch implementation prefers a true two-scene OBS switch (path a): CreateScene + SetCurrentProgramScene, viable headless because obs-websocket resolves scenes via libobs-global name lookup and the frontend-stub rebinds output channel 0 on set_current_scene. Auto-falls-back to a SetCaptureSource URL swap (path b) when the headless build does not honour the program-scene flip; the chosen path is asserted and reported. Zero PC sound is enforced on three fronts: mic/process-audio env popped at spawn (sources never created), browser_source created with reroute_audio=False, and the frontend-stub's unconditional PulsarDesktopAudio loopback muted over the wire with GetInputList/GetInputMute asserting no unmuted audio input remains before going live. Two dependency-free, silent scene pages (scene-a.html cobalt / scene-b.html crimson) make the switch unmistakable on the VOD. Stream key read from TWITCH_STREAM_KEY only, redacted in all logs. Refs: probe-twitch-live.py, probe-m6-live.py (v5 plumbing reused) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
obs-qsv11, win-dshow, and win-dshow's virtualcam-module compile sources that require the MSVC ATL component (atlbase.h / atlcomcli.h / atlstr.h). ATL is present on the CI windows-2022 runner but absent on a dev box without the "C++ ATL" workload, where it broke the local build. The previous local unblock disabled those three plugins unconditionally, which would have regressed CI build coverage if committed. Replace it with a conditional gate: - patches/0002 now wraps the three plugin registrations in if(PULSAR_HAVE_ATL) ... else (disabled stub). The option defaults ON, so CI -- which never passes the flag -- builds all three plugins exactly as upstream does. No coverage regression, no asymmetry. - scripts/build-win.ps1 probes the toolchain via vswhere for the ATL headers under VC/Tools/MSVC/<ver>/atlmfc/include and passes -DPULSAR_HAVE_ATL=OFF only when they are missing, logging an explicit warning. The skip is driven by detection, not by a static patch that amputates upstream in all builds. The upstream submodule stays at its pinned SHA; the gate lives entirely in the replayed patch + build script. None of the three plugins are on the headless browser_source live path (x264/nvenc encode + CEF browser source), so the OFF branch has zero functional impact on the Pulsar probe. Verified: local build (ATL absent) -> BUILD_SCRIPT_EXIT=0, pulsar.exe + pulsar-browser.dll staged, qsv11/win-dshow absent from rundir. Refs #43 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Documents the C1083 / missing ATL headers failure mode introduced by the PULSAR_HAVE_ATL gate (commit d634d35, PR #43): symptom, diagnostic, root cause, gate mechanics (Test-AtlAvailable + patch 0002), verification, rollback, and optional ATL install for full local parity. Adds a pointer in DEVELOPMENT.md Toolchain table and Troubleshooting section so the next developer hitting the error is one click from the runbook. Refs #43 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The Pulsar CI proved the build (pipeline.yml) but carried none of the étage-0 merge-gate conformance checks (docs/rules/git.md §1, docs/rules/security.md §Détection). Add a dedicated compliance.yml so a PR cannot be merged while leaking a secret, shipping a high/critical dependency CVE, drifting its lockfile, or breaking CODEOWNERS. - secret-scan: trufflehog over git history + working tree (verified-only) plus detect-secrets against a new .secrets.baseline. - deps-audit: npm audit --omit=dev --audit-level=high (deps scope = npm; the Python probe glue is dev-only, no lock to pip-audit). - lockfile-check: npm ci --dry-run + post-resolution clean-tree assert, and a guard rejecting a stray yarn.lock. - codeowners-check: structural validation of .github/CODEOWNERS. Separate workflow from pipeline.yml on purpose: governance vs build, all-ubuntu/cheap, independent red surface, one check per gate in gh pr checks. No error-suppression toggle anywhere -- each job blocks. Refs #43 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A missing-ATL detection result on a CI runner is not the benign "dev box without the C++ ATL component" case the warning was written for: the windows-2022 runner ships ATL, so a negative there is a detection false-negative or a broken runner image. The old Write-Warning let the build silently drop to PULSAR_HAVE_ATL=OFF, amputating qsv11/virtualcam/ win-dshow from the binary while CI stayed green -- a thinner artefact shipped under a green check. Throw instead when GITHUB_ACTIONS/CI is set, turning the build red so the gap is visible rather than masked. Local builds (no CI env) keep the warning + skip -- the intended dev convenience documented in the ATL runbook. Refs #43 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two real environment-specific failures surfaced by the gates' own first
run on the ubuntu runners (not reproducible on the win dev box):
- lockfile-check: @clodocapeo/pulsar-bundle pins os:win32/cpu:x64, so
`npm ci --dry-run` aborts with EBADPLATFORM on ubuntu before checking
lockfile sync. Add --force (same bypass npm-publish already uses); a
genuine drift still fails distinctly (EUSAGE), so the sync guarantee
is intact.
- secret-scan: detect-secrets flagged 12 heuristic matches absent from
the empty baseline. All reviewed and confirmed NON-secrets: UI locale
labels ("Server Password"), doc/test placeholder passwords
("dev-only-do-not-ship-this", fake-${Date.now()}), the password= token
in this workflow's own grep, and a viewer-scoped localhost JWT test
fixture in probe-m6-live.py. Record them in .secrets.baseline as the
audited allowlist; any NEW finding beyond these still turns the gate
red. TruffleHog --only-verified found zero live credentials.
Refs #43
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`detect-secrets scan --baseline` rewrites the baseline file in place and always refreshes the volatile `generated_at` timestamp, so the previous `git diff --quiet -- .secrets.baseline` check failed on the timestamp alone with zero new secrets -- a guaranteed false red on the secret-scan gate. Compare only the `results` map (filename + hashed_secret) so the gate fails strictly when a genuinely new secret-shaped finding appears, never on timestamp/metadata churn. A real new finding still turns the PR red; a removed baseline finding is surfaced as a warning to prune. Refs #43 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The previous commit reworded the comment block in compliance.yml that documents the secret-scan mechanism. The detect-secrets "Secret Keyword" plugin flags that meta-text (it talks about `hashed_secret` / secrets), a known self-referential false positive already present in the audited baseline. Editing the block shifted its line (104 -> 110) and changed its content hash, so the committed baseline went stale and the secret-scan gate correctly went red on a "new" finding. This is NOT a leaked credential: the finding is the workflow file describing how the gate hashes findings. Re-baseline so the audited false positive matches the current file; no other finding changes, and no finding is audited as a real secret. Re-scanning against the new baseline yields zero new/removed findings (gate green). Refs #43 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Résout le risque résiduel R1 flaggé par Bastion : ws@8.20.0 est vulnérable (GHSA-58qx-3vcg-4xpx, modéré, CVSS 4.4), résolu en 8.20.1. Bump de la résolution lockfile uniquement — les ranges ^8.18.0 des packages workspace satisfont déjà 8.20.1, package.json inchangé. Refs #43 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Record the three structural build/CI decisions landed in PR #43: ATL-conditional build-gate (PULSAR_HAVE_ATL, default ON, CI-hard throw), the compliance.yml merge gate (secret-scan/deps-audit/lockfile/codeowners), and the URL-swap scene-switch fallback. Carries the Bastion residual-risk ledger (R1 resolved via ws@8.20.1, R2 npm-only deps-audit, R3 baseline re-audit) and traces the headless multi-scene (CreateScene 204) limitation as a deferred item. First ADR of the repo; sets the house ADR format. Refs #43 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Vigil review of PR #43 delta (ws@8.20.1 bump + ADR-001) passed: ws bump is lockfile-only (3 lines, 8.20.0->8.20.1), package.json ranges ^8.18.0 unchanged, no leftover 8.20.0 in lock (R1 closed). ADR-001 format sound (sets house format), zero drift vs artefacts: 3.1 matches build-win.ps1 (PULSAR_HAVE_ATL default ON, CI throw), 3.2 matches compliance.yml four jobs + CODEOWNERS, 3.3 matches scene-a/scene-b.html URL-swap. R2/R3 carry guards, headless multi-scene deferred. Flip Status proposed->accepted, set Decided 2026-06-08. Refs #43 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The live-broadcast probe called StartDestination ~3-4s after spawning pulsar.exe and gave up on the first attempt. The frontend streaming output is wired asynchronously after boot, so a fast probe can hit a transient 'frontend streaming output unavailable' before the output exists — a boot-ordering race, not a broadcast failure (the same binary/key passes on rerun, proven by re-running run 27107906022). Poll StartDestination for a bounded 20s budget, but ONLY while the error is exactly that transient string. Any other error (bad key, RTMP reject) still fails on the first attempt, and exhausting the budget is a hard failure — no masking of a genuine defect. Refs #43 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
A new live probe
scripts/probe-twitch-scene-switch.py: a 30s Twitch broadcast that performs a real mid-course scene change at ~t=15s (duration/2) and carries zero PC sound. Plus two silent, visually-distinct scene pages served locally (scripts/live-test/scene-a.htmlcobalt,scene-b.htmlcrimson).Reuses the obs-websocket v5 plumbing from
probe-m6-live.py(PulsarProcess,Inbox.pump,request,vendor_call,redact).Scene-switch implementation: real scene (a) with auto-fallback (b)
Investigated the headless scene model:
pulsar-frontend-stubcreates one program scene at boot (Default); itsscenesvector is hard-coded single-entry.obs-websocketCreateSceneusesobs_canvas_scene_createon the main canvas → registers in libobs's global source table;SetCurrentProgramScene→AcquireScene→obs_get_source_by_name(global lookup, NOT the stub vector) finds it; the stub'sobs_frontend_set_current_scenerebindsobs_set_output_source(0, scene)+ emitsSCENE_CHANGED→ the new scene composites.So the probe attempts a true two-scene switch first (path a):
CreateScene(pulsar-scene-b)→ put a distinctbrowser_sourceon each scene → go live on scene-a →SetCurrentProgramScene(scene-b)atduration/2, asserted viaGetCurrentProgramScenebefore/after. If the build does not honour the program-scene flip, it auto-falls-back to aSetCaptureSourceURL swap (path b) on a single scene — still an eye-visible content change, clearly labelled as a fallback in logs + run summary.--force-fallbackforces (b).Zero PC sound (impératif)
Enforced three ways:
PULSAR_MIC_DEVICE_ID+PULSAR_PROCESS_AUDIO_NAMEpopped → mic/process sources never created;PULSAR_CAPTURE_WINDOWpopped.browser_sourcecreated withreroute_audio=False(both scene pages are silent anyway — no Web Audio).PulsarDesktopAudio(channel 1, the literal "sound of the PC"): the probeSetInputMutes it, then enumeratesGetInputList+GetInputMuteand asserts no unmuted audio input remains before going live (fails closed otherwise).Validation done
python -m py_compileclean on Python 3.11.9 (the CI interpreter).websockets); config-error guards return exit 2 for missing key / missing exe /--duration < 4.pulsar.exeand no key in this environment, per the task. The live run is for Probe/Vigil.What Vigil / Probe must verify at the real run
[scene-switch] implementation = ...reports which ran.ensure_silenceasserts mute over the wire, but a run-time ffprobe of the recorded MP4 is the ground truth.pulsar.exe; cleanStopDestination/StopRecord/teardown.Surface sensible
Touches secrets handling (Twitch stream key from env, redacted) — flagging for Bastion clearance per the merge gate. No new deps, no network-contract change, no
src/**change (probe + static HTML only).🤖 Generated with Claude Code