CLose story 1.2

This commit is contained in:
2026-05-21 23:08:34 +02:00
commit 110b295a7b
75 changed files with 16065 additions and 0 deletions
+118
View File
@@ -0,0 +1,118 @@
// OQ-1 Spike Result: css-fallback — FoundryVTT v14 — 2026-05-21
//
// Investigation findings (Story 1.2 spike):
// - game.webrtc is foundry.av.AVMaster — no getConnection(userId) method exists.
// - Remote stream access: game.webrtc.client.getMediaStreamForUser(userId)
// via the abstract AVClient public interface.
// - track.enabled = false on remote inbound tracks does NOT stop WebRTC bandwidth.
// RTP packets continue arriving from the remote peer regardless.
// - True bandwidth elimination requires SDP renegotiation (not in AVMaster public API).
// - LiveKit backend (CONFIG.WebRTC.clientClass override) removes peers/remoteStreams;
// only getMediaStreamForUser() remains safe across backends.
//
// Conclusion: this.webrtc = null. scrying-pool.webrtcMode = 'css-fallback' (world default).
// CSS/DOM cosmetic hiding is the only honest implementation path for Story 1.3+.
/**
* Sole gateway to game.* APIs for the Scrying Pool module.
*
* Feature-detects WebRTC track-disabling capability at init time via
* {@link FoundryAdapter.probeCapability}. Exposes a `webrtc` surface
* ({disableTrack, enableTrack}) if track-disable is confirmed; `null` otherwise.
*
* Construction is side-effect-free. The webrtc property must be set by the
* caller (at Hooks.once('ready') when game.webrtc is available) via
* {@link FoundryAdapter.buildWebRTCSurface}. Story 1.3 wires this up.
*/
export class FoundryAdapter {
/** Settings namespace for all scrying-pool world settings. */
static SETTINGS_NS = 'scrying-pool';
/**
* World setting key for the resolved WebRTC capability mode.
* Full identifier: `scrying-pool.webrtcMode`.
* Value is one of: `'track-disable'` | `'css-fallback'` | `'unsupported'`
*/
static SETTING_WEBRTC_MODE = 'webrtcMode';
/**
* Creates a FoundryAdapter. Side-effect-free — no game.* access in constructor.
*/
constructor() {
/**
* WebRTC track-disabling surface, or null on the css-fallback/unsupported path.
*
* Set to `{ disableTrack, enableTrack }` only if probeCapability returns
* `'track-disable'`. As of FoundryVTT v14, the probe always returns
* `'css-fallback'` or `'unsupported'` — see OQ-1 spike comment at top of file.
*
* @type {{ disableTrack(userId: string): void, enableTrack(userId: string): void } | null}
*/
this.webrtc = null;
}
/**
* Probes the game.webrtc (AVMaster) instance for WebRTC track-disabling 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.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'`.
*
* The `'track-disable'` branch in buildWebRTCSurface is kept for forward compatibility
* in case a future FoundryVTT version or custom AVClient exposes real bandwidth control.
*
* @param {unknown} gameWebrtc - The game.webrtc value at ready time (may be null/undefined)
* @returns {'track-disable'|'css-fallback'|'unsupported'}
*/
static probeCapability(gameWebrtc) {
if (!gameWebrtc) return 'unsupported';
const client = /** @type {any} */ (gameWebrtc).client;
if (!client || typeof client.getMediaStreamForUser !== 'function') return 'unsupported';
// track.enabled = false on remote inbound tracks does NOT stop WebRTC bandwidth.
// The 'track-disable' branch is unreachable with the current FoundryVTT v14 API.
return 'css-fallback';
}
/**
* Builds the webrtc surface object for the `'track-disable'` capability path.
*
* NOTE: As of FoundryVTT v14, {@link FoundryAdapter.probeCapability} never returns
* `'track-disable'` because `track.enabled = false` does not stop inbound RTP bandwidth.
* This method is kept for forward compatibility and as tested documentation of the
* interface contract that Story 1.3+ consumers expect.
*
* @param {{ client: { getMediaStreamForUser(userId: string): (MediaStream|null|undefined) } }} gameWebrtc
* @returns {{ disableTrack(userId: string): void, enableTrack(userId: string): void }}
*/
static buildWebRTCSurface(gameWebrtc) {
return {
disableTrack(userId) {
try {
const stream = gameWebrtc.client.getMediaStreamForUser(userId);
const tracks = stream?.getVideoTracks() ?? [];
for (const track of tracks) track.enabled = false;
if (tracks.length === 0) {
console.warn('[ScryingPool] disableTrack: no video tracks found for', userId);
}
} catch (err) {
console.error('[ScryingPool] disableTrack failed:', err);
}
},
enableTrack(userId) {
try {
const stream = gameWebrtc.client.getMediaStreamForUser(userId);
const tracks = stream?.getVideoTracks() ?? [];
for (const track of tracks) track.enabled = true;
if (tracks.length === 0) {
console.warn('[ScryingPool] enableTrack: no video tracks found for', userId);
}
} catch (err) {
console.error('[ScryingPool] enableTrack failed:', err);
}
},
};
}
}