You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

693 lines
16 KiB

<!DOCTYPE html>
<html>
<head>
<title>dml_httpd</title>
<meta charset="UTF-8">
</head>
<body style="background-color: white;">
<script>
/*
Copyright Jeroen Vreeken (jeroen@vreeken.net), 2016, 2017
@licstart The following is the entire license notice for the
JavaScript code in this page.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
@licend The above is the entire license notice
for the JavaScript code in this page.
*/
</script>
<p>
Decentralized Media Linking
</p>
<div id="hello">No HELLO from server received.</div>
<div id="video_div" style="display: none;">
<video style="border: 2px solid" id="video"></video>
<button onclick="curcon.disconnect()">Disconnect</button>
</div>
<div id="dmlc2_div" style="display: none;">
<div style="border: 2px solid" id="dmlc2_state"></div>
<button onclick="curcon.disconnect()">Disconnect</button>
</div>
<div id="fprs_div" style="display: none;">
<div style="border: 2px solid" id="fprs_state"></div>
<button onclick="curcon.disconnect()">Disconnect</button>
</div>
<div id = "routes"></div>
<p>
<a href="jslicense.html" rel="jslicense">JavaScript License</a>
</p>
<script src="dml.js"></script>
<script src="eth_ar.js"></script>
<script src="fprs.js"></script>
<script src="aprs_symbol.js"></script>
<script src="alaw.js"></script>
<script src="ulaw.js"></script>
<script src="resample.js"></script>
<script src="libcodec2.js"></script>
<script src="codec2.js"></script>
<script>
CODEC2.MODE[65] = 'A-law';
CODEC2.MODE[85] = 'u-law';
CODEC2.MODE[83] = 'be16';
CODEC2.MODE[115] = 'le16';
function char2hex(c)
{
str = c.toString(16);
while (str.length < 2)
str = "0" + str;
return str;
}
function data2str(data, length, offset)
{
var str = "";
var i;
var l = typeof length !== 'undefined' ? length : data.byteLength;
var o = typeof offset !== 'undefined' ? offset : 0;
for (i = o; i < l; i++) {
var c = data.getUint8(i, false);
if (!c)
return str;
str += String.fromCharCode(c);
}
return str;
}
function sha2str(data)
{
if (data instanceof ArrayBuffer)
data = new DataView(data);
var i;
var str = "";
for (i = 0; i < 32; i++) {
str += char2hex(data.getUint8(i));
}
return str;
}
/* compare two arraybuffers */
function ab_cmp(ab1, ab2)
{
if (ab1.byteLength != ab2.byteLength)
return true;
var dv1 = new DataView(ab1);
var dv2 = new DataView(ab2);
var i;
for (i = 0; i < ab1.byteLength; i++) {
if (dv1.getUint8(i) != dv2.getUint8(i))
return true;
}
return false;
}
/* copy contents of an array buffer into another */
function ab_copy(dst_ab, dst_off, src_ab, src_off, len)
{
var dst = new DataView(dst_ab);
var src = new DataView(src_ab);
var i;
for (i = 0; i < len; i++) {
dst.setUint8(i + dst_off, src.getUint8(i + src_off));
}
}
var routes = new Array();
function routes_sort(a, b)
{
/* No use comparing if nothing is known, just make it go to the bottom */
if (!a.description)
return 1;
if (!b.description)
return -1;
if (a.description.name < b.description.name) {
return -1;
}
if (a.description.name > b.description.name) {
return 1;
}
if (a.description.mime < b.description.mime) {
return -1;
}
if (a.description.mime > b.description.mime) {
return 1;
}
if (a.description.alias < b.description.alias) {
return -1;
}
if (a.description.alias > b.description.alias) {
return 1;
}
console.log(a.description.name + " == " + b.description.name);
return 0;
}
function update_routes()
{
var i;
routes_str = "";
routes.sort(routes_sort);
for (i = 0; i < routes.length; i++) {
var can_play_mse = false;
var can_play_dml = false;
var can_play_fprs = false;
var shastr = sha2str(routes[i].id);
var ui_id = shastr;
if (routes[i].description) {
ui_id = routes[i].description.name;
if (routes[i].description.alias)
ui_id += " (" + routes[i].description.alias + ")";
can_play_mse = mse.supported(routes[i].description.mime);
can_play_dml = dc2.supported(routes[i].description.mime);
can_play_fprs = fprs.supported(routes[i].description.mime);
}
var str = "";
str += "<div style='background-color:yellow;margin: 1px 1px 1px 30px; display: none' id='desc_" + shastr + "'>";
if (routes[i].description) {
str += "\tdescription:\t" + routes[i].description.description + "<br>";
str += "\tbps:\t" + routes[i].description.bps + "<br>";
str += "\tmime:\t" + routes[i].description.mime + "<br>";
str += "\tname:\t" + routes[i].description.name + "<br>";
str += "\talias:\t" + routes[i].description.alias + "<br>";
}
if (routes[i].certificate) {
str += "\tcertificate length:\t" + routes[i].certificate.data.byteLength + "<br>";
ui8 = new Uint8Array(routes[i].certificate.data);
var pos = 0;
while (pos < ui8.length) {
var size = ui8[0] * 256 + ui8[1];
if (size < 1) {
break;
}
cert = ui8.slice(pos + 2, pos + 2 + size);
b64 = btoa(String.fromCharCode.apply(null, cert));
str += "-----BEGIN CERTIFICATE-----<br>";
var j;
for (j = 0; j < b64.length; j += 64) {
str += b64.slice(j, j+64) + "<br>";
}
str += "-----END CERTIFICATE-----<br>";
pos += size + 2;
}
}
str += "\tid:\t" + shastr + "<br>";
str += "\thops:\t" + routes[i].hops + "<br>";
str += "</div>";
idcolor="yellow";
if (can_play_mse) {
idcolor="lightblue";
button_str = "<button onclick=\"mse.connect(" + i + ")\">Connect & Play</button>";
} else if (can_play_dml) {
idcolor="lightgreen";
button_str = "<button onclick=\"dc2.connect(" + i + ")\">Connect & Play</button>";
} else if (can_play_fprs) {
idcolor="orange";
button_str = "<button onclick=\"fprs.connect(" + i + ")\">Connect & monitor FPRS frames</button>";
} else {
button_str = "Mime type not supported by browser\n";
}
id_str = "<div width='100%' style='background-color:" + idcolor + ";'>";
id_str += "<button onclick=\"div_toggle('desc_" + shastr + "')\">i</button>";
id_str += ui_id + " " + button_str;
id_str += str;
id_str += "</div>";
routes_str += id_str;
}
document.getElementById("routes").innerHTML = routes_str;
}
function div_toggle(id)
{
var dis = document.getElementById(id).style.display;
console.log("toggle " + dis);
if (dis == "block")
dis = "none";
else
dis = "block";
document.getElementById(id).style.display = dis;
}
var eth_ar = new eth_ar();
connected = false;
connected_id = null;
connected_data_id = DML.PACKET.DATA;
connected_timestamp = 0;
function prep_connect(id)
{
if (connected && ab_cmp(routes[id].id, connected_id)) {
curcon.disconnect();
}
connected = true;
connected_id = routes[id].id;
}
function do_connect(id)
{
connection.send_req_header(id);
}
function disconnect()
{
if (connected) {
connection.send_req_disc(connected_id);
connected_data_id++;
connected = false;
}
}
function mse()
{
var mediaSource = new MediaSource();
var sourceBuffer;
var video = document.getElementById("video");
var mse_this = this;
this.supported = function supported(mime) {
return MediaSource.isTypeSupported(mime);
}
this.disconnect = function mse_disconnect()
{
disconnect();
video.pause();
// video.src = "";
// video.play();
document.getElementById("video_div").style.display = "none";
}
this.connect = function mse_connect(id)
{
document.getElementById("video_div").style.display = "block";
prep_connect(id);
curcon = mse_this;
video.src = URL.createObjectURL(mediaSource);
video.play();
console.log("mediaSource.readyState: " + mediaSource.readyState);
mediaSource.addEventListener('sourceopen', sourceOpen_mse);
}
function sourceOpen_mse (_) {
console.log("mediaSource.readyState: " + mediaSource.readyState);
sourceBuffer = mediaSource.addSourceBuffer("video/webm");
do_connect(connected_id);
console.log("Requested header");
};
this.data = function mse_data(newdata) {
sourceBuffer.appendBuffer(newdata);
if (video.buffered.length) {
if (video.buffered.start(0) > video.currentTime)
video.currentTime = video.buffered.start(0);
}
}
}
mse = new mse();
function fprs()
{
var fprs_this = this;
this.supported = function supported(mime) {
return mime == "application/fprs";
}
var ui_name;
this.connect = function fprs_connect(id) {
prep_connect(id);
curcon = fprs_this;
if (routes[id].description) {
fprs_this.ui_name = routes[id].description.name;
} else {
fprs_this.ui_name = "";
}
document.getElementById("fprs_div").style.display = "block";
document.getElementById("fprs_state").innerHTML = fprs_this.ui_name + ": Connect send";
do_connect(connected_id);
fprs_this.ui = new Array();
}
this.disconnect = function fprs_disconnect()
{
disconnect();
document.getElementById("fprs_div").style.display = "none";
}
this.data = function fprs_data(newdata) {
frame = new fprs_frame(newdata);
var str;
str = "";
var i;
for (i = 0; i < frame.elements.length; i++) {
el_str = frame.elements[i].tostring();
switch(frame.elements[i].type_get()) {
case FPRS.ELEMENT.POSITION:
pos = frame.elements[i].position_dec();
str += "<a target='_blank' href='http://www.openstreetmap.org/?" +
"mlat=" + pos.latitude +
"&mlon=" + pos.longitude +
"&zoom=16&layers=M'>" +
el_str +
"</a> ";
break;
case FPRS.ELEMENT.SYMBOL:
sym = frame.elements[i].symbol_dec();
symurl = aprs_symbol(sym);
if (symurl) {
str += "<img src='" + symurl + "'></img> "
} else {
str += el_str + " ";
}
break;
default:
str += el_str + " ";
}
}
fprs_this.update_ui(str);
}
this.update_ui = function (str) {
fprs_this.ui.push(str);
while (fprs_this.ui.length > 10)
fprs_this.ui.shift();
var i;
var str = fprs_this.ui_name + ":<br>\n";
for (i = 0; i < fprs_this.ui.length; i++) {
str = str + fprs_this.ui[i] + "<br>\n";
}
document.getElementById("fprs_state").innerHTML = str;
}
}
fprs = new fprs();
function dmlc2()
{
var dmlc2_this = this;
this.audio_ctx = new (window.AudioContext || window.webkitAudioContext)();
this.scriptnode = undefined;
this.supported = function supported(mime) {
return mime == "audio/dml-codec2";
}
var ui_name;
this.connect = function dmlc2_connect(id) {
prep_connect(id);
curcon = dmlc2_this;
if (routes[id].description) {
dmlc2_this.ui_name = routes[id].description.name;
} else {
dmlc2_this.ui_name = "";
}
document.getElementById("dmlc2_div").style.display = "block";
document.getElementById("dmlc2_state").innerHTML = dmlc2_this.ui_name + ": Connect send";
do_connect(connected_id);
dmlc2_this.audio_ctx = new (window.AudioContext || window.webkitAudioContext)();
dmlc2_this.rate = dmlc2_this.audio_ctx.sampleRate;
dmlc2_this.resample = new resample();
dmlc2_this.queue = new Array();
dmlc2_this.scriptnode = dmlc2_this.audio_ctx.createScriptProcessor(1024, 0, 1);
dmlc2_this.scriptnode.onaudioprocess = function (event) {
// console.log("event: " + event.outputBuffer.length);
var i, out, len;
len = event.outputBuffer.length;
out = event.outputBuffer.getChannelData(0);
for (i = 0; i < len; ++i) {
if (dmlc2_this.queue.length) {
out[i] = dmlc2_this.queue[0].shift();
if (dmlc2_this.queue[0].length == 0) {
dmlc2_this.queue.shift();
}
} else {
out[i] = 0;
}
}
};
dmlc2_this.scriptnode.connect(dmlc2_this.audio_ctx.destination);
dmlc2_this.state = false;
dmlc2_this.mode = -1;
dmlc2_this.callsign = "?";
}
this.disconnect = function dmlc2_disconnect()
{
dmlc2_this.scriptnode.disconnect();
dmlc2_this.scriptnode = undefined;
dmlc2_this.audio_ctx = undefined;
disconnect();
document.getElementById("dmlc2_div").style.display = "none";
}
var state = false;
var mode = -1;
var callsign = "?";
var timeout_var = false;
dmlc2_this.alaw_data = new alaw();
dmlc2_this.ulaw_data = new ulaw();
dmlc2_this.c2 = new codec2();
this.data = function dmlc2_data(newdata) {
if (data.byteLength >= 8) {
var dv = new DataView(newdata);
var newstate = dv.getUint8(7) & 0x1;
var bytemode = dv.getUint8(6);
var newmode = CODEC2.MODE[bytemode];
var newcallsign = eth_ar.call(newdata);
if (newdata.byteLength > 8) {
c2_buf = new Uint8Array(newdata.slice(8));
var samples
if (newmode == 'A-law') {
samples = dmlc2_this.alaw_data.decode(c2_buf);
} else if (newmode == 'u-law') {
samples = dmlc2_this.ulaw_data.decode(c2_buf);
} else if (newmode == 'le16') {
var i;
var nr = c2_buf.byteLength / 2;
samples = new Array(nr);
var dv = new DataView(c2_buf);
for (i = 0; i < nr; i++) {
samples[i] = dv.getInt16(i, true)
}
} else if (newmode == 'be16') {
var i;
var nr = c2_buf.byteLength / 2;
samples = new Array(nr);
var dv = new DataView(c2_buf);
for (i = 0; i < nr; i++) {
samples[i] = dv.getInt16(i, false)
}
} else {
dmlc2_this.c2.create(bytemode);
samples = dmlc2_this.c2.decode(c2_buf);
}
samples = dmlc2_this.resample.convert(samples, 8000, dmlc2_this.rate);
dmlc2_this.queue.push(samples)
}
if (dmlc2_this.timeout_var) {
clearTimeout(dmlc2_this.timeout_var);
dmlc2_this.timeout_var = false;
}
dmlc2_this.update_ui(newstate, newmode, newcallsign);
if (newstate) {
dmlc2_this.timeout_var = setTimeout(this.tx_timeout, 100);
}
}
}
this.tx_timeout = function () {
dmlc2_this.timeout_var = false;
dmlc2_this.update_ui(0, 0, "-");
}
this.update_ui = function (newstate, newmode, newcallsign) {
if (newstate != dmlc2_this.state || newmode != dmlc2_this.mode || dmlc2_this.callsign != newcallsign) {
var str = dmlc2_this.ui_name + ": state: " + newstate + " mode: " + newmode + " callsign: " + newcallsign;
document.getElementById("dmlc2_state").innerHTML = str;
dmlc2_this.state = newstate;
dmlc2_this.mode = newmode;
dmlc2_this.callsign = newcallsign;
}
}
}
var dc2 = new dmlc2();
var curcon;
connection = new dml();
connection.packet_hello_cb = function(flags, ident) {
document.getElementById("hello").innerHTML =
"Server identity: " + ident + "<br>" +
"Server flags: " + connection.hello_flag2str(flags);
}
connection.packet_route_cb = function(hops, id) {
var i;
for(i = 0; i < routes.length; i++) {
if (!ab_cmp(routes[i].id, id))
break;
}
routes[i] = {};
routes[i].id = id;
routes[i].hops = hops;
/* remove route? */
if (hops == 255) {
routes.splice(i, 1);
} else {
/* already got a description? */
if (!routes[i].description)
connection.send_req_description(id);
if (!routes[i].certificate)
connection.send_req_certificate(id);
}
update_routes();
}
connection.packet_description_cb = function(id, version, bps, mime, name, alias, description) {
var i;
for (i = 0; i < routes.length; i++) {
if (!ab_cmp(routes[i].id, id)) {
routes[i].description = {};
routes[i].description.version = version;
routes[i].description.bps = bps;
routes[i].description.mime = mime;
routes[i].description.name = name;
routes[i].description.alias = alias;
routes[i].description.description = description;
}
}
update_routes();
}
connection.packet_certificate_cb = function(id, cert) {
var i;
for (i = 0; i < routes.length; i++) {
if (!ab_cmp(routes[i].id, id)) {
routes[i].certificate = {};
routes[i].certificate.data = cert;
}
}
update_routes();
}
connection.packet_header_cb = function(header_id, header_data, header_sig) {
console.log("header " + header_id.byteLength + " " + header_data.byteLength + " " + header_sig.byteLength);
if (!ab_cmp(connected_id, header_id)) {
console.log("Send connect");
connection.send_connect(connected_id, connected_data_id);
if (header_data.byteLength) {
curcon.data(header_data);
}
}
}
connection.packet_data_cb = function(data, timestamp, signature) {
var i;
if (timestamp <= connected_timestamp) {
console.log("timestamp invalid");
}
curcon.data(data);
}
</script>
</body>
</html>