music-assistant-server

34.4 KBJS
snapcontrol.js
34.4 KB904 lines • javascript
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'>&#9998</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'>&#9998</a>";
673            if (client.connected == false) {
674                content += "      <a href=\"javascript:deleteClient('" + client.id + "');\" class='delete-icon'>&#128465</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