it's working chat

This commit is contained in:
Aubrey 2024-12-20 04:33:50 -06:00
parent dcf885a7b0
commit a1dbfadfb0
No known key found for this signature in database
13 changed files with 526 additions and 229 deletions

View file

@ -20,6 +20,6 @@
"vite": "^6.0.0"
},
"dependencies": {
"peerjs": "^1.5.4"
"uuid": "^11.0.3"
}
}

View file

@ -8,9 +8,9 @@ importers:
.:
dependencies:
peerjs:
specifier: ^1.5.4
version: 1.5.4
uuid:
specifier: ^11.0.3
version: 11.0.3
devDependencies:
'@sveltejs/adapter-node':
specifier: ^5.2.0
@ -205,10 +205,6 @@ packages:
'@jridgewell/trace-mapping@0.3.25':
resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==}
'@msgpack/msgpack@2.8.0':
resolution: {integrity: sha512-h9u4u/jiIRKbq25PM+zymTyW6bhTzELvOoUd+AvYriWOAKpLGnIamaET3pnHYoI5iYphAHBI4ayx0MehR+VVPQ==}
engines: {node: '>= 10'}
'@polka/url@1.0.0-next.28':
resolution: {integrity: sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw==}
@ -446,9 +442,6 @@ packages:
estree-walker@2.0.2:
resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
eventemitter3@4.0.7:
resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==}
fdir@6.4.2:
resolution: {integrity: sha512-KnhMXsKSPZlAhp7+IjUkRZKPb4fUyccpDrdFXbi4QL1qkmFh9kVY09Yox+n4MaOb3lHZ1Tv829C3oaaXoMYPDQ==}
peerDependencies:
@ -520,14 +513,6 @@ packages:
path-parse@1.0.7:
resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
peerjs-js-binarypack@2.1.0:
resolution: {integrity: sha512-YIwCC+pTzp3Bi8jPI9UFKO0t0SLo6xALnHkiNt/iUFmUUZG0fEEmEyFKvjsDKweiFitzHRyhuh6NvyJZ4nNxMg==}
engines: {node: '>= 14.0.0'}
peerjs@1.5.4:
resolution: {integrity: sha512-yFsoLMnurJKlQbx6kVSBpOp+AlNldY1JQS2BrSsHLKCZnq6t7saHleuHM5svuLNbQkUJXHLF3sKOJB1K0xulOw==}
engines: {node: '>= 14'}
picocolors@1.1.1:
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
@ -556,9 +541,6 @@ packages:
resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==}
engines: {node: '>=6'}
sdp@3.2.0:
resolution: {integrity: sha512-d7wDPgDV3DDiqulJjKiV2865wKsJ34YI+NDREbm+FySq6WuKOikwyNQcm+doLAZ1O6ltdO0SeKle2xMpN3Brgw==}
set-cookie-parser@2.7.1:
resolution: {integrity: sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==}
@ -598,6 +580,10 @@ packages:
engines: {node: '>=14.17'}
hasBin: true
uuid@11.0.3:
resolution: {integrity: sha512-d0z310fCWv5dJwnX1Y/MncBAqGMKEzlBb1AOf7z9K8ALnd0utBX/msg/fA0+sbyN1ihbMsLhrBlnl1ak7Wa0rg==}
hasBin: true
vite@6.0.3:
resolution: {integrity: sha512-Cmuo5P0ENTN6HxLSo6IHsjCLn/81Vgrp81oaiFFMRa8gGDj5xEjIcEpf2ZymZtZR8oU0P2JX5WuUp/rlXcHkAw==}
engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
@ -646,10 +632,6 @@ packages:
vite:
optional: true
webrtc-adapter@9.0.1:
resolution: {integrity: sha512-1AQO+d4ElfVSXyzNVTOewgGT/tAomwwztX/6e3totvyyzXPvXIIuUUjAmyZGbKBKbZOXauuJooZm3g6IuFuiNQ==}
engines: {node: '>=6.0.0', npm: '>=3.10.0'}
zimmerframe@1.1.2:
resolution: {integrity: sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w==}
@ -749,8 +731,6 @@ snapshots:
'@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.0
'@msgpack/msgpack@2.8.0': {}
'@polka/url@1.0.0-next.28': {}
'@rollup/plugin-commonjs@28.0.2(rollup@4.28.1)':
@ -966,8 +946,6 @@ snapshots:
estree-walker@2.0.2: {}
eventemitter3@4.0.7: {}
fdir@6.4.2(picomatch@4.0.2):
optionalDependencies:
picomatch: 4.0.2
@ -1019,15 +997,6 @@ snapshots:
path-parse@1.0.7: {}
peerjs-js-binarypack@2.1.0: {}
peerjs@1.5.4:
dependencies:
'@msgpack/msgpack': 2.8.0
eventemitter3: 4.0.7
peerjs-js-binarypack: 2.1.0
webrtc-adapter: 9.0.1
picocolors@1.1.1: {}
picomatch@4.0.2: {}
@ -1075,8 +1044,6 @@ snapshots:
dependencies:
mri: 1.2.0
sdp@3.2.0: {}
set-cookie-parser@2.7.1: {}
sirv@3.0.0:
@ -1126,6 +1093,8 @@ snapshots:
typescript@5.7.2: {}
uuid@11.0.3: {}
vite@6.0.3:
dependencies:
esbuild: 0.24.0
@ -1138,8 +1107,4 @@ snapshots:
optionalDependencies:
vite: 6.0.3
webrtc-adapter@9.0.1:
dependencies:
sdp: 3.2.0
zimmerframe@1.1.2: {}

