Added session recording indexing tool and player support.

Ylian Saint-Hilaire committed Feb 11, 2020 at 19:02 UTC 421c0fc79eb77857be7617984a4eb2a4a29f263f
5 files changed +379 -26
MeshCentralServer.njsproj
+1
@@ -104,6 +104,7 @@
104 <Compile Include="apfserver.js" />
105 <Compile Include="exeHandler.js" />
106 <Compile Include="letsencrypt.js" />
107 + <Compile Include="mcrec.js" />
108 <Compile Include="meshaccelerator.js" />
109 <Compile Include="meshctrl.js" />
110 <Compile Include="meshmail.js" />
mcrec.js new
+294
@@ -0,0 +1,294 @@
1 +/**
2 +* @description MeshCentral MeshAgent
3 +* @author Ylian Saint-Hilaire
4 +* @copyright Intel Corporation 2019-2020
5 +* @license Apache-2.0
6 +* @version v0.0.1
7 +*/
8 +
9 +var fs = require('fs');
10 +var path = require('path');
11 +
12 +var worker = null;
13 +const NodeJSVer = Number(process.version.match(/^v(\d+\.\d+)/)[1]);
14 +var directRun = (require.main === module);
15 +function log() { if (directRun) { console.log(...arguments); } else { if (worker != null) { worker.parentPort.postMessage({ msg: arguments[0] }); } } }
16 +if (directRun && (NodeJSVer >= 12)) { const xworker = require('worker_threads'); try { if (xworker.isMainThread == false) { worker = xworker; } } catch (ex) { log(ex); } }
17 +function start() { startEx(process.argv); }
18 +if (directRun) { setup(); }
19 +
20 +function setup() { InstallModules(['image-size'], start); }
21 +function start() { startEx(process.argv); }
22 +
23 +function startEx(argv) {
24 + var state = { recFileName: null, recFile: null, recFileSize: 0, recFilePtr: 0 };
25 + var infile = null;
26 + if (argv.length > 2) { infile = argv[2]; } else {
27 + log('MeshCentral Session Recodings Processor');
28 + log('This tool will index a .mcrec file so that the player can seek thru the file.');
29 + log('');
30 + log(' Usage: node mcrec [file]');
31 + return;
32 + }
33 + if (fs.existsSync(infile) == false) { log("Missing file: " + infile); return; }
34 + state.recFileName = infile;
35 + state.recFileSize = fs.statSync(infile).size;
36 + if (state.recFileSize < 32) { log("Invalid file: " + infile); return; }
37 + log("Processing file: " + infile + ", " + state.recFileSize + " bytes.");
38 + state.recFile = fs.openSync(infile, 'r');
39 + state.indexTime = 10; // Interval between indexes in seconds
40 + state.lastIndex = 0; // Last time an index was writen in seconds
41 + state.indexes = [];
42 + state.width = 0;
43 + state.height = 0;
44 + state.basePtr = null;
45 + readLastBlock(state, function (state, result) {
46 + if (result == false) { log("Invalid file: " + infile); return; }
47 + readNextBlock(state, processBlock);
48 + });
49 +}
50 +
51 +function createIndex(state, ptr) {
52 + var index = [];
53 + for (var i in state.screen) { if (index.indexOf(state.screen[i]) == -1) { index.push(state.screen[i]); } }
54 + index.sort(function (a, b) { return a - b });
55 + index.unshift(state.height);
56 + index.unshift(state.width);
57 + index.unshift(ptr - state.basePtr);
58 + state.indexes.push(index); // Index = [ Ptr, Width, Height, Block Pointers... ]
59 + //log('Index', state.lastIndex, index.length);
60 + //log('Index', index);
61 + state.lastIndex += 10;
62 +}
63 +
64 +function processBlock(state, block) {
65 + if (block == null) { writeIndexedFile(state, function () { log("Done."); }); return; }
66 + var elapseMilliSeconds = 0;
67 + if (state.startTime != null) { elapseMilliSeconds = (block.time - state.startTime); }
68 + var flagBinary = (block.flags & 1) != 0;
69 + var flagUser = (block.flags & 2) != 0;
70 +
71 + // Start indexing at the first type 2 block
72 + if ((state.basePtr == null) && (block.type == 2)) { state.basePtr = block.ptr; state.startTime = block.time; }
73 +
74 + // Check if we need to create one or more indexes
75 + while (((state.lastIndex + state.indexTime) * 1000) < elapseMilliSeconds) { createIndex(state, block.ptr); }
76 +
77 + if (block.type == 1) {
78 + // Metadata
79 + state.metadata = JSON.parse(block.data.toString());
80 + if (state.metadata.indexInterval != null) { log("This file is already indexed."); return; }
81 + if (state.metadata.protocol != 2) { log("Only remote desktop sessions can currently be indexed."); return; }
82 + state.metadataFlags = block.flags;
83 + state.metadataTime = block.time;
84 + state.recFileProtocol = state.metadata.protocol;
85 + state.dataStartPtr = state.recFilePtr;
86 + if (typeof state.recFileProtocol == 'string') { state.recFileProtocol = parseInt(state.recFileProtocol); }
87 + } else if ((block.type == 2) && flagBinary && !flagUser) {
88 + // Device --> User data
89 + if (state.recFileProtocol == 1) {
90 + // MeshCentral Terminal
91 + // TODO
92 + log('Terminal');
93 + } else if (state.recFileProtocol == 2) {
94 + // MeshCentral Remote Desktop
95 + // TODO
96 + if (block.data.length >= 4) {
97 + var command = block.data.readInt16BE(0);
98 + var cmdsize = block.data.readInt16BE(2);
99 + if ((command == 27) && (cmdsize == 8)) {
100 + // Jumbo packet
101 + if (block.data.length >= 12) {
102 + command = block.data.readInt16BE(8);
103 + cmdsize = block.data.readInt32BE(4);
104 + if (block.data.length == (cmdsize + 8)) {
105 + block.data = block.data.slice(8, block.data.length);
106 + } else {
107 + console.log('TODO-PARTIAL-JUMBO', command, cmdsize, block.data.length);
108 + return; // TODO
109 + }
110 + }
111 + }
112 +
113 + switch (command) {
114 + case 3: // Tile
115 + var x = block.data.readInt16BE(4);
116 + var y = block.data.readInt16BE(6);
117 + var dimensions = require('image-size')(block.data.slice(8));
118 + //log("Tile", x, y, dimensions.width, dimensions.height, block.ptr);
119 + //console.log(elapseSeconds);
120 +
121 + // Update the screen with the correct pointers.
122 + var sx = x/16, sy = y/16, sw = dimensions.width/16, sh = dimensions.height/16;
123 + for (var i = 0; i < sw; i++) {
124 + for (var j = 0; j < sh; j++) {
125 + var k = ((state.swidth * (j + sy)) + (i + sx));
126 + state.screen[k] = (block.ptr - state.basePtr);
127 + }
128 + }
129 +
130 + break;
131 + case 4: // Tile copy
132 + var x = block.data.readInt16BE(4);
133 + var y = block.data.readInt16BE(6);
134 + //log("TileCopy", x, y);
135 + break;
136 + case 7: // Screen Size, clear the screen state and computer the tile count
137 + state.width = block.data.readInt16BE(4);
138 + state.height = block.data.readInt16BE(6);
139 + state.swidth = state.width / 16;
140 + state.sheight = state.height / 16;
141 + if (Math.floor(state.swidth) != state.swidth) { state.swidth = Math.floor(state.swidth) + 1; }
142 + if (Math.floor(state.sheight) != state.sheight) { state.sheight = Math.floor(state.sheight) + 1; }
143 + state.screen = {};
144 + //log("ScreenSize", state.width, state.height, state.swidth, state.sheight, state.swidth * state.sheight);
145 + break;
146 + }
147 +
148 + //log('Desktop', command, cmdsize);
149 + }
150 + } else if (state.recFileProtocol == 101) {
151 + // Intel AMT KVM
152 + // TODO
153 + log('AMTKVM');
154 + }
155 + } else if ((block.type == 2) && flagBinary && flagUser) {
156 + // User --> Device data
157 + if (state.recFileProtocol == 101) {
158 + // Intel AMT KVM
159 + //if (rstr2hex(data) == '0000000008080001000700070003050200000000') { amtDesktop.bpp = 1; } // Switch to 1 byte per pixel.
160 + }
161 + }
162 +
163 + //console.log(block);
164 + readNextBlock(state, processBlock);
165 +}
166 +
167 +function writeIndexedFile(state, func) {
168 + var outfile = state.recFileName;
169 + if (outfile.endsWith('.mcrec')) { outfile = outfile.substring(0, outfile.length - 6) + '-ndx.mcrec'; } else { outfile += '-ndx.mcrec'; }
170 + if (fs.existsSync(outfile)) { log("File already exists: " + outfile); return; }
171 + log("Writing file: " + outfile);
172 + state.writeFile = fs.openSync(outfile, 'w');
173 + state.metadata.indexInterval = state.indexTime;
174 + state.metadata.indexStartTime = state.startTime;
175 + state.metadata.indexes = state.indexes;
176 + var firstBlock = JSON.stringify(state.metadata);
177 + recordingEntry(state.writeFile, 1, state.metadataFlags, state.metadataTime, firstBlock, function (state) {
178 + var len = 0, buffer = Buffer.alloc(4096), ptr = state.dataStartPtr;
179 + while (ptr < state.recFileSize) {
180 + len = fs.readSync(state.recFile, buffer, 0, 4096, ptr);
181 + fs.writeSync(state.writeFile, buffer, 0, len);
182 + ptr += len;
183 + }
184 + func(state);
185 + }, state);
186 +}
187 +
188 +// Record a new entry in a recording log
189 +function recordingEntry(fd, type, flags, time, data, func, tag) {
190 + try {
191 + if (typeof data == 'string') {
192 + // String write
193 + var blockData = Buffer.from(data), header = Buffer.alloc(16); // Header: Type (2) + Flags (2) + Size(4) + Time(8)
194 + header.writeInt16BE(type, 0); // Type (1 = Header, 2 = Network Data)
195 + header.writeInt16BE(flags, 2); // Flags (1 = Binary, 2 = User)
196 + header.writeInt32BE(blockData.length, 4); // Size
197 + header.writeIntBE(time, 10, 6); // Time
198 + var block = Buffer.concat([header, blockData]);
199 + fs.write(fd, block, 0, block.length, function () { func(tag); });
200 + } else {
201 + // Binary write
202 + var header = Buffer.alloc(16); // Header: Type (2) + Flags (2) + Size(4) + Time(8)
203 + header.writeInt16BE(type, 0); // Type (1 = Header, 2 = Network Data)
204 + header.writeInt16BE(flags | 1, 2); // Flags (1 = Binary, 2 = User)
205 + header.writeInt32BE(data.length, 4); // Size
206 + header.writeIntBE(time, 10, 6); // Time
207 + var block = Buffer.concat([header, data]);
208 + fs.write(fd, block, 0, block.length, function () { func(tag); });
209 + }
210 + } catch (ex) { console.log(ex); func(state, tag); }
211 +}
212 +
213 +function readLastBlock(state, func) {
214 + var buf = Buffer.alloc(32);
215 + fs.read(state.recFile, buf, 0, 32, state.recFileSize - 32, function (err, bytesRead, buf) {
216 + var type = buf.readInt16BE(0);
217 + var flags = buf.readInt16BE(2);
218 + var size = buf.readInt32BE(4);
219 + var time = (buf.readInt32BE(8) << 32) + buf.readInt32BE(12);
220 + var magic = buf.toString('utf8', 16, 32);
221 + func(state, (type == 3) && (size == 16) && (magic == 'MeshCentralMCREC'));
222 + });
223 +}
224 +
225 +function readNextBlock(state, func) {
226 + if ((state.recFilePtr + 16) > state.recFileSize) { func(state, null); return; }
227 + var r = {}, buf = Buffer.alloc(16);
228 + fs.read(state.recFile, buf, 0, 16, state.recFilePtr, function (err, bytesRead, buf) {
229 + r.type = buf.readInt16BE(0);
230 + r.flags = buf.readInt16BE(2);
231 + r.size = buf.readInt32BE(4);
232 + r.time = buf.readIntBE(8, 8);
233 + r.date = new Date(r.time);
234 + r.ptr = state.recFilePtr;
235 + if ((state.recFilePtr + 16 + r.size) > state.recFileSize) { func(state, null); return; }
236 + if (r.size == 0) {
237 + r.data = null;
238 + func(state, r);
239 + } else {
240 + r.data = Buffer.alloc(r.size);
241 + fs.read(state.recFile, r.data, 0, r.size, state.recFilePtr + 16, function (err, bytesRead, buf) {
242 + state.recFilePtr += (16 + r.size);
243 + func(state, r);
244 + });
245 + }
246 + });
247 +}
248 +
249 +function isNumber(x) { return (('' + parseInt(x)) === x) || (('' + parseFloat(x)) === x); }
250 +function format(format) { var args = Array.prototype.slice.call(arguments, 1); return format.replace(/{(\d+)}/g, function (match, number) { return typeof args[number] != 'undefined' ? args[number] : match; }); };
251 +
252 +// Check if a list of modules are present and install any missing ones
253 +var InstallModuleChildProcess = null;
254 +var previouslyInstalledModules = {};
255 +function InstallModules(modules, func) {
256 + var missingModules = [];
257 + if (previouslyInstalledModules == null) { previouslyInstalledModules = {}; }
258 + if (modules.length > 0) {
259 + for (var i in modules) {
260 + try {
261 + var xxmodule = require(modules[i]);
262 + } catch (e) {
263 + if (previouslyInstalledModules[modules[i]] !== true) { missingModules.push(modules[i]); }
264 + }
265 + }
266 + if (missingModules.length > 0) { InstallModule(missingModules.shift(), InstallModules, modules, func); } else { func(); }
267 + }
268 +}
269 +
270 +// Check if a module is present and install it if missing
271 +function InstallModule(modulename, func, tag1, tag2) {
272 + log('Installing ' + modulename + '...');
273 + var child_process = require('child_process');
274 + var parentpath = __dirname;
275 +
276 + // Get the working directory
277 + if ((__dirname.endsWith('/node_modules/meshcentral')) || (__dirname.endsWith('\\node_modules\\meshcentral')) || (__dirname.endsWith('/node_modules/meshcentral/')) || (__dirname.endsWith('\\node_modules\\meshcentral\\'))) { parentpath = require('path').join(__dirname, '../..'); }
278 +
279 + // Looks like we need to keep a global reference to the child process object for this to work correctly.
280 + InstallModuleChildProcess = child_process.exec('npm install --no-optional --save ' + modulename, { maxBuffer: 512000, timeout: 120000, cwd: parentpath }, function (error, stdout, stderr) {
281 + InstallModuleChildProcess = null;
282 + if ((error != null) && (error != '')) {
283 + log('ERROR: Unable to install required module "' + modulename + '". May not have access to npm, or npm may not have suffisent rights to load the new module. Try "npm install ' + modulename + '" to manualy install this module.\r\n');
284 + process.exit();
285 + return;
286 + }
287 + previouslyInstalledModules[modulename] = true;
288 + func(tag1, tag2);
289 + return;
290 + });
291 +}
292 +
293 +// Export table
294 +module.exports.startEx = startEx;
\ No newline at end of file
package.json
+2 -1
@@ -1,6 +1,6 @@
1 {
2 "name": "meshcentral",
3 - "version": "0.4.9-b",
3 + "version": "0.4.9-c",
4 "keywords": [
5 "Remote Management",
6 "Intel AMT",
@@ -37,6 +37,7 @@
37 "express": "^4.17.0",
38 "express-handlebars": "^3.1.0",
39 "express-ws": "^4.0.0",
40 + "image-size": "^0.8.3",
41 "ipcheck": "^0.1.0",
42 "minimist": "^1.2.0",
43 "multiparty": "^4.2.1",
translate/translate.json
+28 -4
@@ -1256,6 +1256,12 @@
1256 "agentinvite.handlebars->3->4"
1257 ]
1258 },
1259 + {
1260 + "en": "<<",
1261 + "xloc": [
1262 + "player.handlebars->p11->deskarea0->deskarea4->3"
1263 + ]
1264 + },
1265 {
1266 "cs": "<a href=\\\"https://www.yubico.com/\\\" rel=\\\"noreferrer noopener\\\" target=\\\"_blank\\\">Hardwarové klíče</a> jsou použity jako druhý faktor ověřování.",
1267 "de": "<a href=\\\"https://www.yubico.com/\\\" rel=\\\"noreferrer noopener\\\" target=\\\"_blank\\\">Hardware-Schlüssel</a> werden für Zweifaktor-Anmeldung verwendet.",
@@ -1321,6 +1327,12 @@
1327 "default-mobile.handlebars->9->23"
1328 ]
1329 },
1330 + {
1331 + "en": ">>",
1332 + "xloc": [
1333 + "player.handlebars->p11->deskarea0->deskarea4->3"
1334 + ]
1335 + },
1336 {
1337 "cs": "ACM",
1338 "de": "ACM",
@@ -6282,7 +6294,7 @@
6294 "pt": "Arraste e solte um arquivo .mcrec ou clique em \\\"Abrir arquivo...\\\"",
6295 "ru": "Перетащите .mcrec файл или нажмите \\\"Открыть файл ... \\\"",
6296 "xloc": [
6285 - "player.handlebars->3->18"
6297 + "player.handlebars->3->20"
6298 ]
6299 },
6300 {
@@ -8449,6 +8461,12 @@
8461 "default.handlebars->container->dialog->dialogBody->dialog7->d7amtkvm->3->1"
8462 ]
8463 },
8464 + {
8465 + "en": "Indexed every {0} seconds",
8466 + "xloc": [
8467 + "player.handlebars->3->17"
8468 + ]
8469 + },
8470 {
8471 "cs": "indonézština",
8472 "de": "Indonesisch",
@@ -12920,7 +12938,7 @@
12938 "pt": "Abrir arquivo...",
12939 "ru": "Открыть файл...",
12940 "xloc": [
12923 - "player.handlebars->3->19",
12941 + "player.handlebars->3->21",
12942 "player.handlebars->p11->deskarea0->deskarea1->3"
12943 ]
12944 },
@@ -13882,8 +13900,8 @@
13900 "pt": "Pressione [espaço] para reproduzir / pausar.",
13901 "ru": "Нажмите [пробел] для проигрывания/паузы.",
13902 "xloc": [
13885 - "player.handlebars->3->16",
13886 - "player.handlebars->3->17"
13903 + "player.handlebars->3->18",
13904 + "player.handlebars->3->19"
13905 ]
13906 },
13907 {
@@ -15317,6 +15335,12 @@
15335 "default.handlebars->25->1354"
15336 ]
15337 },
15338 + {
15339 + "en": "Seeking",
15340 + "xloc": [
15341 + "player.handlebars->3->16"
15342 + ]
15343 + },
15344 {
15345 "cs": "Vybrat vše",
15346 "de": "Alle auswählen",
views/player.handlebars
+54 -21
@@ -29,9 +29,9 @@
29 </div>
30 </div>
31 <div id=deskarea2 style="">
32 - <div class="areaProgress"><div id="progressbar" style=""></div></div>
32 + <div class="areaProgress" style="cursor:pointer" onclick="progressBarSeek(event)"><div id="progressbar" style="height:6px;cursor:pointer"></div></div>
33 </div>
34 - <div id=deskarea3x style="max-height:calc(100vh - 54px);height:calc(100vh - 54px);" onclick="togglePause()">
34 + <div id=deskarea3x style="max-height:calc(100vh - 58px);height:calc(100vh - 58px);" onclick="togglePause()">
35 <div id="bigok" style="display:none;left:calc((100vh / 2))"><b>&checkmark;</b></div>
36 <div id="bigfail" style="display:none;left:calc((100vh / 2))"><b>&#10007;</b></div>
37 <div id="metadatadiv" style="padding:20px;color:lightgrey;text-align:left;display:none"></div>
@@ -203,8 +203,13 @@
203 if (recFileMetadata.indexInterval) {
204 recFileIndexBasePtr = recFilePtr;
205 x += addInfoNoEsc("Seeking", format("Indexed every {0} seconds", recFileMetadata.indexInterval));
206 + QV('SeekBackwardButton', true);
207 + QV('SeekForwardButton', true);
208 QE('SeekBackwardButton', true);
209 QE('SeekForwardButton', true);
210 + } else {
211 + QV('SeekBackwardButton', false);
212 + QV('SeekForwardButton', false);
213 }
214 QV('DeskParent', true);
215 QV('TermParent', false);
@@ -247,6 +252,7 @@
252 QV('metadatadiv', true);
253 QH('metadatadiv', x);
254 QH('deskstatus', recFile.name);
255 + QS('progressbar').width = '0px';
256 }
257
258 function processBlock(type, flags, time, data) {
@@ -256,7 +262,7 @@
262 processBlockEx(type, flags, time, data);
263 } else {
264 waitTimerArgs = [type, flags, time, data]
259 - waitTimer = setTimeout(function () { waitTimer = null; processBlockEx(waitTimerArgs[0], waitTimerArgs[1], waitTimerArgs[2], waitTimerArgs[3]); }, waitTime);
265 + waitTimer = setTimeout(function () { waitTimer = null; if (waitTimerArgs) { processBlockEx(waitTimerArgs[0], waitTimerArgs[1], waitTimerArgs[2], waitTimerArgs[3]); } }, waitTime);
266 }
267 }
268
@@ -267,13 +273,12 @@
273 // Update the clock
274 var deltaTimeTotalSec = Math.floor((time - recFileStartTime) / 1000);
275 if (currentDeltaTimeTotalSec != deltaTimeTotalSec) {
276 + // Hours, minutes and seconds
277 currentDeltaTimeTotalSec = deltaTimeTotalSec;
271 - var deltaTimeHours = Math.floor(deltaTimeTotalSec / 3600);
272 - deltaTimeTotalSec -= (deltaTimeHours * 3600)
273 - var deltaTimeMinutes = Math.floor(deltaTimeTotalSec / 60);
274 - deltaTimeTotalSec -= (deltaTimeHours * 60)
275 - var deltaTimeSeconds = Math.floor(deltaTimeTotalSec);
276 - QH('timespan', pad2(deltaTimeHours) + ':' + pad2(deltaTimeMinutes) + ':' + pad2(deltaTimeSeconds))
278 + var hrs = Math.floor(deltaTimeTotalSec / 3600);
279 + var mins = Math.floor((deltaTimeTotalSec % 3600) / 60);
280 + var secs = Math.floor(deltaTimeTotalSec % 60);
281 + QH('timespan', pad2(hrs) + ':' + pad2(mins) + ':' + pad2(secs))
282 }
283
284 if ((type == 2) && flagBinary && !flagUser) {
@@ -519,38 +524,66 @@
524 }
525
526 function seekBackward() {
522 - //console.log('seekBackward');
523 - seek(5);
527 + var ndxNumber = Math.round(currentDeltaTimeTotalSec / recFileMetadata.indexInterval);
528 + if (ndxNumber < 2) {
529 + pause(); restart();
530 + } else {
531 + if (recFileMetadata.indexes[ndxNumber - 2] != null) { seek(ndxNumber - 2); }
532 + }
533 }
534
535 function seekForward() {
527 - //console.log('seekForward');
528 - seek(10);
536 + var ndxNumber = Math.round(currentDeltaTimeTotalSec / recFileMetadata.indexInterval);
537 + if (recFileMetadata.indexes[ndxNumber] != null) { seek(ndxNumber); }
538 }
539
540 + function progressBarSeek(event) {
541 + var ndxNumber = Math.round((event.clientX / document.body.offsetWidth) * (recFileMetadata.indexes.length + 1)) - 1;
542 + if (ndxNumber == -1) { pause(); restart(); } else { seek(ndxNumber); }
543 + }
544
545 var SeekIndex;
546 var SeekIndexPtr;
547 + var SeekIndexTime;
548 + var SeekPlayState;
549 function seek(indexNumber) {
550 //console.log('seek', indexNumber);
551 if ((recFileMetadata.indexes == null) || (recFileMetadata.indexes[indexNumber] == null)) return null;
552 + SeekPlayState = playing;
553 + pause();
554 restart();
538 - //pause();
539 - if (agentDesktop) { agentDesktop.Canvas.clearRect(0, 0, agentDesktop.CanvasId.width, agentDesktop.CanvasId.height); }
555 SeekIndex = recFileMetadata.indexes[indexNumber];
556 SeekIndexPtr = 3;
542 - var ptr = SeekIndex[0];
557 + recFileLastTime = SeekIndexTime = recFileStartTime + ((1 + indexNumber) * recFileMetadata.indexInterval * 1000);
558 + recFilePtr = recFileIndexBasePtr + SeekIndex[0];
559 var width = SeekIndex[1];
560 var height = SeekIndex[2];
545 - seekFetchNext();
561 +
562 + if (recFileEndTime == 0) {
563 + // File pointer progress bar
564 + QS('progressbar').width = Math.floor(100 * (recFilePtr / recFile.size)) + '%';
565 + } else {
566 + // Time progress bar
567 + QS('progressbar').width = Math.floor(((recFileLastTime - recFileStartTime) / (recFileEndTime - recFileStartTime)) * 100) + '%';
568 + }
569 +
570 + if (agentDesktop) {
571 + agentDesktop.Canvas.clearRect(0, 0, agentDesktop.CanvasId.width, agentDesktop.CanvasId.height);
572 + agentDesktop.ProcessScreenMsg(width, height);
573 + }
574 +
575 + QV('metadatadiv', false);
576 + QV('Desk', false);
577 +
578 + seekFetchNext(function () { QV('Desk', true); if (SeekPlayState) { play(); } });
579 }
580
548 - function seekFetchNext() {
549 - if (SeekIndex[SeekIndexPtr] == null) { return; }
581 + function seekFetchNext(func) {
582 + if (SeekIndex[SeekIndexPtr] == null) { func(); return; }
583 readBlockAt(recFileIndexBasePtr + SeekIndex[SeekIndexPtr], function (type, flags, time, data) {
584 SeekIndexPtr++;
552 - processBlockEx(type, flags, time, data, true);
553 - seekFetchNext();
585 + processBlockEx(type, flags, SeekIndexTime, data, true);
586 + seekFetchNext(func);
587 });
588 }
589