From e1fb29b7c80ecafebd0d5ebd1509c3618fb7c3e7 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Sun, 7 Jun 2020 12:33:56 +0200 Subject: [PATCH] Add first implementation of a config servlet --- .../ctbrec/recorder/server/ConfigServlet.java | 64 ++++ .../ctbrec/recorder/server/HttpServer.java | 4 + .../src/main/resources/html/static/index.html | 347 +----------------- .../src/main/resources/html/static/models.js | 142 +++++++ .../main/resources/html/static/recordings.js | 203 ++++++++++ 5 files changed, 419 insertions(+), 341 deletions(-) create mode 100644 server/src/main/java/ctbrec/recorder/server/ConfigServlet.java create mode 100644 server/src/main/resources/html/static/models.js create mode 100644 server/src/main/resources/html/static/recordings.js diff --git a/server/src/main/java/ctbrec/recorder/server/ConfigServlet.java b/server/src/main/java/ctbrec/recorder/server/ConfigServlet.java new file mode 100644 index 00000000..df3facbb --- /dev/null +++ b/server/src/main/java/ctbrec/recorder/server/ConfigServlet.java @@ -0,0 +1,64 @@ +package ctbrec.recorder.server; + +import static javax.servlet.http.HttpServletResponse.*; + +import java.io.IOException; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.json.JSONArray; +import org.json.JSONObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ctbrec.Config; +import ctbrec.Settings; + +public class ConfigServlet extends AbstractCtbrecServlet { + + private static final Logger LOG = LoggerFactory.getLogger(ConfigServlet.class); + private Config config; + private Settings settings; + + public enum DataType { + STRING, + BOOLEAN, + INTEGER, + DOUBLE + } + + public ConfigServlet(Config config) { + this.config = config; + this.settings = config.getSettings(); + } + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + resp.setStatus(SC_OK); + resp.setContentType("application/json"); + + JSONArray json = new JSONArray(); + addParameter("httpPort", "HTTP port", DataType.INTEGER, settings.httpPort, json); + + resp.getWriter().print(json.toString(2)); + LOG.debug("GET {} {}", req.getRequestURI(), req.getRequestURL()); + } + + private void addParameter(String key, String name, DataType type, Object value, JSONArray json) { + JSONObject param = new JSONObject(); + param.put("key", key); + param.put("name", name); + param.put("type", type); + param.put("value", value); + json.put(param); + } + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + resp.setStatus(SC_OK); + resp.setContentType("application/json"); + LOG.debug("POST"); + } +} diff --git a/server/src/main/java/ctbrec/recorder/server/HttpServer.java b/server/src/main/java/ctbrec/recorder/server/HttpServer.java index 95c38fbf..f97d49b2 100644 --- a/server/src/main/java/ctbrec/recorder/server/HttpServer.java +++ b/server/src/main/java/ctbrec/recorder/server/HttpServer.java @@ -207,6 +207,10 @@ public class HttpServer { ServletHolder holder = new ServletHolder(recorderServlet); defaultContext.addServlet(holder, "/rec"); + ConfigServlet configServlet = new ConfigServlet(config); + holder = new ServletHolder(configServlet); + defaultContext.addServlet(holder, "/config"); + HlsServlet hlsServlet = new HlsServlet(this.config); holder = new ServletHolder(hlsServlet); defaultContext.addServlet(holder, "/hls/*"); diff --git a/server/src/main/resources/html/static/index.html b/server/src/main/resources/html/static/index.html index f37e7bca..0627acc8 100644 --- a/server/src/main/resources/html/static/index.html +++ b/server/src/main/resources/html/static/index.html @@ -197,7 +197,6 @@ - @@ -209,12 +208,16 @@ - + + + + + - + diff --git a/server/src/main/resources/html/static/models.js b/server/src/main/resources/html/static/models.js new file mode 100644 index 00000000..55ac777a --- /dev/null +++ b/server/src/main/resources/html/static/models.js @@ -0,0 +1,142 @@ +function updateOnlineModels() { + try { + let action = '{"action": "listOnline"}'; + $.ajax({ + type : 'POST', + url : '../rec', + dataType : 'json', + async : true, + timeout : 60000, + headers : { + 'CTBREC-HMAC' : CryptoJS.HmacSHA256(action, hmac) + }, + data : action + }).done(function(data, textStatus, jqXHR) { + if (data.status === 'success') { + onlineModels = data.models; + } else { + if (console) + console.log('request failed', data); + } + updateModels(); + }).fail(function(jqXHR, textStatus, errorThrown) { + if (console) + console.log(jqXHR, textStatus, errorThrown); + }); + } catch (e) { + if (console) + console.log('Unexpected error', e); + } + setTimeout(updateOnlineModels, 3000); +} + +function isModelInArray(array, model) { + for ( let idx in array) { + let m = array[idx]; + if (m.url === model.url) { + return true; + } + } + return false; +} + +/** + * Synchronizes models from the server with the displayed knockout model table + */ +function syncModels(models) { + // remove models from the observable array, which are not in the + // updated list + for ( let idx in observableModelsArray()) { + let model = observableModelsArray()[idx]; + if (!isModelInArray(models, model)) { + observableModelsArray.remove(model); + } + } + + // add models to the observable array, which are new in the + // updated list + for ( let idx in models) { + let model = models[idx]; + if (!isModelInArray(observableModelsArray(), model)) { + model.ko_name = ko.observable(model.name); + model.ko_url = ko.observable(model.url); + model.ko_online = ko.observable(false); + for ( let i in onlineModels) { + let onlineModel = onlineModels[i]; + if (onlineModel.url === model.url) { + model.ko_online(true); + break; + } + } + model.ko_recording = ko.observable(model.online && !model.suspended); + model.ko_suspended = ko.observable(model.suspended); + model.swallowEvents = false; + model.ko_suspended.subscribe(function(checked) { + if (model.swallowEvents) { + return; + } + if (!checked) { + ctbrec.resume(model); + } else { + ctbrec.suspend(model); + } + }); + observableModelsArray.push(model); + } + } + + // update existing models + for ( let i in models) { + let model = models[i]; + for ( let j in observableModelsArray()) { + let m = observableModelsArray()[j]; + if (model.url === m.ko_url()) { + m.ko_name(model.name); + m.ko_url(model.url); + let onlineState = false; + for ( let i in onlineModels) { + let onlineModel = onlineModels[i]; + if (onlineModel.url === model.url) { + onlineState = true; + break; + } + } + m.ko_online(onlineState); + m.swallowEvents = true; + m.ko_suspended(model.suspended); + m.swallowEvents = false; + m.ko_recording(m.ko_online() && !m.ko_suspended()); + } + } + } +} + +function updateModels() { + try { + let action = '{"action": "list"}'; + $.ajax({ + type : 'POST', + url : '../rec', + dataType : 'json', + async : true, + timeout : 60000, + headers : { + 'CTBREC-HMAC' : CryptoJS.HmacSHA256(action, hmac) + }, + data : action + }).done(function(data) { + if (data.status === 'success') { + syncModels(data.models); + } else { + if (console) + console.log('request failed', data); + } + }).fail(function(jqXHR, textStatus, errorThrown) { + if (console) + console.log(textStatus, errorThrown); + }); + } catch (e) { + if (console) + console.log('Unexpected error', e); + } +} \ No newline at end of file diff --git a/server/src/main/resources/html/static/recordings.js b/server/src/main/resources/html/static/recordings.js new file mode 100644 index 00000000..6fbd79a1 --- /dev/null +++ b/server/src/main/resources/html/static/recordings.js @@ -0,0 +1,203 @@ +function play(recording) { + let src = recording.singleFile ? '/hls' + recording.path : recording.playlist; + let hmacOfPath = CryptoJS.HmacSHA256(src, hmac); + src = '..' + src; + if(console) console.log("Path", src, "HMAC", hmacOfPath); + if (hmac.length > 0) { + src += "?hmac=" + hmacOfPath; + } + + if(console) console.log('Playing video from', src); + + if(recording.singleFile) { + var video = document.getElementById('player'); + video.src = src; + video.load(); + console.log(video); + video.oncanplay = function() { + video.height = window.innerHeight * 0.85; + $('#player-window').css('display', 'block'); + video.play(); + }; + video.onerror = function() { + $.notify(video.error.message, 'error'); + }; + } else { + if (Hls.isSupported()) { + var video = document.getElementById('player'); + var hls = new Hls(); + hls.attachMedia(video); + hls.on(Hls.Events.MEDIA_ATTACHED, function () { + if(console) console.log(src); + hls.loadSource(src); + hls.on(Hls.Events.MANIFEST_PARSED, function (event, data) { + if(console) { + console.log(data); + console.log("manifest loaded, found " + data.levels.length + " quality level"); + } + $('#player-window').css('display', 'block'); + video.play(); + }); + + }); + } else { + $.notify('Loading HLS video streaming support failed', 'error'); + if(console) console.log('HLS is not supported'); + } + } +} + +function download(recording) { + let src = recording.singleFile ? '/hls' + recording.path : recording.playlist; + let hmacOfPath = CryptoJS.HmacSHA256(src, hmac); + src = '..' + src; + if(console) console.log("Path", src, "HMAC", hmacOfPath); + if (hmac.length > 0) { + src += "?hmac=" + hmacOfPath; + } + location.href = src; +} + +function calculateSize(sizeInByte) { + let size = sizeInByte; + let unit = "Bytes"; + if(size > 1024.0 * 1024 * 1024) { + size = size / 1024.0 / 1024 / 1024; + unit = "GiB"; + } else if(size > 1024.0 * 1024) { + size = size / 1024.0 / 1024; + unit = "MiB"; + } else if(size > 1024.0) { + size = size / 1024.0; + unit = "KiB"; + } + return size.toFixed(2) + ' ' + unit; +} + +function isRecordingInArray(array, recording) { + for ( let idx in array) { + let r = array[idx]; + if (r.path === recording.path) { + return true; + } + } + return false; +} + +/** + * Synchronizes recordings from the server with the displayed knockout recordings table + */ +function syncRecordings(recordings) { + // remove recordings from the observable array, which are not in the + // updated list + for ( let idx in observableRecordingsArray()) { + let recording = observableRecordingsArray()[idx]; + if (!isRecordingInArray(recordings, recording)) { + observableRecordingsArray.remove(recording); + } + } + + // add recordings to the observable array, which are new in the + // updated list + for ( let idx in recordings) { + let recording = recordings[idx]; + if (!isRecordingInArray(observableRecordingsArray(), recording)) { + recording.ko_date = ko.computed(function() { + return new Date(recording.startDate).toLocaleString('default', { + 'day' : '2-digit', + 'month' : '2-digit', + 'year' : 'numeric', + 'hour' : '2-digit', + 'minute' : '2-digit', + 'second' : '2-digit' + }) + }); + recording.ko_progressString = ko.observable(recording.progress === -1 ? '' : recording.progress); + recording.ko_size = ko.observable(calculateSize(recording.sizeInByte)); + recording.ko_status = ko.observable(recording.status); + if (recording.path.endsWith('.mp4')) { + recording.playlist = '/hls' + recording.path; + } else { + recording.playlist = '/hls' + recording.path + '/playlist.m3u8'; + } + observableRecordingsArray.push(recording); + } + } + + // update existing recordings + for ( let i in recordings) { + let recording = recordings[i]; + for ( let j in observableRecordingsArray()) { + let r = observableRecordingsArray()[j]; + if (recording.path === r.path) { + r.progress = recording.progress; + r.sizeInByte = recording.sizeInByte; + r.status = recording.status; + r.startDate = recording.startDate; + r.ko_progressString(recording.progress === -1 ? '' : (recording.progress + '%')); + r.ko_size(calculateSize(recording.sizeInByte)); + r.ko_status(recording.status); + } + } + } +} + +function updateRecordings() { + try { + let action = '{"action": "recordings"}'; + $.ajax({ + type : 'POST', + url : '../rec', + dataType : 'json', + async : true, + timeout : 60000, + headers : { + 'CTBREC-HMAC' : CryptoJS.HmacSHA256(action, hmac) + }, + data : action + }).done(function(data) { + if (data.status === 'success') { + syncRecordings(data.recordings); + updateDiskSpace(); + } else { + if (console) + console.log('request failed', data); + } + }).fail(function(jqXHR, textStatus, errorThrown) { + if (console) + console.log(textStatus, errorThrown); + }); + } catch (e) { + if (console) + console.log('Unexpected error', e); + } + setTimeout(updateRecordings, 3000); +} + +function updateDiskSpace() { + let action = '{"action": "space"}'; + $.ajax({ + type : 'POST', + url : '../rec', + dataType : 'json', + async : true, + timeout : 60000, + headers : { + 'CTBREC-HMAC' : CryptoJS.HmacSHA256(action, hmac) + }, + data : action + }).done(function(data) { + if (data.status === 'success') { + space.total(data.spaceTotal); + space.free(data.spaceFree); + space.percent((data.spaceFree / data.spaceTotal * 100).toFixed(2)); + space.text(calculateSize(data.spaceFree) + ' / ' + calculateSize(data.spaceTotal)); + } else { + if (console) + console.log('request failed', data); + } + }).fail(function(jqXHR, textStatus, errorThrown) { + if (console) + console.log(textStatus, errorThrown); + }); +} \ No newline at end of file