34
src/lib/audio.svelte.ts Normal file
View file

@ -0,0 +1,34 @@
export class AudioManager {
globalGain = $state(1.0);
useMusic: boolean;
ac: AudioContext;
dest: MediaStreamAudioDestinationNode;
gain: GainNode;
source: AudioNode;
public readonly output: MediaStream;
constructor(media: HTMLAudioElement | MediaStream) {
this.ac = new AudioContext();
this.dest = this.ac.createMediaStreamDestination();
this.gain = this.ac.createGain();
globalThis.audioPlay = this;
if (media instanceof HTMLAudioElement) {
this.source = this.ac.createMediaElementSource(media);
media.autoplay = true;
media.controls = true;
media.play();
this.useMusic = true;
} else {
this.source = this.ac.createMediaStreamSource(media);
this.useMusic = false;
}
// .connect(this.gain)
// this.source.connect(this.dest);
this.gain.gain.value /= 32;
this.source.connect(this.gain).connect(this.dest)
this.output = this.dest.stream;
// this.output = media;
console.log("epic", this.dest.stream, this.output);
}
}

View file

@ -1,149 +1,127 @@
// import { BinaryStreamReader } from "./readerStream";
// import { AsyncReadMethod, AsyncReadMethodReturnValue, ObjectValue, ObjectValueType, ReadMethod, ReadMethodReturnValue } from "./types";
import { ReadMethod } from "./types";
// export class BinaryReader {
// protected index: number = 0;
export class BinaryReader {
protected index: number = 0;
// constructor(
// protected readonly buffer: DataView,
// ) {}
constructor(
protected readonly buffer: DataView,
) { }
// readBoolean(): boolean {
// return this.readUInt8() != 0;
// }
readBoolean(): boolean {
return this.readUInt8() != 0;
}
// readSInt8(): number {
// const value = this.buffer.getInt8(this.index);
readSInt8(): number {
const value = this.buffer.getInt8(this.index);
// this.index++;
this.index++;
// return value;
// }
return value;
}
// readUInt8(): number {
// const value = this.buffer.getUint8(this.index);
readUInt8(): number {
const value = this.buffer.getUint8(this.index);
// this.index++;
this.index++;
// return value;
// }
return value;
}
// readSInt16(): number {
// const value = this.buffer.getInt16(this.index, true);
readSInt16(): number {
const value = this.buffer.getInt16(this.index, true);
// this.index += 2;
this.index += 2;
// return value;
// }
return value;
}
// readUInt16(): number {
// const value = this.buffer.getUint16(this.index, true);
readUInt16(): number {
const value = this.buffer.getUint16(this.index, true);
// this.index += 2;
this.index += 2;
// return value;
// }
return value;
}
// readSInt32(): number {
// const value = this.buffer.getInt32(this.index, true);
readSInt32(): number {
const value = this.buffer.getInt32(this.index, true);
// this.index += 4;
this.index += 4;
// return value;
// }
return value;
}
// readUInt32(): number {
// const value = this.buffer.getUint32(this.index, true);
readUInt32(): number {
const value = this.buffer.getUint32(this.index, true);
// this.index += 4;
this.index += 4;
// return value;
// }
return value;
}
// readSInt64(): bigint {
// const value = this.buffer.getBigInt64(this.index, true);
readSInt64(): bigint {
const value = this.buffer.getBigInt64(this.index, true);
// this.index += 8;
this.index += 8;
// return value;
// }
return value;
}
// readUInt64(): bigint {
// const value = this.buffer.getBigUint64(this.index, true);
readUInt64(): bigint {
const value = this.buffer.getBigUint64(this.index, true);
// this.index += 8;
this.index += 8;
// return value;
// }
return value;
}
// readFloat32(): number {
// const value = this.buffer.getFloat32(this.index, true);
readFloat32(): number {
const value = this.buffer.getFloat32(this.index, true);
// this.index += 4;
this.index += 4;
// return value;
// }
return value;
}
// readFloat64(): number {
// const value = this.buffer.getFloat64(this.index, true);
readFloat64(): number {
const value = this.buffer.getFloat64(this.index, true);
// this.index += 8;
this.index += 8;
// return value;
// }
return value;
}
// readString(length: number, encoding?: string): string {
// const decoder = new TextDecoder(encoding);
readString(length: number, encoding?: string): string {
const decoder = new TextDecoder(encoding);
// return decoder.decode(this.buffer.buffer.slice(this.index, this.index += length));
// }
return decoder.decode(this.buffer.buffer.slice(this.index, this.index += length));
}
// async readArray<T>(size: number, readMethod: ReadMethod<T> | AsyncReadMethod<T>): Promise<T[]> {
// const array = new Array<T>(size);
async readFixedString(length: number, encoding?: string): Promise<string> {
const decoder = new TextDecoder(encoding);
// for (let i = 0; i < size; i++) {
// array[i] = await this.read(readMethod);
// }
const slice = new Uint8Array(this.buffer.buffer.slice(this.index, this.index += length));
const foundIndex = slice.findIndex(byte => byte == 0);
const end = foundIndex === -1 ? this.buffer.byteLength : foundIndex
// return array;
// }
return decoder.decode(slice.slice(0, end));
}
// async readObject(): Promise<ObjectValue> {
// return await this.readMappedShortEnum({
// [ObjectValueType.Object]: async (r: BinaryReader | BinaryStreamReader) => Object.fromEntries(await r.readArray(await r.readUInt32(), async r2 => [ await r2.readString(await r2.readUInt32()), await r2.readObject() ])),
// [ObjectValueType.Array]: async (r: BinaryReader | BinaryStreamReader) => await r.readArray(await r.readUInt32(), async (r2: BinaryReader | BinaryStreamReader) => await r2.readObject()),
// [ObjectValueType.Boolean]: async (r: BinaryReader | BinaryStreamReader) => await r.readBoolean(),
// [ObjectValueType.Null]: () => null,
// [ObjectValueType.Number]: async (r: BinaryReader | BinaryStreamReader) => await r.readFloat64(),
// [ObjectValueType.String]: async (r: BinaryReader | BinaryStreamReader) => await r.readString(await r.readUInt32()),
// })
// }
async readArray<T>(size: number, readMethod: ReadMethod<T, BinaryReader>): Promise<T[]> {
const array = new Array<T>(size);
// async read<T>(readMethod: ReadMethod<T> | AsyncReadMethod<T>): Promise<T> {
// if ("deserialize" in readMethod) {
// return await readMethod.deserialize(this);
// }
for (let i = 0; i < size; i++) {
array[i] = await this.read(readMethod);
}
// return await readMethod(this);
// }
return array;
}
// readShortEnum<T extends number>(): T {
// return this.readUInt8() as T;
// }
async read<T>(readMethod: ReadMethod<T, BinaryReader>): Promise<T> {
if ("deserialize" in readMethod) {
return await readMethod.deserialize(this);
}
// readEnum<T extends number>(): T {
// return this.readUInt16() as T;
// }
// readDate(): Date {
// return new Date(Number(this.readUInt64()));
// }
// async readMappedEnum<T extends number, R extends Record<T, ReadMethod<any> | AsyncReadMethod<T>>>(mappedType: R): Promise<ReadMethodReturnValue<R[keyof R]> | AsyncReadMethodReturnValue<R[keyof R]>> {
// return await this.read(mappedType[this.readEnum<T>()]);
// }
// async readMappedShortEnum<T extends number, R extends Record<T, ReadMethod<any> | AsyncReadMethod<T>>>(mappedType: R): Promise<ReadMethodReturnValue<R[keyof R]> | AsyncReadMethodReturnValue<R[keyof R]>> {
// return await this.read(mappedType[this.readShortEnum<T>()]);
// }
// }
return await readMethod(this);
}
}

