Neale Pickett
·
2024-10-24
api-demo.html
1<!DOCTYPE html>
2<html>
3 <head>
4 <title>Vail API Demo</title>
5 <style>
6body {
7 font-family: Arimo, Arial, monospace;
8}
9
10section {
11 border: 1px solid black;
12 margin: 1em;
13}
14
15p {
16 max-width: 40em;
17}
18
19.log {
20 max-height: 10em;
21 overflow: scroll;
22}
23 </style>
24 <script type="module">
25const Millisecond = 1
26const Second = Millisecond * 1000
27
28/** Vail server connection.
29 *
30 * This opens a websocket to the given repeater on vail.woozle.org,
31 * and dispatches events to itself. Use .addEventListener to set up
32 * event handlers as you desire.
33 *
34 * Events:
35 * clients: the number of connected clients has changed
36 * message: a message was recieved
37 * sound: your application should start making a sound
38 * nosound: your application should stop making a sound
39 *
40 * Multiple sequential sound events may be dispatched before a nosound event is.
41 * This indicates "cross-talk": two people are sending to the repeater at the
42 * same time.
43 *
44 * Class variables you may enjoy:
45 *
46 * delay: rx delay to add to incoming messages, higher-latency networks will need a larger value
47 * transmitters: how many clients are sounding
48 * reconnect: if true, will try to maintain a connection to the server
49 * reconnectDelay: how long to wait between disconnection and reconnect attempt
50 * clients: how many connected clients the repeater last reported
51 * offset: clock skew between us and the repeater
52 */
53class Vail extends EventTarget {
54 constructor(repeater) {
55 super()
56
57 /** URL to our WebSocket */
58 this.url = new URL("wss://vail.woozle.org/chat")
59 this.url.searchParams.set("repeater", repeater)
60
61 /** Delay to add */
62 this.delay = 4 * Second
63
64 /** How many things are making sound right now. 0 == be quiet */
65 this.transmitters = 0
66
67 /** Attempt to reconnect if the websocket closes */
68 this.reconnect = true
69
70 /** How long to wait before trying to reconnect */
71 this.reconnectDelay = 3 * Second
72
73 this.reopen()
74 }
75
76 /** Close the web socket and stop trying to reconnect */
77 close() {
78 this.reconnect = false
79 if (!this.socket) {
80 return
81 }
82 this.socket.close()
83 }
84
85 reopen() {
86 /** Number of clients connected */
87 this.clients = 0
88
89 /** Timestamp offset for incoming messages */
90 this.offset = 0
91
92 /** Our current websocket */
93 this.socket = new WebSocket(this.url, ["json.vail.woozle.org"])
94 this.socket.addEventListener("close", event => {
95 addTimeout(() => this.reopen(), this.reconnectDelay)
96 })
97 this.socket.addEventListener("message", event => {
98 this.message(event)
99 })
100 }
101
102 message(event) {
103 let message = JSON.parse(event.data)
104 this.dispatchEvent(new CustomEvent("message", {detail: {message}}))
105 if (message.Clients != this.clients) {
106 this.clients = message.Clients
107 this.dispatchEvent(new CustomEvent("clients", {detail: {clients: this.clients}}))
108 }
109 if (message.Duration.length == 0) {
110 this.offset = Date.now() - message.Timestamp
111 return
112 }
113
114 // Defer dispatching sound events
115 let now = Date.now()
116 let when = message.Timestamp + this.offset + this.delay
117 let tx = true
118 let detail = {}
119 for (let duration of message.Duration) {
120 let delay = when - now
121 if (tx && (delay >= 0)) {
122 setTimeout(() => this.sound(true, {when, duration}), delay)
123 setTimeout(() => this.sound(false, {when: when+duration, duration}), delay + duration)
124 }
125 when += duration
126 tx = !tx
127 }
128 }
129
130 /** Dispatch a sound event.
131 *
132 * This keeps an internal count of how many things are making sound at once.
133 * When that count reaches 0, a "nosound" event is dispatched.
134 *
135 * @param {bool} tx True to make sound, false to stop making sound.
136 * @param {Object} detail CustomEvent detail
137 */
138 sound(tx, detail) {
139 if (tx) {
140 this.transmitters++
141 } else {
142 this.transmitters--
143 }
144 detail.transmitters = this.transmitters
145
146 if (this.transmitters > 0) {
147 this.dispatchEvent(new CustomEvent("sound", {detail}))
148 } else {
149 this.dispatchEvent(new CustomEvent("nosound", {detail}))
150 }
151 }
152}
153
154/** Log an event's detail */
155function log(selector, event) {
156 let section = document.querySelector(selector)
157 let log = section.querySelector(".log")
158 let line = log.appendChild(document.createElement("div"))
159 line.textContent = event.type + ": " + JSON.stringify(event.detail)
160 line.scrollIntoView()
161}
162
163/** Update the displayed number of clients. */
164function clients(event) {
165 let clients = document.querySelector("#nclients")
166 clients.textContent = event.target.clients
167}
168
169/** Handle the sound and nosound events by displaying different glyphs. */
170function sound(event, enable) {
171 let sounder = document.querySelector("#sounder")
172 sounder.textContent = enable ? "█" : ""
173}
174
175let vail = new Vail("demo") // Connect to the "demo" repeater
176vail.addEventListener("message", event => log("#messages", event))
177vail.addEventListener("clients", event => log("#clients", event))
178vail.addEventListener("sound", event => log("#sounds", event))
179vail.addEventListener("nosound", event => log("#sounds", event))
180
181vail.addEventListener("clients", event => clients(event))
182vail.addEventListener("sound", event => sound(event, true))
183vail.addEventListener("nosound", event => sound(event, false))
184 </script>
185 </head>
186 <body>
187 <h1>Vail API Demo</h1>
188
189 <div><a href="https://vail.woozle.org/#demo" target="_blank">Repeater</a></div>
190 <div>Sounder: <span id="sounder"></span></div>
191 <div>Clients: <span id="nclients">0</span></div>
192
193 <section id="sounds">
194 <h2>sound events</h2>
195 <div class="log"></div>
196 <p>
197 Implementations should use sound events, which will be dispatched
198 in the correct order, and only have one duration per event.
199 </p>
200 </section>
201
202 <section id="clients">
203 <h2>clients events</h2>
204 <div class="log"></div>
205 <p>
206 Clients events are dispatched whenever the number of
207 connected clients changes.
208 </p>
209 </section>
210
211 <section id="messages">
212 <h2>message events</h2>
213 <div class="log"></div>
214 <p>
215 Message events are "raw" events. They may arrive out of order,
216 and can have multiple durations.
217 The Vail class parses these into properly-sequenced
218 "sound" events.
219 </p>
220 </section>
221 </body>
222</html>