var timer = null;
var lasttime = null;
var seeking = false;
var lastposition = 0.0;
var streaming = false;
var needsync = false;
var audiotag = null;
var s = null;
var accesskey = '';
var sign = function(path) {
sh = new SipHash();
ts = new Date().getTime();
message = path + "\n" + ts;
return "sig=" + sh.hash_hex(sh.string_to_key(accesskey), message) + "&ts=" + ts;
};
var pad = function(str, len, chr, left) {
var padding = (str.length >= len) ? '' : new Array(1 + len - str.length >>> 0).join(chr);
return left ? str + padding : padding + str;
};
var timestr = function(s) {
var h = Math.floor(s / (60*60));
var m = Math.floor((s % (60*60)) / 60);
var sec = Math.floor(s % 60);
if (h >= 1) {
return pad(h.toString(), 2, '0', false) + ":" +
pad(m.toString(), 2, '0', false) + ":" +
pad(sec.toString(), 2, '0', false);
} else {
return pad(m.toString(), 2, '0', false) + ":" +
pad(sec.toString(), 2, '0', false);
}
};
var tick = function() {
now = new Date();
var elapsed = (now.getTime() - lasttime.getTime());
lasttime = now;
if (seeking === false) {
lastposition = lastposition + elapsed;
document.getElementById("seekbar-range").value = lastposition;
document.getElementById("trackposition").textContent = timestr(lastposition/1000);
}
if (streaming && needsync) {
audioTimeSync();
}
};
var audioCanSeekTo = function(seektime) {
for (i = 0; i < audiotag.seekable.length; i++) {
if (audiotag.seekable.start(i) > seektime) {
return false;
}
if (audiotag.seekable.end(i) > seektime) {
return true;
}
}
return false;
};
var audioTimeSync = function() {
if (needsync) {
if (audioCanSeekTo(lastposition/1000)) {
// probably check seekable ranges and stuff?
audiotag.currentTime = lastposition/1000;
if (timer != null) {
audiotag.play();
}
needsync = false;
}
}
};
var createAudioTag = function() {
audiotag = document.createElement("audio");
// plugin should give us direct stream urls if they exist
// and should tell us if the thing can't be streamed (cdda etc.) too
path = "/entry/current/stream";
audiotag.setAttribute("src", path + "?" + sign(path));
audiotag.setAttribute("preload", "auto");
audiotag.addEventListener('canplay', audioTimeSync);
needsync = true;
c = document.getElementById("stream-container");
while (c.firstChild) {
c.removeChild(c.firstChild);
}
c.appendChild(audiotag);
};
var replaceImage = function(element, imgclass, imgsrc) {
img = document.createElement("img");
img.setAttribute("class", imgclass);
img.setAttribute("src", imgsrc);
e = document.getElementById(element);
while (e.firstChild) {
e.removeChild(e.firstChild);
}
e.appendChild(img);
};
var connectionState = function(connected) {
document.getElementById("connectform").hidden = connected;
document.getElementById("trackbits").hidden = !connected;
document.getElementById("seekbar").hidden = !connected;
pb = document.getElementById("playerbuttons");
if (connected) {
pb.classList.remove("pure-button-disabled");
} else {
pb.classList.add("pure-button-disabled");
}
}
var connect = function() {
accesskey = document.getElementById("accesskey").value;
loc = document.location.hostname + ":" + document.location.port;
s = new WebSocket("ws://" + loc + "/ws/player:" + sign("/ws/player"));
s.onopen = function(event) {
connectionState(true);
msg = { "action": "status" };
s.send(JSON.stringify(msg));
};
s.onclose = function(event) {
document.getElementById("connectionhostname").textContent = "";
window.clearInterval(timer);
timer = null;
lasttime = null;
connectionState(false);
};
s.onmessage = function(event) {
m = JSON.parse(event.data);
if ("shutdown" in m) {
if (streaming) {
toggleStreaming();
}
}
if ("hostname" in m) {
document.getElementById("connectionhostname").textContent = m['hostname'];
}
if ("id" in m) {
lastposition = 0;
document.getElementById("seekbar-range").value = 0;
document.getElementById("trackposition").textContent = timestr(0);
if (streaming) {
createAudioTag();
}
}
if ("title" in m) {
document.title = m.title;
document.getElementById("tracktitle").textContent = m.title;
}
if ("artist" in m) {
document.getElementById("trackartist").textContent = m.artist;
}
if ("album" in m) {
document.getElementById("trackalbum").textContent = m.album;
}
if ("duration" in m) {
range = document.getElementById("seekbar-range");
range.max = m.duration*1000;
range.min = 0;
document.getElementById('trackduration').textContent = timestr(m.duration);
}
if ("playing" in m) {
if (m.playing) {
iconname = "media-playback-pause-symbolic";
if (timer == null) {
timer = window.setInterval(tick, 250);
lasttime = new Date();
}
if (streaming) {
audiotag.play();
}
} else {
iconname = "media-playback-start-symbolic";
window.clearInterval(timer);
timer = null;
lasttime = null;
if (streaming) {
audiotag.pause();
}
}
replaceImage("playpause", "", "/icon/" + iconname + "/48");
}
if ("position" in m) {
lastposition = m.position;
document.getElementById("seekbar-range").value = m.position;
document.getElementById('trackposition').textContent = timestr(m.position / 1000);
seeking = false;
if (streaming) {
needsync = true;
audioTimeSync();
}
}
if ("albumart" in m) {
if (m.albumart != null) {
path = "/art/" + m.albumart;
imgsrc = path + "?" + sign(path);
imgclass = "albumart";
} else {
imgsrc = "/icon/rhythmbox-symbolic/128";
imgclass = "noalbumart";
}
replaceImage("trackimage", imgclass, imgsrc);
}
};
return false;
};
var sendevent = function() {
s.send(JSON.stringify({ 'action': this.id }));
};
var seekinput = function() {
if (seeking === false) {
time = this.value / 1000;
s.send(JSON.stringify({ 'action': 'seek', 'time': time}));
seeking = true;
}
};
var toggleStreaming = function() {
if (streaming) {
streaming = false;
audiotag.pause();
c = document.getElementById("stream-container");
c.removeChild(audiotag);
audiotag = null;
} else {
streaming = true;
createAudioTag();
}
};
window.onload = function() {
document.getElementById("previous").addEventListener('click', sendevent);
document.getElementById("playpause").addEventListener('click', sendevent);
document.getElementById("next").addEventListener('click', sendevent);
document.getElementById("seekbar-range").addEventListener('input', seekinput);
// document.getElementById("stream-check").addEventListener('click', toggleStreaming);
document.getElementById("connectform").onsubmit = connect;
connectionState(false);
}
// siphash implementation from https://github.com/jedisct1/siphash-js
function SipHash() {
function _add(a, b) {
var rl = a.l + b.l,
a2 = { h: a.h + b.h + (rl / 2 >>> 31) >>> 0,
l: rl >>> 0 };
a.h = a2.h; a.l = a2.l;
}
function _xor(a, b) {
a.h ^= b.h; a.h >>>= 0;
a.l ^= b.l; a.l >>>= 0;
}
function _rotl(a, n) {
var a2 = {
h: a.h << n | a.l >>> (32 - n),
l: a.l << n | a.h >>> (32 - n)
};
a.h = a2.h; a.l = a2.l;
}
function _rotl32(a) {
var al = a.l;
a.l = a.h; a.h = al;
}
function _compress(v0, v1, v2, v3) {
_add(v0, v1);
_add(v2, v3);
_rotl(v1, 13);
_rotl(v3, 16);
_xor(v1, v0);
_xor(v3, v2);
_rotl32(v0);
_add(v2, v1);
_add(v0, v3);
_rotl(v1, 17);
_rotl(v3, 21);
_xor(v1, v2);
_xor(v3, v0);
_rotl32(v2);
}
function _get_int(a, offset) {
return a.charCodeAt(offset + 3) << 24 |
a.charCodeAt(offset + 2) << 16 |
a.charCodeAt(offset + 1) << 8 |
a.charCodeAt(offset);
}
function hash(key, m) {
var k0 = { h: key[1] >>> 0, l: key[0] >>> 0 },
k1 = { h: key[3] >>> 0, l: key[2] >>> 0 },
v0 = { h: k0.h, l: k0.l }, v2 = k0,
v1 = { h: k1.h, l: k1.l }, v3 = k1,
mi, mp = 0, ml = m.length, ml7 = ml - 7,
buf = new Uint8Array(new ArrayBuffer(8));
_xor(v0, { h: 0x736f6d65, l: 0x70736575 });
_xor(v1, { h: 0x646f7261, l: 0x6e646f6d });
_xor(v2, { h: 0x6c796765, l: 0x6e657261 });
_xor(v3, { h: 0x74656462, l: 0x79746573 });
while (mp < ml7) {
mi = { h: _get_int(m, mp + 4), l: _get_int(m, mp) };
_xor(v3, mi);
_compress(v0, v1, v2, v3);
_compress(v0, v1, v2, v3);
_xor(v0, mi);
mp += 8;
}
buf[7] = ml;
var ic = 0;
while (mp < ml) {
buf[ic++] = m.charCodeAt(mp++);
}
while (ic < 7) {
buf[ic++] = 0;
}
mi = { h: buf[7] << 24 | buf[6] << 16 | buf[5] << 8 | buf[4],
l: buf[3] << 24 | buf[2] << 16 | buf[1] << 8 | buf[0] };
_xor(v3, mi);
_compress(v0, v1, v2, v3);
_compress(v0, v1, v2, v3);
_xor(v0, mi);
_xor(v2, { h: 0, l: 0xff });
_compress(v0, v1, v2, v3);
_compress(v0, v1, v2, v3);
_compress(v0, v1, v2, v3);
_compress(v0, v1, v2, v3);
var h = v0;
_xor(h, v1);
_xor(h, v2);
_xor(h, v3);
return h;
}
function string_to_key(a) {
var k = [0, 0, 0, 0];
var i = 0, ki = 0;
pa = a + "\0\0\0\0";
while (i < a.length) {
k[ki] = (k[ki] + _get_int(pa, i)) % 0xffffffff;
i += 4;
ki = (ki + 1) % 4;
}
return k;
}
function hash_hex(key, m) {
var r = hash(key, m);
return ("0000000" + r.h.toString(16)).substr(-8) +
("0000000" + r.l.toString(16)).substr(-8);
}
function hash_uint(key, m) {
var r = hash(key, m);
return (r.h & 0x1fffff) * 0x100000000 + r.l;
}
return {
string_to_key: string_to_key,
hash: hash,
hash_hex: hash_hex,
hash_uint: hash_uint
};
};