View file

@ -24,7 +24,6 @@ export class BinaryStreamReader {
(async () => {
while (true) {
const data = await reader.read();
console.log("got data", data);
streamReader.push(data.value);
}
})();
@ -94,35 +93,35 @@ export class BinaryStreamReader {
}
async readSInt16(): Promise<number> {
return (await this.bytes(2)).getInt16(0);
return (await this.bytes(2)).getInt16(0, true);
}
async readUInt16(): Promise<number> {
return (await this.bytes(2)).getUint16(0);
return (await this.bytes(2)).getUint16(0, true);
}
async readSInt32(): Promise<number> {
return (await this.bytes(4)).getInt32(0);
return (await this.bytes(4)).getInt32(0, true);
}
async readUInt32(): Promise<number> {
return (await this.bytes(4)).getUint32(0);
return (await this.bytes(4)).getUint32(0, true);
}
async readSInt64(): Promise<bigint> {
return (await this.bytes(8)).getBigInt64(0);
return (await this.bytes(8)).getBigInt64(0, true);
}
async readUInt64(): Promise<bigint> {
return (await this.bytes(8)).getBigUint64(0);
return (await this.bytes(8)).getBigUint64(0, true);
}
async readFloat32(): Promise<number> {
return (await this.bytes(4)).getFloat32(0);
return (await this.bytes(4)).getFloat32(0, true);
}
async readFloat64(): Promise<number> {
return (await this.bytes(8)).getFloat64(0);
return (await this.bytes(8)).getFloat64(0, true);
}
async readString(length: number, encoding?: string): Promise<string> {
@ -136,10 +135,10 @@ export class BinaryStreamReader {
const bytes = await this.bytes(length);
const slice = new Uint8Array(bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength));
const end = slice.findIndex(byte => byte == 0);
console.log(bytes.byteOffset, bytes.byteOffset + end, end, bytes.byteLength);
const foundIndex = slice.findIndex(byte => byte == 0);
const end = foundIndex === -1 ? slice.byteLength : foundIndex;
return decoder.decode(bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + end));
return decoder.decode(slice.slice(0, end));
}
async readArray<T>(size: number, readMethod: AsyncReadMethod<T, this>): Promise<T[]> {

View file

@ -1,9 +1,9 @@
type Method<T, R, F extends string> = ((reader: R) => T) | { [key in F]: (reader: R) => T };
export type ReadMethod<T, R> = Method<T, R, "deserialize">;
export type WriteMethod<W> = Method<void, W, "serialize">;
export type AsyncReadMethod<T, R> = Method<T | Promise<T>, R, "deserialize">;
export type WriteMethod<T, W> = ((writer: W, value: T) => void) | { serialize: (writer: W, value: T) => void };
export type ReadMethodReturnValue<X> = X extends ReadMethod<infer Y, any> ? (Y extends Promise<infer Z> ? Z : Y) : never;
export enum ObjectValueType {

View file

@ -43,7 +43,7 @@ export class BinaryWriter {
writeSInt16(value: number): void {
this.request(2);
this.buffer.setInt16(this.index, value);
this.buffer.setInt16(this.index, value, true);
this.index += 2;
}
@ -51,7 +51,7 @@ export class BinaryWriter {
writeUInt16(value: number): void {
this.request(2);
this.buffer.setUint16(this.index, value);
this.buffer.setUint16(this.index, value, true);
this.index += 2;
}
@ -59,7 +59,7 @@ export class BinaryWriter {
writeSInt32(value: number): void {
this.request(4);
this.buffer.setInt32(this.index, value);
this.buffer.setInt32(this.index, value, true);
this.index += 4;
}
@ -67,7 +67,7 @@ export class BinaryWriter {
writeUInt32(value: number): void {
this.request(4);
this.buffer.setUint32(this.index, value);
this.buffer.setUint32(this.index, value, true);
this.index += 4;
}
@ -75,7 +75,7 @@ export class BinaryWriter {
writeSInt64(value: bigint): void {
this.request(8);
this.buffer.setBigInt64(this.index, value);
this.buffer.setBigInt64(this.index, value, true);
this.index += 8;
}
@ -83,7 +83,7 @@ export class BinaryWriter {
writeUInt64(value: bigint): void {
this.request(8);
this.buffer.setBigUint64(this.index, value);
this.buffer.setBigUint64(this.index, value, true);
this.index += 8;
}
@ -91,7 +91,7 @@ export class BinaryWriter {
writeFloat32(value: number): void {
this.request(4);
this.buffer.setFloat32(this.index, value);
this.buffer.setFloat32(this.index, value, true);
this.index += 4;
}
@ -99,7 +99,7 @@ export class BinaryWriter {
writeFloat64(value: number): void {
this.request(8);
this.buffer.setFloat64(this.index, value);
this.buffer.setFloat64(this.index, value, true);
this.index += 8;
}
@ -114,17 +114,41 @@ export class BinaryWriter {
this.index += encoded.byteLength;
}
writeArray(array: WriteMethod<BinaryWriter>[]): void {
writePrefixedString(string: string): void {
const encoder = new TextEncoder();
const encoded = encoder.encode(string);
this.writeUInt32(encoded.byteLength);
this.request(encoded.byteLength);
new Uint8Array(this.buffer.buffer).set(encoded, this.index);
this.index += encoded.byteLength;
}
writeFixedString(size: number, string: string): void {
this.request(size);
const encoder = new TextEncoder();
encoder.encodeInto(string, new Uint8Array(this.buffer.buffer, this.index, size));
this.index += size;
}
writeArray<T>(array: T[], method: WriteMethod<T, BinaryWriter>): void {
for (let i = 0; i < array.length; i++) {
this.write(array[i]);
this.write(array[i], method);
}
}
write(writeMethod: WriteMethod<BinaryWriter>): void {
write<T>(value: T, writeMethod: WriteMethod<T, BinaryWriter>): void {
if ("serialize" in writeMethod) {
writeMethod.serialize(this);
writeMethod.serialize(this, value);
return;
}
writeMethod(this);
writeMethod(this, value);
}
toBytes(): Uint8Array {
return new Uint8Array(this.buffer.buffer.slice(this.buffer.byteOffset, this.buffer.byteOffset + this.index));
}
}

View file

@ -1,49 +1,213 @@
import Peer from "peerjs";
import { v4 } from "uuid";
import { readHello } from "./protocol/hello";
import { BinaryStreamReader } from "./binary/readerStream";
import { ReadKind, readEvent } from "./protocol/serverEvent";
import { AudioManager } from "./audio.svelte";
import { type WriteEvent, writeEvent, WriteKind } from "./protocol/clientEvent";
import { BinaryWriter } from "./binary/writer";
export class Client {
static async connect() {
static async connect(audioManager: AudioManager, outputAudio: HTMLDivElement) {
const transport = new WebTransport(`https://${location.hostname}:4433`, { allowPooling: false });
await transport.ready;
console.log("ready!");
const stream = await transport.createBidirectionalStream();
const peer = new Peer();
await new Promise((res) => {
peer.on("open", () => {
console.log("open!", peer.id)
res(void 0);
});
});
await stream.writable.getWriter().write(new TextEncoder().encode(peer.id));
const uuid = v4();
const writer = stream.writable.getWriter();
await writer.write(new TextEncoder().encode(uuid));
const reader = BinaryStreamReader.fromReader(stream.readable.getReader());
const players = $state(await readHello(reader));
const { players, peers } = await readHello(reader);
console.log("done reading hello");
return new Client(players, peer, transport, stream);
const playersBind = $state(players);
const client = new Client(audioManager, outputAudio, playersBind, uuid, transport, stream, writer);
for (const id of peers) {
client.getPeer(id).offer();
}
// peer.on("call", (conn) => {
// conn.answer(audioManager.output);
// client.peers[conn.connectionId] = new Peer(conn.connectionId, outputAudio, conn)
// });
console.log("done setup, calling handlers");
client.datagramEventHandler();
client.streamEventHandler(reader);
client.closeHandler();
return client;
}
private async closeHandler() {
await this.transport.closed;
console.error("closed");
}
private async datagramEventHandler() {
const dgramReader = this.transport.datagrams.readable.getReader();
this.eventHandler(async () => {
const dgram = await dgramReader.read();
console.log(dgram);
throw dgram;
})
}
private getPeer(id: string) {
if (this.peers[id] !== undefined) {
return this.peers[id];
}
return this.peers[id] = new Peer(id, this);
}
async writeEvent(event: WriteEvent) {
const binaryWriter = new BinaryWriter(0x100);
binaryWriter.write(event, writeEvent);
await this.writer.write(binaryWriter.toBytes());
}
private async streamEventHandler(reader: BinaryStreamReader) {
this.eventHandler(() => readEvent(reader))
}
private async eventHandler(eventSource: () => ReturnType<typeof readEvent>) {
while (true) {
const event = await eventSource();
console.log("got event", event);
switch (event.kind) {
case ReadKind.Connected: {
console.log("hello", event.name);
this.players[event.id] = new Player(event.name, event.id)
break;
}
case ReadKind.Disconnected: {
console.log("goodbye", event.id);
delete this.players[event.id];
break;
}
case ReadKind.Moved: {
if (this.players[event.id] !== undefined)
this.players[event.id].position = event.position;
break;
}
case ReadKind.StageChanged: {
console.log("goodbye", event.id);
break;
}
case ReadKind.PeerConnectionChanged: {
console.log("peer changed", event.id, event.connected);
// if (event.connected) {
// this.getPeer(event.id)
// }
break;
}
case ReadKind.Offer: {
this.getPeer(event.id).gotOffer(event.offerSdp);
break;
}
case ReadKind.Answer: {
this.getPeer(event.id).gotAnswer(event.answerSdp);
break;
}
case ReadKind.IceCandidate: {
this.getPeer(event.id).gotCandidate(event.candidate);
break;
}
}
}
}
private peers: Record<string, Peer> = $state({});
private constructor(
public players: Player[],
private peer: Peer,
private transport: WebTransport,
private stream: WebTransportBidirectionalStream
public audioManager: AudioManager,
public outputAudio: HTMLDivElement,
public players: Record<number, Player>,
public uuid: string,
public transport: WebTransport,
public stream: WebTransportBidirectionalStream,
public writer: WritableStreamDefaultWriter,
) {
}
get id() {
return this.peer.id;
}
closed() {
// return transport.closed;
return this.transport.closed;
}
}
export class Player {
constructor(public name: string, public id: number, public position: number[], public stage: string) { }
constructor(public name: string, public id: number, public position: number[] = [0, 0, 0], public stage: string = "nostage" + Math.random()) { }
}
export class Peer {
private connection: RTCPeerConnection;
private audel: HTMLAudioElement;
private state: string = $state("not connected");
constructor(public peerId: string, public client: Client) {
this.connection = new RTCPeerConnection({
iceServers: [
{
urls: ["stun:stun.l.google.com:19302"],
}
]
});
for (const track of this.client.audioManager.output.getAudioTracks()) {
this.connection.addTrack(track, this.client.audioManager.output);
}
this.connection.addEventListener("iceconnectionstatechange", (state) => {
console.log(this.connection.iceConnectionState);
this.state = this.connection.iceConnectionState;
});
this.connection.addEventListener("icecandidate", (event) => {
if (event.candidate)
this.client.writeEvent({ kind: WriteKind.IceCandidate, to: this.peerId, candidate: event.candidate });
else
console.log(this.peerId, "finished candidate gathering")
});
this.audel = document.createElement("audio");
this.connection.addEventListener("track", (ev) => {
this.audel.srcObject = ev.streams[0];
this.audel.autoplay = true;
this.audel.controls = true;
this.client.outputAudio.appendChild(this.audel);
console.log('got track :)');
});
}
public async offer() {
console.log('offering!');
const offer = await this.connection.createOffer({ offerToReceiveAudio: true });
await this.connection.setLocalDescription(offer);
this.client.writeEvent({
kind: WriteKind.Offer,
to: this.peerId,
offerSdp: offer.sdp ?? throwExpr("it didn't create an offer???"),
});
}
public async gotOffer(offer: string) {
console.log('got offer!');
await this.connection.setRemoteDescription(new RTCSessionDescription({ type: "offer", sdp: offer }));
const answer = await this.connection.createAnswer({ offerToReceiveAudio: true });
await this.connection.setLocalDescription(answer);
this.client.writeEvent({
kind: WriteKind.Answer,
to: this.peerId,
answerSdp: answer.sdp ?? throwExpr("it didn't create an offer???"),
});
}
public async gotAnswer(answer: string) {
console.log('got answer!');
await this.connection.setRemoteDescription(new RTCSessionDescription({ type: "answer", sdp: answer }));
}
public async gotCandidate(candidate: RTCIceCandidateInit) {
console.log('got candidate...');
await this.connection.addIceCandidate(candidate)
}
}
function throwExpr(...args: Parameters<ErrorConstructor>): never {
throw new Error(...args);
}

View file

@ -0,0 +1,30 @@
import { BinaryWriter } from "../binary/writer";
export enum WriteKind {
Offer,
Answer,
IceCandidate,
}
export type WriteEvent = {
kind: WriteKind.IceCandidate,
to: string,
candidate: RTCIceCandidate
} | {
kind: WriteKind.Offer,
to: string,
offerSdp: string
} | {
kind: WriteKind.Answer,
to: string,
answerSdp: string
}
export function writeEvent(writer: BinaryWriter, event: WriteEvent) {
writer.writeUInt8(event.kind);
writer.writeFixedString(36, event.to);
switch (event.kind) {
case WriteKind.Offer: writer.writePrefixedString(event.offerSdp); return;
case WriteKind.Answer: writer.writePrefixedString(event.answerSdp); return;
case WriteKind.IceCandidate: writer.writePrefixedString(JSON.stringify(event.candidate.toJSON())); return;
default: throw new Error("kind not good");
}
}

View file

@ -2,23 +2,24 @@ import { BinaryStreamReader } from "../binary/readerStream";
import { Player } from "../client.svelte";
export async function readHello(reader: BinaryStreamReader) {
console.log("reading");
const count = await reader.readUInt8();
console.log("reading", count);
const players = await reader.readArray(count, async (reader) => {
const name = await reader.readFixedString(0x20);
console.log(name);
const players: Record<number, Player> = {};
for (let i = 0; i < count; i++) {
const id = await reader.readUInt32();
const position = await reader.readArray(3, async reader => await reader.readFloat32());
const name = await reader.readFixedString(0x20);
const stage = await reader.readFixedString(0x40);
const position = await reader.readArray(3, async reader => await reader.readFloat32());
return new Player(
players[id] = new Player(
name,
id,
position,
stage,
);
};
const peers = await reader.readArray(await reader.readUInt32(), async (reader) => {
return await reader.readFixedString(36);
});
return players;
return { players, peers };
}

View file

@ -0,0 +1,59 @@
import { BinaryStreamReader } from "../binary/readerStream";
export enum ReadKind {
Connected = 0,
Disconnected = 1,
Moved = 2,
StageChanged = 3,
PeerConnectionChanged = 4,
Offer = 5,
Answer = 6,
IceCandidate = 7,
}
export async function readEvent(reader: BinaryStreamReader) {
const kind = await reader.readUInt8();
switch (kind) {
case ReadKind.Connected: {
const id = await reader.readUInt32();
const name = await reader.readFixedString(0x20);
return { kind, id, name };
}
case ReadKind.Disconnected: {
const id = await reader.readUInt32();
return { kind, id };
}
case ReadKind.Moved: {
const id = await reader.readUInt32();
const position = await reader.readArray(3, async reader => reader.readFloat32());
return { kind, id, position };
}
case ReadKind.StageChanged: {
const id = await reader.readUInt32();
const stage = await reader.readFixedString(0x40);
return { kind, id, stage };
}
case ReadKind.PeerConnectionChanged: {
const id = await reader.readFixedString(36);
const connected = await reader.readBoolean();
return { kind, id, connected };
}
case ReadKind.Offer: {
const id = await reader.readFixedString(36);
const offerSdp = await reader.readString(await reader.readUInt32());
return { kind, id, offerSdp }
}
case ReadKind.Answer: {
const id = await reader.readFixedString(36);
const answerSdp = await reader.readString(await reader.readUInt32());
return { kind, id, answerSdp }
}
case ReadKind.IceCandidate: {
const id = await reader.readFixedString(36);
const candidate: RTCIceCandidateInit = JSON.parse(await reader.readString(await reader.readUInt32()));
return { kind, id, candidate }
}
default:
throw new Error(`unsupported kind ${kind}`)
}
}

View file

@ -1,33 +1,76 @@
<script lang="ts">
import { onMount } from "svelte";
import { Client } from "$lib/client.svelte.ts";
import { AudioManager } from "$lib/audio.svelte.ts";
import { BinaryStreamReader } from "$lib/binary/readerStream";
let name = $state("none");
let client: Client = undefined as any;
let players = $derived(client?.players ?? []);
let name = $state("");
let useMusic = $state(false);
let client: Client = $state(undefined as any);
let players = $derived.by(() => {
if (client === undefined) return {};
return client.players;
});
let ac: AudioManager;
let wyrm: HTMLAudioElement;
let outputAudio: HTMLDivElement;
async function a() {
client = await Client.connect();
name = localStorage.getItem("name") ?? "";
useMusic = localStorage.getItem("useMusic") == "true";
}
async function audioPrep() {
let media = useMusic
? wyrm
: await navigator.mediaDevices.getUserMedia({
audio: true,
video: false,
});
ac = new AudioManager(media);
}
async function connect() {
await audioPrep();
client = await Client.connect(ac, outputAudio);
console.log(client);
}
onMount(a);
</script>
<div bind:this={outputAudio} />
<div>
I am <input type="text" maxlength="32" list="connectedPlayers" />
</div>
<datalist>
{#each players as player}
<option>{player.name}</option>
{/each}
</datalist>
<div>
Connected players - {players.length}<br />
{#each players as player}
<pre class="name">{player.name}</pre>
{/each}
I am <input
type="text"
maxlength="32"
list="connectedPlayers"
bind:value={name}
onchange={() => localStorage.setItem("name", name)}
/>
<label for="useMusic">Use Music</label>
<input
id="useMusic"
type="checkbox"
bind:checked={useMusic}
onchange={() => localStorage.setItem("useMusic", useMusic)}
/>
<button onclick={() => connect()}>Connect</button>
<audio bind:this={wyrm} src="/wyrmjewelbox.wav" autoplay={false} controls
>audio of the wyrm's houseki box</audio
>
</div>
{#if client !== undefined}
<datalist>
{#each players as player}
<option>{player.name}</option>
{/each}
</datalist>
<div>
Connected players - {players.length}<br />
{#each players as player}
<pre class="name">{player.name}</pre>
{/each}
</div>
{/if}
<style>
.name {

BIN
static/wyrmjewelbox.wav Executable file

Binary file not shown.