the proximity in proximity chat
This commit is contained in:
parent
a1dbfadfb0
commit
30fe1ae80b
85
flake.lock
Normal file
85
flake.lock
Normal 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
46
flake.nix
Normal 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;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
|
@ -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
90
src/lib/audio.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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;
|
||||||
|
|
|
@ -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 };
|
||||||
|
|
|
@ -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
72
src/lib/stores.ts
Normal 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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
118
src/webtransport.d.ts
vendored
Normal 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";
|
|
@ -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
29392
webtransport.d.ts
vendored
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue