Session recording viewer can now stream.
Ylian Saint-Hilaire committed
Nov 7, 2021 at 18:07 UTC
7974b43b3d0085cb2a9757bc3b3c371c2ef5a5ad
5 files changed
+297
-37
public/images/link7.png
Binary files /dev/null and b/public/images/link7.png differ
public/scripts/common-0.0.1.js
+2
-1
@@ -115,7 +115,7 @@ function isSafeString(str) { return ((typeof str == 'string') && (str.indexOf('<
115
function isSafeString2(str) { return ((typeof str == 'string') && (str.indexOf('<') == -1) && (str.indexOf('>') == -1) && (str.indexOf('&') == -1) && (str.indexOf('"') == -1) && (str.indexOf('\'') == -1) && (str.indexOf('+') == -1) && (str.indexOf('(') == -1) && (str.indexOf(')') == -1) && (str.indexOf('#') == -1) && (str.indexOf('%') == -1)) };
116
117
// Parse URL arguments, only keep safe values
118
-function parseUriArgs() {
118
+function parseUriArgs(decodeUrl) {
119
var href = window.document.location.href;
120
if (href.endsWith('#')) { href = href.substring(0, href.length - 1); }
121
var name, r = {}, parsedUri = href.split(/[\?&|]/);
@@ -124,6 +124,7 @@ function parseUriArgs() {
124
var arg = parsedUri[j], i = arg.indexOf('=');
125
name = arg.substring(0, i);
126
r[name] = arg.substring(i + 1);
127
+ if (decodeUrl) { r[name] = decodeURIComponent(arg.substring(i + 1)); }
128
if (!isSafeString(r[name])) { delete r[name]; } else { var x = parseInt(r[name]); if (x == r[name]) { r[name] = x; } }
129
}
130
return r;
views/default.handlebars
+5
-1
@@ -14986,7 +14986,11 @@
14986
if (rec.protocol == 200) { sessionName += ' - ' + "Messenger"; }
14987
14988
var actions = '', icon = 'm0';
14989
- if (rec.present == 1) { icon = 'm1'; actions = '<div style=cursor:pointer;float:right><a onclick=downloadFile("recordings.ashx?file=' + encodeURIComponentEx(rec.filename) + '")><img src=images/link4.png height=10 width=10 title="Download Recording"></a> </div>'; }
14989
+ if (rec.present == 1) {
14990
+ icon = 'm1';
14991
+ actions = '<div style=cursor:pointer;float:right><a onclick=downloadFile("recordings.ashx?file=' + encodeURIComponentEx(rec.filename) + '")><img src=images/link4.png height=10 width=10 title="Download Recording"></a> </div>';
14992
+ actions += '<div style=cursor:pointer;float:right><a href="player.htm?stream=' + encodeURIComponentEx(rec.filename) + '")><img src=images/link7.png height=10 width=10 title="Play Recording"></a> </div>';
14993
+ }
14994
var x = '<tr tabindex=0 onmouseover=userMouseHover2(this,1) onmouseout=userMouseHover2(this,0) onkeypress="if (event.key==\'Enter\') showRecordingDialog(event,\'' + i + '\')"><td style=cursor:pointer>';
14995
x += '<div class=bar style=width:100%>';
14996
//x += '<div class=baricon><input class=RecordingCheckbox value="' + encodeURIComponentEx(rec.filename) + '" onclick=p52updateInfo() type=checkbox></div>';
views/player.handlebars
+227
-33
@@ -29,7 +29,7 @@
29
<input id="ConvertAsWebM" style="display:none" type=button value="Convert to WebM" onclick="saveAsWebMfile()">
30
</div>
31
<div>
32
- <input id="OpenFileButton" type=button value="Open File..." onclick="openfile()">
32
+ <input id="OpenFileButton" type=button value="Open File..." onclick="openfile()" style="display:none">
33
<span id="deskstatus"></span>
34
</div>
35
</div>
@@ -113,6 +113,13 @@
113
var videoWriterCurrentFrame = null;
114
var videoFrameDuration = 100;
115
var browser = null;
116
+ var domainUrl = '{{{domainurl}}}';
117
+ var urlargs;
118
+
119
+ // Streaming values
120
+ var ws = null;
121
+ var streamingBlockSize = 102400; // 100k block
122
+ var streamingBlockCache = {};
123
124
function start() {
125
// Detect what browser is in use
@@ -129,6 +136,7 @@
136
}
137
})(window.navigator.userAgent.toLowerCase());
138
139
+ urlargs = parseUriArgs(true);
140
window.onresize = deskAdjust;
141
document.ondrop = ondrop;
142
document.ondragover = ondragover;
@@ -139,19 +147,127 @@
147
148
// Make the dialog box movable
149
dialogBoxDrag();
150
+
151
+ // Check if we need to stream a session
152
+ if (urlargs.stream != null) {
153
+ QV('metadatadiv', true);
154
+ QH('metadatadiv', "Connecting to server...");
155
+ ws = new WebSocket(window.location.protocol.replace('http', 'ws') + '//' + window.location.host + domainUrl + 'recordings.ashx?file=' + urlargs.stream + (urlargs.key ? ('&key=' + urlargs.key) : ''));
156
+ ws.binaryType = 'arraybuffer';
157
+ ws.onopen = function (e) { console.log('Session Streaming - Connected'); }
158
+ ws.onmessage = function (msg) {
159
+ if (typeof msg.data != 'string') {
160
+ var uint8View = new Uint8Array(msg.data);
161
+ var blocknum = (((uint8View[4] << 24) + (uint8View[5] << 16) + (uint8View[6] << 8) + uint8View[7]) / streamingBlockSize);
162
+ //console.log('Session Streaming - Got block: ' + blocknum);
163
+ streamingBlockCache[blocknum] = msg.data;
164
+ var pendingFetchStreamingData2 = [], pendingFetchStreamingData3 = [];
165
+ for (var i in pendingFetchStreamingData) {
166
+ var j = pendingFetchStreamingData[i].missingBlocks.indexOf(blocknum);
167
+ if (j >= 0) { pendingFetchStreamingData[i].missingBlocks.splice(i, 1); }
168
+ if (pendingFetchStreamingData[i].missingBlocks.length == 0) {
169
+ pendingFetchStreamingData3.push(pendingFetchStreamingData[i]);
170
+ } else {
171
+ pendingFetchStreamingData2.push(pendingFetchStreamingData[i]);
172
+ }
173
+ }
174
+ pendingFetchStreamingData = pendingFetchStreamingData2;
175
+ for (var i in pendingFetchStreamingData3) {
176
+ fetchStreamingData(pendingFetchStreamingData3[i].fr, pendingFetchStreamingData3[i].start, pendingFetchStreamingData3[i].end);
177
+ }
178
+ return;
179
+ } else {
180
+ var command = null;
181
+ try { command = JSON.parse(msg.data); } catch (ex) { console.log(ex); return; }
182
+ if ((command == null) || (typeof command.action != 'string')) return;
183
+ switch (command.action) {
184
+ case 'info': {
185
+ console.log('Session Streaming - Session file size: ' + command.size);
186
+ if ((typeof command.name != 'string') || (typeof command.size != 'number')) break;
187
+ recFile = { name: command.name, size: command.size, streaming: true };
188
+ readLastBlock(function (type, flags, time, extras) {
189
+ if (type == 3) {
190
+ // File is ok
191
+ recFileEndTime = time;
192
+ recFileExtras = extras;
193
+ readNextBlock(processFirstBlock);
194
+ } else {
195
+ // This is not a good file
196
+ recFileEndTime = 0;
197
+ }
198
+ });
199
+ break;
200
+ }
201
+ }
202
+
203
+ }
204
+ }
205
+ ws.onclose = function (e) { console.log('Session Streaming - Disconnected'); restart(); }
206
+ } else {
207
+ QV('OpenFileButton', true);
208
+ }
209
+ }
210
+
211
+ // Pending fetch requests
212
+ var pendingFetchStreamingData = [];
213
+
214
+ // Get a section of the recorded file
215
+ function fetchStreamingData(fr, start, end) {
216
+ // Start by looking at what blocks are required
217
+ var firstBlock = Math.floor(start / streamingBlockSize);
218
+ var lastBlock = Math.floor(end / streamingBlockSize);
219
+ var missingBlocks = [];
220
+ for (var i = firstBlock; i <= lastBlock; i++) {
221
+ if ((streamingBlockCache[i] == null) || (streamingBlockCache[i] === 1)) { missingBlocks.push(i); fetchStreamingBlock(i); }
222
+ fetchStreamingBlock(i + 1); // Pre-fetch block
223
+ fetchStreamingBlock(i + 2); // Pre-fetch block
224
+ }
225
+ if (missingBlocks.length == 0) {
226
+ // We have all the blocks we need, assemble the data now
227
+ var outputptr = 0;
228
+ var output = new ArrayBuffer(end - start);
229
+ var outputBytes = new Uint8Array(output);
230
+ for (var i = firstBlock; i <= lastBlock; i++) {
231
+ var block = streamingBlockCache[i]; // Get a block with data we need
232
+ var blockstart = (i * streamingBlockSize); // Compute the block starting data pointer
233
+ var blockend = blockstart + (block.byteLength - 8); // Compute the block ending data pointer
234
+ var r1 = Math.max(start, blockstart); // Compute where we need to start data copy
235
+ var r2 = Math.min(end, blockend); // Compute where we need to end data copy
236
+ var p1 = r1 - blockstart; // Compute where in the block to start data copy
237
+ var p2 = r2 - r1; // Computer how many byte to copy from the block
238
+ var subblock = block.slice(8 + p1, 8 + p1 + p2); // Get the sub-block of data we need
239
+ outputBytes.set(new Uint8Array(subblock), outputptr); // Copy the sub-block into the main block
240
+ outputptr += p2; // Move the pointer forward
241
+ }
242
+ fr.onload({ target: { result: ArrayBufferToString(output) } } ); // Event the block of data
243
+ } else {
244
+ pendingFetchStreamingData.push({ fr: fr, start: start, end: end, missingBlocks: missingBlocks });
245
+ }
246
+ }
247
+
248
+ // Request a block of data from the server
249
+ function fetchStreamingBlock(n) {
250
+ if (streamingBlockCache[n] != null) return;
251
+ streamingBlockCache[n] = 1; // Mark the block as being requested
252
+ if ((n * streamingBlockSize) >= recFile.size) return;
253
+ var len = streamingBlockSize;
254
+ if (((n + 1) * streamingBlockSize) >= recFile.size) { len = (recFile.size - (n * streamingBlockSize)); }
255
+ ws.send('{"action":"get","ptr":' + (n * streamingBlockSize) + ',"size":' + len + '}');
256
}
257
258
function readNextBlock(func) {
259
if ((recFilePtr + 16) > recFile.size) { QS('progressbar').width = '100%'; func(-1); } else {
260
var fr = new FileReader();
147
- fr.onload = function () {
148
- var type = ReadShort(this.result, 0);
149
- var flags = ReadShort(this.result, 2);
150
- var size = ReadInt(this.result, 4);
151
- var time = (ReadInt(this.result, 8) << 32) + ReadInt(this.result, 12);
261
+ fr.onload = function (r) {
262
+ var result = r.target.result;
263
+ var type = ReadShort(result, 0);
264
+ var flags = ReadShort(result, 2);
265
+ var size = ReadInt(result, 4);
266
+ var time = (ReadInt(result, 8) << 32) + ReadInt(result, 12);
267
if ((recFilePtr + 16 + size) > recFile.size) { QS('progressbar').width = '100%'; func(-1); } else {
268
var fr2 = new FileReader();
154
- fr2.onload = function () {
269
+ fr2.onload = function (r) {
270
+ var result = r.target.result;
271
recFilePtr += (16 + size);
272
if (recFileEndTime == 0) {
273
// File pointer progress bar
@@ -160,59 +276,89 @@
276
// Time progress bar
277
QS('progressbar').width = Math.floor(((recFileLastTime - recFileStartTime) / (recFileEndTime - recFileStartTime)) * 100) + '%';
278
}
163
- func(type, flags, time, this.result);
279
+ func(type, flags, time, result);
280
};
165
- fr2.readAsBinaryString(recFile.slice(recFilePtr + 16, recFilePtr + 16 + size));
281
+ if (ws == null) {
282
+ fr2.readAsBinaryString(recFile.slice(recFilePtr + 16, recFilePtr + 16 + size));
283
+ } else {
284
+ fetchStreamingData(fr2, recFilePtr + 16, recFilePtr + 16 + size);
285
+ }
286
}
287
};
168
- fr.readAsBinaryString(recFile.slice(recFilePtr, recFilePtr + 16));
288
+ if (ws == null) {
289
+ fr.readAsBinaryString(recFile.slice(recFilePtr, recFilePtr + 16));
290
+ } else {
291
+ fetchStreamingData(fr, recFilePtr, recFilePtr + 16);
292
+ }
293
}
294
}
295
296
function readBlockAt(ptr, func) {
297
var fr = new FileReader();
174
- fr.onload = function () {
175
- var type = ReadShort(this.result, 0);
176
- var flags = ReadShort(this.result, 2);
177
- var size = ReadInt(this.result, 4);
178
- var time = (ReadInt(this.result, 8) << 32) + ReadInt(this.result, 12);
298
+ fr.onload = function (r) {
299
+ var result = r.target.result;
300
+ var type = ReadShort(result, 0);
301
+ var flags = ReadShort(result, 2);
302
+ var size = ReadInt(result, 4);
303
+ var time = (ReadInt(result, 8) << 32) + ReadInt(result, 12);
304
if ((ptr + 16 + size) > recFile.size) { func(-1); } else {
305
var fr2 = new FileReader();
181
- fr2.onload = function () { func(type, flags, time, this.result); };
182
- fr2.readAsBinaryString(recFile.slice(ptr + 16, ptr + 16 + size));
306
+ fr2.onload = function (r) {
307
+ var result = r.target.result;
308
+ func(type, flags, time, result);
309
+ };
310
+ if (ws == null) {
311
+ fr2.readAsBinaryString(recFile.slice(ptr + 16, ptr + 16 + size));
312
+ } else {
313
+ fetchStreamingData(fr2, ptr + 16, ptr + 16 + size);
314
+ }
315
}
316
};
185
- fr.readAsBinaryString(recFile.slice(ptr, ptr + 16));
317
+ if (ws == null) {
318
+ fr.readAsBinaryString(recFile.slice(ptr, ptr + 16));
319
+ } else {
320
+ fetchStreamingData(fr, ptr, ptr + 16);
321
+ }
322
}
323
324
function readLastBlock(func) {
325
if (recFile.size < 32) { func(-1); } else {
326
var fr = new FileReader();
191
- fr.onload = function () {
192
- var type = ReadShort(this.result, 0);
193
- var flags = ReadShort(this.result, 2);
194
- var size = ReadInt(this.result, 4);
195
- var time = (ReadInt(this.result, 8) << 32) + ReadInt(this.result, 12);
196
- var magic = this.result.substring(16, 32);
327
+ fr.onload = function (r) {
328
+ var result = r.target.result;
329
+ var type = ReadShort(result, 0);
330
+ var flags = ReadShort(result, 2);
331
+ var size = ReadInt(result, 4);
332
+ var time = (ReadInt(result, 8) << 32) + ReadInt(result, 12);
333
+ var magic = result.substring(16, 32);
334
if ((type == 3) && (size == 16) && (magic == 'MeshCentralMCNDX')) {
335
// Extra metadata present, lets read it.
336
var fr2 = new FileReader();
200
- fr2.onload = function () {
201
- var xtype = ReadShort(this.result, 0);
202
- var xflags = ReadShort(this.result, 2);
203
- var xsize = ReadInt(this.result, 4);
204
- var xtime = (ReadInt(this.result, 8) << 32) + ReadInt(this.result, 12);
205
- var extras = JSON.parse(this.result.substring(16));
337
+ fr2.onload = function (r) {
338
+ var result = r.target.result;
339
+ var xtype = ReadShort(result, 0);
340
+ var xflags = ReadShort(result, 2);
341
+ var xsize = ReadInt(result, 4);
342
+ var xtime = (ReadInt(result, 8) << 32) + ReadInt(result, 12);
343
+ var extras = JSON.parse(result.substring(16));
344
func(type, flags, xtime, extras); // Include extra metadata
345
}
208
- fr2.readAsBinaryString(recFile.slice(time, recFile.size - 32));
346
+ if (ws == null) {
347
+ fr2.readAsBinaryString(recFile.slice(time, recFile.size - 32));
348
+ } else {
349
+ fetchStreamingData(fr2, time, recFile.size - 32);
350
+ }
351
} else if ((type == 3) && (size == 16) && (magic == 'MeshCentralMCREC')) {
352
func(type, flags, time); // No extra metadata
353
} else {
354
func(-1); // Fail
355
}
356
};
215
- fr.readAsBinaryString(recFile.slice(recFile.size - 32, recFile.size));
357
+ if (ws == null) {
358
+ fr.readAsBinaryString(recFile.slice(recFile.size - 32, recFile.size));
359
+ } else {
360
+ fetchStreamingData(fr, recFile.size - 32, recFile.size);
361
+ }
362
}
363
}
364
@@ -451,7 +597,11 @@
597
QS('progressbar').width = '0px';
598
QH('timespan', '00:00:00');
599
QV('metadatadiv', true);
454
- QH('metadatadiv', '<span style=\"font-family:Arial,Helvetica Neue,Helvetica,sans-serif;font-size:28px\">MeshCentral Session Player</span><br /><br /><span style=color:gray>' + "Drag & drop a .mcrec file or click \"Open File...\"" + '</span>');
600
+ if (urlargs.stream == null) {
601
+ QH('metadatadiv', '<span style=\"font-family:Arial,Helvetica Neue,Helvetica,sans-serif;font-size:28px\">MeshCentral Session Player</span><br /><br /><span style=color:gray>' + "Drag & drop a .mcrec file or click \"Open File...\"" + '</span>');
602
+ } else {
603
+ QH('metadatadiv', '');
604
+ }
605
QV('DeskParent', true);
606
QV('TermParent', false);
607
}
@@ -845,6 +995,50 @@
995
}
996
}
997
998
+ function ArrayBufferToString(buffer) {
999
+ return BinaryToString(String.fromCharCode.apply(null, Array.prototype.slice.apply(new Uint8Array(buffer))));
1000
+ }
1001
+
1002
+ function StringToArrayBuffer(string) {
1003
+ return StringToUint8Array(string).buffer;
1004
+ }
1005
+
1006
+ function BinaryToString(binary) {
1007
+ var error;
1008
+ try {
1009
+ return decodeURIComponent(escape(binary));
1010
+ } catch (_error) {
1011
+ error = _error;
1012
+ if (error instanceof URIError) { return binary; } else { throw error; }
1013
+ }
1014
+ }
1015
+
1016
+ function StringToBinary(string) {
1017
+ var chars, code, i, isUCS2, len, _i;
1018
+ len = string.length;
1019
+ chars = [];
1020
+ isUCS2 = false;
1021
+ for (i = _i = 0; 0 <= len ? _i < len : _i > len; i = 0 <= len ? ++_i : --_i) {
1022
+ code = String.prototype.charCodeAt.call(string, i);
1023
+ if (code > 255) { isUCS2 = true; chars = null; break; } else { chars.push(code); }
1024
+ }
1025
+ if (isUCS2 === true) {
1026
+ return unescape(encodeURIComponent(string));
1027
+ } else {
1028
+ return String.fromCharCode.apply(null, Array.prototype.slice.apply(chars));
1029
+ }
1030
+ }
1031
+
1032
+ function StringToUint8Array(string) {
1033
+ var binary, binLen, buffer, chars, i, _i;
1034
+ binary = StringToBinary(string);
1035
+ binLen = binary.length;
1036
+ buffer = new ArrayBuffer(binLen);
1037
+ chars = new Uint8Array(buffer);
1038
+ for (i = _i = 0; 0 <= binLen ? _i < binLen : _i > binLen; i = 0 <= binLen ? ++_i : --_i) { chars[i] = String.prototype.charCodeAt.call(binary, i); }
1039
+ return chars;
1040
+ }
1041
+
1042
start();
1043
</script>
1044
</body>
webserver.js
+63
-2
@@ -3456,13 +3456,13 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
3456
}
3457
}
3458
3459
- // Download a desktop recording
3459
+ // Download a session recording
3460
function handleGetRecordings(req, res) {
3461
const domain = checkUserIpAddress(req, res);
3462
if (domain == null) return;
3463
3464
// Check the query
3465
- if ((domain.sessionrecording == null) || (req.query.file == null) || (obj.common.IsFilenameValid(req.query.file) !== true)) { res.sendStatus(401); return; }
3465
+ if ((domain.sessionrecording == null) || (req.query.file == null) || (obj.common.IsFilenameValid(req.query.file) !== true) || (req.query.file.endsWith('.mcrec') == false)) { res.sendStatus(401); return; }
3466
3467
// Get the recording path
3468
var recordingsPath = null;
@@ -3482,6 +3482,66 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
3482
try { res.sendFile(obj.path.join(recordingsPath, req.query.file)); } catch (ex) { res.sendStatus(404); }
3483
}
3484
3485
+ // Stream a session recording
3486
+ function handleGetRecordingsWebSocket(ws, req) {
3487
+ var domain = checkAgentIpAddress(ws, req);
3488
+ if (domain == null) { parent.debug('web', 'Got recordings file transfer connection with bad domain or blocked IP address ' + req.clientIp + ', dropping.'); try { ws.close(); } catch (ex) { } return; }
3489
+
3490
+ // Check the query
3491
+ if ((domain.sessionrecording == null) || (req.query.file == null) || (obj.common.IsFilenameValid(req.query.file) !== true) || (req.query.file.endsWith('.mcrec') == false)) { try { ws.close(); } catch (ex) { } return; }
3492
+
3493
+ // Get the recording path
3494
+ var recordingsPath = null;
3495
+ if (domain.sessionrecording.filepath) { recordingsPath = domain.sessionrecording.filepath; } else { recordingsPath = parent.recordpath; }
3496
+ if (recordingsPath == null) { try { ws.close(); } catch (ex) { } return; }
3497
+
3498
+ // Get the user and check user rights
3499
+ var authUserid = null;
3500
+ if ((req.session != null) && (typeof req.session.userid == 'string')) { authUserid = req.session.userid; }
3501
+ if (authUserid == null) { try { ws.close(); } catch (ex) { } return; }
3502
+ const user = obj.users[authUserid];
3503
+ if (user == null) { try { ws.close(); } catch (ex) { } return; }
3504
+ if ((user.siteadmin & 512) == 0) { try { ws.close(); } catch (ex) { } return; } // Check if we have right to get recordings
3505
+ const filefullpath = obj.path.join(recordingsPath, req.query.file);
3506
+
3507
+ obj.fs.stat(filefullpath, function(err, stats) {
3508
+ if (err) {
3509
+ try { ws.close(); } catch (ex) { } // File does not exist
3510
+ } else {
3511
+ obj.fs.open(filefullpath, function (err, fd) {
3512
+ if (err == null) {
3513
+ // When data is received from the web socket
3514
+ ws.on('message', function (msg) {
3515
+ if (typeof msg != 'string') return;
3516
+ var command;
3517
+ try { command = JSON.parse(msg); } catch (e) { return; }
3518
+ if ((command == null) || (typeof command.action != 'string')) return;
3519
+ switch (command.action) {
3520
+ case 'get': {
3521
+ const buffer = Buffer.alloc(8 + command.size);
3522
+ //buffer.writeUInt32BE((command.ptr >> 32), 0);
3523
+ buffer.writeUInt32BE((command.ptr & 0xFFFFFFFF), 4);
3524
+ obj.fs.read(fd, buffer, 8, command.size, command.ptr, function (err, bytesRead, buffer) { if (bytesRead > (buffer.length - 8)) { buffer = buffer.slice(0, bytesRead + 8); } ws.send(buffer); });
3525
+ break;
3526
+ }
3527
+ }
3528
+ });
3529
+
3530
+ // If error, do nothing
3531
+ ws.on('error', function (err) { try { ws.close(); } catch (ex) { } obj.fs.close(fd, function (err) { }); });
3532
+
3533
+ // If the web socket is closed
3534
+ ws.on('close', function (req) { try { ws.close(); } catch (ex) { } obj.fs.close(fd, function (err) { }); });
3535
+
3536
+ ws.send(JSON.stringify({ "action": "info", "name": req.query.file, "size": stats.size }));
3537
+ } else {
3538
+ try { ws.close(); } catch (ex) { }
3539
+ }
3540
+ });
3541
+ }
3542
+ });
3543
+ }
3544
+
3545
// Serve the player page
3546
function handlePlayerRequest(req, res) {
3547
const domain = checkUserIpAddress(req, res);
@@ -5738,6 +5798,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
5798
obj.app.get(url + 'welcome.jpg', handleWelcomeImageRequest);
5799
obj.app.get(url + 'welcome.png', handleWelcomeImageRequest);
5800
obj.app.get(url + 'recordings.ashx', handleGetRecordings);
5801
+ obj.app.ws(url + 'recordings.ashx', handleGetRecordingsWebSocket);
5802
obj.app.get(url + 'player.htm', handlePlayerRequest);
5803
obj.app.get(url + 'player', handlePlayerRequest);
5804
obj.app.get(url + 'sharing', handleSharingRequest);