"use strict"; class Host { constructor(json) { this.arch = ""; this.ip = ""; this.mac = ""; this.name = ""; this.os = ""; this.fromJson(json); } fromJson(json) { this.arch = json.arch; this.ip = json.ip; this.mac = json.mac; this.name = json.name; this.os = json.os; } } class Client { constructor(json) { this.id = ""; this.connected = false; this.fromJson(json); } fromJson(json) { this.id = json.id; this.host = new Host(json.host); let jsnapclient = json.snapclient; this.snapclient = { name: jsnapclient.name, protocolVersion: jsnapclient.protocolVersion, version: jsnapclient.version }; let jconfig = json.config; this.config = { instance: jconfig.instance, latency: jconfig.latency, name: jconfig.name, volume: { muted: jconfig.volume.muted, percent: jconfig.volume.percent } }; this.lastSeen = { sec: json.lastSeen.sec, usec: json.lastSeen.usec }; this.connected = Boolean(json.connected); } } class Group { constructor(json) { this.name = ""; this.id = ""; this.stream_id = ""; this.muted = false; this.clients = []; this.fromJson(json); } fromJson(json) { this.name = json.name; this.id = json.id; this.stream_id = json.stream_id; this.muted = Boolean(json.muted); for (let client of json.clients) this.clients.push(new Client(client)); } getClient(id) { for (let client of this.clients) { if (client.id == id) return client; } return null; } } class Stream { constructor(json) { this.id = ""; this.status = ""; this.fromJson(json); } fromJson(json) { this.id = json.id; this.status = json.status; let juri = json.uri; this.uri = { raw: juri.raw, scheme: juri.scheme, host: juri.host, path: juri.path, fragment: juri.fragment, query: juri.query }; } } class Server { constructor(json) { this.groups = []; this.streams = []; if (json) this.fromJson(json); } fromJson(json) { this.groups = []; for (let jgroup of json.groups) this.groups.push(new Group(jgroup)); let jsnapserver = json.server.snapserver; this.server = { host: new Host(json.server.host), snapserver: { controlProtocolVersion: jsnapserver.controlProtocolVersion, name: jsnapserver.name, protocolVersion: jsnapserver.protocolVersion, version: jsnapserver.version } }; this.streams = []; for (let jstream of json.streams) { this.streams.push(new Stream(jstream)); } } getClient(id) { for (let group of this.groups) { let client = group.getClient(id); if (client) return client; } return null; } getGroup(id) { for (let group of this.groups) { if (group.id == id) return group; } return null; } getStream(id) { for (let stream of this.streams) { if (stream.id == id) return stream; } return null; } } class SnapControl { constructor(host, port) { this.server = new Server(); this.connection = new WebSocket('ws://' + host + ':' + port + '/jsonrpc'); this.msg_id = 0; this.status_req_id = -1; // console.log(navigator); this.connection.onmessage = (msg) => this.onMessage(msg.data); this.connection.onopen = (ev) => { this.status_req_id = this.sendRequest('Server.GetStatus'); }; this.connection.onerror = (ev) => { alert("error: " + ev.type); }; //this.onError(ev); } action(answer) { switch (answer.method) { case 'Client.OnVolumeChanged': let client = this.getClient(answer.params.id); client.config.volume = answer.params.volume; updateGroupVolume(this.getGroupFromClient(client.id)); break; case 'Client.OnLatencyChanged': this.getClient(answer.params.id).config.latency = answer.params.latency; break; case 'Client.OnNameChanged': this.getClient(answer.params.id).config.name = answer.params.name; break; case 'Client.OnConnect': case 'Client.OnDisconnect': this.getClient(answer.params.client.id).fromJson(answer.params.client); break; case 'Group.OnMute': this.getGroup(answer.params.id).muted = Boolean(answer.params.mute); break; case 'Group.OnStreamChanged': this.getGroup(answer.params.id).stream_id = answer.params.stream_id; break; case 'Stream.OnUpdate': this.getStream(answer.params.id).fromJson(answer.params.stream); break; case 'Server.OnUpdate': this.server.fromJson(answer.params.server); break; default: break; } } getClient(client_id) { return this.server.getClient(client_id); } getGroup(group_id) { return this.server.getGroup(group_id); } getGroupVolume(group, online) { if (group.clients.length == 0) return 0; let group_vol = 0; let client_count = 0; for (let client of group.clients) { if (online && !client.connected) continue; group_vol += client.config.volume.percent; ++client_count; } if (client_count == 0) return 0; return group_vol / client_count; } getGroupFromClient(client_id) { for (let group of this.server.groups) for (let client of group.clients) if (client.id == client_id) return group; return null; } getStream(stream_id) { return this.server.getStream(stream_id); } setVolume(client_id, percent, mute) { percent = Math.max(0, Math.min(100, percent)); let client = this.getClient(client_id); client.config.volume.percent = percent; if (mute != undefined) client.config.volume.muted = mute; this.sendRequest('Client.SetVolume', '{"id":"' + client_id + '","volume":{"muted":' + (client.config.volume.muted ? "true" : "false") + ',"percent":' + client.config.volume.percent + '}}'); } setClientName(client_id, name) { let client = this.getClient(client_id); let current_name = (client.config.name != "") ? client.config.name : client.host.name; if (name != current_name) { this.sendRequest('Client.SetName', '{"id":"' + client_id + '","name":"' + name + '"}'); client.config.name = name; } } setClientLatency(client_id, latency) { let client = this.getClient(client_id); let current_latency = client.config.latency; if (latency != current_latency) { this.sendRequest('Client.SetLatency', '{"id":"' + client_id + '","latency":' + latency + '}'); client.config.latency = latency; } } deleteClient(client_id) { let client = this.getClient(client_id); this.sendRequest('Server.DeleteClient', '{"id": "' + client_id + '"}'); this.server.groups.forEach((g, gi) => { g.clients.forEach((c, ci) => { if (c.id == client_id) { this.server.groups[gi].clients.splice(ci, 1); } }); }); this.server.groups.forEach((g, gi) => { if (g.clients.length == 0) { this.server.groups.splice(gi, 1); } }); show(); } setStream(group_id, stream_id) { this.getGroup(group_id).stream_id = stream_id; this.sendRequest('Group.SetStream', '{"id":"' + group_id + '","stream_id":"' + stream_id + '"}'); } setClients(group_id, clients) { this.status_req_id = this.sendRequest('Group.SetClients', '{"clients":' + JSON.stringify(clients) + ',"id":"' + group_id + '"}'); } muteGroup(group_id, mute) { this.getGroup(group_id).muted = mute; this.sendRequest('Group.SetMute', '{"id":"' + group_id + '","mute":' + (mute ? "true" : "false") + '}'); } sendRequest(method, params) { let msg = '{"id": ' + (++this.msg_id) + ',"jsonrpc":"2.0","method":"' + method + '"'; if (params) msg += ',"params": ' + params; msg += '}'; console.log("Sending: " + msg); this.connection.send(msg); return this.msg_id; } onMessage(msg) { let answer = JSON.parse(msg); let is_response = (answer.id != undefined); console.log("Received " + (is_response ? "response" : "notification") + ", json: " + JSON.stringify(answer)); if (is_response) { if (answer.id == this.status_req_id) { this.server = new Server(answer.result.server); show(); } } else { if (Array.isArray(answer)) { for (let a of answer) { this.action(a); } } else { this.action(answer); } // TODO: don't update everything, but only the changed, // e.g. update the values for the volume sliders show(); } } } let snapcontrol; let snapstream = null; let hide_offline = true; let autoplay_done = false; function autoplayRequested() { return document.location.hash.match(/autoplay/) !== null; } function show() { // Render the page let play_img; if (snapstream) { play_img = 'stop.png'; } else { play_img = 'play.png'; } let content = ""; content += ""; content += "
"; let server = snapcontrol.server; for (let group of server.groups) { if (hide_offline) { let groupActive = false; for (let client of group.clients) { if (client.connected) { groupActive = true; break; } } if (!groupActive) continue; } // Set mute variables let classgroup; let muted; let mute_img; if (group.muted == true) { classgroup = 'group muted'; muted = true; mute_img = 'mute_icon.png'; } else { classgroup = 'group'; muted = false; mute_img = 'speaker_icon.png'; } // Start group div content += "
"; // Create stream selection dropdown let streamselect = ""; // Group mute and refresh button content += "
"; content += streamselect; let clientCount = 0; for (let client of group.clients) if (!hide_offline || client.connected) clientCount++; if (clientCount > 1) { let volume = snapcontrol.getGroupVolume(group, hide_offline); content += ""; content += "
"; content += " "; // content += " "; content += "
"; } // transparent placeholder edit icon content += "
"; content += "
"; content += "
"; // Create clients in group for (let client of group.clients) { if (!client.connected && hide_offline) continue; // Set name and connection state vars, start client div let name; let clas = 'client'; if (client.config.name != "") { name = client.config.name; } else { name = client.host.name; } if (client.connected == false) { clas = 'client disconnected'; } content += "
"; // Client mute status vars let muted; let mute_img; let sliderclass; if (client.config.volume.muted == true) { muted = true; sliderclass = 'slider muted'; mute_img = 'mute_icon.png'; } else { sliderclass = 'slider'; muted = false; mute_img = 'speaker_icon.png'; } // Populate client div content += ""; content += "
"; content += " "; content += "
"; content += " "; content += " "; if (client.connected == false) { content += " 🗑"; content += " "; } else { content += ""; } content += "
" + name + "
"; content += "
"; } content += "
"; } content += "
"; // content content += "
"; content += "
"; content += "
"; content += " "; content += " "; content += " "; content += " "; content += " "; content += " "; content += " "; content += "
"; content += "
"; content += "
"; // Pad then update page content = content + "

"; document.getElementById('show').innerHTML = content; for (let group of snapcontrol.server.groups) { if (group.clients.length > 1) { let slider = document.getElementById("vol_" + group.id); if (slider == null) continue; slider.addEventListener('pointerdown', function (ev) { groupVolumeEnter(group.id); }); slider.addEventListener('touchstart', function () { groupVolumeEnter(group.id); }); } } } function updateGroupVolume(group) { let group_vol = snapcontrol.getGroupVolume(group, hide_offline); let slider = document.getElementById("vol_" + group.id); if (slider == null) return; console.log("updateGroupVolume group: " + group.id + ", volume: " + group_vol + ", slider: " + (slider != null)); slider.value = String(group_vol); } let client_volumes; let group_volume; function setGroupVolume(group_id) { let group = snapcontrol.getGroup(group_id); let percent = document.getElementById('vol_' + group.id).valueAsNumber; console.log("setGroupVolume id: " + group.id + ", volume: " + percent); // show() let delta = percent - group_volume; let ratio; if (delta < 0) ratio = (group_volume - percent) / group_volume; else ratio = (percent - group_volume) / (100 - group_volume); for (let i = 0; i < group.clients.length; ++i) { let new_volume = client_volumes[i]; if (delta < 0) new_volume -= ratio * client_volumes[i]; else new_volume += ratio * (100 - client_volumes[i]); let client_id = group.clients[i].id; // TODO: use batch request to update all client volumes at once snapcontrol.setVolume(client_id, new_volume); let slider = document.getElementById('vol_' + client_id); if (slider) slider.value = String(new_volume); } } function groupVolumeEnter(group_id) { let group = snapcontrol.getGroup(group_id); let percent = document.getElementById('vol_' + group.id).valueAsNumber; console.log("groupVolumeEnter id: " + group.id + ", volume: " + percent); group_volume = percent; client_volumes = []; for (let i = 0; i < group.clients.length; ++i) { client_volumes.push(group.clients[i].config.volume.percent); } // show() } function setVolume(id, mute) { console.log("setVolume id: " + id + ", mute: " + mute); let percent = document.getElementById('vol_' + id).valueAsNumber; let client = snapcontrol.getClient(id); let needs_update = (mute != client.config.volume.muted); snapcontrol.setVolume(id, percent, mute); let group = snapcontrol.getGroupFromClient(id); updateGroupVolume(group); if (needs_update) show(); } function play() { if (snapstream) { snapstream.stop(); snapstream = null; } else { snapstream = new SnapStream(window.location.hostname, 1780); } show(); } function setMuteGroup(id, mute) { snapcontrol.muteGroup(id, mute); show(); } function setStream(id) { snapcontrol.setStream(id, document.getElementById('stream_' + id).value); show(); } function setGroup(client_id, group_id) { console.log("setGroup id: " + client_id + ", group: " + group_id); let server = snapcontrol.server; // Get client group id let current_group = snapcontrol.getGroupFromClient(client_id); // Get // List of target group's clients // OR // List of current group's other clients let send_clients = []; for (let i_group = 0; i_group < server.groups.length; i_group++) { if (server.groups[i_group].id == group_id || (group_id == "new" && server.groups[i_group].id == current_group.id)) { for (let i_client = 0; i_client < server.groups[i_group].clients.length; i_client++) { if (group_id == "new" && server.groups[i_group].clients[i_client].id == client_id) { } else { send_clients[send_clients.length] = server.groups[i_group].clients[i_client].id; } } } } if (group_id == "new") group_id = current_group.id; else send_clients[send_clients.length] = client_id; snapcontrol.setClients(group_id, send_clients); } function setName(id) { // Get current name and lacency let client = snapcontrol.getClient(id); let current_name = (client.config.name != "") ? client.config.name : client.host.name; let current_latency = client.config.latency; let new_name = window.prompt("New Name", current_name); let new_latency = Number(window.prompt("New Latency", String(current_latency))); if (new_name != null) snapcontrol.setClientName(id, new_name); if (new_latency != null) snapcontrol.setClientLatency(id, new_latency); show(); } function openClientSettings(id) { let modal = document.getElementById("client-settings"); let client = snapcontrol.getClient(id); let current_name = (client.config.name != "") ? client.config.name : client.host.name; let name = document.getElementById("client-name"); name.name = id; name.value = current_name; let latency = document.getElementById("client-latency"); latency.valueAsNumber = client.config.latency; let group = snapcontrol.getGroupFromClient(id); let group_input = document.getElementById("client-group"); while (group_input.length > 0) group_input.remove(0); let group_num = 0; for (let ogroup of snapcontrol.server.groups) { let option = document.createElement('option'); option.value = ogroup.id; option.text = "Group " + (group_num + 1) + " (" + ogroup.clients.length + " Clients)"; group_input.add(option); if (ogroup == group) { console.log("Selected: " + group_num); group_input.selectedIndex = group_num; } ++group_num; } let option = document.createElement('option'); option.value = option.text = "new"; group_input.add(option); modal.style.display = "block"; } function closeClientSettings() { let name = document.getElementById("client-name"); let id = name.name; console.log("onclose " + id + ", value: " + name.value); snapcontrol.setClientName(id, name.value); let latency = document.getElementById("client-latency"); snapcontrol.setClientLatency(id, latency.valueAsNumber); let group_input = document.getElementById("client-group"); let option = group_input.options[group_input.selectedIndex]; setGroup(id, option.value); let modal = document.getElementById("client-settings"); modal.style.display = "none"; show(); } function deleteClient(id) { if (confirm('Are you sure?')) { snapcontrol.deleteClient(id); } } window.onload = function (event) { snapcontrol = new SnapControl(window.location.hostname, 1780); }; // When the user clicks anywhere outside of the modal, close it window.onclick = function (event) { let modal = document.getElementById("client-settings"); if (event.target == modal) { modal.style.display = "none"; } }; //# sourceMappingURL=snapcontrol.js.map