-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathstage.html
340 lines (320 loc) · 11.6 KB
/
stage.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
<!doctype html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta charset="utf-8">
<title>Remote Applause stage</title>
<link href="https://fonts.googleapis.com/css2?family=Fira+Sans:wght@600&family=Merriweather&display=swap" rel="stylesheet">
<style>
html, body { margin: 0; padding: 0; }
#nojs-warning { opacity: 0; color: red; animation: fadein 250ms ease-out; animation-delay: 3000ms; }
body.js #nojs-warning { animation: none; display: none; }
@keyframes fadein { from { opacity: 0; display: block; } to { opacity: 1; } }
body {
background: url(audience.jpg) center center;
background-size: cover;
background-repeat: no-repeat;
height: 100vh;
font-family: Merriweather, serif;
color: #f1f2f3;
}
h1 {
font-family: "Fira Sans";
text-align: center;
margin: 0;
padding: 1em;
text-shadow: 0 0 4px black;
}
#messages { color: #ee9; margin: 1em; }
#credits { display: none; }
.box {
background: rgba(0,0,0,0.7);
border: 3px solid white;
box-sizing: border-box;
width: 90%;
padding: 30px;
margin-left: 5%;
margin-top: 2em;
}
#link a {
color: white;
font-weight: bold;
}
input + label::after {
color: red;
content: " disabled";
}
input:checked + label::after {
color: green;
content: " enabled";
}
#vu p {
display: flex;
}
#vu p span {
display: block;
flex: 1 1 auto;
margin-left: 2em;
position: relative;
background: repeating-linear-gradient(to right, transparent, transparent 5%, rgba(0, 0, 0, 0.5) 5%, rgba(0, 0, 0, 0.5) 10%),
linear-gradient(to right, #0f0 75%, #f00 75%), #666;
}
#vu p span::after {
position: absolute;
width: 100%;
transform: scaleX(var(--w, 1));
transition: transform 100ms linear;
transform-origin: 100% 50%;
top: 0;
right: 0;
height: 100%;
content: "";
background: #666;
}
</style></head>
<body>
<h1>Remote Applause stage for <output>a conference</output></h1>
<div id="messages">
loading...
<div id="nojs-warning">Unfortunately, this service requires JavaScript, which
does not seem to be available. If you have it turned off then turning it
on may help; if not, then refreshing this page may help. Sorry about that.</div>
</div>
<div id="controls" class="box">
<p>Audience members connected: <output id="audience">(don't know, connecting)</output></p>
<p><input id="enable" type="checkbox" autocomplete="off">
<label for="enable">Laughter and clapping</label></p>
</div>
<div id="link" class="box">
<p>The link to send to your audience:</p>
<p><a href="#"></a></p>
</div>
<div id="vu" class="box">
<p id="vclap">Clapping <span></span></p>
<p id="vlaugh">Laughing <span></span></p>
</div>
<audio id="a-clap" src="clap.wav" loop></audio>
<audio id="a-laugh" src="laugh.wav" loop></audio>
<div id="credits">https://freesound.org/people/daehedon/sounds/340355/
All emojis designed by OpenMoji – the open-source emoji and icon project. License: CC BY-SA 4.0
https://commons.wikimedia.org/wiki/File:Metropolitan_Opera_House,_a_concert_by_pianist_Josef_Hofmann_-_NARA_541890_-_Edit.jpg
https://commons.wikimedia.org/wiki/File:De_Rika_Jansen_One_Woman_Show_in_Rembrandttheater,_Rika_Jansen_in_haar_show,_Bestanddeelnr_916-6526.jpg
https://commons.wikimedia.org/w/index.php?title=File%3A72843_lonemonk_approx-800-laughter-only-1.wav
</div>
<script src="https://unpkg.com/[email protected]/dist/peerjs.min.js"></script>
<script>
console.clear();
function hasher(inp) { var hash = 0; if (inp.length == 0) { return hash; }
for (var i = 0; i < inp.length; i++) { var char = inp.charCodeAt(i);
hash = ((hash<<5)-hash)+char; hash = hash & hash; } return hash; }
function update() {
console.log("updating", CONNECTIONS, Object.keys(CONNECTIONS).length);
document.getElementById("audience").value = Object.keys(CONNECTIONS).length;
}
var conf = location.search;
if (conf == "") {
location.href = "index.html";
} else {
var usp = new URLSearchParams(window.location.search);
conf = usp.get('c');
}
document.querySelector("output").textContent = conf;
var confid = "rapp" + Math.abs(hasher(conf));
var retryTimer = 1;
var readyForClaps = false;
var processingQueue = false;
var QUEUE = [];
var CONNECTIONS = {};
var SOUND_REQUESTS = {clap: {requests: new Set(), destination: 0}, laugh: {requests: new Set(), destination: 0}};
var rti = null;
var aclap = document.getElementById("a-clap");
var alaugh = document.getElementById("a-laugh");
var vclap = document.getElementById("vclap");
var vlaugh = document.getElementById("vlaugh");
function broadcast(message, from) {
console.log("broadcast", message, from);
if (QUEUE.length > 50) {
// don't queue too many
console.log("abort broadcast, queue too long");
return;
}
QUEUE.push({m: message, f: from});
if (!processingQueue) {
//console.log("broadcast", message, from, "startup");
sendNext();
} else {
//console.log("broadcast", message, from, "not starting because running");
}
}
function sendNext() {
//console.log("sendNext");
processingQueue = true;
var nxt = QUEUE.shift();
if (!nxt) {
processingQueue = false;
//console.log("stop processing");
return;
}
var destinations = Object.keys(CONNECTIONS);
function sendNextToMember() {
//console.log("sendNextToMember");
var member = destinations.shift();
if (!member) {
//console.log("end of members, calling sendNext");
setTimeout(sendNext, 1);
return;
}
CONNECTIONS[member].send(nxt.m);
//console.log("send", nxt.m, "to", member);
sendNextToMember();
}
sendNextToMember();
}
function connect() {
document.getElementById("audience").value = "(don't know, connecting)";
function retry() {
if (rti !== null) {
console.log("already retrying, so not retrying again", rti);
return;
}
retryTimer *= 2;
if (retryTimer > 30) retryTimer = 30;
messages.innerHTML = "Connection problems. Reconnecting in <span>" + retryTimer + "</span> seconds...";
var rt = retryTimer;
clearInterval(rti);
rti = setInterval(function() {
rt -= 1;
console.log("count down", rt);
if (rt == 0) {
clearInterval(rti);
rti = null;
connect();
console.log("Ended retry spin with rti", rti, "now reconnecting");
return;
}
messages.getElementsByTagName("span")[0].textContent = rt;
}, 1000);
console.log("Started retry with rti", rti);
}
console.log("Attemptig to set up conf", confid);
var peer = new Peer(confid, {debug: 3}); // conf is our ID, because we're the stage
peer.on('open', function(id) {
console.log("stage peer connected to server with ID", id);
messages.textContent = "";
retryTimer = 1;
CONNECTIONS = {};
update();
});
peer.on('error', function(err) {
console.log("stage peer error connecting to server", err.type);
if (err.type == "unavailable-id") {
alert("Apologies. That conference name is already being used; you can't have two stages with the same name open at once. Please choose another.");
location.href = "index.html";
return;
}
peer.destroy();
latest_error = " (seeing the server)"
retry();
});
peer.on('disconnected', function() {
console.log("stage peer disconnected from server");
peer.destroy();
latest_error = " (with the connection to the server)"
retry();
});
peer.on('close', function() {
console.log("stage: I am destroyed");
latest_error = " (everything fell apart)"
retry();
});
peer.on('connection', function(conn) {
console.log("incoming connection from", conn.peer);
conn.on('open', function() {
console.log("stage connection to audience member", conn.peer, "open");
CONNECTIONS[conn.peer] = conn;
update();
if (readyForClaps) { conn.send("readyForClaps"); }
});
conn.on('close', function() {
console.log("stage disconnected from audience member", conn.peer);
delete CONNECTIONS[conn.peer];
update();
});
conn.on('error', function(err) {
console.log("stage to audience member", conn.peer, "connection error", err);
delete CONNECTIONS[conn.peer];
update();
});
conn.on('data', function(data) {
console.log("stage from audience", conn.peer, "data:", data);
if (data == "clap") {
broadcast("clap", conn.peer);
var rnd = conn.peer + Math.random();
SOUND_REQUESTS.clap.requests.add(rnd);
setTimeout(function() { SOUND_REQUESTS.clap.requests.delete(rnd); }, 3000);
} else if (data == "laugh") {
broadcast("laugh", conn.peer);
var rnd = conn.peer + Math.random();
SOUND_REQUESTS.laugh.requests.add(rnd);
setTimeout(function() { SOUND_REQUESTS.laugh.requests.delete(rnd); }, 3000);
}
});
});
}
setInterval(function() {
function check(data, sound, vu) {
data.destination = data.requests.size / audience_size;
// correct for fraction of audience: what percentage of people need to be clapping
// to count as "play the clapping as loud as possible"?
// if this next line has *5 in, it means: if one fifth (or more) of the audience clap, that's
// to play the sound as loud as possible.
data.destination = data.destination * 5;
if (data.destination > 1) data.destination = 1;
if (sound.volume != data.destination) {
var diff = data.destination - sound.volume;
if (Math.abs(diff) < 0.1) {
// when we get close to destination, just jump there; this avoids
// rounding errors and asymptotically approaching the destination
sound.volume = data.destination;
} else if (diff > 0) {
// destination is higher than actual volume, so jump there instantly
sound.volume = data.destination;
} else {
sound.volume += diff / 4;
}
// console.log("changed volume", sound, sound.volume);
vu.style.setProperty("--w", 1-sound.volume);
}
}
var audience_size = Object.keys(CONNECTIONS).length;
if (audience_size === 0) return;
check(SOUND_REQUESTS.clap, aclap, vclap);
check(SOUND_REQUESTS.laugh, alaugh, vlaugh);
}, 200); // fire this frequently so that sounds trend down in volume smoothly
document.body.className = "js";
document.getElementById("enable").onchange = function() {
if (this.checked) {
readyForClaps = true;
broadcast("readyForClaps", "stage");
aclap.volume = 0;
aclap.play();
alaugh.volume = 0;
alaugh.play();
vclap.style.setProperty("--w", "100%");
vlaugh.style.setProperty("--w", "100%");
} else {
readyForClaps = false;
broadcast("pauseClaps", "stage");
aclap.pause();
}
}
aclap.volume = 0;
vclap.style.setProperty("--w", "100%");
vlaugh.style.setProperty("--w", "100%");
var linka = document.querySelector("#link a");
linka.href = location.href.replace("stage.html", "audience.html");
linka.textContent = linka.href;
connect();
</script>
</body>
</html>