onelobby 0.0.0-stage → 0.2.0
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/LICENSE +21 -0
- package/README.md +77 -2
- package/dist/chat.d.ts +60 -0
- package/dist/chat.js +163 -0
- package/dist/chat.js.map +1 -0
- package/dist/chunking.d.ts +17 -0
- package/dist/chunking.js +59 -0
- package/dist/chunking.js.map +1 -0
- package/dist/clock.d.ts +8 -0
- package/dist/clock.js +39 -0
- package/dist/clock.js.map +1 -0
- package/dist/codec.d.ts +141 -0
- package/dist/codec.js +308 -0
- package/dist/codec.js.map +1 -0
- package/dist/crockford.d.ts +14 -0
- package/dist/crockford.js +55 -0
- package/dist/crockford.js.map +1 -0
- package/dist/deflate.d.ts +4 -0
- package/dist/deflate.js +32 -0
- package/dist/deflate.js.map +1 -0
- package/dist/deprecated.d.ts +18 -0
- package/dist/deprecated.js +11 -0
- package/dist/deprecated.js.map +1 -0
- package/dist/direct/hub-host.d.ts +82 -0
- package/dist/direct/hub-host.js +393 -0
- package/dist/direct/hub-host.js.map +1 -0
- package/dist/direct/hub.d.ts +178 -0
- package/dist/direct/hub.js +862 -0
- package/dist/direct/hub.js.map +1 -0
- package/dist/direct/index.d.ts +57 -0
- package/dist/direct/index.js +71 -0
- package/dist/direct/index.js.map +1 -0
- package/dist/direct/portmap.d.ts +84 -0
- package/dist/direct/portmap.js +465 -0
- package/dist/direct/portmap.js.map +1 -0
- package/dist/direct-address.d.ts +20 -0
- package/dist/direct-address.js +61 -0
- package/dist/direct-address.js.map +1 -0
- package/dist/emitter.d.ts +9 -0
- package/dist/emitter.js +36 -0
- package/dist/emitter.js.map +1 -0
- package/dist/friendly.d.ts +20 -0
- package/dist/friendly.js +120 -0
- package/dist/friendly.js.map +1 -0
- package/dist/global.d.ts +8 -0
- package/dist/global.js +9 -0
- package/dist/global.js.map +1 -0
- package/dist/gridpaint.d.ts +27 -0
- package/dist/gridpaint.js +106 -0
- package/dist/gridpaint.js.map +1 -0
- package/dist/hub.d.ts +25 -0
- package/dist/hub.js +2 -0
- package/dist/hub.js.map +1 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.js +25 -0
- package/dist/index.js.map +1 -0
- package/dist/internals.d.ts +28 -0
- package/dist/internals.js +24 -0
- package/dist/internals.js.map +1 -0
- package/dist/joincode.d.ts +21 -0
- package/dist/joincode.js +84 -0
- package/dist/joincode.js.map +1 -0
- package/dist/lobby.d.ts +203 -0
- package/dist/lobby.js +840 -0
- package/dist/lobby.js.map +1 -0
- package/dist/lockstep.d.ts +132 -0
- package/dist/lockstep.js +471 -0
- package/dist/lockstep.js.map +1 -0
- package/dist/migration.d.ts +17 -0
- package/dist/migration.js +34 -0
- package/dist/migration.js.map +1 -0
- package/dist/onelobby.bundle.mjs +140 -0
- package/dist/onelobby.bundle.mjs.map +7 -0
- package/dist/onelobby.d.ts +151 -0
- package/dist/onelobby.global.js +3897 -0
- package/dist/onelobby.global.js.map +7 -0
- package/dist/onelobby.global.min.js +140 -0
- package/dist/onelobby.global.min.js.map +7 -0
- package/dist/onelobby.js +337 -0
- package/dist/onelobby.js.map +1 -0
- package/dist/signaling.d.ts +94 -0
- package/dist/signaling.js +134 -0
- package/dist/signaling.js.map +1 -0
- package/dist/socket.d.ts +63 -0
- package/dist/socket.js +142 -0
- package/dist/socket.js.map +1 -0
- package/dist/testing.d.ts +135 -0
- package/dist/testing.js +438 -0
- package/dist/testing.js.map +1 -0
- package/dist/transport.d.ts +80 -0
- package/dist/transport.js +7 -0
- package/dist/transport.js.map +1 -0
- package/dist/ui/controller.d.ts +137 -0
- package/dist/ui/controller.js +337 -0
- package/dist/ui/controller.js.map +1 -0
- package/dist/ui/element.d.ts +22 -0
- package/dist/ui/element.js +284 -0
- package/dist/ui/element.js.map +1 -0
- package/dist/ui/index.d.ts +5 -0
- package/dist/ui/index.js +9 -0
- package/dist/ui/index.js.map +1 -0
- package/dist/ui/server-url.d.ts +9 -0
- package/dist/ui/server-url.js +39 -0
- package/dist/ui/server-url.js.map +1 -0
- package/dist/ui/styles.d.ts +2 -0
- package/dist/ui/styles.js +70 -0
- package/dist/ui/styles.js.map +1 -0
- package/dist/voice-browser.d.ts +19 -0
- package/dist/voice-browser.js +151 -0
- package/dist/voice-browser.js.map +1 -0
- package/dist/voice-core.d.ts +98 -0
- package/dist/voice-core.js +233 -0
- package/dist/voice-core.js.map +1 -0
- package/dist/voice-types.d.ts +35 -0
- package/dist/voice-types.js +3 -0
- package/dist/voice-types.js.map +1 -0
- package/dist/voice.d.ts +68 -0
- package/dist/voice.js +223 -0
- package/dist/voice.js.map +1 -0
- package/dist/webrtc.d.ts +29 -0
- package/dist/webrtc.js +169 -0
- package/dist/webrtc.js.map +1 -0
- package/package.json +93 -4
package/dist/voice.js
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { Emitter } from "./emitter.js";
|
|
2
|
+
import { FLAG_END_OF_BURST, FRAME_MS, JitterBuffer, VoiceGate, VoicePacketizer, VoiceRelayPolicy, rms, } from "./voice-core.js";
|
|
3
|
+
const SPEAKING_TIMEOUT_MS = 300;
|
|
4
|
+
/** Voice chat (DESIGN s.9a): Opus frames on the `voice` channel, star-routed through the host, never in a frame. */
|
|
5
|
+
export class Voice extends Emitter {
|
|
6
|
+
hub;
|
|
7
|
+
deps;
|
|
8
|
+
mode = "pushToTalk";
|
|
9
|
+
/** Master mute for capture (self). */
|
|
10
|
+
muted = false;
|
|
11
|
+
/** 0..1 RMS of the last captured frame. */
|
|
12
|
+
inputLevel = 0;
|
|
13
|
+
bitrate = 24000;
|
|
14
|
+
_enabled = false;
|
|
15
|
+
gate = new VoiceGate();
|
|
16
|
+
packetizer = new VoicePacketizer();
|
|
17
|
+
relay = new VoiceRelayPolicy();
|
|
18
|
+
speakers = new Map();
|
|
19
|
+
peerMuted = new Set();
|
|
20
|
+
gains = new Map();
|
|
21
|
+
encoder = null;
|
|
22
|
+
source = null;
|
|
23
|
+
sink = null;
|
|
24
|
+
cancelPlayout = null;
|
|
25
|
+
endTimer = null;
|
|
26
|
+
sending = false;
|
|
27
|
+
constructor(hub, deps) {
|
|
28
|
+
super();
|
|
29
|
+
this.hub = hub;
|
|
30
|
+
this.deps = deps;
|
|
31
|
+
}
|
|
32
|
+
get enabled() {
|
|
33
|
+
return this._enabled;
|
|
34
|
+
}
|
|
35
|
+
set enabled(v) {
|
|
36
|
+
if (v === this._enabled)
|
|
37
|
+
return;
|
|
38
|
+
this._enabled = v;
|
|
39
|
+
if (v)
|
|
40
|
+
void this.start();
|
|
41
|
+
else
|
|
42
|
+
this.stop();
|
|
43
|
+
}
|
|
44
|
+
pushToTalk(down) {
|
|
45
|
+
this.gate.ptt = down;
|
|
46
|
+
}
|
|
47
|
+
isMutedPeer(peerId) {
|
|
48
|
+
return this.peerMuted.has(peerId);
|
|
49
|
+
}
|
|
50
|
+
setMuted(peerId, muted) {
|
|
51
|
+
if (muted)
|
|
52
|
+
this.peerMuted.add(peerId);
|
|
53
|
+
else
|
|
54
|
+
this.peerMuted.delete(peerId);
|
|
55
|
+
}
|
|
56
|
+
setVolume(peerId, gain) {
|
|
57
|
+
this.gains.set(peerId, Math.min(2, Math.max(0, gain)));
|
|
58
|
+
}
|
|
59
|
+
volumeOf(peerId) {
|
|
60
|
+
return this.gains.get(peerId) ?? 1;
|
|
61
|
+
}
|
|
62
|
+
// ---- capture -------------------------------------------------------------------------------------------------
|
|
63
|
+
async start() {
|
|
64
|
+
try {
|
|
65
|
+
if (!this.deps.codec)
|
|
66
|
+
throw new Error("no Opus codec available: supply one through the voice.codec option (WASM fallback)");
|
|
67
|
+
if (!this.deps.createSink)
|
|
68
|
+
throw new Error("no audio output available");
|
|
69
|
+
this.sink = this.deps.createSink();
|
|
70
|
+
this.cancelPlayout = this.hub.clock.setInterval(() => this.playoutTick(), FRAME_MS);
|
|
71
|
+
if (this.deps.createSource) {
|
|
72
|
+
this.encoder = this.deps.codec.createEncoder({ bitrate: this.bitrate }, (opus) => this.onEncoded(opus));
|
|
73
|
+
this.source = this.deps.createSource();
|
|
74
|
+
await this.source.start((pcm) => this.onCapturedFrame(pcm, this.hub.clock.now()));
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
catch (e) {
|
|
78
|
+
this._enabled = false;
|
|
79
|
+
this.stop();
|
|
80
|
+
this.emit("error", { message: e instanceof Error ? e.message : String(e), cause: e });
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
stop() {
|
|
84
|
+
if (this.sending)
|
|
85
|
+
this.closeBurst();
|
|
86
|
+
this.source?.stop();
|
|
87
|
+
this.source = null;
|
|
88
|
+
this.encoder?.close();
|
|
89
|
+
this.encoder = null;
|
|
90
|
+
this.cancelPlayout?.();
|
|
91
|
+
this.cancelPlayout = null;
|
|
92
|
+
this.sink?.close();
|
|
93
|
+
this.sink = null;
|
|
94
|
+
for (const [id, s] of this.speakers) {
|
|
95
|
+
s.decoder?.close();
|
|
96
|
+
if (s.talking)
|
|
97
|
+
this.emit("speaking", id, false);
|
|
98
|
+
}
|
|
99
|
+
this.speakers.clear();
|
|
100
|
+
}
|
|
101
|
+
/** Feed one 20 ms mono 48 kHz frame (called by the mic source; exposed so tests and custom sources can drive it). */
|
|
102
|
+
onCapturedFrame(pcm, nowMs) {
|
|
103
|
+
const level = rms(pcm);
|
|
104
|
+
this.inputLevel = level;
|
|
105
|
+
if (!this._enabled || this.muted) {
|
|
106
|
+
if (this.sending)
|
|
107
|
+
this.closeBurst();
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const g = this.gate.update(level, nowMs);
|
|
111
|
+
if (g.send) {
|
|
112
|
+
this.sending = true;
|
|
113
|
+
this.endTimer?.();
|
|
114
|
+
this.endTimer = null;
|
|
115
|
+
this.encoder?.encode(pcm);
|
|
116
|
+
}
|
|
117
|
+
else if (g.edge === "close" && this.sending) {
|
|
118
|
+
this.closeBurst();
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
closeBurst() {
|
|
122
|
+
this.sending = false;
|
|
123
|
+
// Encoders can emit asynchronously (WebCodecs); give them one packet's worth of time before the end flag.
|
|
124
|
+
this.endTimer?.();
|
|
125
|
+
this.endTimer = this.hub.clock.setTimeout(() => {
|
|
126
|
+
this.endTimer = null;
|
|
127
|
+
this.sendPacket(this.packetizer.endBurst());
|
|
128
|
+
}, FRAME_MS * 2);
|
|
129
|
+
}
|
|
130
|
+
/** Send already-encoded Opus frames as one packet (no codec involved); used by tools and conformance. */
|
|
131
|
+
sendEncoded(frames, endOfBurst = false) {
|
|
132
|
+
this.sendPacket(this.packetizer.packet(frames, endOfBurst ? FLAG_END_OF_BURST : 0));
|
|
133
|
+
}
|
|
134
|
+
/** Encoded frame from the encoder (also the test entry point for packetization). */
|
|
135
|
+
onEncoded(opus) {
|
|
136
|
+
const p = this.packetizer.push(opus);
|
|
137
|
+
if (p)
|
|
138
|
+
this.sendPacket(p);
|
|
139
|
+
}
|
|
140
|
+
sendPacket(p) {
|
|
141
|
+
const msg = { type: "voice", seq: p.seq, flags: p.flags, frames: p.frames };
|
|
142
|
+
if (this.hub.isHost) {
|
|
143
|
+
for (const peer of this.hub.connectedPeers())
|
|
144
|
+
this.hub.sendTo(peer, msg, this.hub.localPeerId, "voice");
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
this.hub.sendToHost(msg, 0, "voice");
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
// ---- receive -------------------------------------------------------------------------------------------------
|
|
151
|
+
handle(from, route, msg) {
|
|
152
|
+
if (msg.type !== "voice")
|
|
153
|
+
return;
|
|
154
|
+
const size = msg.frames.reduce((a, f) => a + f.length + 2, 0) + 7; // whole wire frame: header 3 + seq/flags/n 4
|
|
155
|
+
const now = this.hub.clock.now();
|
|
156
|
+
let speaker = route;
|
|
157
|
+
if (this.hub.isHost) {
|
|
158
|
+
speaker = from;
|
|
159
|
+
if (!this.relay.accept(from, size, msg.flags, now))
|
|
160
|
+
return;
|
|
161
|
+
for (const peer of this.hub.connectedPeers()) {
|
|
162
|
+
if (peer !== from)
|
|
163
|
+
this.hub.sendTo(peer, msg, from, "voice");
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
this.emit("packet", speaker, msg.seq, msg.flags, msg.frames);
|
|
167
|
+
if (!this._enabled || speaker === this.hub.localPeerId || this.peerMuted.has(speaker))
|
|
168
|
+
return;
|
|
169
|
+
this.receivePacket(speaker, { seq: msg.seq, flags: msg.flags, frames: msg.frames }, now);
|
|
170
|
+
}
|
|
171
|
+
/** Local playback path for one speaker's packet (after host forwarding). */
|
|
172
|
+
receivePacket(speaker, p, now) {
|
|
173
|
+
let s = this.speakers.get(speaker);
|
|
174
|
+
if (!s) {
|
|
175
|
+
s = { jitter: new JitterBuffer(), decoder: null, talking: false, lastHeard: now };
|
|
176
|
+
this.speakers.set(speaker, s);
|
|
177
|
+
}
|
|
178
|
+
s.lastHeard = now;
|
|
179
|
+
s.jitter.push(p);
|
|
180
|
+
if (!s.talking && p.frames.length > 0) {
|
|
181
|
+
s.talking = true;
|
|
182
|
+
this.emit("speaking", speaker, true);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
playoutTick() {
|
|
186
|
+
const now = this.hub.clock.now();
|
|
187
|
+
for (const [id, s] of this.speakers) {
|
|
188
|
+
const r = s.jitter.pull();
|
|
189
|
+
if (r.kind === "frame" || r.kind === "plc") {
|
|
190
|
+
if (!s.decoder && this.deps.codec) {
|
|
191
|
+
s.decoder = this.deps.codec.createDecoder((pcm) => this.sink?.play(id, pcm, this.volumeOf(id)));
|
|
192
|
+
}
|
|
193
|
+
s.decoder?.decode(r.kind === "frame" ? r.opus : null);
|
|
194
|
+
}
|
|
195
|
+
else if (r.kind === "idle" && s.talking) {
|
|
196
|
+
s.talking = false;
|
|
197
|
+
this.emit("speaking", id, false);
|
|
198
|
+
}
|
|
199
|
+
else if (s.talking && now - s.lastHeard > SPEAKING_TIMEOUT_MS + 5 * FRAME_MS && !s.jitter.active) {
|
|
200
|
+
s.talking = false;
|
|
201
|
+
this.emit("speaking", id, false);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
onPeerLeft(peerId) {
|
|
206
|
+
this.relay.forget(peerId);
|
|
207
|
+
const s = this.speakers.get(peerId);
|
|
208
|
+
if (s) {
|
|
209
|
+
s.decoder?.close();
|
|
210
|
+
if (s.talking)
|
|
211
|
+
this.emit("speaking", peerId, false);
|
|
212
|
+
this.speakers.delete(peerId);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
close() {
|
|
216
|
+
this._enabled = false;
|
|
217
|
+
this.stop();
|
|
218
|
+
this.endTimer?.();
|
|
219
|
+
}
|
|
220
|
+
/** For tests: expose the end-of-burst flag constant. */
|
|
221
|
+
static END = FLAG_END_OF_BURST;
|
|
222
|
+
}
|
|
223
|
+
//# sourceMappingURL=voice.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"voice.js","sourceRoot":"","sources":["../src/voice.ts"],"names":[],"mappings":"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","sourcesContent":["import type { VoiceError } from \"./voice-types.js\";\nimport { Emitter } from \"./emitter.js\";\nimport type { Hub } from \"./hub.js\";\nimport type { Message } from \"./codec.js\";\nimport {\n FLAG_END_OF_BURST,\n FRAME_MS,\n JitterBuffer,\n VoiceGate,\n VoicePacketizer,\n VoiceRelayPolicy,\n rms,\n type VoiceMode,\n type VoicePacket,\n} from \"./voice-core.js\";\nimport type { AudioSink, FrameSource, VoiceCodec, VoiceDecoderHandle, VoiceEncoderHandle } from \"./voice-types.js\";\n\nexport type VoiceEvents = {\n speaking: [peerId: number, talking: boolean];\n error: [VoiceError];\n /** Every voice packet that reached this peer (after the host's validity checks), before mute/enable filtering. */\n packet: [speaker: number, seq: number, flags: number, frames: Uint8Array[]];\n};\n\ninterface Speaker {\n jitter: JitterBuffer;\n decoder: VoiceDecoderHandle | null;\n talking: boolean;\n lastHeard: number;\n}\n\nexport interface VoiceDeps {\n codec: VoiceCodec | null;\n /** Creates the mic capture; resolved on `enabled = true`. */\n createSource: (() => FrameSource) | null;\n createSink: (() => AudioSink) | null;\n}\n\nconst SPEAKING_TIMEOUT_MS = 300;\n\n/** Voice chat (DESIGN s.9a): Opus frames on the `voice` channel, star-routed through the host, never in a frame. */\nexport class Voice extends Emitter<VoiceEvents> {\n mode: VoiceMode = \"pushToTalk\";\n /** Master mute for capture (self). */\n muted = false;\n /** 0..1 RMS of the last captured frame. */\n inputLevel = 0;\n bitrate = 24000;\n\n private _enabled = false;\n private gate = new VoiceGate();\n private packetizer = new VoicePacketizer();\n private relay = new VoiceRelayPolicy();\n private speakers = new Map<number, Speaker>();\n private peerMuted = new Set<number>();\n private gains = new Map<number, number>();\n private encoder: VoiceEncoderHandle | null = null;\n private source: FrameSource | null = null;\n private sink: AudioSink | null = null;\n private cancelPlayout: (() => void) | null = null;\n private endTimer: (() => void) | null = null;\n private sending = false;\n\n constructor(private hub: Hub, private deps: VoiceDeps) {\n super();\n }\n\n get enabled(): boolean {\n return this._enabled;\n }\n set enabled(v: boolean) {\n if (v === this._enabled) return;\n this._enabled = v;\n if (v) void this.start();\n else this.stop();\n }\n\n pushToTalk(down: boolean): void {\n this.gate.ptt = down;\n }\n\n isMutedPeer(peerId: number): boolean {\n return this.peerMuted.has(peerId);\n }\n setMuted(peerId: number, muted: boolean): void {\n if (muted) this.peerMuted.add(peerId);\n else this.peerMuted.delete(peerId);\n }\n setVolume(peerId: number, gain: number): void {\n this.gains.set(peerId, Math.min(2, Math.max(0, gain)));\n }\n volumeOf(peerId: number): number {\n return this.gains.get(peerId) ?? 1;\n }\n\n // ---- capture -------------------------------------------------------------------------------------------------\n\n private async start(): Promise<void> {\n try {\n if (!this.deps.codec) throw new Error(\"no Opus codec available: supply one through the voice.codec option (WASM fallback)\");\n if (!this.deps.createSink) throw new Error(\"no audio output available\");\n this.sink = this.deps.createSink();\n this.cancelPlayout = this.hub.clock.setInterval(() => this.playoutTick(), FRAME_MS);\n if (this.deps.createSource) {\n this.encoder = this.deps.codec.createEncoder({ bitrate: this.bitrate }, (opus) => this.onEncoded(opus));\n this.source = this.deps.createSource();\n await this.source.start((pcm) => this.onCapturedFrame(pcm, this.hub.clock.now()));\n }\n } catch (e) {\n this._enabled = false;\n this.stop();\n this.emit(\"error\", { message: e instanceof Error ? e.message : String(e), cause: e });\n }\n }\n\n private stop(): void {\n if (this.sending) this.closeBurst();\n this.source?.stop();\n this.source = null;\n this.encoder?.close();\n this.encoder = null;\n this.cancelPlayout?.();\n this.cancelPlayout = null;\n this.sink?.close();\n this.sink = null;\n for (const [id, s] of this.speakers) {\n s.decoder?.close();\n if (s.talking) this.emit(\"speaking\", id, false);\n }\n this.speakers.clear();\n }\n\n /** Feed one 20 ms mono 48 kHz frame (called by the mic source; exposed so tests and custom sources can drive it). */\n onCapturedFrame(pcm: Float32Array, nowMs: number): void {\n const level = rms(pcm);\n this.inputLevel = level;\n if (!this._enabled || this.muted) {\n if (this.sending) this.closeBurst();\n return;\n }\n const g = this.gate.update(level, nowMs);\n if (g.send) {\n this.sending = true;\n this.endTimer?.();\n this.endTimer = null;\n this.encoder?.encode(pcm);\n } else if (g.edge === \"close\" && this.sending) {\n this.closeBurst();\n }\n }\n\n private closeBurst(): void {\n this.sending = false;\n // Encoders can emit asynchronously (WebCodecs); give them one packet's worth of time before the end flag.\n this.endTimer?.();\n this.endTimer = this.hub.clock.setTimeout(() => {\n this.endTimer = null;\n this.sendPacket(this.packetizer.endBurst());\n }, FRAME_MS * 2);\n }\n\n /** Send already-encoded Opus frames as one packet (no codec involved); used by tools and conformance. */\n sendEncoded(frames: Uint8Array[], endOfBurst = false): void {\n this.sendPacket(this.packetizer.packet(frames, endOfBurst ? FLAG_END_OF_BURST : 0));\n }\n\n /** Encoded frame from the encoder (also the test entry point for packetization). */\n onEncoded(opus: Uint8Array): void {\n const p = this.packetizer.push(opus);\n if (p) this.sendPacket(p);\n }\n\n private sendPacket(p: VoicePacket): void {\n const msg: Message = { type: \"voice\", seq: p.seq, flags: p.flags, frames: p.frames };\n if (this.hub.isHost) {\n for (const peer of this.hub.connectedPeers()) this.hub.sendTo(peer, msg, this.hub.localPeerId, \"voice\");\n } else {\n this.hub.sendToHost(msg, 0, \"voice\");\n }\n }\n\n // ---- receive -------------------------------------------------------------------------------------------------\n\n handle(from: number, route: number, msg: Message): void {\n if (msg.type !== \"voice\") return;\n const size = msg.frames.reduce((a, f) => a + f.length + 2, 0) + 7; // whole wire frame: header 3 + seq/flags/n 4\n const now = this.hub.clock.now();\n let speaker = route;\n if (this.hub.isHost) {\n speaker = from;\n if (!this.relay.accept(from, size, msg.flags, now)) return;\n for (const peer of this.hub.connectedPeers()) {\n if (peer !== from) this.hub.sendTo(peer, msg, from, \"voice\");\n }\n }\n this.emit(\"packet\", speaker, msg.seq, msg.flags, msg.frames);\n if (!this._enabled || speaker === this.hub.localPeerId || this.peerMuted.has(speaker)) return;\n this.receivePacket(speaker, { seq: msg.seq, flags: msg.flags, frames: msg.frames }, now);\n }\n\n /** Local playback path for one speaker's packet (after host forwarding). */\n receivePacket(speaker: number, p: VoicePacket, now: number): void {\n let s = this.speakers.get(speaker);\n if (!s) {\n s = { jitter: new JitterBuffer(), decoder: null, talking: false, lastHeard: now };\n this.speakers.set(speaker, s);\n }\n s.lastHeard = now;\n s.jitter.push(p);\n if (!s.talking && p.frames.length > 0) {\n s.talking = true;\n this.emit(\"speaking\", speaker, true);\n }\n }\n\n private playoutTick(): void {\n const now = this.hub.clock.now();\n for (const [id, s] of this.speakers) {\n const r = s.jitter.pull();\n if (r.kind === \"frame\" || r.kind === \"plc\") {\n if (!s.decoder && this.deps.codec) {\n s.decoder = this.deps.codec.createDecoder((pcm) => this.sink?.play(id, pcm, this.volumeOf(id)));\n }\n s.decoder?.decode(r.kind === \"frame\" ? r.opus : null);\n } else if (r.kind === \"idle\" && s.talking) {\n s.talking = false;\n this.emit(\"speaking\", id, false);\n } else if (s.talking && now - s.lastHeard > SPEAKING_TIMEOUT_MS + 5 * FRAME_MS && !s.jitter.active) {\n s.talking = false;\n this.emit(\"speaking\", id, false);\n }\n }\n }\n\n onPeerLeft(peerId: number): void {\n this.relay.forget(peerId);\n const s = this.speakers.get(peerId);\n if (s) {\n s.decoder?.close();\n if (s.talking) this.emit(\"speaking\", peerId, false);\n this.speakers.delete(peerId);\n }\n }\n\n close(): void {\n this._enabled = false;\n this.stop();\n this.endTimer?.();\n }\n\n /** For tests: expose the end-of-burst flag constant. */\n static readonly END = FLAG_END_OF_BURST;\n}\n"]}
|
package/dist/webrtc.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type ChannelName, type LinkFactory, type LinkOptions, type PeerLink, type SignalPayload } from "./transport.js";
|
|
2
|
+
/** Browser (or any WebRTC-capable runtime) PeerLink over RTCPeerConnection with three negotiated channels. */
|
|
3
|
+
export declare class WebRtcLink implements PeerLink {
|
|
4
|
+
readonly remoteId: number;
|
|
5
|
+
isOpen: boolean;
|
|
6
|
+
onSignal: PeerLink["onSignal"];
|
|
7
|
+
onOpen: PeerLink["onOpen"];
|
|
8
|
+
onClose: PeerLink["onClose"];
|
|
9
|
+
onMessage: PeerLink["onMessage"];
|
|
10
|
+
onPath: PeerLink["onPath"];
|
|
11
|
+
private pc;
|
|
12
|
+
private channels;
|
|
13
|
+
private pendingIce;
|
|
14
|
+
private closed;
|
|
15
|
+
private opened;
|
|
16
|
+
private extra;
|
|
17
|
+
/** `rtc`: the RTCPeerConnection class to use (default the global one; in Node, node-datachannel's polyfill). */
|
|
18
|
+
constructor(opts: LinkOptions, rtc?: typeof RTCPeerConnection);
|
|
19
|
+
startOffer(): void;
|
|
20
|
+
handleSignal(s: SignalPayload): Promise<void>;
|
|
21
|
+
/** Selected candidate pair has a relay candidate -> "relayed", else "direct". Retries while stats settle. */
|
|
22
|
+
private reportPath;
|
|
23
|
+
send(channel: ChannelName, bytes: Uint8Array): void;
|
|
24
|
+
close(): void;
|
|
25
|
+
private fail;
|
|
26
|
+
}
|
|
27
|
+
export declare const webRtcLinkFactory: LinkFactory;
|
|
28
|
+
/** A link factory over a given RTCPeerConnection class, e.g. `import { RTCPeerConnection } from "node-datachannel/polyfill"`. */
|
|
29
|
+
export declare function webRtcLinkFactoryFor(rtc: typeof RTCPeerConnection): LinkFactory;
|
package/dist/webrtc.js
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { CHANNEL_IDS, } from "./transport.js";
|
|
2
|
+
const CHANNEL_INIT = {
|
|
3
|
+
reliable: { negotiated: true, id: CHANNEL_IDS.reliable, ordered: true },
|
|
4
|
+
fast: { negotiated: true, id: CHANNEL_IDS.fast, ordered: false, maxRetransmits: 0 },
|
|
5
|
+
voice: { negotiated: true, id: CHANNEL_IDS.voice, ordered: false, maxRetransmits: 0 },
|
|
6
|
+
};
|
|
7
|
+
/** Browser (or any WebRTC-capable runtime) PeerLink over RTCPeerConnection with three negotiated channels. */
|
|
8
|
+
export class WebRtcLink {
|
|
9
|
+
remoteId;
|
|
10
|
+
isOpen = false;
|
|
11
|
+
onSignal = null;
|
|
12
|
+
onOpen = null;
|
|
13
|
+
onClose = null;
|
|
14
|
+
onMessage = null;
|
|
15
|
+
onPath = null;
|
|
16
|
+
pc;
|
|
17
|
+
channels = {};
|
|
18
|
+
pendingIce = [];
|
|
19
|
+
closed = false;
|
|
20
|
+
opened = 0;
|
|
21
|
+
extra;
|
|
22
|
+
/** `rtc`: the RTCPeerConnection class to use (default the global one; in Node, node-datachannel's polyfill). */
|
|
23
|
+
constructor(opts, rtc = globalThis.RTCPeerConnection) {
|
|
24
|
+
this.remoteId = opts.remoteId;
|
|
25
|
+
this.extra = opts.extraRemoteCandidates ?? [];
|
|
26
|
+
const config = {
|
|
27
|
+
iceServers: opts.iceServers,
|
|
28
|
+
iceTransportPolicy: opts.relayOnly ? "relay" : "all",
|
|
29
|
+
};
|
|
30
|
+
if (opts.portRange) {
|
|
31
|
+
// libdatachannel names (node-datachannel passes its config through); one pinned port = every peer on it
|
|
32
|
+
config.portRangeBegin = opts.portRange.begin;
|
|
33
|
+
config.portRangeEnd = opts.portRange.end;
|
|
34
|
+
if (opts.portRange.begin === opts.portRange.end)
|
|
35
|
+
config.enableIceUdpMux = true;
|
|
36
|
+
}
|
|
37
|
+
this.pc = new rtc(config);
|
|
38
|
+
this.pc.onicecandidate = (e) => {
|
|
39
|
+
if (e.candidate)
|
|
40
|
+
this.onSignal?.({ ice: e.candidate.toJSON() });
|
|
41
|
+
};
|
|
42
|
+
this.pc.onconnectionstatechange = () => {
|
|
43
|
+
const s = this.pc.connectionState;
|
|
44
|
+
if (s === "failed" || s === "closed" || s === "disconnected") {
|
|
45
|
+
// "disconnected" can recover; give ICE a moment before declaring the link dead.
|
|
46
|
+
if (s === "disconnected")
|
|
47
|
+
setTimeout(() => this.pc.connectionState === "disconnected" && this.fail("disconnected"), 4000);
|
|
48
|
+
else
|
|
49
|
+
this.fail(s);
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
for (const name of ["reliable", "fast", "voice"]) {
|
|
53
|
+
const ch = this.pc.createDataChannel(name, CHANNEL_INIT[name]);
|
|
54
|
+
ch.binaryType = "arraybuffer";
|
|
55
|
+
ch.onopen = () => {
|
|
56
|
+
if (++this.opened === 3 && !this.closed) {
|
|
57
|
+
this.isOpen = true;
|
|
58
|
+
this.onOpen?.();
|
|
59
|
+
void this.reportPath();
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
ch.onclose = () => this.fail(`channel ${name} closed`);
|
|
63
|
+
ch.onmessage = (e) => {
|
|
64
|
+
const d = e.data;
|
|
65
|
+
const data = d instanceof ArrayBuffer ? new Uint8Array(d) : ArrayBuffer.isView(d) ? new Uint8Array(d.buffer, d.byteOffset, d.byteLength) : new Uint8Array(d);
|
|
66
|
+
this.onMessage?.(name, data);
|
|
67
|
+
};
|
|
68
|
+
this.channels[name] = ch;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
startOffer() {
|
|
72
|
+
void (async () => {
|
|
73
|
+
try {
|
|
74
|
+
const offer = await this.pc.createOffer();
|
|
75
|
+
await this.pc.setLocalDescription(offer);
|
|
76
|
+
this.onSignal?.({ sdp: { type: "offer", sdp: offer.sdp ?? "" } });
|
|
77
|
+
}
|
|
78
|
+
catch (e) {
|
|
79
|
+
this.fail(`offer failed: ${String(e)}`);
|
|
80
|
+
}
|
|
81
|
+
})();
|
|
82
|
+
}
|
|
83
|
+
async handleSignal(s) {
|
|
84
|
+
try {
|
|
85
|
+
if (s.sdp) {
|
|
86
|
+
await this.pc.setRemoteDescription({ type: s.sdp.type, sdp: s.sdp.sdp });
|
|
87
|
+
for (const c of this.pendingIce.splice(0))
|
|
88
|
+
await this.pc.addIceCandidate(c);
|
|
89
|
+
if (this.extra.length) {
|
|
90
|
+
// docs/direct-ip.md s.2: the address the joiner typed is the host's reachable candidate
|
|
91
|
+
const mid = /^a=mid:(\S+)/m.exec(s.sdp.sdp)?.[1] ?? "0";
|
|
92
|
+
for (const candidate of this.extra.splice(0))
|
|
93
|
+
await this.pc.addIceCandidate({ candidate, sdpMid: mid, sdpMLineIndex: 0 });
|
|
94
|
+
}
|
|
95
|
+
if (s.sdp.type === "offer") {
|
|
96
|
+
const answer = await this.pc.createAnswer();
|
|
97
|
+
await this.pc.setLocalDescription(answer);
|
|
98
|
+
this.onSignal?.({ sdp: { type: "answer", sdp: answer.sdp ?? "" } });
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (s.ice) {
|
|
102
|
+
if (this.pc.remoteDescription)
|
|
103
|
+
await this.pc.addIceCandidate(s.ice);
|
|
104
|
+
else
|
|
105
|
+
this.pendingIce.push(s.ice);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
catch (e) {
|
|
109
|
+
this.fail(`signal failed: ${String(e)}`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/** Selected candidate pair has a relay candidate -> "relayed", else "direct". Retries while stats settle. */
|
|
113
|
+
async reportPath(attempt = 0) {
|
|
114
|
+
if (this.closed || attempt > 10)
|
|
115
|
+
return;
|
|
116
|
+
try {
|
|
117
|
+
const stats = await this.pc.getStats();
|
|
118
|
+
const byId = new Map();
|
|
119
|
+
stats.forEach((v) => byId.set(v.id, v));
|
|
120
|
+
let pair;
|
|
121
|
+
stats.forEach((v) => {
|
|
122
|
+
if (v.type === "transport" && v.selectedCandidatePairId)
|
|
123
|
+
pair = byId.get(v.selectedCandidatePairId);
|
|
124
|
+
});
|
|
125
|
+
if (!pair)
|
|
126
|
+
stats.forEach((v) => { if (v.type === "candidate-pair" && (v.selected || (v.nominated && v.state === "succeeded")))
|
|
127
|
+
pair = v; });
|
|
128
|
+
const l = pair && byId.get(pair.localCandidateId);
|
|
129
|
+
const r = pair && byId.get(pair.remoteCandidateId);
|
|
130
|
+
if (l && r) {
|
|
131
|
+
this.onPath?.(l.candidateType === "relay" || r.candidateType === "relay" ? "relayed" : "direct");
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
/* stats unavailable: retry */
|
|
137
|
+
}
|
|
138
|
+
setTimeout(() => void this.reportPath(attempt + 1), 300);
|
|
139
|
+
}
|
|
140
|
+
send(channel, bytes) {
|
|
141
|
+
const ch = this.channels[channel];
|
|
142
|
+
if (ch && ch.readyState === "open")
|
|
143
|
+
ch.send(bytes);
|
|
144
|
+
}
|
|
145
|
+
close() {
|
|
146
|
+
this.fail("closed locally");
|
|
147
|
+
}
|
|
148
|
+
fail(reason) {
|
|
149
|
+
if (this.closed)
|
|
150
|
+
return;
|
|
151
|
+
this.closed = true;
|
|
152
|
+
const wasOpen = this.isOpen;
|
|
153
|
+
this.isOpen = false;
|
|
154
|
+
try {
|
|
155
|
+
this.pc.close();
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
/* ignore */
|
|
159
|
+
}
|
|
160
|
+
void wasOpen;
|
|
161
|
+
this.onClose?.(reason);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
export const webRtcLinkFactory = { create: (opts) => new WebRtcLink(opts) };
|
|
165
|
+
/** A link factory over a given RTCPeerConnection class, e.g. `import { RTCPeerConnection } from "node-datachannel/polyfill"`. */
|
|
166
|
+
export function webRtcLinkFactoryFor(rtc) {
|
|
167
|
+
return { create: (opts) => new WebRtcLink(opts, rtc) };
|
|
168
|
+
}
|
|
169
|
+
//# sourceMappingURL=webrtc.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webrtc.js","sourceRoot":"","sources":["../src/webrtc.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n CHANNEL_IDS,\n type ChannelName,\n type LinkFactory,\n type LinkOptions,\n type PeerLink,\n type SignalPayload,\n} from \"./transport.js\";\n\nconst CHANNEL_INIT: Record<ChannelName, RTCDataChannelInit> = {\n reliable: { negotiated: true, id: CHANNEL_IDS.reliable, ordered: true },\n fast: { negotiated: true, id: CHANNEL_IDS.fast, ordered: false, maxRetransmits: 0 },\n voice: { negotiated: true, id: CHANNEL_IDS.voice, ordered: false, maxRetransmits: 0 },\n};\n\n/** Browser (or any WebRTC-capable runtime) PeerLink over RTCPeerConnection with three negotiated channels. */\nexport class WebRtcLink implements PeerLink {\n readonly remoteId: number;\n isOpen = false;\n onSignal: PeerLink[\"onSignal\"] = null;\n onOpen: PeerLink[\"onOpen\"] = null;\n onClose: PeerLink[\"onClose\"] = null;\n onMessage: PeerLink[\"onMessage\"] = null;\n onPath: PeerLink[\"onPath\"] = null;\n\n private pc: RTCPeerConnection;\n private channels = {} as Record<ChannelName, RTCDataChannel>;\n private pendingIce: RTCIceCandidateInit[] = [];\n private closed = false;\n private opened = 0;\n private extra: string[];\n\n /** `rtc`: the RTCPeerConnection class to use (default the global one; in Node, node-datachannel's polyfill). */\n constructor(opts: LinkOptions, rtc: typeof RTCPeerConnection = globalThis.RTCPeerConnection) {\n this.remoteId = opts.remoteId;\n this.extra = opts.extraRemoteCandidates ?? [];\n const config: RTCConfiguration & Record<string, unknown> = {\n iceServers: opts.iceServers as RTCIceServer[],\n iceTransportPolicy: opts.relayOnly ? \"relay\" : \"all\",\n };\n if (opts.portRange) {\n // libdatachannel names (node-datachannel passes its config through); one pinned port = every peer on it\n config.portRangeBegin = opts.portRange.begin;\n config.portRangeEnd = opts.portRange.end;\n if (opts.portRange.begin === opts.portRange.end) config.enableIceUdpMux = true;\n }\n this.pc = new rtc(config);\n this.pc.onicecandidate = (e) => {\n if (e.candidate) this.onSignal?.({ ice: e.candidate.toJSON() as { candidate: string } });\n };\n this.pc.onconnectionstatechange = () => {\n const s = this.pc.connectionState;\n if (s === \"failed\" || s === \"closed\" || s === \"disconnected\") {\n // \"disconnected\" can recover; give ICE a moment before declaring the link dead.\n if (s === \"disconnected\") setTimeout(() => this.pc.connectionState === \"disconnected\" && this.fail(\"disconnected\"), 4000);\n else this.fail(s);\n }\n };\n for (const name of [\"reliable\", \"fast\", \"voice\"] as ChannelName[]) {\n const ch = this.pc.createDataChannel(name, CHANNEL_INIT[name]);\n ch.binaryType = \"arraybuffer\";\n ch.onopen = () => {\n if (++this.opened === 3 && !this.closed) {\n this.isOpen = true;\n this.onOpen?.();\n void this.reportPath();\n }\n };\n ch.onclose = () => this.fail(`channel ${name} closed`);\n ch.onmessage = (e) => {\n const d = e.data as unknown;\n const data = d instanceof ArrayBuffer ? new Uint8Array(d) : ArrayBuffer.isView(d) ? new Uint8Array(d.buffer, d.byteOffset, d.byteLength) : new Uint8Array(d as never);\n this.onMessage?.(name, data);\n };\n this.channels[name] = ch;\n }\n }\n\n startOffer(): void {\n void (async () => {\n try {\n const offer = await this.pc.createOffer();\n await this.pc.setLocalDescription(offer);\n this.onSignal?.({ sdp: { type: \"offer\", sdp: offer.sdp ?? \"\" } });\n } catch (e) {\n this.fail(`offer failed: ${String(e)}`);\n }\n })();\n }\n\n async handleSignal(s: SignalPayload): Promise<void> {\n try {\n if (s.sdp) {\n await this.pc.setRemoteDescription({ type: s.sdp.type, sdp: s.sdp.sdp });\n for (const c of this.pendingIce.splice(0)) await this.pc.addIceCandidate(c);\n if (this.extra.length) {\n // docs/direct-ip.md s.2: the address the joiner typed is the host's reachable candidate\n const mid = /^a=mid:(\\S+)/m.exec(s.sdp.sdp)?.[1] ?? \"0\";\n for (const candidate of this.extra.splice(0)) await this.pc.addIceCandidate({ candidate, sdpMid: mid, sdpMLineIndex: 0 });\n }\n if (s.sdp.type === \"offer\") {\n const answer = await this.pc.createAnswer();\n await this.pc.setLocalDescription(answer);\n this.onSignal?.({ sdp: { type: \"answer\", sdp: answer.sdp ?? \"\" } });\n }\n }\n if (s.ice) {\n if (this.pc.remoteDescription) await this.pc.addIceCandidate(s.ice as RTCIceCandidateInit);\n else this.pendingIce.push(s.ice as RTCIceCandidateInit);\n }\n } catch (e) {\n this.fail(`signal failed: ${String(e)}`);\n }\n }\n\n /** Selected candidate pair has a relay candidate -> \"relayed\", else \"direct\". Retries while stats settle. */\n private async reportPath(attempt = 0): Promise<void> {\n if (this.closed || attempt > 10) return;\n try {\n const stats = await this.pc.getStats();\n const byId = new Map<string, any>();\n stats.forEach((v) => byId.set(v.id, v));\n let pair: any;\n stats.forEach((v) => {\n if (v.type === \"transport\" && v.selectedCandidatePairId) pair = byId.get(v.selectedCandidatePairId);\n });\n if (!pair) stats.forEach((v) => { if (v.type === \"candidate-pair\" && (v.selected || (v.nominated && v.state === \"succeeded\"))) pair = v; });\n const l = pair && byId.get(pair.localCandidateId);\n const r = pair && byId.get(pair.remoteCandidateId);\n if (l && r) {\n this.onPath?.(l.candidateType === \"relay\" || r.candidateType === \"relay\" ? \"relayed\" : \"direct\");\n return;\n }\n } catch {\n /* stats unavailable: retry */\n }\n setTimeout(() => void this.reportPath(attempt + 1), 300);\n }\n\n send(channel: ChannelName, bytes: Uint8Array): void {\n const ch = this.channels[channel];\n if (ch && ch.readyState === \"open\") ch.send(bytes as unknown as ArrayBuffer);\n }\n\n close(): void {\n this.fail(\"closed locally\");\n }\n\n private fail(reason: string): void {\n if (this.closed) return;\n this.closed = true;\n const wasOpen = this.isOpen;\n this.isOpen = false;\n try {\n this.pc.close();\n } catch {\n /* ignore */\n }\n void wasOpen;\n this.onClose?.(reason);\n }\n}\n\nexport const webRtcLinkFactory: LinkFactory = { create: (opts) => new WebRtcLink(opts) };\n\n/** A link factory over a given RTCPeerConnection class, e.g. `import { RTCPeerConnection } from \"node-datachannel/polyfill\"`. */\nexport function webRtcLinkFactoryFor(rtc: typeof RTCPeerConnection): LinkFactory {\n return { create: (opts) => new WebRtcLink(opts, rtc) };\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,95 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "onelobby",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Multiplayer for browser games: host/join lobbies by short code, peer-to-peer WebRTC, lockstep sync, text and voice chat. Zero runtime dependencies; works from a plain <script> tag.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"multiplayer",
|
|
7
|
+
"lobby",
|
|
8
|
+
"webrtc",
|
|
9
|
+
"p2p",
|
|
10
|
+
"peer-to-peer",
|
|
11
|
+
"netcode",
|
|
12
|
+
"lockstep",
|
|
13
|
+
"game",
|
|
14
|
+
"gamedev",
|
|
15
|
+
"browser-game",
|
|
16
|
+
"voice-chat",
|
|
17
|
+
"onelobby"
|
|
18
|
+
],
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"author": "Parker Tonra",
|
|
21
|
+
"homepage": "https://github.com/ParkerTonra/onelobby#readme",
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "git+https://github.com/ParkerTonra/onelobby.git",
|
|
25
|
+
"directory": "clients/js/packages/client"
|
|
26
|
+
},
|
|
27
|
+
"bugs": {
|
|
28
|
+
"url": "https://github.com/ParkerTonra/onelobby/issues"
|
|
29
|
+
},
|
|
30
|
+
"type": "module",
|
|
31
|
+
"main": "./dist/index.js",
|
|
32
|
+
"types": "./dist/index.d.ts",
|
|
33
|
+
"unpkg": "./dist/onelobby.global.min.js",
|
|
34
|
+
"jsdelivr": "./dist/onelobby.global.min.js",
|
|
35
|
+
"exports": {
|
|
36
|
+
".": {
|
|
37
|
+
"types": "./dist/index.d.ts",
|
|
38
|
+
"import": "./dist/index.js",
|
|
39
|
+
"default": "./dist/index.js"
|
|
40
|
+
},
|
|
41
|
+
"./internals": {
|
|
42
|
+
"types": "./dist/internals.d.ts",
|
|
43
|
+
"import": "./dist/internals.js",
|
|
44
|
+
"default": "./dist/internals.js"
|
|
45
|
+
},
|
|
46
|
+
"./testing": {
|
|
47
|
+
"types": "./dist/testing.d.ts",
|
|
48
|
+
"import": "./dist/testing.js",
|
|
49
|
+
"default": "./dist/testing.js"
|
|
50
|
+
},
|
|
51
|
+
"./direct": {
|
|
52
|
+
"types": "./dist/direct/index.d.ts",
|
|
53
|
+
"import": "./dist/direct/index.js",
|
|
54
|
+
"default": "./dist/direct/index.js"
|
|
55
|
+
},
|
|
56
|
+
"./ui": {
|
|
57
|
+
"types": "./dist/ui/index.d.ts",
|
|
58
|
+
"import": "./dist/ui/index.js",
|
|
59
|
+
"default": "./dist/ui/index.js"
|
|
60
|
+
},
|
|
61
|
+
"./global": "./dist/onelobby.global.js",
|
|
62
|
+
"./bundle": {
|
|
63
|
+
"types": "./dist/global.d.ts",
|
|
64
|
+
"default": "./dist/onelobby.bundle.mjs"
|
|
65
|
+
},
|
|
66
|
+
"./package.json": "./package.json"
|
|
67
|
+
},
|
|
68
|
+
"files": [
|
|
69
|
+
"dist",
|
|
70
|
+
"README.md",
|
|
71
|
+
"LICENSE"
|
|
72
|
+
],
|
|
73
|
+
"sideEffects": [
|
|
74
|
+
"./dist/onelobby.global.js",
|
|
75
|
+
"./dist/onelobby.global.min.js",
|
|
76
|
+
"./dist/ui/index.js",
|
|
77
|
+
"./dist/ui/element.js"
|
|
78
|
+
],
|
|
79
|
+
"engines": {
|
|
80
|
+
"node": ">=18"
|
|
81
|
+
},
|
|
82
|
+
"publishConfig": {
|
|
83
|
+
"access": "public"
|
|
84
|
+
},
|
|
85
|
+
"scripts": {
|
|
86
|
+
"build": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\" && tsc -p tsconfig.build.json && node scripts/bundle.mjs",
|
|
87
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
88
|
+
"test": "vitest run",
|
|
89
|
+
"prepublishOnly": "npm run typecheck && npm run build && npm test && node scripts/prepublish-check.mjs"
|
|
90
|
+
},
|
|
91
|
+
"devDependencies": {
|
|
92
|
+
"esbuild": "^0.28.2",
|
|
93
|
+
"node-datachannel": "^0.33.4"
|
|
94
|
+
}
|
|
95
|
+
}
|