diff --git a/package.json b/package.json index f36b8cb..2ad83e0 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,6 @@ "vite": "^6.0.0" }, "dependencies": { - "peerjs": "^1.5.4" + "uuid": "^11.0.3" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c0ab125..09ea658 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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: {} diff --git a/src/lib/audio.svelte.ts b/src/lib/audio.svelte.ts new file mode 100644 index 0000000..b555e8b --- /dev/null +++ b/src/lib/audio.svelte.ts @@ -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); + } +} diff --git a/src/lib/binary/reader.ts b/src/lib/binary/reader.ts index d7dc22b..d535ee8 100644 --- a/src/lib/binary/reader.ts +++ b/src/lib/binary/reader.ts @@ -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(size: number, readMethod: ReadMethod | AsyncReadMethod): Promise { -// const array = new Array(size); + async readFixedString(length: number, encoding?: string): Promise { + 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 { -// 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(size: number, readMethod: ReadMethod): Promise { + const array = new Array(size); -// async read(readMethod: ReadMethod | AsyncReadMethod): Promise { -// 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 { -// return this.readUInt8() as T; -// } + async read(readMethod: ReadMethod): Promise { + if ("deserialize" in readMethod) { + return await readMethod.deserialize(this); + } -// readEnum(): T { -// return this.readUInt16() as T; -// } - -// readDate(): Date { -// return new Date(Number(this.readUInt64())); -// } - -// async readMappedEnum | AsyncReadMethod>>(mappedType: R): Promise | AsyncReadMethodReturnValue> { -// return await this.read(mappedType[this.readEnum()]); -// } - -// async readMappedShortEnum | AsyncReadMethod>>(mappedType: R): Promise | AsyncReadMethodReturnValue> { -// return await this.read(mappedType[this.readShortEnum()]); -// } -// } + return await readMethod(this); + } +} diff --git a/src/lib/binary/readerStream.ts b/src/lib/binary/readerStream.ts index 8e9cd87..fffd6bb 100644 --- a/src/lib/binary/readerStream.ts +++ b/src/lib/binary/readerStream.ts @@ -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 { - return (await this.bytes(2)).getInt16(0); + return (await this.bytes(2)).getInt16(0, true); } async readUInt16(): Promise { - return (await this.bytes(2)).getUint16(0); + return (await this.bytes(2)).getUint16(0, true); } async readSInt32(): Promise { - return (await this.bytes(4)).getInt32(0); + return (await this.bytes(4)).getInt32(0, true); } async readUInt32(): Promise { - return (await this.bytes(4)).getUint32(0); + return (await this.bytes(4)).getUint32(0, true); } async readSInt64(): Promise { - return (await this.bytes(8)).getBigInt64(0); + return (await this.bytes(8)).getBigInt64(0, true); } async readUInt64(): Promise { - return (await this.bytes(8)).getBigUint64(0); + return (await this.bytes(8)).getBigUint64(0, true); } async readFloat32(): Promise { - return (await this.bytes(4)).getFloat32(0); + return (await this.bytes(4)).getFloat32(0, true); } async readFloat64(): Promise { - return (await this.bytes(8)).getFloat64(0); + return (await this.bytes(8)).getFloat64(0, true); } async readString(length: number, encoding?: string): Promise { @@ -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(size: number, readMethod: AsyncReadMethod): Promise { diff --git a/src/lib/binary/types.ts b/src/lib/binary/types.ts index f0a30ff..808ae9a 100644 --- a/src/lib/binary/types.ts +++ b/src/lib/binary/types.ts @@ -1,9 +1,9 @@ type Method = ((reader: R) => T) | { [key in F]: (reader: R) => T }; - export type ReadMethod = Method; -export type WriteMethod = Method; export type AsyncReadMethod = Method, R, "deserialize">; +export type WriteMethod = ((writer: W, value: T) => void) | { serialize: (writer: W, value: T) => void }; + export type ReadMethodReturnValue = X extends ReadMethod ? (Y extends Promise ? Z : Y) : never; export enum ObjectValueType { diff --git a/src/lib/binary/writer.ts b/src/lib/binary/writer.ts index 2a3bece..c42cfe7 100644 --- a/src/lib/binary/writer.ts +++ b/src/lib/binary/writer.ts @@ -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[]): 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(array: T[], method: WriteMethod): void { for (let i = 0; i < array.length; i++) { - this.write(array[i]); + this.write(array[i], method); } } - write(writeMethod: WriteMethod): void { + write(value: T, writeMethod: WriteMethod): 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)); } } diff --git a/src/lib/client.svelte.ts b/src/lib/client.svelte.ts index ac6d191..06f7152 100644 --- a/src/lib/client.svelte.ts +++ b/src/lib/client.svelte.ts @@ -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) { + 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 = $state({}); + private constructor( - public players: Player[], - private peer: Peer, - private transport: WebTransport, - private stream: WebTransportBidirectionalStream + public audioManager: AudioManager, + public outputAudio: HTMLDivElement, + public players: Record, + 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): never { + throw new Error(...args); } diff --git a/src/lib/protocol/clientEvent.ts b/src/lib/protocol/clientEvent.ts new file mode 100644 index 0000000..d667727 --- /dev/null +++ b/src/lib/protocol/clientEvent.ts @@ -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"); + } +} diff --git a/src/lib/protocol/hello.ts b/src/lib/protocol/hello.ts index 273eed3..f0321c9 100644 --- a/src/lib/protocol/hello.ts +++ b/src/lib/protocol/hello.ts @@ -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 = {}; + 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 }; } diff --git a/src/lib/protocol/serverEvent.ts b/src/lib/protocol/serverEvent.ts new file mode 100644 index 0000000..41e4326 --- /dev/null +++ b/src/lib/protocol/serverEvent.ts @@ -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}`) + } +} diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 70639d0..85f602d 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -1,33 +1,76 @@ +
+
- I am -
- - {#each players as player} - - {/each} - -
- Connected players - {players.length}
- {#each players as player} -
{player.name}
- {/each} + I am localStorage.setItem("name", name)} + /> + + localStorage.setItem("useMusic", useMusic)} + /> + +
+{#if client !== undefined} + + {#each players as player} + + {/each} + +
+ Connected players - {players.length}
+ {#each players as player} +
{player.name}
+ {/each} +
+{/if}