the proximity in proximity chat

This commit is contained in:
Aubrey 2024-12-21 05:21:14 -06:00
parent a1dbfadfb0
commit 30fe1ae80b
No known key found for this signature in database
14 changed files with 516 additions and 29508 deletions

85
flake.lock Normal file
View file

@ -0,0 +1,85 @@
{
"nodes": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1734424634,
"narHash": "sha256-cHar1vqHOOyC7f1+tVycPoWTfKIaqkoe1Q6TnKzuti4=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "d3c42f187194c26d9f0309a8ecc469d6c878ce33",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"pnpm2nix-nzbr": {
"inputs": {
"flake-utils": [
"flake-utils"
],
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1706694632,
"narHash": "sha256-ytyTwNPiUR8aq74QlxFI+Wv3MyvXz5POO1xZxQIoi0c=",
"owner": "nzbr",
"repo": "pnpm2nix-nzbr",
"rev": "0366b7344171accc2522525710e52a8abbf03579",
"type": "github"
},
"original": {
"owner": "nzbr",
"repo": "pnpm2nix-nzbr",
"type": "github"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs",
"pnpm2nix-nzbr": "pnpm2nix-nzbr"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

46
flake.nix Normal file
View file

@ -0,0 +1,46 @@
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
flake-utils.url = "github:numtide/flake-utils";
pnpm2nix-nzbr = {
url = "github:nzbr/pnpm2nix-nzbr";
inputs = {
nixpkgs.follows = "nixpkgs";
flake-utils.follows = "flake-utils";
};
};
};
outputs = {
self,
nixpkgs,
flake-utils,
pnpm2nix-nzbr
}:
flake-utils.lib.eachDefaultSystem
(
system: let
overlays = [pnpm2nix-nzbr.overlays.default];
pkgs = import nixpkgs {
inherit system overlays;
};
in
with pkgs; {
devShells.default = mkShell {
buildInputs = [
pkgs.fenix.stable.completeToolchain
pkg-config
openssl
];
};
formatter = pkgs.alejandra;
packages.default = with pkgs; mkPnpmPackage {
pname = "proximity-chat";
version = "0.0.1";
src = ./.;
installInPlace = true;
};
}
);
}

View file

@ -1,34 +0,0 @@
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);
}
}

90
src/lib/audio.ts Normal file
View file

@ -0,0 +1,90 @@
import { derived, type Readable, type Unsubscriber, writable } from "svelte/store";
import { Player } from "./client";
export class AudioManager {
globalGain = writable(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;
}
this.gain.gain.value /= 32;
this.source.connect(this.gain).connect(this.dest)
this.output = this.dest.stream;
}
createPlayerAudio(outputDiv: HTMLDivElement, localPlayer: Readable<Player | undefined>, peerPlayer: Readable<Player | undefined>) {
const panner = this.ac.createPanner();
panner.distanceModel = "linear";
panner.refDistance = 1500;
panner.maxDistance = 2500;
const gain = this.ac.createGain();
const dest = this.ac.createMediaStreamDestination();
gain.connect(panner).connect(dest);
let audio = document.createElement("audio");
audio.srcObject = dest.stream;
audio.autoplay = true;
audio.controls = false;
outputDiv.appendChild(audio);
return new PlayerAudio(this.ac, panner, gain, localPlayer, peerPlayer);
}
}
export class PlayerAudio {
private source: MediaStreamAudioSourceNode | undefined;
private playerWatcher: Readable<[number[], boolean]>;
constructor(
private context: AudioContext,
private panner: PannerNode,
private gain: GainNode,
localPlayer: Readable<Player | undefined>,
peerPlayer: Readable<Player | undefined>
) {
this.playerWatcher = derived([localPlayer, peerPlayer], ([localPlayer, peerPlayer], set) => {
if (!localPlayer || !peerPlayer) {
console.log("got player binding")
set([[0, 0, 0], false]);
return () => { };
}
return derived([localPlayer.stageInfo, peerPlayer.stageInfo], ([localInfo, peerInfo]): [number[], boolean] => {
return [
localInfo.position.map((localPosition, i) => peerInfo.position[i] - localPosition),
localInfo.stage === peerInfo.stage
];
}).subscribe(set);
});
this.playerWatcher.subscribe(([[x, y, z], sameStage]) => {
this.panner.positionX.value = x;
this.panner.positionY.value = y;
this.panner.positionZ.value = z;
this.gain.gain.value = +sameStage;
});
}
gotTrack(stream: MediaStream) {
this.source = this.context.createMediaStreamSource(stream);
this.source.connect(this.gain);
}
}

