/
/
/
1"use strict";
2class Host {
3 constructor(json) {
4 this.fromJson(json);
5 }
6 fromJson(json) {
7 this.arch = json.arch;
8 this.ip = json.ip;
9 this.mac = json.mac;
10 this.name = json.name;
11 this.os = json.os;
12 }
13 arch = "";
14 ip = "";
15 mac = "";
16 name = "";
17 os = "";
18}
19class Client {
20 constructor(json) {
21 this.fromJson(json);
22 }
23 fromJson(json) {
24 this.id = json.id;
25 this.host = new Host(json.host);
26 let jsnapclient = json.snapclient;
27 this.snapclient = { name: jsnapclient.name, protocolVersion: jsnapclient.protocolVersion, version: jsnapclient.version };
28 let jconfig = json.config;
29 this.config = { instance: jconfig.instance, latency: jconfig.latency, name: jconfig.name, volume: { muted: jconfig.volume.muted, percent: jconfig.volume.percent } };
30 this.lastSeen = { sec: json.lastSeen.sec, usec: json.lastSeen.usec };
31 this.connected = Boolean(json.connected);
32 }
33 id = "";
34 host;
35 snapclient;
36 config;
37 lastSeen;
38 connected = false;
39}
40class Group {
41 constructor(json) {
42 this.fromJson(json);
43 }
44 fromJson(json) {
45 this.name = json.name;
46 this.id = json.id;
47 this.stream_id = json.stream_id;
48 this.muted = Boolean(json.muted);
49 for (let client of json.clients)
50 this.clients.push(new Client(client));
51 }
52 name = "";
53 id = "";
54 stream_id = "";
55 muted = false;
56 clients = [];
57 getClient(id) {
58 for (let client of this.clients) {
59 if (client.id == id)
60 return client;
61 }
62 return null;
63 }
64}
65class Metadata {
66 constructor(json) {
67 this.fromJson(json);
68 }
69 fromJson(json) {
70 this.title = json.title;
71 this.artist = json.artist;
72 this.album = json.album;
73 this.artUrl = json.artUrl;
74 this.duration = json.duration;
75 }
76 title;
77 artist;
78 album;
79 artUrl;
80 duration;
81}
82class Properties {
83 constructor(json) {
84 this.fromJson(json);
85 }
86 fromJson(json) {
87 this.loopStatus = json.loopStatus;
88 this.shuffle = json.shuffle;
89 this.volume = json.volume;
90 this.rate = json.rate;
91 this.playbackStatus = json.playbackStatus;
92 this.position = json.position;
93 this.minimumRate = json.minimumRate;
94 this.maximumRate = json.maximumRate;
95 this.canGoNext = Boolean(json.canGoNext);
96 this.canGoPrevious = Boolean(json.canGoPrevious);
97 this.canPlay = Boolean(json.canPlay);
98 this.canPause = Boolean(json.canPause);
99 this.canSeek = Boolean(json.canSeek);
100 this.canControl = Boolean(json.canControl);
101 if (json.metadata != undefined) {
102 this.metadata = new Metadata(json.metadata);
103 }
104 else {
105 this.metadata = new Metadata({});
106 }
107 }
108 loopStatus;
109 shuffle;
110 volume;
111 rate;
112 playbackStatus;
113 position;
114 minimumRate;
115 maximumRate;
116 canGoNext = false;
117 canGoPrevious = false;
118 canPlay = false;
119 canPause = false;
120 canSeek = false;
121 canControl = false;
122 metadata;
123}
124class Stream {
125 constructor(json) {
126 this.fromJson(json);
127 }
128 fromJson(json) {
129 this.id = json.id;
130 this.status = json.status;
131 if (json.properties != undefined) {
132 this.properties = new Properties(json.properties);
133 }
134 else {
135 this.properties = new Properties({});
136 }
137 let juri = json.uri;
138 this.uri = { raw: juri.raw, scheme: juri.scheme, host: juri.host, path: juri.path, fragment: juri.fragment, query: juri.query };
139 }
140 id = "";
141 status = "";
142 uri;
143 properties;
144}
145class Server {
146 constructor(json) {
147 if (json)
148 this.fromJson(json);
149 }
150 fromJson(json) {
151 this.groups = [];
152 for (let jgroup of json.groups)
153 this.groups.push(new Group(jgroup));
154 let jsnapserver = json.server.snapserver;
155 this.server = { host: new Host(json.server.host), snapserver: { controlProtocolVersion: jsnapserver.controlProtocolVersion, name: jsnapserver.name, protocolVersion: jsnapserver.protocolVersion, version: jsnapserver.version } };
156 this.streams = [];
157 for (let jstream of json.streams) {
158 this.streams.push(new Stream(jstream));
159 }
160 }
161 groups = [];
162 server;
163 streams = [];
164 getClient(id) {
165 for (let group of this.groups) {
166 let client = group.getClient(id);
167 if (client)
168 return client;
169 }
170 return null;
171 }
172 getGroup(id) {
173 for (let group of this.groups) {
174 if (group.id == id)
175 return group;
176 }
177 return null;
178 }
179 getStream(id) {
180 for (let stream of this.streams) {
181 if (stream.id == id)
182 return stream;
183 }
184 return null;
185 }
186}
187class SnapControl {
188 constructor(baseUrl) {
189 this.server = new Server();
190 this.baseUrl = baseUrl;
191 this.msg_id = 0;
192 this.status_req_id = -1;
193 this.connect();
194 }
195 connect() {
196 this.connection = new WebSocket(this.baseUrl + '/jsonrpc');
197 this.connection.onmessage = (msg) => this.onMessage(msg.data);
198 this.connection.onopen = () => { this.status_req_id = this.sendRequest('Server.GetStatus'); };
199 this.connection.onerror = (ev) => { console.error('error:', ev); };
200 this.connection.onclose = () => {
201 console.info('connection lost, reconnecting in 1s');
202 setTimeout(() => this.connect(), 1000);
203 };
204 }
205 onNotification(notification) {
206 let stream;
207 switch (notification.method) {
208 case 'Client.OnVolumeChanged':
209 let client = this.getClient(notification.params.id);
210 client.config.volume = notification.params.volume;
211 updateGroupVolume(this.getGroupFromClient(client.id));
212 return true;
213 case 'Client.OnLatencyChanged':
214 this.getClient(notification.params.id).config.latency = notification.params.latency;
215 return false;
216 case 'Client.OnNameChanged':
217 this.getClient(notification.params.id).config.name = notification.params.name;
218 return true;
219 case 'Client.OnConnect':
220 case 'Client.OnDisconnect':
221 this.getClient(notification.params.client.id).fromJson(notification.params.client);
222 return true;
223 case 'Group.OnMute':
224 this.getGroup(notification.params.id).muted = Boolean(notification.params.mute);
225 return true;
226 case 'Group.OnStreamChanged':
227 this.getGroup(notification.params.id).stream_id = notification.params.stream_id;
228 this.updateProperties(notification.params.stream_id);
229 return true;
230 case 'Stream.OnUpdate':
231 stream = this.getStream(notification.params.id);
232 stream.fromJson(notification.params.stream);
233 this.updateProperties(stream.id);
234 return true;
235 case 'Server.OnUpdate':
236 this.server.fromJson(notification.params.server);
237 this.updateProperties(this.getMyStreamId());
238 return true;
239 case 'Stream.OnProperties':
240 stream = this.getStream(notification.params.id);
241 stream.properties.fromJson(notification.params.properties);
242 if (this.getMyStreamId() == stream.id)
243 this.updateProperties(stream.id);
244 return false;
245 default:
246 return false;
247 }
248 }
249 updateProperties(stream_id) {
250 if (!('mediaSession' in navigator)) {
251 console.log('updateProperties: mediaSession not supported');
252 return;
253 }
254 if (stream_id != this.getMyStreamId()) {
255 console.log('updateProperties: not my stream id: ' + stream_id + ', mine: ' + this.getMyStreamId());
256 return;
257 }
258 let props;
259 let metadata;
260 try {
261 props = this.getStreamFromClient(SnapStream.getClientId()).properties;
262 metadata = this.getStreamFromClient(SnapStream.getClientId()).properties.metadata;
263 }
264 catch (e) {
265 console.log('updateProperties failed: ' + e);
266 return;
267 }
268 // https://developers.google.com/web/updates/2017/02/media-session
269 // https://github.com/googlechrome/samples/tree/gh-pages/media-session
270 // https://googlechrome.github.io/samples/media-session/audio.html
271 // https://developer.mozilla.org/en-US/docs/Web/API/MediaSession/setActionHandler#seekto
272 console.log('updateProperties: ', props);
273 let play_state = "none";
274 if (props.playbackStatus != undefined) {
275 if (props.playbackStatus == "playing") {
276 audio.play();
277 play_state = "playing";
278 }
279 else if (props.playbackStatus == "paused") {
280 audio.pause();
281 play_state = "paused";
282 }
283 else if (props.playbackStatus == "stopped") {
284 audio.pause();
285 play_state = "none";
286 }
287 }
288 let mediaSession = navigator.mediaSession;
289 mediaSession.playbackState = play_state;
290 console.log('updateProperties playbackState: ', navigator.mediaSession.playbackState);
291 // if (props.canGoNext == undefined || !props.canGoNext!)
292 mediaSession.setActionHandler('play', () => {
293 props.canPlay ?
294 this.sendRequest('Stream.Control', { id: stream_id, command: 'play' }) : null;
295 });
296 mediaSession.setActionHandler('pause', () => {
297 props.canPause ?
298 this.sendRequest('Stream.Control', { id: stream_id, command: 'pause' }) : null;
299 });
300 mediaSession.setActionHandler('previoustrack', () => {
301 props.canGoPrevious ?
302 this.sendRequest('Stream.Control', { id: stream_id, command: 'previous' }) : null;
303 });
304 mediaSession.setActionHandler('nexttrack', () => {
305 props.canGoNext ?
306 this.sendRequest('Stream.Control', { id: stream_id, command: 'next' }) : null;
307 });
308 try {
309 mediaSession.setActionHandler('stop', () => {
310 props.canControl ?
311 this.sendRequest('Stream.Control', { id: stream_id, command: 'stop' }) : null;
312 });
313 }
314 catch (error) {
315 console.log('Warning! The "stop" media session action is not supported.');
316 }
317 let defaultSkipTime = 10; // Time to skip in seconds by default
318 mediaSession.setActionHandler('seekbackward', (event) => {
319 let offset = (event.seekOffset || defaultSkipTime) * -1;
320 if (props.position != undefined)
321 Math.max(props.position + offset, 0);
322 props.canSeek ?
323 this.sendRequest('Stream.Control', { id: stream_id, command: 'seek', params: { 'offset': offset } }) : null;
324 });
325 mediaSession.setActionHandler('seekforward', (event) => {
326 let offset = event.seekOffset || defaultSkipTime;
327 if ((metadata.duration != undefined) && (props.position != undefined))
328 Math.min(props.position + offset, metadata.duration);
329 props.canSeek ?
330 this.sendRequest('Stream.Control', { id: stream_id, command: 'seek', params: { 'offset': offset } }) : null;
331 });
332 try {
333 mediaSession.setActionHandler('seekto', (event) => {
334 let position = event.seekTime || 0;
335 if (metadata.duration != undefined)
336 Math.min(position, metadata.duration);
337 props.canSeek ?
338 this.sendRequest('Stream.Control', { id: stream_id, command: 'setPosition', params: { 'position': position } }) : null;
339 });
340 }
341 catch (error) {
342 console.log('Warning! The "seekto" media session action is not supported.');
343 }
344 if ((metadata.duration != undefined) && (props.position != undefined) && (props.position <= metadata.duration)) {
345 if ('setPositionState' in mediaSession) {
346 console.log('Updating position state: ' + props.position + '/' + metadata.duration);
347 mediaSession.setPositionState({
348 duration: metadata.duration,
349 playbackRate: 1.0,
350 position: props.position
351 });
352 }
353 }
354 else {
355 mediaSession.setPositionState({
356 duration: 0,
357 playbackRate: 1.0,
358 position: 0
359 });
360 }
361 console.log('updateMetadata: ', metadata);
362 // https://github.com/Microsoft/TypeScript/issues/19473
363 let title = metadata.title || "Unknown Title";
364 let artist = (metadata.artist != undefined) ? metadata.artist.join(', ') : "Unknown Artist";
365 let album = metadata.album || "";
366 let artwork = [{ src: 'snapcast-512.png', sizes: '512x512', type: 'image/png' }];
367 if (metadata.artUrl != undefined) {
368 artwork = [
369 { src: metadata.artUrl, sizes: '96x96', type: 'image/png' },
370 { src: metadata.artUrl, sizes: '128x128', type: 'image/png' },
371 { src: metadata.artUrl, sizes: '192x192', type: 'image/png' },
372 { src: metadata.artUrl, sizes: '256x256', type: 'image/png' },
373 { src: metadata.artUrl, sizes: '384x384', type: 'image/png' },
374 { src: metadata.artUrl, sizes: '512x512', type: 'image/png' },
375 ];
376 } // || 'snapcast-512.png';
377 console.log('Metadata title: ' + title + ', artist: ' + artist + ', album: ' + album + ", artwork: " + artwork);
378 navigator.mediaSession.metadata = new MediaMetadata({
379 title: title,
380 artist: artist,
381 album: album,
382 artwork: artwork
383 });
384 // mediaSession.setActionHandler('seekbackward', function () { });
385 // mediaSession.setActionHandler('seekforward', function () { });
386 }
387 getClient(client_id) {
388 let client = this.server.getClient(client_id);
389 if (client == null) {
390 throw new Error(`client ${client_id} was null`);
391 }
392 return client;
393 }
394 getGroup(group_id) {
395 let group = this.server.getGroup(group_id);
396 if (group == null) {
397 throw new Error(`group ${group_id} was null`);
398 }
399 return group;
400 }
401 getGroupVolume(group, online) {
402 if (group.clients.length == 0)
403 return 0;
404 let group_vol = 0;
405 let client_count = 0;
406 for (let client of group.clients) {
407 if (online && !client.connected)
408 continue;
409 group_vol += client.config.volume.percent;
410 ++client_count;
411 }
412 if (client_count == 0)
413 return 0;
414 return group_vol / client_count;
415 }
416 getGroupFromClient(client_id) {
417 for (let group of this.server.groups)
418 for (let client of group.clients)
419 if (client.id == client_id)
420 return group;
421 throw new Error(`group for client ${client_id} was null`);
422 }
423 getStreamFromClient(client_id) {
424 let group = this.getGroupFromClient(client_id);
425 return this.getStream(group.stream_id);
426 }
427 getMyStreamId() {
428 try {
429 let group = this.getGroupFromClient(SnapStream.getClientId());
430 return this.getStream(group.stream_id).id;
431 }
432 catch (e) {
433 return "";
434 }
435 }
436 getStream(stream_id) {
437 let stream = this.server.getStream(stream_id);
438 if (stream == null) {
439 throw new Error(`stream ${stream_id} was null`);
440 }
441 return stream;
442 }
443 setVolume(client_id, percent, mute) {
444 percent = Math.max(0, Math.min(100, percent));
445 let client = this.getClient(client_id);
446 client.config.volume.percent = percent;
447 if (mute != undefined)
448 client.config.volume.muted = mute;
449 this.sendRequest('Client.SetVolume', { id: client_id, volume: { muted: client.config.volume.muted, percent: client.config.volume.percent } });
450 }
451 setClientName(client_id, name) {
452 let client = this.getClient(client_id);
453 let current_name = (client.config.name != "") ? client.config.name : client.host.name;
454 if (name != current_name) {
455 this.sendRequest('Client.SetName', { id: client_id, name: name });
456 client.config.name = name;
457 }
458 }
459 setClientLatency(client_id, latency) {
460 let client = this.getClient(client_id);
461 let current_latency = client.config.latency;
462 if (latency != current_latency) {
463 this.sendRequest('Client.SetLatency', { id: client_id, latency: latency });
464 client.config.latency = latency;
465 }
466 }
467 deleteClient(client_id) {
468 this.sendRequest('Server.DeleteClient', { id: client_id });
469 this.server.groups.forEach((g, gi) => {
470 g.clients.forEach((c, ci) => {
471 if (c.id == client_id) {
472 this.server.groups[gi].clients.splice(ci, 1);
473 }
474 });
475 });
476 this.server.groups.forEach((g, gi) => {
477 if (g.clients.length == 0) {
478 this.server.groups.splice(gi, 1);
479 }
480 });
481 show();
482 }
483 setStream(group_id, stream_id) {
484 this.getGroup(group_id).stream_id = stream_id;
485 this.updateProperties(stream_id);
486 this.sendRequest('Group.SetStream', { id: group_id, stream_id: stream_id });
487 }
488 setClients(group_id, clients) {
489 this.status_req_id = this.sendRequest('Group.SetClients', { id: group_id, clients: clients });
490 }
491 muteGroup(group_id, mute) {
492 this.getGroup(group_id).muted = mute;
493 this.sendRequest('Group.SetMute', { id: group_id, mute: mute });
494 }
495 sendRequest(method, params) {
496 let msg = {
497 id: ++this.msg_id,
498 jsonrpc: '2.0',
499 method: method
500 };
501 if (params)
502 msg.params = params;
503 let msgJson = JSON.stringify(msg);
504 console.log("Sending: " + msgJson);
505 this.connection.send(msgJson);
506 return this.msg_id;
507 }
508 onMessage(msg) {
509 let json_msg = JSON.parse(msg);
510 let is_response = (json_msg.id != undefined);
511 console.log("Received " + (is_response ? "response" : "notification") + ", json: " + JSON.stringify(json_msg));
512 if (is_response) {
513 if (json_msg.id == this.status_req_id) {
514 this.server = new Server(json_msg.result.server);
515 this.updateProperties(this.getMyStreamId());
516 show();
517 }
518 }
519 else {
520 let refresh = false;
521 if (Array.isArray(json_msg)) {
522 for (let notification of json_msg) {
523 refresh = this.onNotification(notification) || refresh;
524 }
525 }
526 else {
527 refresh = this.onNotification(json_msg);
528 }
529 // TODO: don't update everything, but only the changed,
530 // e.g. update the values for the volume sliders
531 if (refresh)
532 show();
533 }
534 }
535 baseUrl;
536 connection;
537 server;
538 msg_id;
539 status_req_id;
540}
541let snapcontrol;
542let snapstream = null;
543let hide_offline = true;
544let autoplay_done = false;
545let audio = document.createElement('audio');
546function autoplayRequested() {
547 return document.location.hash.match(/autoplay/) !== null;
548}
549function show() {
550 // Render the page
551 const versionElem = document.getElementsByTagName("meta").namedItem("version");
552 console.log("Snapweb version " + (versionElem ? versionElem.content : "null"));
553 let play_img;
554 if (snapstream) {
555 play_img = 'stop.png';
556 }
557 else {
558 play_img = 'play.png';
559 }
560 let content = "";
561 content += "<div class='navbar'>Snapcast";
562 let serverVersion = snapcontrol.server.server.snapserver.version.split('.');
563 if ((serverVersion.length >= 2) && (+serverVersion[1] >= 21)) {
564 content += " <img src='" + play_img + "' class='play-button' id='play-button'></a>";
565 // Stream became ready and was not playing. If autoplay is requested, start playing.
566 if (!snapstream && !autoplay_done && autoplayRequested()) {
567 autoplay_done = true;
568 play();
569 }
570 }
571 content += "</div>";
572 content += "<div class='content'>";
573 let server = snapcontrol.server;
574 for (let group of server.groups) {
575 if (hide_offline) {
576 let groupActive = false;
577 for (let client of group.clients) {
578 if (client.connected) {
579 groupActive = true;
580 break;
581 }
582 }
583 if (!groupActive)
584 continue;
585 }
586 // Set mute variables
587 let classgroup;
588 let muted;
589 let mute_img;
590 if (group.muted == true) {
591 classgroup = 'group muted';
592 muted = true;
593 mute_img = 'mute_icon.png';
594 }
595 else {
596 classgroup = 'group';
597 muted = false;
598 mute_img = 'speaker_icon.png';
599 }
600 // Start group div
601 content += "<div id='g_" + group.id + "' class='" + classgroup + "'>";
602 // Create stream selection dropdown
603 let streamselect = "<select id='stream_" + group.id + "' onchange='setStream(\"" + group.id + "\")' class='stream'>";
604 for (let i_stream = 0; i_stream < server.streams.length; i_stream++) {
605 let streamselected = "";
606 if (group.stream_id == server.streams[i_stream].id) {
607 streamselected = 'selected';
608 }
609 streamselect += "<option value='" + server.streams[i_stream].id + "' " + streamselected + ">" + server.streams[i_stream].id + ": " + server.streams[i_stream].status + "</option>";
610 }
611 streamselect += "</select>";
612 // Group mute and refresh button
613 content += "<div class='groupheader'>";
614 content += streamselect;
615 // let cover_img: string = server.getStream(group.stream_id)!.properties.metadata.artUrl || "snapcast-512.png";
616 // content += "<img src='" + cover_img + "' class='cover-img' id='cover_" + group.id + "'>";
617 let clientCount = 0;
618 for (let client of group.clients)
619 if (!hide_offline || client.connected)
620 clientCount++;
621 if (clientCount > 1) {
622 let volume = snapcontrol.getGroupVolume(group, hide_offline);
623 // content += "<div class='client'>";
624 content += "<a href=\"javascript:setMuteGroup('" + group.id + "'," + !muted + ");\"><img src='" + mute_img + "' class='mute-button'></a>";
625 content += "<div class='slidergroupdiv'>";
626 content += " <input type='range' draggable='false' min=0 max=100 step=1 id='vol_" + group.id + "' oninput='javascript:setGroupVolume(\"" + group.id + "\")' value=" + volume + " class='slider'>";
627 // content += " <input type='range' min=0 max=100 step=1 id='vol_" + group.id + "' oninput='javascript:setVolume(\"" + client.id + "\"," + client.config.volume.muted + ")' value=" + client.config.volume.percent + " class='" + sliderclass + "'>";
628 content += "</div>";
629 // content += "</div>";
630 }
631 // transparent placeholder edit icon
632 content += "<div class='edit-group-icon'>✎</div>";
633 content += "</div>";
634 content += "<hr class='groupheader-separator'>";
635 // Create clients in group
636 for (let client of group.clients) {
637 if (!client.connected && hide_offline)
638 continue;
639 // Set name and connection state vars, start client div
640 let name;
641 let clas = 'client';
642 if (client.config.name != "") {
643 name = client.config.name;
644 }
645 else {
646 name = client.host.name;
647 }
648 if (client.connected == false) {
649 clas = 'client disconnected';
650 }
651 content += "<div id='c_" + client.id + "' class='" + clas + "'>";
652 // Client mute status vars
653 let muted;
654 let mute_img;
655 let sliderclass;
656 if (client.config.volume.muted == true) {
657 muted = true;
658 sliderclass = 'slider muted';
659 mute_img = 'mute_icon.png';
660 }
661 else {
662 sliderclass = 'slider';
663 muted = false;
664 mute_img = 'speaker_icon.png';
665 }
666 // Populate client div
667 content += "<a href=\"javascript:setVolume('" + client.id + "'," + !muted + ");\"><img src='" + mute_img + "' class='mute-button'></a>";
668 content += " <div class='sliderdiv'>";
669 content += " <input type='range' min=0 max=100 step=1 id='vol_" + client.id + "' oninput='javascript:setVolume(\"" + client.id + "\"," + client.config.volume.muted + ")' value=" + client.config.volume.percent + " class='" + sliderclass + "'>";
670 content += " </div>";
671 content += " <span class='edit-icons'>";
672 content += " <a href=\"javascript:openClientSettings('" + client.id + "');\" class='edit-icon'>✎</a>";
673 if (client.connected == false) {
674 content += " <a href=\"javascript:deleteClient('" + client.id + "');\" class='delete-icon'>🗑</a>";
675 content += " </span>";
676 }
677 else {
678 content += "</span>";
679 }
680 content += " <div class='name'>" + name + "</div>";
681 content += "</div>";
682 }
683 content += "</div>";
684 }
685 content += "</div>"; // content
686 content += "<div id='client-settings' class='client-settings'>";
687 content += " <div class='client-setting-content'>";
688 content += " <form action='javascript:closeClientSettings()'>";
689 content += " <label for='client-name'>Name</label>";
690 content += " <input type='text' class='client-input' id='client-name' name='client-name' placeholder='Client name..'>";
691 content += " <label for='client-latency'>Latency</label>";
692 content += " <input type='number' class='client-input' min='-10000' max='10000' id='client-latency' name='client-latency' placeholder='Latency in ms..'>";
693 content += " <label for='client-group'>Group</label>";
694 content += " <select id='client-group' class='client-input' name='client-group'>";
695 content += " </select>";
696 content += " <input type='submit' value='Submit'>";
697 content += " </form>";
698 content += " </div>";
699 content += "</div>";
700 // Pad then update page
701 content = content + "<br><br>";
702 document.getElementById('show').innerHTML = content;
703 let playElem = document.getElementById('play-button');
704 playElem.onclick = () => {
705 play();
706 };
707 for (let group of snapcontrol.server.groups) {
708 if (group.clients.length > 1) {
709 let slider = document.getElementById("vol_" + group.id);
710 if (slider == null)
711 continue;
712 slider.addEventListener('pointerdown', function () {
713 groupVolumeEnter(group.id);
714 });
715 slider.addEventListener('touchstart', function () {
716 groupVolumeEnter(group.id);
717 });
718 }
719 }
720}
721function updateGroupVolume(group) {
722 let group_vol = snapcontrol.getGroupVolume(group, hide_offline);
723 let slider = document.getElementById("vol_" + group.id);
724 if (slider == null)
725 return;
726 console.log("updateGroupVolume group: " + group.id + ", volume: " + group_vol + ", slider: " + (slider != null));
727 slider.value = String(group_vol);
728}
729let client_volumes;
730let group_volume;
731function setGroupVolume(group_id) {
732 let group = snapcontrol.getGroup(group_id);
733 let percent = document.getElementById('vol_' + group.id).valueAsNumber;
734 console.log("setGroupVolume id: " + group.id + ", volume: " + percent);
735 // show()
736 let delta = percent - group_volume;
737 let ratio;
738 if (delta < 0)
739 ratio = (group_volume - percent) / group_volume;
740 else
741 ratio = (percent - group_volume) / (100 - group_volume);
742 for (let i = 0; i < group.clients.length; ++i) {
743 let new_volume = client_volumes[i];
744 if (delta < 0)
745 new_volume -= ratio * client_volumes[i];
746 else
747 new_volume += ratio * (100 - client_volumes[i]);
748 let client_id = group.clients[i].id;
749 // TODO: use batch request to update all client volumes at once
750 snapcontrol.setVolume(client_id, new_volume);
751 let slider = document.getElementById('vol_' + client_id);
752 if (slider)
753 slider.value = String(new_volume);
754 }
755}
756function groupVolumeEnter(group_id) {
757 let group = snapcontrol.getGroup(group_id);
758 let percent = document.getElementById('vol_' + group.id).valueAsNumber;
759 console.log("groupVolumeEnter id: " + group.id + ", volume: " + percent);
760 group_volume = percent;
761 client_volumes = [];
762 for (let i = 0; i < group.clients.length; ++i) {
763 client_volumes.push(group.clients[i].config.volume.percent);
764 }
765 // show()
766}
767function setVolume(id, mute) {
768 console.log("setVolume id: " + id + ", mute: " + mute);
769 let percent = document.getElementById('vol_' + id).valueAsNumber;
770 let client = snapcontrol.getClient(id);
771 let needs_update = (mute != client.config.volume.muted);
772 snapcontrol.setVolume(id, percent, mute);
773 let group = snapcontrol.getGroupFromClient(id);
774 updateGroupVolume(group);
775 if (needs_update)
776 show();
777}
778function play() {
779 if (snapstream) {
780 snapstream.stop();
781 snapstream = null;
782 audio.pause();
783 audio.src = '';
784 document.body.removeChild(audio);
785 }
786 else {
787 snapstream = new SnapStream(config.baseUrl);
788 // User interacted with the page. Let's play audio...
789 document.body.appendChild(audio);
790 audio.src = "10-seconds-of-silence.mp3";
791 audio.loop = true;
792 audio.play().then(() => {
793 snapcontrol.updateProperties(snapcontrol.getMyStreamId());
794 });
795 }
796}
797function setMuteGroup(id, mute) {
798 snapcontrol.muteGroup(id, mute);
799 show();
800}
801function setStream(id) {
802 snapcontrol.setStream(id, document.getElementById('stream_' + id).value);
803 show();
804}
805function setGroup(client_id, group_id) {
806 console.log("setGroup id: " + client_id + ", group: " + group_id);
807 let server = snapcontrol.server;
808 // Get client group id
809 let current_group = snapcontrol.getGroupFromClient(client_id);
810 // Get
811 // List of target group's clients
812 // OR
813 // List of current group's other clients
814 let send_clients = [];
815 for (let i_group = 0; i_group < server.groups.length; i_group++) {
816 if (server.groups[i_group].id == group_id || (group_id == "new" && server.groups[i_group].id == current_group.id)) {
817 for (let i_client = 0; i_client < server.groups[i_group].clients.length; i_client++) {
818 if (group_id == "new" && server.groups[i_group].clients[i_client].id == client_id) { }
819 else {
820 send_clients[send_clients.length] = server.groups[i_group].clients[i_client].id;
821 }
822 }
823 }
824 }
825 if (group_id == "new")
826 group_id = current_group.id;
827 else
828 send_clients[send_clients.length] = client_id;
829 snapcontrol.setClients(group_id, send_clients);
830}
831function setName(id) {
832 // Get current name and lacency
833 let client = snapcontrol.getClient(id);
834 let current_name = (client.config.name != "") ? client.config.name : client.host.name;
835 let current_latency = client.config.latency;
836 let new_name = window.prompt("New Name", current_name);
837 let new_latency = Number(window.prompt("New Latency", String(current_latency)));
838 if (new_name != null)
839 snapcontrol.setClientName(id, new_name);
840 if (new_latency != null)
841 snapcontrol.setClientLatency(id, new_latency);
842 show();
843}
844function openClientSettings(id) {
845 let modal = document.getElementById("client-settings");
846 let client = snapcontrol.getClient(id);
847 let current_name = (client.config.name != "") ? client.config.name : client.host.name;
848 let name = document.getElementById("client-name");
849 name.name = id;
850 name.value = current_name;
851 let latency = document.getElementById("client-latency");
852 latency.valueAsNumber = client.config.latency;
853 let group = snapcontrol.getGroupFromClient(id);
854 let group_input = document.getElementById("client-group");
855 while (group_input.length > 0)
856 group_input.remove(0);
857 let group_num = 0;
858 for (let ogroup of snapcontrol.server.groups) {
859 let option = document.createElement('option');
860 option.value = ogroup.id;
861 option.text = "Group " + (group_num + 1) + " (" + ogroup.clients.length + " Clients)";
862 group_input.add(option);
863 if (ogroup == group) {
864 console.log("Selected: " + group_num);
865 group_input.selectedIndex = group_num;
866 }
867 ++group_num;
868 }
869 let option = document.createElement('option');
870 option.value = option.text = "new";
871 group_input.add(option);
872 modal.style.display = "block";
873}
874function closeClientSettings() {
875 let name = document.getElementById("client-name");
876 let id = name.name;
877 console.log("onclose " + id + ", value: " + name.value);
878 snapcontrol.setClientName(id, name.value);
879 let latency = document.getElementById("client-latency");
880 snapcontrol.setClientLatency(id, latency.valueAsNumber);
881 let group_input = document.getElementById("client-group");
882 let option = group_input.options[group_input.selectedIndex];
883 setGroup(id, option.value);
884 let modal = document.getElementById("client-settings");
885 modal.style.display = "none";
886 show();
887}
888function deleteClient(id) {
889 if (confirm('Are you sure?')) {
890 snapcontrol.deleteClient(id);
891 }
892}
893window.onload = function () {
894 snapcontrol = new SnapControl(config.baseUrl);
895};
896// When the user clicks anywhere outside of the modal, close it
897window.onclick = function (event) {
898 let modal = document.getElementById("client-settings");
899 if (event.target == modal) {
900 modal.style.display = "none";
901 }
902};
903//# sourceMappingURL=snapcontrol.js.map
904