wenay-react2 1.0.49 → 1.0.51
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -14
- package/doc/EXAMPLE_USAGE.md +48 -0
- package/doc/PROJECT_FUNCTIONALITY.md +13 -2
- package/doc/PROJECT_RULES.md +27 -27
- package/doc/WENAY_REACT2_RENAMES.md +170 -170
- package/doc/changes/v1.0.41.md +35 -35
- package/doc/changes/v1.0.42.md +26 -26
- package/doc/changes/v1.0.43.md +10 -10
- package/doc/changes/v1.0.44.md +14 -14
- package/doc/changes/v1.0.45.md +8 -8
- package/doc/changes/v1.0.46.md +11 -11
- package/doc/changes/v1.0.47.md +9 -9
- package/doc/changes/v1.0.48.md +8 -8
- package/doc/changes/v1.0.49.md +7 -7
- package/doc/changes/v1.0.50.md +7 -0
- package/doc/changes/v1.0.51.md +10 -0
- package/doc/examples/README.md +1 -1
- package/doc/examples/conference-client.html +34 -34
- package/doc/examples/conference-client.ts +212 -212
- package/doc/examples/conference-server.mjs +150 -150
- package/doc/examples/peer-call-media.tsx +7 -7
- package/doc/examples/stand.tsx +5 -5
- package/doc/native.md +37 -37
- package/doc/progress/README.md +13 -13
- package/doc/progress/architecture-fix-queue.md +74 -74
- package/doc/progress/column-state-present-gate.md +28 -28
- package/doc/progress/common2-adoption-1.0.73.md +28 -28
- package/doc/progress/common2-adoption-1.0.74.md +24 -24
- package/doc/progress/common2-adoption-1.0.75.md +120 -0
- package/doc/progress/hook-controller-opportunities.md +363 -363
- package/doc/progress/hook-extraction-audit.md +195 -195
- package/doc/progress/public-surface-normalization.md +351 -351
- package/doc/progress/qa-stand-walkthrough-2026-07-12.md +62 -62
- package/doc/progress/stand-as-examples-audit.md +20 -20
- package/doc/progress/style-system-normalization.md +121 -121
- package/doc/target/README.md +32 -32
- package/doc/target/my.md +11 -3
- package/doc/wenay-react2-rare.md +11 -7
- package/doc/wenay-react2.md +2 -2
- package/lib/common/src/grid/agGrid4/core.js +7 -2
- package/lib/common/src/hooks/useReplay.js +13 -2
- package/lib/common/src/logs/logs.d.ts +4 -1
- package/lib/common/src/logs/logs.js +19 -11
- package/lib/common/testUseReact/replayVideo.d.ts +5 -0
- package/lib/common/testUseReact/replayVideo.js +98 -1
- package/lib/style/menuRight.css +19 -19
- package/lib/style/style.css +23 -23
- package/lib/style/tokens.css +184 -184
- package/package.json +54 -54
- package/doc/changes/v1.0.35.md +0 -26
- package/doc/changes/v1.0.37.md +0 -13
- package/doc/changes/v1.0.38.md +0 -48
- package/doc/changes/v1.0.39.md +0 -35
- package/doc/changes/v1.0.40.md +0 -15
|
@@ -1,212 +1,212 @@
|
|
|
1
|
-
// Browser seat for conference-server.mjs — the real-backend variant of demo/peer-conference.
|
|
2
|
-
// One tab = one participant. Media tiles ride the SERVER relay (room-gated fan-out);
|
|
3
|
-
// each peer row is a peer-store link that starts on the server relay and can promote to
|
|
4
|
-
// a real RTCPeerConnection datachannel (negotiated through the same server signal hub),
|
|
5
|
-
// then re-interpose back — the two technologies of the conference bridge, cross-tab.
|
|
6
|
-
//
|
|
7
|
-
// Serve this page from the wenay-react2 dev stand (vite transpiles the .ts):
|
|
8
|
-
// /doc/examples/conference-client.html?me=a&peers=b,c&host=1&room=demo
|
|
9
|
-
import {io} from "socket.io-client";
|
|
10
|
-
import {createRpcClientHub, Peer} from "wenay-common2";
|
|
11
|
-
|
|
12
|
-
type SeatWorld = {name: string, hue: number, beat: number, status: string};
|
|
13
|
-
type ConfFrame = {n: number, at: number, image: string};
|
|
14
|
-
|
|
15
|
-
const params = new URLSearchParams(location.search);
|
|
16
|
-
const me = params.get("me") ?? "a";
|
|
17
|
-
const peers = (params.get("peers") ?? (me === "a" ? "b,c" : "a")).split(",").map(value => value.trim()).filter(Boolean);
|
|
18
|
-
const isHost = params.get("host") === "1" || me === "a";
|
|
19
|
-
const room = params.get("room") ?? "demo";
|
|
20
|
-
const serverUrl = params.get("server") ?? `${location.protocol}//${location.hostname}:8391`;
|
|
21
|
-
|
|
22
|
-
const el = (id: string) => document.getElementById(id)!;
|
|
23
|
-
const logBox = el("log");
|
|
24
|
-
function log(line: string) {
|
|
25
|
-
const row = document.createElement("div");
|
|
26
|
-
row.textContent = `${new Date().toLocaleTimeString()} ${line}`;
|
|
27
|
-
logBox.prepend(row);
|
|
28
|
-
while (logBox.children.length > 60) logBox.lastChild?.remove();
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const seatHue = (account: string) => {
|
|
32
|
-
let hash = 0;
|
|
33
|
-
for (const ch of account) hash = (hash * 31 + ch.charCodeAt(0)) % 360;
|
|
34
|
-
return hash;
|
|
35
|
-
};
|
|
36
|
-
const paintFrame = (account: string, n: number): ConfFrame => {
|
|
37
|
-
const hue = seatHue(account);
|
|
38
|
-
const cx = 16 + (n * 9) % 128;
|
|
39
|
-
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="160" height="90">` +
|
|
40
|
-
`<rect width="160" height="90" fill="hsl(${hue},65%,42%)"/>` +
|
|
41
|
-
`<circle cx="${cx}" cy="${24 + (n * 5) % 44}" r="7" fill="#fff" opacity="0.9"/>` +
|
|
42
|
-
`<text x="8" y="82" font-size="12" font-family="monospace" fill="#fff">${account} #${n}</text></svg>`;
|
|
43
|
-
return {n, at: Date.now(), image: "data:image/svg+xml," + encodeURIComponent(svg)};
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
async function main() {
|
|
47
|
-
document.title = `conf seat ${me}`;
|
|
48
|
-
el("who").textContent = `seat ${me}${isHost ? " (host)" : ""} · room "${room}" · peers: ${peers.join(", ")}`;
|
|
49
|
-
|
|
50
|
-
const hub = createRpcClientHub(
|
|
51
|
-
() => io(serverUrl, {transports: ["websocket"], auth: {account: me}}),
|
|
52
|
-
r => ({app: r<any>("app")}) as const,
|
|
53
|
-
);
|
|
54
|
-
const clients = await hub.setToken(null);
|
|
55
|
-
await clients.app.readyStrict();
|
|
56
|
-
const deep: any = clients.app.func;
|
|
57
|
-
log("connected; server time " + await deep.serverTime());
|
|
58
|
-
|
|
59
|
-
// ============== calls: host-star membership over the server policy ==============
|
|
60
|
-
const manager = Peer.createCallManager({port: Peer.callPortOf(deep.peer), self: me});
|
|
61
|
-
const live = new Map<string, any>();
|
|
62
|
-
const controls = el("callControls");
|
|
63
|
-
function renderCalls() {
|
|
64
|
-
controls.innerHTML = "";
|
|
65
|
-
const inRoom = [...live.values()].some(call => call.state() === "active");
|
|
66
|
-
const state = document.createElement("b");
|
|
67
|
-
state.textContent = inRoom ? "in room" : "not in room";
|
|
68
|
-
controls.append(state);
|
|
69
|
-
if (isHost) {
|
|
70
|
-
for (const peer of peers) {
|
|
71
|
-
const current = [...live.values()].find(call => call.peer === peer && call.state() !== "ended");
|
|
72
|
-
const button = document.createElement("button");
|
|
73
|
-
if (!current) {
|
|
74
|
-
button.textContent = `ring ${peer}`;
|
|
75
|
-
button.onclick = () => bindCall(manager.call(peer, {room}));
|
|
76
|
-
} else if (current.state() === "ringing") {
|
|
77
|
-
button.textContent = `cancel ${peer}…`;
|
|
78
|
-
button.onclick = () => current.hangup();
|
|
79
|
-
} else {
|
|
80
|
-
button.textContent = `hang up ${peer}`;
|
|
81
|
-
button.onclick = () => current.hangup();
|
|
82
|
-
}
|
|
83
|
-
controls.append(button);
|
|
84
|
-
}
|
|
85
|
-
} else {
|
|
86
|
-
const incoming = [...live.values()].find(call => call.direction === "in" && call.state() === "ringing");
|
|
87
|
-
if (incoming) {
|
|
88
|
-
const accept = document.createElement("button");
|
|
89
|
-
accept.textContent = `accept ${incoming.peer}`;
|
|
90
|
-
accept.onclick = () => incoming.accept();
|
|
91
|
-
const decline = document.createElement("button");
|
|
92
|
-
decline.textContent = "decline";
|
|
93
|
-
decline.onclick = () => incoming.decline();
|
|
94
|
-
controls.append(accept, decline);
|
|
95
|
-
}
|
|
96
|
-
const active = [...live.values()].find(call => call.state() === "active");
|
|
97
|
-
if (active) {
|
|
98
|
-
const leave = document.createElement("button");
|
|
99
|
-
leave.textContent = "leave room";
|
|
100
|
-
leave.onclick = () => active.hangup();
|
|
101
|
-
controls.append(leave);
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
function bindCall(call: any) {
|
|
106
|
-
live.set(call.id, call);
|
|
107
|
-
call.changed.on((state: string) => { log(`call ${call.peer}: ${state}${call.reason() ? ` (${call.reason()})` : ""}`); renderCalls(); });
|
|
108
|
-
void call.ended.then(() => { live.delete(call.id); renderCalls(); });
|
|
109
|
-
renderCalls();
|
|
110
|
-
}
|
|
111
|
-
manager.rings.on(bindCall);
|
|
112
|
-
await manager.ready;
|
|
113
|
-
renderCalls();
|
|
114
|
-
const amInRoom = () => [...live.values()].some(call => call.state() === "active");
|
|
115
|
-
|
|
116
|
-
// ============== technology 1: media grid through the server relay ==============
|
|
117
|
-
let frameN = 0;
|
|
118
|
-
setInterval(function publishSyntheticCam() {
|
|
119
|
-
if (!amInRoom()) return; // stream only while in the room (the server ACL gates viewers anyway)
|
|
120
|
-
frameN++;
|
|
121
|
-
const frame = paintFrame(me, frameN);
|
|
122
|
-
void deep.media.publish("cam", frame, frame.at);
|
|
123
|
-
}, 125);
|
|
124
|
-
|
|
125
|
-
const tiles = el("tiles");
|
|
126
|
-
for (const peer of peers) {
|
|
127
|
-
const figure = document.createElement("figure");
|
|
128
|
-
const img = document.createElement("img");
|
|
129
|
-
img.className = "tile";
|
|
130
|
-
const caption = document.createElement("figcaption");
|
|
131
|
-
caption.textContent = `${peer} · waiting`;
|
|
132
|
-
figure.append(img, caption);
|
|
133
|
-
tiles.append(figure);
|
|
134
|
-
// the server's watch proxy resolves per access through canWatch: a subscription
|
|
135
|
-
// made before the room grant lands on nothing, so keep re-attaching until the
|
|
136
|
-
// FIRST FRAME proves the line is live (then stop churning)
|
|
137
|
-
let off: (() => void) | null = null;
|
|
138
|
-
let lastN = 0, lastAt = 0;
|
|
139
|
-
const attach = () => {
|
|
140
|
-
if (lastAt) return;
|
|
141
|
-
try { off?.(); } catch { /* stale RPC handle */ }
|
|
142
|
-
off = null;
|
|
143
|
-
try {
|
|
144
|
-
const line = deep.media.watch?.[peer]?.cam;
|
|
145
|
-
if (!line?.on) return;
|
|
146
|
-
off = line.on((frame: ConfFrame) => {
|
|
147
|
-
if (!lastAt) log(`watching ${peer} through the server relay`);
|
|
148
|
-
lastN = frame.n; lastAt = frame.at;
|
|
149
|
-
img.src = frame.image;
|
|
150
|
-
});
|
|
151
|
-
} catch (error) { log(`watch ${peer} failed: ${error}`); }
|
|
152
|
-
};
|
|
153
|
-
setInterval(attach, 1500);
|
|
154
|
-
setInterval(() => {
|
|
155
|
-
const age = lastAt ? Math.round((Date.now() - lastAt) / 100) / 10 : 0;
|
|
156
|
-
caption.textContent = `${peer} · #${lastN}${lastAt ? ` · ${age}s ago` : " · waiting"}`;
|
|
157
|
-
}, 500);
|
|
158
|
-
attach();
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// ============== technology 2: peer-store links, relay <-> direct per pair ==============
|
|
162
|
-
const client = Peer.createPeerClient<SeatWorld>({
|
|
163
|
-
remote: deep.peer,
|
|
164
|
-
account: me,
|
|
165
|
-
initial: {name: me, hue: seatHue(me), beat: 0, status: ""},
|
|
166
|
-
rtc: () => new RTCPeerConnection(),
|
|
167
|
-
drain: "micro",
|
|
168
|
-
});
|
|
169
|
-
client.onRoute((event: any) => log(`route ${event.key}: ${event.from} -> ${event.to}${event.reason ? ` (${String(event.reason)})` : ""}`));
|
|
170
|
-
setInterval(function heartbeat() { client.store.state.beat++; }, 1000);
|
|
171
|
-
(el("status") as HTMLInputElement).addEventListener("input", function onStatusInput(event) {
|
|
172
|
-
client.store.state.status = (event.target as HTMLInputElement).value;
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
const peersBox = el("peers");
|
|
176
|
-
for (const account of peers) {
|
|
177
|
-
const link = client.peer(account);
|
|
178
|
-
const row = document.createElement("div");
|
|
179
|
-
row.className = "peerRow";
|
|
180
|
-
const title = document.createElement("b");
|
|
181
|
-
const chip = document.createElement("span");
|
|
182
|
-
const info = document.createElement("span");
|
|
183
|
-
info.className = "fine";
|
|
184
|
-
const direct = document.createElement("button");
|
|
185
|
-
direct.textContent = "go direct";
|
|
186
|
-
direct.onclick = async () => {
|
|
187
|
-
log(`promoteDirect ${account}…`);
|
|
188
|
-
const result = await link.promoteDirect({timeoutMs: 8000});
|
|
189
|
-
log(result.ok ? `direct with ${account}: ok (${result.state})` : `direct with ${account} denied: ${String(result.reason)}`);
|
|
190
|
-
};
|
|
191
|
-
const relay = document.createElement("button");
|
|
192
|
-
relay.textContent = "back to relay";
|
|
193
|
-
relay.onclick = async () => {
|
|
194
|
-
const result = await link.reinterposeRelay("manual");
|
|
195
|
-
log(result.ok ? `relay with ${account}` : `re-interpose ${account} failed: ${String(result.reason)}`);
|
|
196
|
-
};
|
|
197
|
-
row.append(title, chip, info, direct, relay);
|
|
198
|
-
peersBox.append(row);
|
|
199
|
-
setInterval(function renderPeerRow() {
|
|
200
|
-
const mirrored: any = link.store.state;
|
|
201
|
-
title.textContent = account;
|
|
202
|
-
const route = link.route();
|
|
203
|
-
chip.textContent = `${route} · ${link.state()}`;
|
|
204
|
-
chip.className = "chip" + (route === "direct" ? " direct" : link.state() === "fallback" ? " fallback" : "");
|
|
205
|
-
info.textContent = mirrored
|
|
206
|
-
? `beat ${mirrored.beat} · seq ${link.seq()}${mirrored.status ? ` · "${mirrored.status}"` : ""}`
|
|
207
|
-
: "no mirror yet";
|
|
208
|
-
}, 500);
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
main().catch(error => { console.error(error); log("FATAL: " + error); });
|
|
1
|
+
// Browser seat for conference-server.mjs — the real-backend variant of demo/peer-conference.
|
|
2
|
+
// One tab = one participant. Media tiles ride the SERVER relay (room-gated fan-out);
|
|
3
|
+
// each peer row is a peer-store link that starts on the server relay and can promote to
|
|
4
|
+
// a real RTCPeerConnection datachannel (negotiated through the same server signal hub),
|
|
5
|
+
// then re-interpose back — the two technologies of the conference bridge, cross-tab.
|
|
6
|
+
//
|
|
7
|
+
// Serve this page from the wenay-react2 dev stand (vite transpiles the .ts):
|
|
8
|
+
// /doc/examples/conference-client.html?me=a&peers=b,c&host=1&room=demo
|
|
9
|
+
import {io} from "socket.io-client";
|
|
10
|
+
import {createRpcClientHub, Peer} from "wenay-common2";
|
|
11
|
+
|
|
12
|
+
type SeatWorld = {name: string, hue: number, beat: number, status: string};
|
|
13
|
+
type ConfFrame = {n: number, at: number, image: string};
|
|
14
|
+
|
|
15
|
+
const params = new URLSearchParams(location.search);
|
|
16
|
+
const me = params.get("me") ?? "a";
|
|
17
|
+
const peers = (params.get("peers") ?? (me === "a" ? "b,c" : "a")).split(",").map(value => value.trim()).filter(Boolean);
|
|
18
|
+
const isHost = params.get("host") === "1" || me === "a";
|
|
19
|
+
const room = params.get("room") ?? "demo";
|
|
20
|
+
const serverUrl = params.get("server") ?? `${location.protocol}//${location.hostname}:8391`;
|
|
21
|
+
|
|
22
|
+
const el = (id: string) => document.getElementById(id)!;
|
|
23
|
+
const logBox = el("log");
|
|
24
|
+
function log(line: string) {
|
|
25
|
+
const row = document.createElement("div");
|
|
26
|
+
row.textContent = `${new Date().toLocaleTimeString()} ${line}`;
|
|
27
|
+
logBox.prepend(row);
|
|
28
|
+
while (logBox.children.length > 60) logBox.lastChild?.remove();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const seatHue = (account: string) => {
|
|
32
|
+
let hash = 0;
|
|
33
|
+
for (const ch of account) hash = (hash * 31 + ch.charCodeAt(0)) % 360;
|
|
34
|
+
return hash;
|
|
35
|
+
};
|
|
36
|
+
const paintFrame = (account: string, n: number): ConfFrame => {
|
|
37
|
+
const hue = seatHue(account);
|
|
38
|
+
const cx = 16 + (n * 9) % 128;
|
|
39
|
+
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="160" height="90">` +
|
|
40
|
+
`<rect width="160" height="90" fill="hsl(${hue},65%,42%)"/>` +
|
|
41
|
+
`<circle cx="${cx}" cy="${24 + (n * 5) % 44}" r="7" fill="#fff" opacity="0.9"/>` +
|
|
42
|
+
`<text x="8" y="82" font-size="12" font-family="monospace" fill="#fff">${account} #${n}</text></svg>`;
|
|
43
|
+
return {n, at: Date.now(), image: "data:image/svg+xml," + encodeURIComponent(svg)};
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
async function main() {
|
|
47
|
+
document.title = `conf seat ${me}`;
|
|
48
|
+
el("who").textContent = `seat ${me}${isHost ? " (host)" : ""} · room "${room}" · peers: ${peers.join(", ")}`;
|
|
49
|
+
|
|
50
|
+
const hub = createRpcClientHub(
|
|
51
|
+
() => io(serverUrl, {transports: ["websocket"], auth: {account: me}}),
|
|
52
|
+
r => ({app: r<any>("app")}) as const,
|
|
53
|
+
);
|
|
54
|
+
const clients = await hub.setToken(null);
|
|
55
|
+
await clients.app.readyStrict();
|
|
56
|
+
const deep: any = clients.app.func;
|
|
57
|
+
log("connected; server time " + await deep.serverTime());
|
|
58
|
+
|
|
59
|
+
// ============== calls: host-star membership over the server policy ==============
|
|
60
|
+
const manager = Peer.createCallManager({port: Peer.callPortOf(deep.peer), self: me});
|
|
61
|
+
const live = new Map<string, any>();
|
|
62
|
+
const controls = el("callControls");
|
|
63
|
+
function renderCalls() {
|
|
64
|
+
controls.innerHTML = "";
|
|
65
|
+
const inRoom = [...live.values()].some(call => call.state() === "active");
|
|
66
|
+
const state = document.createElement("b");
|
|
67
|
+
state.textContent = inRoom ? "in room" : "not in room";
|
|
68
|
+
controls.append(state);
|
|
69
|
+
if (isHost) {
|
|
70
|
+
for (const peer of peers) {
|
|
71
|
+
const current = [...live.values()].find(call => call.peer === peer && call.state() !== "ended");
|
|
72
|
+
const button = document.createElement("button");
|
|
73
|
+
if (!current) {
|
|
74
|
+
button.textContent = `ring ${peer}`;
|
|
75
|
+
button.onclick = () => bindCall(manager.call(peer, {room}));
|
|
76
|
+
} else if (current.state() === "ringing") {
|
|
77
|
+
button.textContent = `cancel ${peer}…`;
|
|
78
|
+
button.onclick = () => current.hangup();
|
|
79
|
+
} else {
|
|
80
|
+
button.textContent = `hang up ${peer}`;
|
|
81
|
+
button.onclick = () => current.hangup();
|
|
82
|
+
}
|
|
83
|
+
controls.append(button);
|
|
84
|
+
}
|
|
85
|
+
} else {
|
|
86
|
+
const incoming = [...live.values()].find(call => call.direction === "in" && call.state() === "ringing");
|
|
87
|
+
if (incoming) {
|
|
88
|
+
const accept = document.createElement("button");
|
|
89
|
+
accept.textContent = `accept ${incoming.peer}`;
|
|
90
|
+
accept.onclick = () => incoming.accept();
|
|
91
|
+
const decline = document.createElement("button");
|
|
92
|
+
decline.textContent = "decline";
|
|
93
|
+
decline.onclick = () => incoming.decline();
|
|
94
|
+
controls.append(accept, decline);
|
|
95
|
+
}
|
|
96
|
+
const active = [...live.values()].find(call => call.state() === "active");
|
|
97
|
+
if (active) {
|
|
98
|
+
const leave = document.createElement("button");
|
|
99
|
+
leave.textContent = "leave room";
|
|
100
|
+
leave.onclick = () => active.hangup();
|
|
101
|
+
controls.append(leave);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
function bindCall(call: any) {
|
|
106
|
+
live.set(call.id, call);
|
|
107
|
+
call.changed.on((state: string) => { log(`call ${call.peer}: ${state}${call.reason() ? ` (${call.reason()})` : ""}`); renderCalls(); });
|
|
108
|
+
void call.ended.then(() => { live.delete(call.id); renderCalls(); });
|
|
109
|
+
renderCalls();
|
|
110
|
+
}
|
|
111
|
+
manager.rings.on(bindCall);
|
|
112
|
+
await manager.ready;
|
|
113
|
+
renderCalls();
|
|
114
|
+
const amInRoom = () => [...live.values()].some(call => call.state() === "active");
|
|
115
|
+
|
|
116
|
+
// ============== technology 1: media grid through the server relay ==============
|
|
117
|
+
let frameN = 0;
|
|
118
|
+
setInterval(function publishSyntheticCam() {
|
|
119
|
+
if (!amInRoom()) return; // stream only while in the room (the server ACL gates viewers anyway)
|
|
120
|
+
frameN++;
|
|
121
|
+
const frame = paintFrame(me, frameN);
|
|
122
|
+
void deep.media.publish("cam", frame, frame.at);
|
|
123
|
+
}, 125);
|
|
124
|
+
|
|
125
|
+
const tiles = el("tiles");
|
|
126
|
+
for (const peer of peers) {
|
|
127
|
+
const figure = document.createElement("figure");
|
|
128
|
+
const img = document.createElement("img");
|
|
129
|
+
img.className = "tile";
|
|
130
|
+
const caption = document.createElement("figcaption");
|
|
131
|
+
caption.textContent = `${peer} · waiting`;
|
|
132
|
+
figure.append(img, caption);
|
|
133
|
+
tiles.append(figure);
|
|
134
|
+
// the server's watch proxy resolves per access through canWatch: a subscription
|
|
135
|
+
// made before the room grant lands on nothing, so keep re-attaching until the
|
|
136
|
+
// FIRST FRAME proves the line is live (then stop churning)
|
|
137
|
+
let off: (() => void) | null = null;
|
|
138
|
+
let lastN = 0, lastAt = 0;
|
|
139
|
+
const attach = () => {
|
|
140
|
+
if (lastAt) return;
|
|
141
|
+
try { off?.(); } catch { /* stale RPC handle */ }
|
|
142
|
+
off = null;
|
|
143
|
+
try {
|
|
144
|
+
const line = deep.media.watch?.[peer]?.cam;
|
|
145
|
+
if (!line?.on) return;
|
|
146
|
+
off = line.on((frame: ConfFrame) => {
|
|
147
|
+
if (!lastAt) log(`watching ${peer} through the server relay`);
|
|
148
|
+
lastN = frame.n; lastAt = frame.at;
|
|
149
|
+
img.src = frame.image;
|
|
150
|
+
});
|
|
151
|
+
} catch (error) { log(`watch ${peer} failed: ${error}`); }
|
|
152
|
+
};
|
|
153
|
+
setInterval(attach, 1500);
|
|
154
|
+
setInterval(() => {
|
|
155
|
+
const age = lastAt ? Math.round((Date.now() - lastAt) / 100) / 10 : 0;
|
|
156
|
+
caption.textContent = `${peer} · #${lastN}${lastAt ? ` · ${age}s ago` : " · waiting"}`;
|
|
157
|
+
}, 500);
|
|
158
|
+
attach();
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ============== technology 2: peer-store links, relay <-> direct per pair ==============
|
|
162
|
+
const client = Peer.createPeerClient<SeatWorld>({
|
|
163
|
+
remote: deep.peer,
|
|
164
|
+
account: me,
|
|
165
|
+
initial: {name: me, hue: seatHue(me), beat: 0, status: ""},
|
|
166
|
+
rtc: () => new RTCPeerConnection(),
|
|
167
|
+
drain: "micro",
|
|
168
|
+
});
|
|
169
|
+
client.onRoute((event: any) => log(`route ${event.key}: ${event.from} -> ${event.to}${event.reason ? ` (${String(event.reason)})` : ""}`));
|
|
170
|
+
setInterval(function heartbeat() { client.store.state.beat++; }, 1000);
|
|
171
|
+
(el("status") as HTMLInputElement).addEventListener("input", function onStatusInput(event) {
|
|
172
|
+
client.store.state.status = (event.target as HTMLInputElement).value;
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
const peersBox = el("peers");
|
|
176
|
+
for (const account of peers) {
|
|
177
|
+
const link = client.peer(account);
|
|
178
|
+
const row = document.createElement("div");
|
|
179
|
+
row.className = "peerRow";
|
|
180
|
+
const title = document.createElement("b");
|
|
181
|
+
const chip = document.createElement("span");
|
|
182
|
+
const info = document.createElement("span");
|
|
183
|
+
info.className = "fine";
|
|
184
|
+
const direct = document.createElement("button");
|
|
185
|
+
direct.textContent = "go direct";
|
|
186
|
+
direct.onclick = async () => {
|
|
187
|
+
log(`promoteDirect ${account}…`);
|
|
188
|
+
const result = await link.promoteDirect({timeoutMs: 8000});
|
|
189
|
+
log(result.ok ? `direct with ${account}: ok (${result.state})` : `direct with ${account} denied: ${String(result.reason)}`);
|
|
190
|
+
};
|
|
191
|
+
const relay = document.createElement("button");
|
|
192
|
+
relay.textContent = "back to relay";
|
|
193
|
+
relay.onclick = async () => {
|
|
194
|
+
const result = await link.reinterposeRelay("manual");
|
|
195
|
+
log(result.ok ? `relay with ${account}` : `re-interpose ${account} failed: ${String(result.reason)}`);
|
|
196
|
+
};
|
|
197
|
+
row.append(title, chip, info, direct, relay);
|
|
198
|
+
peersBox.append(row);
|
|
199
|
+
setInterval(function renderPeerRow() {
|
|
200
|
+
const mirrored: any = link.store.state;
|
|
201
|
+
title.textContent = account;
|
|
202
|
+
const route = link.route();
|
|
203
|
+
chip.textContent = `${route} · ${link.state()}`;
|
|
204
|
+
chip.className = "chip" + (route === "direct" ? " direct" : link.state() === "fallback" ? " fallback" : "");
|
|
205
|
+
info.textContent = mirrored
|
|
206
|
+
? `beat ${mirrored.beat} · seq ${link.seq()}${mirrored.status ? ` · "${mirrored.status}"` : ""}`
|
|
207
|
+
: "no mirror yet";
|
|
208
|
+
}, 500);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
main().catch(error => { console.error(error); log("FATAL: " + error); });
|