Skip to content

Commit f76acbb

Browse files
kixelatedclaude
andauthored
moq-boy: Review+revamp JS player (#1224)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 7c12495 commit f76acbb

29 files changed

Lines changed: 1426 additions & 1034 deletions

bun.lock

Lines changed: 28 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

demo/boy/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
"devDependencies": {
1515
"esbuild": "^0.27.0",
1616
"typescript": "^6.0.0",
17-
"vite": "^7.3.1"
17+
"vite": "^7.3.1",
18+
"vite-plugin-solid": "^2.11.10"
1819
}
1920
}

demo/boy/src/index.html

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,62 @@
66
<title>MoQ Boy</title>
77
<style>
88
* { margin: 0; padding: 0; box-sizing: border-box; }
9-
body { font-family: system-ui, sans-serif; background: #0a0a0a; color: #e0e0e0; }
9+
body {
10+
font-family: system-ui, sans-serif;
11+
color-scheme: light dark;
12+
min-height: 100vh;
13+
display: flex;
14+
flex-direction: column;
15+
}
16+
17+
header {
18+
border-bottom: 1px solid light-dark(#ddd, #333);
19+
padding: 0.75rem 1.5rem;
20+
display: flex;
21+
align-items: center;
22+
gap: 1rem;
23+
}
24+
header h1 { font-size: 1.1rem; font-weight: 600; color: #8bac0f; }
25+
26+
.about {
27+
max-width: 500px;
28+
margin: 1.5rem auto;
29+
padding: 0 1.5rem;
30+
font-size: 0.75rem;
31+
color: light-dark(#777, #555);
32+
line-height: 1.7;
33+
}
34+
.about p { margin-bottom: 0.5rem; }
35+
.about a { color: #8bac0f; text-decoration: none; }
36+
.about a:hover { text-decoration: underline; }
37+
.about ul { list-style: none; padding: 0; margin: 0; }
38+
.about li { padding-left: 1rem; position: relative; }
39+
.about li::before { content: "\2022"; color: #8bac0f; position: absolute; left: 0; }
40+
41+
moq-boy-ui { flex: 1; display: flex; flex-direction: column; }
1042
</style>
1143
</head>
1244
<body>
45+
<header>
46+
<h1>MoQ Boy</h1>
47+
</header>
48+
49+
<moq-boy-ui>
50+
<moq-boy></moq-boy>
51+
</moq-boy-ui>
52+
53+
<div class="about">
54+
<p>Click a game to play. Multiple players can control it. (anarchy!)</p>
55+
<p>
56+
A generic <a href="https://moq.dev">MoQ</a> relay is used for everything:
57+
</p>
58+
<ul>
59+
<li>Discovering online games and players.</li>
60+
<li>Transmitting audio/video tracks, metadata, and player controls.</li>
61+
<li>Pausing emulation/encoding when there are no subscribers.</li>
62+
</ul>
63+
</div>
64+
1365
<script type="module" src="./index.ts"></script>
1466
</body>
1567
</html>

demo/boy/src/index.ts

Lines changed: 4 additions & 132 deletions
Original file line numberDiff line numberDiff line change
@@ -1,135 +1,7 @@
1-
import { GameCard, Moq } from "@moq/boy";
2-
import { gridStyles } from "@moq/boy/styles";
1+
import "@moq/boy/element";
2+
import "@moq/boy/ui";
33

4-
// Inject styles into the document.
5-
const style = document.createElement("style");
6-
style.textContent = gridStyles;
7-
document.head.appendChild(style);
8-
9-
// Header.
10-
const header = document.createElement("header");
11-
const h1 = document.createElement("h1");
12-
h1.textContent = "MoQ Boy";
13-
const statusEl = document.createElement("span");
14-
statusEl.className = "status";
15-
statusEl.textContent = "Disconnected";
16-
header.appendChild(h1);
17-
header.appendChild(statusEl);
18-
document.body.appendChild(header);
19-
20-
// Grid.
21-
const gridEl = document.createElement("div");
22-
gridEl.className = "grid";
23-
document.body.appendChild(gridEl);
24-
25-
// Empty state.
26-
const emptyState = document.createElement("div");
27-
emptyState.className = "empty-state";
28-
29-
const emptyIcon = document.createElement("div");
30-
emptyIcon.className = "icon";
31-
emptyIcon.textContent = "\u{1F3AE}";
32-
emptyState.appendChild(emptyIcon);
33-
34-
const emptyMsg = document.createElement("div");
35-
emptyMsg.className = "msg";
36-
emptyMsg.textContent = "No games online";
37-
emptyState.appendChild(emptyMsg);
38-
39-
const emptyHint = document.createElement("div");
40-
emptyHint.className = "hint";
41-
emptyHint.textContent = "Waiting for Game Boy sessions to connect...";
42-
emptyState.appendChild(emptyHint);
43-
44-
gridEl.appendChild(emptyState);
45-
46-
// About section.
47-
const about = document.createElement("div");
48-
about.className = "about";
49-
50-
const aboutP1 = document.createElement("p");
51-
aboutP1.textContent = "Click a game to play. Everyone controls the same game (anarchy mode).";
52-
about.appendChild(aboutP1);
53-
54-
const aboutP2 = document.createElement("p");
55-
aboutP2.textContent = "A generic ";
56-
const moqLink = document.createElement("a");
57-
moqLink.href = "https://moq.dev";
58-
moqLink.textContent = "MoQ";
59-
aboutP2.appendChild(moqLink);
60-
aboutP2.appendChild(document.createTextNode(" relay is used for everything:"));
61-
about.appendChild(aboutP2);
62-
63-
const aboutUl = document.createElement("ul");
64-
for (const text of [
65-
"Discovering online games and players.",
66-
"Transmitting audio/video tracks, metadata, and (multiple) player controls.",
67-
"Subscribing to audio/video on-demand.",
68-
"Pausing emulation/encoding when there are no subscribers.",
69-
]) {
70-
const li = document.createElement("li");
71-
li.textContent = text;
72-
aboutUl.appendChild(li);
73-
}
74-
about.appendChild(aboutUl);
75-
document.body.appendChild(about);
76-
77-
// Connection.
784
const url = import.meta.env.VITE_RELAY_URL || "http://localhost:4443/anon";
79-
const enabled = new Moq.Signals.Signal(true);
80-
const connection = new Moq.Connection.Reload({ url: new URL(url), enabled });
81-
82-
const signals = new Moq.Signals.Effect();
83-
const sessions = new Map<string, GameCard>();
84-
const expanded = new Moq.Signals.Signal<string | undefined>(undefined);
85-
86-
function updateEmptyState() {
87-
emptyState.style.display = sessions.size === 0 ? "block" : "none";
88-
}
89-
90-
// Track connection status.
91-
signals.run((e) => {
92-
const status = e.get(connection.status);
93-
statusEl.textContent = status.charAt(0).toUpperCase() + status.slice(1);
94-
statusEl.style.color = status === "connected" ? "#8bac0f" : status === "connecting" ? "#facc15" : "#888";
95-
});
96-
97-
// Discover game sessions via announcements.
98-
signals.run((effect) => {
99-
const conn = effect.get(connection.established);
100-
if (!conn) return;
101-
102-
const announced = conn.announced(Moq.Path.from("boy"));
103-
effect.cleanup(() => announced.close());
104-
105-
effect.spawn(async () => {
106-
for (;;) {
107-
const entry = await Promise.race([effect.cancel, announced.next()]);
108-
if (!entry) break;
109-
110-
const suffix = Moq.Path.stripPrefix(Moq.Path.from("boy"), entry.path);
111-
if (!suffix || suffix.includes("/")) continue;
1125

113-
const id = suffix;
114-
if (entry.active && !sessions.has(id)) {
115-
const card = new GameCard({
116-
sessionId: id,
117-
connection,
118-
expanded,
119-
root: document.body,
120-
});
121-
sessions.set(id, card);
122-
gridEl.appendChild(card.el);
123-
updateEmptyState();
124-
} else if (!entry.active) {
125-
const card = sessions.get(id);
126-
if (card) {
127-
card.close();
128-
card.el.remove();
129-
sessions.delete(id);
130-
updateEmptyState();
131-
}
132-
}
133-
}
134-
});
135-
});
6+
const boy = document.querySelector("moq-boy");
7+
if (boy) boy.url = url;

demo/boy/vite.config.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import { resolve } from "path";
22
import { defineConfig } from "vite";
3+
import solidPlugin from "vite-plugin-solid";
34
import { workletInline } from "../../js/common/vite-plugin-worklet";
45

56
export default defineConfig({
67
root: "src",
78
envDir: resolve(__dirname),
8-
plugins: [workletInline()],
9+
plugins: [solidPlugin(), workletInline()],
910
build: {
1011
target: "esnext",
1112
rollupOptions: {

0 commit comments

Comments
 (0)