View file

@ -1,4 +1,4 @@
import { ReadMethod } from "./types"; import type { ReadMethod } from "./types";
export class BinaryReader { export class BinaryReader {
protected index: number = 0; protected index: number = 0;

View file

@ -1,13 +1,16 @@
import { v4 } from "uuid"; import { v4 } from "uuid";
import { derived, get, type Readable, type Writable, writable } from "svelte/store";
import { readHello } from "./protocol/hello"; import { readHello } from "./protocol/hello";
import { BinaryStreamReader } from "./binary/readerStream"; import { BinaryStreamReader } from "./binary/readerStream";
import { ReadKind, readEvent } from "./protocol/serverEvent"; import { ReadKind, readEvent } from "./protocol/serverEvent";
import { AudioManager } from "./audio.svelte"; import { AudioManager, PlayerAudio } from "./audio";
import { type WriteEvent, writeEvent, WriteKind } from "./protocol/clientEvent"; import { type WriteEvent, writeEvent, WriteKind } from "./protocol/clientEvent";
import { BinaryWriter } from "./binary/writer"; import { BinaryWriter } from "./binary/writer";
import { cached, type CachedWritable, type Cached, cachedWritable } from "./stores";
import { BinaryReader } from "./binary/reader";
export class Client { export class Client {
static async connect(audioManager: AudioManager, outputAudio: HTMLDivElement) { static async connect(name: Readable<string>, audioManager: AudioManager, outputAudio: HTMLDivElement) {
const transport = new WebTransport(`https://${location.hostname}:4433`, { allowPooling: false }); const transport = new WebTransport(`https://${location.hostname}:4433`, { allowPooling: false });
await transport.ready; await transport.ready;
console.log("ready!"); console.log("ready!");
@ -16,22 +19,23 @@ export class Client {
const uuid = v4(); const uuid = v4();
const writer = stream.writable.getWriter(); const writer = stream.writable.getWriter();
await writer.write(new TextEncoder().encode(uuid)); await writer.write(new TextEncoder().encode(uuid));
const array = new Uint8Array(0x20);
new TextEncoder().encodeInto(get(name), array);
await writer.write(array);
const reader = BinaryStreamReader.fromReader(stream.readable.getReader()); const reader = BinaryStreamReader.fromReader(stream.readable.getReader());
const { players, peers } = await readHello(reader); const { players, peers } = await readHello(reader);
console.log("done reading hello"); console.log("done reading hello");
const playersBind = $state(players); const client = new Client(audioManager, outputAudio, name, cachedWritable(players), uuid, transport, stream, writer);
const client = new Client(audioManager, outputAudio, playersBind, uuid, transport, stream, writer); globalThis.client = client;
for (const id of peers) { for (const [id, name] of peers) {
client.getPeer(id).offer(); const peer = client.getPeer(id);
peer.targetPlayerName.set(name);
peer.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"); console.log("done setup, calling handlers");
client.datagramEventHandler(); client.datagramEventHandler();
client.streamEventHandler(reader); client.streamEventHandler(reader);
@ -45,15 +49,6 @@ export class Client {
console.error("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) { private getPeer(id: string) {
if (this.peers[id] !== undefined) { if (this.peers[id] !== undefined) {
return this.peers[id]; return this.peers[id];
@ -67,38 +62,56 @@ export class Client {
await this.writer.write(binaryWriter.toBytes()); await this.writer.write(binaryWriter.toBytes());
} }
private async datagramEventHandler() {
const dgramReader = this.transport.datagrams.readable.getReader();
this.eventHandler(async () => {
const dgram = await dgramReader.read();
return readEvent(new BinaryReader(new DataView(dgram.value.buffer)));
})
}
private async streamEventHandler(reader: BinaryStreamReader) { private async streamEventHandler(reader: BinaryStreamReader) {
this.eventHandler(() => readEvent(reader)) this.eventHandler(() => readEvent(reader))
} }
private async eventHandler(eventSource: () => ReturnType<typeof readEvent>) { private async eventHandler(eventSource: () => ReturnType<typeof readEvent>) {
while (true) { while (true) {
const event = await eventSource(); const event = await eventSource();
console.log("got event", event); // console.log("got event", event);
switch (event.kind) { switch (event.kind) {
case ReadKind.Connected: { case ReadKind.Connected: {
console.log("hello", event.name); console.log("hello", event.name);
this.players[event.id] = new Player(event.name, event.id) this.players.update(x => Object.assign(x, { [event.id]: new Player(event.name, event.id) }))
break; break;
} }
case ReadKind.Disconnected: { case ReadKind.Disconnected: {
console.log("goodbye", event.id); console.log("goodbye", event.id);
delete this.players[event.id]; delete this.players[event.id];
this.players.update(x => {
delete x[event.id];
return x;
})
break;
break; break;
} }
case ReadKind.Moved: { case ReadKind.Moved: {
if (this.players[event.id] !== undefined) const player = this.players.get()[event.id];
this.players[event.id].position = event.position; if (player)
player.stageInfo.update(info => Object.assign(info, { position: event.position }));
break; break;
} }
case ReadKind.StageChanged: { case ReadKind.StageChanged: {
console.log("goodbye", event.id); console.log("goodbye", event.id);
const player = this.players.get()[event.id];
if (player)
player.stageInfo.update(info => Object.assign(info, { stage: event.stage }));
break; break;
} }
case ReadKind.PeerConnectionChanged: { case ReadKind.PeerConnectionChanged: {
console.log("peer changed", event.id, event.connected); console.log("peer changed", event.id, event.connected, event.target);
// if (event.connected) { if (event.connected)
// this.getPeer(event.id) this.getPeer(event.id).targetPlayerName.set(event.target);
// } else
delete this.peers[event.id];
break; break;
} }
case ReadKind.Offer: { case ReadKind.Offer: {
@ -117,17 +130,25 @@ export class Client {
} }
} }
private peers: Record<string, Peer> = $state({}); private peers: Record<string, Peer> = {};
public boundPlayer: Readable<Player | undefined>;
public playersByName: Cached<Record<string, Player>>;
private constructor( private constructor(
public audioManager: AudioManager, public audioManager: AudioManager,
public outputAudio: HTMLDivElement, public outputAudio: HTMLDivElement,
public players: Record<number, Player>, boundPlayerName: Readable<string>,
public players: CachedWritable<Record<number, Player>>,
public uuid: string, public uuid: string,
public transport: WebTransport, public transport: WebTransport,
public stream: WebTransportBidirectionalStream, public stream: WebTransportBidirectionalStream,
public writer: WritableStreamDefaultWriter, public writer: WritableStreamDefaultWriter,
) { ) {
this.playersByName = cached(this.players, (players) => {
console.log("players by name derivation", Object.values(players), Object.fromEntries(Object.values(players).map(x => [x.name, x])))
return Object.fromEntries(Object.values(players).map(x => [x.name, x]))
})
this.boundPlayer = derived([boundPlayerName, this.playersByName], ([playerName, players]) => players[playerName]);
} }
closed() { closed() {
@ -136,13 +157,19 @@ export class Client {
} }
export class Player { export class Player {
constructor(public name: string, public id: number, public position: number[] = [0, 0, 0], public stage: string = "nostage" + Math.random()) { } public stageInfo: Writable<{ position: number[], stage: string }>;
constructor(public name: string, public id: number, stageInfo?: { position: number[], stage: string }) {
this.stageInfo = writable(Object.assign({ position: [0, 0, 0], stage: "nostage" + Math.random() }, stageInfo ?? {}));
}
} }
export class Peer { export class Peer {
private connection: RTCPeerConnection; private connection: RTCPeerConnection;
private audel: HTMLAudioElement; private playerAudio: PlayerAudio;
private state: string = $state("not connected"); public targetPlayerName: Writable<string>;
private targetPlayer: Readable<Player | undefined>;
private state: Writable<string> = writable("not connected");
constructor(public peerId: string, public client: Client) { constructor(public peerId: string, public client: Client) {
this.connection = new RTCPeerConnection({ this.connection = new RTCPeerConnection({
iceServers: [ iceServers: [
@ -151,12 +178,15 @@ export class Peer {
} }
] ]
}); });
this.targetPlayerName = writable("");
this.targetPlayer = derived([this.targetPlayerName, this.client.playersByName], ([playerName, players]) => players[playerName]);
this.playerAudio = this.client.audioManager.createPlayerAudio(this.client.outputAudio, this.client.boundPlayer, this.targetPlayer);
for (const track of this.client.audioManager.output.getAudioTracks()) { for (const track of this.client.audioManager.output.getAudioTracks()) {
this.connection.addTrack(track, this.client.audioManager.output); this.connection.addTrack(track, this.client.audioManager.output);
} }
this.connection.addEventListener("iceconnectionstatechange", (state) => { this.connection.addEventListener("iceconnectionstatechange", (state) => {
console.log(this.connection.iceConnectionState); console.log(this.connection.iceConnectionState);
this.state = this.connection.iceConnectionState; this.state.set(this.connection.iceConnectionState);
}); });
this.connection.addEventListener("icecandidate", (event) => { this.connection.addEventListener("icecandidate", (event) => {
if (event.candidate) if (event.candidate)
@ -164,18 +194,12 @@ export class Peer {
else else
console.log(this.peerId, "finished candidate gathering") console.log(this.peerId, "finished candidate gathering")
}); });
this.audel = document.createElement("audio");
this.connection.addEventListener("track", (ev) => { this.connection.addEventListener("track", (ev) => {
this.audel.srcObject = ev.streams[0]; this.playerAudio.gotTrack(ev.streams[0]);
this.audel.autoplay = true;
this.audel.controls = true;
this.client.outputAudio.appendChild(this.audel);
console.log('got track :)');
}); });
} }
public async offer() { public async offer() {
console.log('offering!');
const offer = await this.connection.createOffer({ offerToReceiveAudio: true }); const offer = await this.connection.createOffer({ offerToReceiveAudio: true });
await this.connection.setLocalDescription(offer); await this.connection.setLocalDescription(offer);
this.client.writeEvent({ this.client.writeEvent({
@ -186,7 +210,6 @@ export class Peer {
} }
public async gotOffer(offer: string) { public async gotOffer(offer: string) {
console.log('got offer!');
await this.connection.setRemoteDescription(new RTCSessionDescription({ type: "offer", sdp: offer })); await this.connection.setRemoteDescription(new RTCSessionDescription({ type: "offer", sdp: offer }));
const answer = await this.connection.createAnswer({ offerToReceiveAudio: true }); const answer = await this.connection.createAnswer({ offerToReceiveAudio: true });
await this.connection.setLocalDescription(answer); await this.connection.setLocalDescription(answer);
@ -199,11 +222,9 @@ export class Peer {
} }
public async gotAnswer(answer: string) { public async gotAnswer(answer: string) {
console.log('got answer!');
await this.connection.setRemoteDescription(new RTCSessionDescription({ type: "answer", sdp: answer })); await this.connection.setRemoteDescription(new RTCSessionDescription({ type: "answer", sdp: answer }));
} }
public async gotCandidate(candidate: RTCIceCandidateInit) { public async gotCandidate(candidate: RTCIceCandidateInit) {
console.log('got candidate...');
await this.connection.addIceCandidate(candidate) await this.connection.addIceCandidate(candidate)
} }
} }

View file

@ -4,6 +4,7 @@ export enum WriteKind {
Offer, Offer,
Answer, Answer,
IceCandidate, IceCandidate,
TargetChanged,
} }
export type WriteEvent = { export type WriteEvent = {
kind: WriteKind.IceCandidate, kind: WriteKind.IceCandidate,
@ -17,9 +18,17 @@ export type WriteEvent = {
kind: WriteKind.Answer, kind: WriteKind.Answer,
to: string, to: string,
answerSdp: string answerSdp: string
} | {
kind: WriteKind.TargetChanged,
name: string
} }
export function writeEvent(writer: BinaryWriter, event: WriteEvent) { export function writeEvent(writer: BinaryWriter, event: WriteEvent) {
writer.writeUInt8(event.kind); writer.writeUInt8(event.kind);
if (event.kind === WriteKind.TargetChanged) {
writer.writeFixedString(0x20, event.name);
return;
}
writer.writeFixedString(36, event.to); writer.writeFixedString(36, event.to);
switch (event.kind) { switch (event.kind) {
case WriteKind.Offer: writer.writePrefixedString(event.offerSdp); return; case WriteKind.Offer: writer.writePrefixedString(event.offerSdp); return;

View file

@ -1,5 +1,5 @@
import { BinaryStreamReader } from "../binary/readerStream"; import { BinaryStreamReader } from "../binary/readerStream";
import { Player } from "../client.svelte"; import { Player } from "../client";
export async function readHello(reader: BinaryStreamReader) { export async function readHello(reader: BinaryStreamReader) {
const count = await reader.readUInt8(); const count = await reader.readUInt8();
@ -13,12 +13,11 @@ export async function readHello(reader: BinaryStreamReader) {
players[id] = new Player( players[id] = new Player(
name, name,
id, id,
position, { position, stage },
stage,
); );
}; };
const peers = await reader.readArray(await reader.readUInt32(), async (reader) => { const peers = await reader.readArray(await reader.readUInt32(), async (reader): Promise<[string, string]> => {
return await reader.readFixedString(36); return [await reader.readFixedString(36), await reader.readFixedString(0x20)];
}); });
return { players, peers }; return { players, peers };

View file

@ -1,4 +1,5 @@
import { BinaryStreamReader } from "../binary/readerStream"; import { BinaryStreamReader } from "../binary/readerStream";
import { BinaryReader } from "../binary/reader";
export enum ReadKind { export enum ReadKind {
Connected = 0, Connected = 0,
@ -11,7 +12,7 @@ export enum ReadKind {
IceCandidate = 7, IceCandidate = 7,
} }
export async function readEvent(reader: BinaryStreamReader) { export async function readEvent(reader: BinaryReader | BinaryStreamReader) {
const kind = await reader.readUInt8(); const kind = await reader.readUInt8();
switch (kind) { switch (kind) {
case ReadKind.Connected: { case ReadKind.Connected: {
@ -36,7 +37,8 @@ export async function readEvent(reader: BinaryStreamReader) {
case ReadKind.PeerConnectionChanged: { case ReadKind.PeerConnectionChanged: {
const id = await reader.readFixedString(36); const id = await reader.readFixedString(36);
const connected = await reader.readBoolean(); const connected = await reader.readBoolean();
return { kind, id, connected }; const target = await reader.readFixedString(0x20);
return { kind, id, connected, target };
} }
case ReadKind.Offer: { case ReadKind.Offer: {
const id = await reader.readFixedString(36); const id = await reader.readFixedString(36);

72
src/lib/stores.ts Normal file
View file

@ -0,0 +1,72 @@
import { writable, type Writable, type Readable, derived } from "svelte/store";
export function localStore<T>(key: string, defaultValue: T, initialConverter: (value: string) => T, stringifier: (value: T) => string): Writable<T> {
let baseWritable = writable(defaultValue);
if (globalThis.localStorage) {
let value = localStorage.getItem(key);
if (value !== null) baseWritable.set(initialConverter(value));
else baseWritable.set(defaultValue);
}
return {
set(value) {
if (globalThis.localStorage)
localStorage.setItem(key, stringifier(value));
return baseWritable.set(value);
},
subscribe(run, invalidate) {
return baseWritable.subscribe(run, invalidate);
},
update(updater) {
baseWritable.update((value) => {
const newValue = updater(value);
if (globalThis.localStorage)
localStorage.setItem(key, stringifier(newValue));
return newValue;
})
}
}
}
export function identity<T>(value: T) { return value; }
/** One or more `Readable`s. */
type Stores = Readable<any> | [Readable<any>, ...Array<Readable<any>>] | Array<Readable<any>>;
/** One or more values from `Readable` stores. */
type StoresValues<T> =
T extends Readable<infer U> ? U : { [K in keyof T]: T[K] extends Readable<infer U> ? U : never };
export type Cached<T> = Readable<T> & { v: T };
/**
* Caches the value of a store for later access.
*/
export function cached<S extends Stores, T>(stores: S, updater: (values: StoresValues<S>) => T) {
let store: Cached<T> = derived(stores, updater) as any;
store.subscribe((value) => store.v = value);
return store
}
export type CachedWritable<T> = Writable<T> & { get(): T };
export function cachedWritable<T>(initial: T): CachedWritable<T> {
const store = writable(initial);
let current = initial;
return {
get() {
return current;
},
set(value) {
current = value;
store.set(value)
},
update(updater) {
store.update((value) => {
const newValue = updater(value);
current = newValue;
return newValue;
});
},
subscribe: store.subscribe
}
}

View file

@ -1,25 +1,24 @@
<script lang="ts"> <script lang="ts">
import { onMount } from "svelte"; import { onMount } from "svelte";
import { Client } from "$lib/client.svelte.ts"; import { Client, Player } from "$lib/client";
import { AudioManager } from "$lib/audio.svelte.ts"; import { AudioManager } from "$lib/audio";
import { BinaryStreamReader } from "$lib/binary/readerStream"; import { identity, localStore } from "$lib/stores";
import { readonly, type Writable } from "svelte/store";
let name = $state(""); let name = localStore("name", "", identity, identity);
let useMusic = $state(false); let useMusic = localStore(
"useMusic",
false,
(v) => v == "true",
(v) => v.toString()
);
let client: Client = $state(undefined as any); let client: Client = $state(undefined as any);
let players = $derived.by(() => { let players: Writable<Record<number, Player>> = undefined as any;
if (client === undefined) return {};
return client.players;
});
let ac: AudioManager; let ac: AudioManager;
let wyrm: HTMLAudioElement; let wyrm: HTMLAudioElement;
let outputAudio: HTMLDivElement; let outputAudio: HTMLDivElement;
async function a() {
name = localStorage.getItem("name") ?? "";
useMusic = localStorage.getItem("useMusic") == "true";
}
async function audioPrep() { async function audioPrep() {
let media = useMusic let media = $useMusic
? wyrm ? wyrm
: await navigator.mediaDevices.getUserMedia({ : await navigator.mediaDevices.getUserMedia({
audio: true, audio: true,
@ -29,30 +28,23 @@
} }
async function connect() { async function connect() {
await audioPrep(); await audioPrep();
client = await Client.connect(ac, outputAudio); client = await Client.connect(readonly(name), ac, outputAudio);
players = client.players;
console.log(client); console.log(client);
} }
onMount(a);
</script> </script>
<div bind:this={outputAudio} /> <div bind:this={outputAudio}>audio outputs</div>
<div> <div>
I am <input I am <input
type="text" type="text"
maxlength="32" maxlength="32"
list="connectedPlayers" list="connectedPlayers"
bind:value={name} bind:value={$name}
onchange={() => localStorage.setItem("name", name)}
/> />
<label for="useMusic">Use Music</label> <label for="useMusic">Use Music</label>
<input <input id="useMusic" type="checkbox" bind:checked={$useMusic} />
id="useMusic"
type="checkbox"
bind:checked={useMusic}
onchange={() => localStorage.setItem("useMusic", useMusic)}
/>
<button onclick={() => connect()}>Connect</button> <button onclick={() => connect()}>Connect</button>
<audio bind:this={wyrm} src="/wyrmjewelbox.wav" autoplay={false} controls <audio bind:this={wyrm} src="/wyrmjewelbox.wav" autoplay={false} controls
>audio of the wyrm's houseki box</audio >audio of the wyrm's houseki box</audio
@ -60,13 +52,13 @@
</div> </div>
{#if client !== undefined} {#if client !== undefined}
<datalist> <datalist>
{#each players as player} {#each Object.values($players) as player}
<option>{player.name}</option> <option>{player.name}</option>
{/each} {/each}
</datalist> </datalist>
<div> <div>
Connected players - {players.length}<br /> Connected players - {Object.values($players).length}<br />
{#each players as player} {#each Object.values($players) as player}
<pre class="name">{player.name}</pre> <pre class="name">{player.name}</pre>
{/each} {/each}
</div> </div>

118
src/webtransport.d.ts vendored Normal file
View file

@ -0,0 +1,118 @@
interface WebTransportCloseInfo {
closeCode?: number;
reason?: string;
}
interface WebTransportErrorOptions {
source?: WebTransportErrorSource;
streamErrorCode?: number | null;
}
interface WebTransportHash {
algorithm?: string;
value?: BufferSource;
}
interface WebTransportOptions {
allowPooling?: boolean;
congestionControl?: WebTransportCongestionControl;
requireUnreliable?: boolean;
serverCertificateHashes?: WebTransportHash[];
}
interface WebTransportSendStreamOptions {
sendOrder?: number;
}
/**
* Available only in secure contexts.
*
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/WebTransport)
*/
interface WebTransport {
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/WebTransport/closed) */
readonly closed: Promise<WebTransportCloseInfo>;
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/WebTransport/datagrams) */
readonly datagrams: WebTransportDatagramDuplexStream;
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/WebTransport/incomingBidirectionalStreams) */
readonly incomingBidirectionalStreams: ReadableStream;
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/WebTransport/incomingUnidirectionalStreams) */
readonly incomingUnidirectionalStreams: ReadableStream;
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/WebTransport/ready) */
readonly ready: Promise<undefined>;
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/WebTransport/close) */
close(closeInfo?: WebTransportCloseInfo): void;
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/WebTransport/createBidirectionalStream) */
createBidirectionalStream(options?: WebTransportSendStreamOptions): Promise<WebTransportBidirectionalStream>;
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/WebTransport/createUnidirectionalStream) */
createUnidirectionalStream(options?: WebTransportSendStreamOptions): Promise<WritableStream>;
}
declare var WebTransport: {
prototype: WebTransport;
new(url: string | URL, options?: WebTransportOptions): WebTransport;
};
/**
* Available only in secure contexts.
*
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/WebTransportBidirectionalStream)
*/
interface WebTransportBidirectionalStream {
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/WebTransportBidirectionalStream/readable) */
readonly readable: ReadableStream;
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/WebTransportBidirectionalStream/writable) */
readonly writable: WritableStream;
}
declare var WebTransportBidirectionalStream: {
prototype: WebTransportBidirectionalStream;
new(): WebTransportBidirectionalStream;
};
/**
* Available only in secure contexts.
*
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/WebTransportDatagramDuplexStream)
*/
interface WebTransportDatagramDuplexStream {
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/WebTransportDatagramDuplexStream/incomingHighWaterMark) */
incomingHighWaterMark: number;
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/WebTransportDatagramDuplexStream/incomingMaxAge) */
incomingMaxAge: number | null;
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/WebTransportDatagramDuplexStream/maxDatagramSize) */
readonly maxDatagramSize: number;
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/WebTransportDatagramDuplexStream/outgoingHighWaterMark) */
outgoingHighWaterMark: number;
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/WebTransportDatagramDuplexStream/outgoingMaxAge) */
outgoingMaxAge: number | null;
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/WebTransportDatagramDuplexStream/readable) */
readonly readable: ReadableStream;
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/WebTransportDatagramDuplexStream/writable) */
readonly writable: WritableStream;
}
declare var WebTransportDatagramDuplexStream: {
prototype: WebTransportDatagramDuplexStream;
new(): WebTransportDatagramDuplexStream;
};
/**
* Available only in secure contexts.
*
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/WebTransportError)
*/
interface WebTransportError extends DOMException {
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/WebTransportError/source) */
readonly source: WebTransportErrorSource;
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/WebTransportError/streamErrorCode) */
readonly streamErrorCode: number | null;
}
declare var WebTransportError: {
prototype: WebTransportError;
new(message?: string, options?: WebTransportErrorOptions): WebTransportError;
};
type WebTransportCongestionControl = "default" | "low-latency" | "throughput";
type WebTransportErrorSource = "session" | "stream";

View file

@ -12,7 +12,7 @@
"moduleResolution": "bundler" "moduleResolution": "bundler"
}, },
"include": [ "include": [
"./webtransport.d.ts" "./src/webtransport.d.ts"
] ]
// Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias // Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias
// except $lib which is handled by https://svelte.dev/docs/kit/configuration#files // except $lib which is handled by https://svelte.dev/docs/kit/configuration#files

29392
webtransport.d.ts vendored

File diff suppressed because it is too large Load diff