diff --git a/_bmad-output/implementation-artifacts/bmad-review-blind-hunter-prompt.md b/_bmad-output/implementation-artifacts/bmad-review-blind-hunter-prompt.md new file mode 100644 index 0000000..b3e77e0 --- /dev/null +++ b/_bmad-output/implementation-artifacts/bmad-review-blind-hunter-prompt.md @@ -0,0 +1,703 @@ +# Blind Hunter — WebRTC Full AV Replacement Code Review + +**Diff Source:** Commit range HEAD~1..HEAD (c4a375f) +**Review Target:** WebRTC full AV replacement implementation +**Date:** 2026-05-26 + +--- + +## INSTRUCTIONS + +You are the **Blind Hunter** — a cynical, jaded code reviewer with zero patience for sloppy work. You expect to find problems. Be skeptical of everything. Look for what's missing, not just what's wrong. Use a precise, professional tone — no profanity or personal attacks. + +**YOU RECEIVE ONLY THIS DIFF.** You have NO project context, NO spec, NO access to the codebase. Review ONLY what is shown in the diff below. + +## DIFF TO REVIEW + +```diff +diff --git a/module.js b/module.js +index 86d48ac..c5fe82f 100644 +--- a/module.js ++++ b/module.js +@@ -57,15 +57,17 @@ let directorsBoardButtonAdded = false; + Hooks.once("init", () => { + console.log("[ScryingPool] init — module loading"); + +- // OQ-1 resolved (Story 1.2 spike): probe result is 'css-fallback' — see FoundryAdapter.js ++ // WebRTC mode setting — determines how the module handles AV integration ++ // Updated for FULL REPLACEMENT architecture (hiding Foundry's dock, showing our own) + game.settings.register("scrying-pool", "webrtcMode", { + scope: "world", + config: false, + type: String, +- default: "css-fallback", ++ default: "stream-access", + choices: { +- "track-disable": "Track Disable (bandwidth-saving)", +- "css-fallback": "CSS Fallback (cosmetic hiding)", ++ "stream-access": "Stream Access (full replacement - hide Foundry dock, show VVM dock with actual video)", ++ "track-disable": "Track Disable (bandwidth-saving) - DEPRECATED", ++ "css-fallback": "CSS Fallback (cosmetic hiding) - DEPRECATED", + "unsupported": "Unsupported (AV not available)", + }, + }); +@@ -175,14 +177,25 @@ Hooks.once("ready", () => { + // Hydrate StateStore from persisted world setting (AC-6, AC-7) + stateStore.init(); + +- // Probe WebRTC capability and set adapter.webrtc (AC-8) ++ // Probe WebRTC capability and set adapter.webrtc ++ // For FULL REPLACEMENT: we need stream access to create our own video tiles + const outcome = FoundryAdapter.probeCapability(game.webrtc); +- adapter.webrtc = outcome === 'track-disable' +- ? FoundryAdapter.buildWebRTCSurface(game.webrtc) +- : null; + adapter.settings.set(FoundryAdapter.SETTING_WEBRTC_MODE, outcome).catch(err => { + console.error('[ScryingPool] Failed to set webrtcMode setting:', err); + }); ++ ++ // Build WebRTC surface for stream access (full replacement mode) ++ if (outcome === 'stream-access') { ++ adapter.webrtc = FoundryAdapter.buildWebRTCSurface(game.webrtc); ++ console.log('[ScryingPool] WebRTC stream access available - full replacement mode enabled'); ++ } else if (outcome === 'unsupported') { ++ adapter.webrtc = null; ++ console.log('[ScryingPool] WebRTC not available - AV features disabled'); ++ } else { ++ // Legacy: track-disable or css-fallback (deprecated) ++ adapter.webrtc = FoundryAdapter.buildWebRTCSurface(game.webrtc); ++ console.warn('[ScryingPool] WebRTC mode is deprecated:', outcome, '- consider using stream-access'); ++ } + + // Wire core managers — construct both before setReady so handler can reference both + visibilityManager = new VisibilityManager(stateStore, adapter); +diff --git a/scripts/test-stream-access.mjs b/scripts/test-stream-access.mjs +new file mode 100644 +index 0000000..8d91db7 +--- /dev/null ++++ b/scripts/test-stream-access.mjs +@@ -0,0 +1,159 @@ ++/** ++ * Test script to verify WebRTC stream access implementation ++ * This script tests the full AV replacement architecture ++ * ++ * Run with: node scripts/test-stream-access.mjs ++ * ++ * Tests: ++ * 1. FoundryAdapter.probeCapability() returns 'stream-access' when getMediaStreamForUser is available ++ * 2. FoundryAdapter.buildWebRTCSurface() creates proper WebRTC surface ++ * 3. ScryingPoolStrip.getData() includes hasStreamAccess flag ++ * 4. buildParticipantList() includes hasStreamAccess in participant objects ++ * 5. CSS includes rules to hide Foundry's AV dock ++ * 6. Template includes video container element ++ */ ++ ++import { FoundryAdapter } from '../src/foundry/FoundryAdapter.js'; ++import { buildParticipantList } from '../src/ui/gm/ScryingPoolStrip.js'; ++ ++console.log('=== WebRTC Stream Access Implementation Tests ===\n'); ++ ++// Test 1: probeCapability returns 'stream-access' when getMediaStreamForUser is available ++console.log('Test 1: probeCapability with stream access'); ++const mockWebRTCWithStream = { ++ client: { ++ getMediaStreamForUser: (userId) => new MediaStream(), ++ getConnectedUsers: () => ['user1', 'user2'], ++ }, ++}; ++const result1 = FoundryAdapter.probeCapability(mockWebRTCWithStream); ++console.log(` probeCapability result: ${result1}`); ++console.assert(result1 === 'stream-access', 'Should return stream-access'); ++console.log(' ✓ PASSED\n'); ++ ++// Test 2: probeCapability returns 'unsupported' when getMediaStreamForUser is missing ++console.log('Test 2: probeCapability without stream access'); ++const mockWebRTCWithoutStream = { ++ client: {}, ++}; ++const result2 = FoundryAdapter.probeCapability(mockWebRTCWithoutStream); ++console.log(` probeCapability result: ${result2}`); ++console.assert(result2 === 'unsupported', 'Should return unsupported'); ++console.log(' ✓ PASSED\n'); ++ ++// Test 3: buildWebRTCSurface creates proper surface with all methods ++console.log('Test 3: buildWebRTCSurface creates complete WebRTC surface'); ++const surface = FoundryAdapter.buildWebRTCSurface(mockWebRTCWithStream); ++console.log(' WebRTC surface methods:'); ++console.log(' - getMediaStreamForUser:', typeof surface.getMediaStreamForUser === 'function' ? '✓' : '✗'); ++console.log(' - getConnectedUsers:', typeof surface.getConnectedUsers === 'function' ? '✓' : '✗'); ++console.log(' - getLevelsStreamForUser:', typeof surface.getLevelsStreamForUser === 'function' ? '✓' : '✗'); ++console.log(' - isAudioEnabled:', typeof surface.isAudioEnabled === 'function' ? '✓' : '✗'); ++console.log(' - isVideoEnabled:', typeof surface.isVideoEnabled === 'function' ? '✓' : '✗'); ++console.log(' - toggleAudio:', typeof surface.toggleAudio === 'function' ? '✓' : '✗'); ++console.log(' - toggleVideo:', typeof surface.toggleVideo === 'function' ? '✓' : '✗'); ++console.log(' - toggleBroadcast:', typeof surface.toggleBroadcast === 'function' ? '✓' : '✗'); ++console.log(' - setUserVideo:', typeof surface.setUserVideo === 'function' ? '✓' : '✗'); ++console.log(' - disableTrack:', typeof surface.disableTrack === 'function' ? '✓' : '✗'); ++console.log(' - enableTrack:', typeof surface.enableTrack === 'function' ? '✓' : '✗'); ++ ++const allMethodsPresent = ++ typeof surface.getMediaStreamForUser === 'function' && ++ typeof surface.getConnectedUsers === 'function' && ++ typeof surface.getLevelsStreamForUser === 'function' && ++ typeof surface.isAudioEnabled === 'function' && ++ typeof surface.isVideoEnabled === 'function' && ++ typeof surface.toggleAudio === 'function' && ++ typeof surface.toggleVideo === 'function' && ++ typeof surface.toggleBroadcast === 'function' && ++ typeof surface.setUserVideo === 'function' && ++ typeof surface.disableTrack === 'function' && ++ typeof surface.enableTrack === 'function'; ++ ++console.assert(allMethodsPresent, 'All WebRTC surface methods should be functions'); ++console.log(' ✓ PASSED\n'); ++ ++// Test 4: buildParticipantList includes hasStreamAccess ++console.log('Test 4: buildParticipantList includes hasStreamAccess flag'); ++const mockAdapter = { ++ users: { ++ get: (userId) => ({ name: userId, avatar: null }), ++ }, ++}; ++const mockStateStore = { ++ getState: (userId) => 'active', ++}; ++const mockController = { ++ hasPendingOp: (userId) => false, ++}; ++ ++const participants = buildParticipantList( ++ ['user1', 'user2'], ++ mockStateStore, ++ mockController, ++ mockAdapter, ++ true // hasStreamAccess ++); ++ ++console.log(` Participant count: ${participants.length}`); ++console.log(` First participant hasStreamAccess: ${participants[0].hasStreamAccess}`); ++console.assert(participants.length === 2, 'Should have 2 participants'); ++console.assert(participants[0].hasStreamAccess === true, 'Participant should have hasStreamAccess flag'); ++console.log(' ✓ PASSED\n'); ++ ++// Test 5: CSS includes rules to hide Foundry's AV dock ++console.log('Test 5: CSS includes AV dock hiding rules'); ++import fs from 'fs'; ++import path from 'path'; ++ ++const cssPath = path.join(process.cwd(), 'styles', 'scrying-pool.less'); ++const cssContent = fs.readFileSync(cssPath, 'utf8'); ++ ++const hasAvHiding = cssContent.includes('#av') && cssContent.includes('display: none'); ++const hasCameraViewHiding = cssContent.includes('.camera-view') && cssContent.includes('display: none'); ++ ++console.log(` Has #av hiding rule: ${hasAvHiding ? '✓' : '✗'}`); ++console.log(` Has .camera-view hiding rule: ${hasCameraViewHiding ? '✓' : '✗'}`); ++console.assert(hasAvHiding, 'CSS should include #av hiding rule'); ++console.assert(hasCameraViewHiding, 'CSS should include .camera-view hiding rule'); ++console.log(' ✓ PASSED\n'); ++ ++// Test 6: Template includes video container ++console.log('Test 6: Template includes video container element'); ++const templatePath = path.join(process.cwd(), 'templates', 'roster-strip.hbs'); ++const templateContent = fs.readFileSync(templatePath, 'utf8'); ++ ++const hasVideoContainer = templateContent.includes('sp-participant-video'); ++const hasStreamAccessCheck = templateContent.includes('hasStreamAccess'); ++ ++console.log(` Has video container class: ${hasVideoContainer ? '✓' : '✗'}`); ++console.log(` Has stream access check: ${hasStreamAccessCheck ? '✓' : '✗'}`); ++console.assert(hasVideoContainer, 'Template should include video container'); ++console.assert(hasStreamAccessCheck, 'Template should check for stream access'); ++console.log(' ✓ PASSED\n'); ++ ++// Test 7: CSS includes video element styling ++console.log('Test 7: CSS includes video element styling'); ++const rosterCssPath = path.join(process.cwd(), 'styles', 'components', '_roster-strip.less'); ++const rosterCssContent = fs.readFileSync(rosterCssPath, 'utf8'); ++ ++const hasVideoContainerClass = rosterCssContent.includes('.sp-participant-video'); ++const hasVideoElementClass = rosterCssContent.includes('.sp-participant-video__element'); ++ ++console.log(` Has .sp-participant-video class: ${hasVideoContainerClass ? '✓' : '✗'}`); ++console.log(` Has .sp-participant-video__element class: ${hasVideoElementClass ? '✓' : '✗'}`); ++console.assert(hasVideoContainerClass, 'CSS should include video container class'); ++console.assert(hasVideoElementClass, 'CSS should include video element class'); ++console.log(' ✓ PASSED\n'); ++ ++// Summary ++console.log('=== All Tests Passed! ==='); ++console.log('\nImplementation Summary:'); ++console.log('✓ FoundryAdapter.probeCapability() correctly identifies stream-access mode'); ++console.log('✓ FoundryAdapter.buildWebRTCSurface() provides full WebRTC client API'); ++console.log('✓ ScryingPoolStrip.getData() includes hasStreamAccess flag'); ++console.log('✓ buildParticipantList() passes hasStreamAccess to participants'); ++console.log('✓ CSS hides Foundry AV dock (#av and .camera-view)'); ++console.log('✓ Template includes video container with conditional rendering'); ++console.log('✓ CSS includes styling for video containers and elements'); ++console.log('\nFull AV replacement architecture is implemented and ready!'); +diff --git a/src/foundry/FoundryAdapter.js b/src/foundry/FoundryAdapter.js +index 035b75a..680a063 100644 +--- a/src/foundry/FoundryAdapter.js ++++ b/src/foundry/FoundryAdapter.js +@@ -308,51 +308,183 @@ export class FoundryAdapter { + } + + /** +- * Probes the game.webrtc (AVMaster) instance for WebRTC track-disabling capability. ++ * Probes the game.webrtc (AVMaster) instance for WebRTC capability. + * + * Probe logic and results (FoundryVTT v14, 2026-05-21): + * - If game.webrtc is null/falsy → AV disabled or not yet initialised → `'unsupported'` + * - If game.webrtc is not an object → invalid type → `'unsupported'` + * - If game.webrtc.client lacks getMediaStreamForUser() → non-standard backend → `'unsupported'` +- * - Otherwise: tracks are technically reachable but track.enabled = false does NOT reduce +- * inbound WebRTC bandwidth (RTP packets keep arriving). The `'track-disable'` outcome +- * requires true bandwidth elimination, so the result is `'css-fallback'`. ++ * - Otherwise: getMediaStreamForUser() is available for stream access → `'stream-access'` + * +- * The `'track-disable'` branch in buildWebRTCSurface is kept for forward compatibility +- * in case a future FoundryVTT version or custom AVClient exposes real bandwidth control. ++ * NOTE: For FULL REPLACEMENT architecture (hiding Foundry's AV dock and showing our own): ++ * - We use 'stream-access' mode to get actual MediaStream objects via getMediaStreamForUser() ++ * - We create our own