source: OpenRLabs-Git/web2py/applications/rlabs/static/js/guacamole-common-js/modules/SessionRecording.js

main
Last change on this file was 42bd667, checked in by David Fuertes <dfuertes@…>, 4 years ago

Historial Limpio

  • Property mode set to 100644
File size: 25.4 KB
Line 
1/*
2 * Licensed to the Apache Software Foundation (ASF) under one
3 * or more contributor license agreements.  See the NOTICE file
4 * distributed with this work for additional information
5 * regarding copyright ownership.  The ASF licenses this file
6 * to you under the Apache License, Version 2.0 (the
7 * "License"); you may not use this file except in compliance
8 * with the License.  You may obtain a copy of the License at
9 *
10 *   http://www.apache.org/licenses/LICENSE-2.0
11 *
12 * Unless required by applicable law or agreed to in writing,
13 * software distributed under the License is distributed on an
14 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 * KIND, either express or implied.  See the License for the
16 * specific language governing permissions and limitations
17 * under the License.
18 */
19
20var Guacamole = Guacamole || {};
21
22/**
23 * A recording of a Guacamole session. Given a {@link Guacamole.Tunnel}, the
24 * Guacamole.SessionRecording automatically handles incoming Guacamole
25 * instructions, storing them for playback. Playback of the recording may be
26 * controlled through function calls to the Guacamole.SessionRecording, even
27 * while the recording has not yet finished being created or downloaded.
28 *
29 * @constructor
30 * @param {Guacamole.Tunnel} tunnel
31 *     The Guacamole.Tunnel from which the instructions of the recording should
32 *     be read.
33 */
34Guacamole.SessionRecording = function SessionRecording(tunnel) {
35
36    /**
37     * Reference to this Guacamole.SessionRecording.
38     *
39     * @private
40     * @type {Guacamole.SessionRecording}
41     */
42    var recording = this;
43
44    /**
45     * The minimum number of characters which must have been read between
46     * keyframes.
47     *
48     * @private
49     * @constant
50     * @type {Number}
51     */
52    var KEYFRAME_CHAR_INTERVAL = 16384;
53
54    /**
55     * The minimum number of milliseconds which must elapse between keyframes.
56     *
57     * @private
58     * @constant
59     * @type {Number}
60     */
61    var KEYFRAME_TIME_INTERVAL = 5000;
62
63    /**
64     * The maximum amount of time to spend in any particular seek operation
65     * before returning control to the main thread, in milliseconds. Seek
66     * operations exceeding this amount of time will proceed asynchronously.
67     *
68     * @private
69     * @constant
70     * @type {Number}
71     */
72    var MAXIMUM_SEEK_TIME = 5;
73
74    /**
75     * All frames parsed from the provided tunnel.
76     *
77     * @private
78     * @type {Guacamole.SessionRecording._Frame[]}
79     */
80    var frames = [];
81
82    /**
83     * All instructions which have been read since the last frame was added to
84     * the frames array.
85     *
86     * @private
87     * @type {Guacamole.SessionRecording._Frame.Instruction[]}
88     */
89    var instructions = [];
90
91    /**
92     * The approximate number of characters which have been read from the
93     * provided tunnel since the last frame was flagged for use as a keyframe.
94     *
95     * @private
96     * @type {Number}
97     */
98    var charactersSinceLastKeyframe = 0;
99
100    /**
101     * The timestamp of the last frame which was flagged for use as a keyframe.
102     * If no timestamp has yet been flagged, this will be 0.
103     *
104     * @private
105     * @type {Number}
106     */
107    var lastKeyframeTimestamp = 0;
108
109    /**
110     * Tunnel which feeds arbitrary instructions to the client used by this
111     * Guacamole.SessionRecording for playback of the session recording.
112     *
113     * @private
114     * @type {Guacamole.SessionRecording._PlaybackTunnel}
115     */
116    var playbackTunnel = new Guacamole.SessionRecording._PlaybackTunnel();
117
118    /**
119     * Guacamole.Client instance used for visible playback of the session
120     * recording.
121     *
122     * @private
123     * @type {Guacamole.Client}
124     */
125    var playbackClient = new Guacamole.Client(playbackTunnel);
126
127    /**
128     * The current frame rendered within the playback client. If no frame is
129     * yet rendered, this will be -1.
130     *
131     * @private
132     * @type {Number}
133     */
134    var currentFrame = -1;
135
136    /**
137     * The timestamp of the frame when playback began, in milliseconds. If
138     * playback is not in progress, this will be null.
139     *
140     * @private
141     * @type {Number}
142     */
143    var startVideoTimestamp = null;
144
145    /**
146     * The real-world timestamp when playback began, in milliseconds. If
147     * playback is not in progress, this will be null.
148     *
149     * @private
150     * @type {Number}
151     */
152    var startRealTimestamp = null;
153
154    /**
155     * The ID of the timeout which will continue the in-progress seek
156     * operation. If no seek operation is in progress, the ID stored here (if
157     * any) will not be valid.
158     *
159     * @private
160     * @type {Number}
161     */
162    var seekTimeout = null;
163
164    // Start playback client connected
165    playbackClient.connect();
166
167    // Hide cursor unless mouse position is received
168    playbackClient.getDisplay().showCursor(false);
169
170    // Read instructions from provided tunnel, extracting each frame
171    tunnel.oninstruction = function handleInstruction(opcode, args) {
172
173        // Store opcode and arguments for received instruction
174        var instruction = new Guacamole.SessionRecording._Frame.Instruction(opcode, args.slice());
175        instructions.push(instruction);
176        charactersSinceLastKeyframe += instruction.getSize();
177
178        // Once a sync is received, store all instructions since the last
179        // frame as a new frame
180        if (opcode === 'sync') {
181
182            // Parse frame timestamp from sync instruction
183            var timestamp = parseInt(args[0]);
184
185            // Add a new frame containing the instructions read since last frame
186            var frame = new Guacamole.SessionRecording._Frame(timestamp, instructions);
187            frames.push(frame);
188
189            // This frame should eventually become a keyframe if enough data
190            // has been processed and enough recording time has elapsed, or if
191            // this is the absolute first frame
192            if (frames.length === 1 || (charactersSinceLastKeyframe >= KEYFRAME_CHAR_INTERVAL
193                    && timestamp - lastKeyframeTimestamp >= KEYFRAME_TIME_INTERVAL)) {
194                frame.keyframe = true;
195                lastKeyframeTimestamp = timestamp;
196                charactersSinceLastKeyframe = 0;
197            }
198
199            // Clear set of instructions in preparation for next frame
200            instructions = [];
201
202            // Notify that additional content is available
203            if (recording.onprogress)
204                recording.onprogress(recording.getDuration());
205
206        }
207
208    };
209
210    /**
211     * Converts the given absolute timestamp to a timestamp which is relative
212     * to the first frame in the recording.
213     *
214     * @private
215     * @param {Number} timestamp
216     *     The timestamp to convert to a relative timestamp.
217     *
218     * @returns {Number}
219     *     The difference in milliseconds between the given timestamp and the
220     *     first frame of the recording, or zero if no frames yet exist.
221     */
222    var toRelativeTimestamp = function toRelativeTimestamp(timestamp) {
223
224        // If no frames yet exist, all timestamps are zero
225        if (frames.length === 0)
226            return 0;
227
228        // Calculate timestamp relative to first frame
229        return timestamp - frames[0].timestamp;
230
231    };
232
233    /**
234     * Searches through the given region of frames for the frame having a
235     * relative timestamp closest to the timestamp given.
236     *
237     * @private
238     * @param {Number} minIndex
239     *     The index of the first frame in the region (the frame having the
240     *     smallest timestamp).
241     *
242     * @param {Number} maxIndex
243     *     The index of the last frame in the region (the frame having the
244     *     largest timestamp).
245     *
246     * @param {Number} timestamp
247     *     The relative timestamp to search for, where zero denotes the first
248     *     frame in the recording.
249     *
250     * @returns {Number}
251     *     The index of the frame having a relative timestamp closest to the
252     *     given value.
253     */
254    var findFrame = function findFrame(minIndex, maxIndex, timestamp) {
255
256        // Do not search if the region contains only one element
257        if (minIndex === maxIndex)
258            return minIndex;
259
260        // Split search region into two halves
261        var midIndex = Math.floor((minIndex + maxIndex) / 2);
262        var midTimestamp = toRelativeTimestamp(frames[midIndex].timestamp);
263
264        // If timestamp is within lesser half, search again within that half
265        if (timestamp < midTimestamp && midIndex > minIndex)
266            return findFrame(minIndex, midIndex - 1, timestamp);
267
268        // If timestamp is within greater half, search again within that half
269        if (timestamp > midTimestamp && midIndex < maxIndex)
270            return findFrame(midIndex + 1, maxIndex, timestamp);
271
272        // Otherwise, we lucked out and found a frame with exactly the
273        // desired timestamp
274        return midIndex;
275
276    };
277
278    /**
279     * Replays the instructions associated with the given frame, sending those
280     * instructions to the playback client.
281     *
282     * @private
283     * @param {Number} index
284     *     The index of the frame within the frames array which should be
285     *     replayed.
286     */
287    var replayFrame = function replayFrame(index) {
288
289        var frame = frames[index];
290
291        // Replay all instructions within the retrieved frame
292        for (var i = 0; i < frame.instructions.length; i++) {
293            var instruction = frame.instructions[i];
294            playbackTunnel.receiveInstruction(instruction.opcode, instruction.args);
295        }
296
297        // Store client state if frame is flagged as a keyframe
298        if (frame.keyframe && !frame.clientState) {
299            playbackClient.exportState(function storeClientState(state) {
300                frame.clientState = state;
301            });
302        }
303
304    };
305
306    /**
307     * Moves the playback position to the given frame, resetting the state of
308     * the playback client and replaying frames as necessary. The seek
309     * operation will proceed asynchronously. If a seek operation is already in
310     * progress, that seek is first aborted. The progress of the seek operation
311     * can be observed through the onseek handler and the provided callback.
312     *
313     * @private
314     * @param {Number} index
315     *     The index of the frame which should become the new playback
316     *     position.
317     *
318     * @param {function} callback
319     *     The callback to invoke once the seek operation has completed.
320     *
321     * @param {Number} [delay=0]
322     *     The number of milliseconds that the seek operation should be
323     *     scheduled to take.
324     */
325    var seekToFrame = function seekToFrame(index, callback, delay) {
326
327        // Abort any in-progress seek
328        abortSeek();
329
330        // Replay frames asynchronously
331        seekTimeout = window.setTimeout(function continueSeek() {
332
333            var startIndex;
334
335            // Back up until startIndex represents current state
336            for (startIndex = index; startIndex >= 0; startIndex--) {
337
338                var frame = frames[startIndex];
339
340                // If we've reached the current frame, startIndex represents
341                // current state by definition
342                if (startIndex === currentFrame)
343                    break;
344
345                // If frame has associated absolute state, make that frame the
346                // current state
347                if (frame.clientState) {
348                    playbackClient.importState(frame.clientState);
349                    break;
350                }
351
352            }
353
354            // Advance to frame index after current state
355            startIndex++;
356
357            var startTime = new Date().getTime();
358
359            // Replay any applicable incremental frames
360            for (; startIndex <= index; startIndex++) {
361
362                // Stop seeking if the operation is taking too long
363                var currentTime = new Date().getTime();
364                if (currentTime - startTime >= MAXIMUM_SEEK_TIME)
365                    break;
366
367                replayFrame(startIndex);
368            }
369
370            // Current frame is now at requested index
371            currentFrame = startIndex - 1;
372
373            // Notify of changes in position
374            if (recording.onseek)
375                recording.onseek(recording.getPosition());
376
377            // If the seek operation has not yet completed, schedule continuation
378            if (currentFrame !== index)
379                seekToFrame(index, callback,
380                    Math.max(delay - (new Date().getTime() - startTime), 0));
381
382            // Notify that the requested seek has completed
383            else
384                callback();
385
386        }, delay || 0);
387
388    };
389
390    /**
391     * Aborts the seek operation currently in progress, if any. If no seek
392     * operation is in progress, this function has no effect.
393     *
394     * @private
395     */
396    var abortSeek = function abortSeek() {
397        window.clearTimeout(seekTimeout);
398    };
399
400    /**
401     * Advances playback to the next frame in the frames array and schedules
402     * playback of the frame following that frame based on their associated
403     * timestamps. If no frames exist after the next frame, playback is paused.
404     *
405     * @private
406     */
407    var continuePlayback = function continuePlayback() {
408
409        // If frames remain after advancing, schedule next frame
410        if (currentFrame + 1 < frames.length) {
411
412            // Pull the upcoming frame
413            var next = frames[currentFrame + 1];
414
415            // Calculate the real timestamp corresponding to when the next
416            // frame begins
417            var nextRealTimestamp = next.timestamp - startVideoTimestamp + startRealTimestamp;
418
419            // Calculate the relative delay between the current time and
420            // the next frame start
421            var delay = Math.max(nextRealTimestamp - new Date().getTime(), 0);
422
423            // Advance to next frame after enough time has elapsed
424            seekToFrame(currentFrame + 1, function frameDelayElapsed() {
425                continuePlayback();
426            }, delay);
427
428        }
429
430        // Otherwise stop playback
431        else
432            recording.pause();
433
434    };
435
436    /**
437     * Fired when new frames have become available while the recording is
438     * being downloaded.
439     *
440     * @event
441     * @param {Number} duration
442     *     The new duration of the recording, in milliseconds.
443     */
444    this.onprogress = null;
445
446    /**
447     * Fired whenever playback of the recording has started.
448     *
449     * @event
450     */
451    this.onplay = null;
452
453    /**
454     * Fired whenever playback of the recording has been paused. This may
455     * happen when playback is explicitly paused with a call to pause(), or
456     * when playback is implicitly paused due to reaching the end of the
457     * recording.
458     *
459     * @event
460     */
461    this.onpause = null;
462
463    /**
464     * Fired whenever the playback position within the recording changes.
465     *
466     * @event
467     * @param {Number} position
468     *     The new position within the recording, in milliseconds.
469     */
470    this.onseek = null;
471
472    /**
473     * Connects the underlying tunnel, beginning download of the Guacamole
474     * session. Playback of the Guacamole session cannot occur until at least
475     * one frame worth of instructions has been downloaded.
476     *
477     * @param {String} data
478     *     The data to send to the tunnel when connecting.
479     */
480    this.connect = function connect(data) {
481        tunnel.connect(data);
482    };
483
484    /**
485     * Disconnects the underlying tunnel, stopping further download of the
486     * Guacamole session.
487     */
488    this.disconnect = function disconnect() {
489        tunnel.disconnect();
490    };
491
492    /**
493     * Returns the underlying display of the Guacamole.Client used by this
494     * Guacamole.SessionRecording for playback. The display contains an Element
495     * which can be added to the DOM, causing the display (and thus playback of
496     * the recording) to become visible.
497     *
498     * @return {Guacamole.Display}
499     *     The underlying display of the Guacamole.Client used by this
500     *     Guacamole.SessionRecording for playback.
501     */
502    this.getDisplay = function getDisplay() {
503        return playbackClient.getDisplay();
504    };
505
506    /**
507     * Returns whether playback is currently in progress.
508     *
509     * @returns {Boolean}
510     *     true if playback is currently in progress, false otherwise.
511     */
512    this.isPlaying = function isPlaying() {
513        return !!startVideoTimestamp;
514    };
515
516    /**
517     * Returns the current playback position within the recording, in
518     * milliseconds, where zero is the start of the recording.
519     *
520     * @returns {Number}
521     *     The current playback position within the recording, in milliseconds.
522     */
523    this.getPosition = function getPosition() {
524
525        // Position is simply zero if playback has not started at all
526        if (currentFrame === -1)
527            return 0;
528
529        // Return current position as a millisecond timestamp relative to the
530        // start of the recording
531        return toRelativeTimestamp(frames[currentFrame].timestamp);
532
533    };
534
535    /**
536     * Returns the duration of this recording, in milliseconds. If the
537     * recording is still being downloaded, this value will gradually increase.
538     *
539     * @returns {Number}
540     *     The duration of this recording, in milliseconds.
541     */
542    this.getDuration = function getDuration() {
543
544        // If no frames yet exist, duration is zero
545        if (frames.length === 0)
546            return 0;
547
548        // Recording duration is simply the timestamp of the last frame
549        return toRelativeTimestamp(frames[frames.length - 1].timestamp);
550
551    };
552
553    /**
554     * Begins continuous playback of the recording downloaded thus far.
555     * Playback of the recording will continue until pause() is invoked or
556     * until no further frames exist. Playback is initially paused when a
557     * Guacamole.SessionRecording is created, and must be explicitly started
558     * through a call to this function. If playback is already in progress,
559     * this function has no effect. If a seek operation is in progress,
560     * playback resumes at the current position, and the seek is aborted as if
561     * completed.
562     */
563    this.play = function play() {
564
565        // If playback is not already in progress and frames remain,
566        // begin playback
567        if (!recording.isPlaying() && currentFrame + 1 < frames.length) {
568
569            // Notify that playback is starting
570            if (recording.onplay)
571                recording.onplay();
572
573            // Store timestamp of playback start for relative scheduling of
574            // future frames
575            var next = frames[currentFrame + 1];
576            startVideoTimestamp = next.timestamp;
577            startRealTimestamp = new Date().getTime();
578
579            // Begin playback of video
580            continuePlayback();
581
582        }
583
584    };
585
586    /**
587     * Seeks to the given position within the recording. If the recording is
588     * currently being played back, playback will continue after the seek is
589     * performed. If the recording is currently paused, playback will be
590     * paused after the seek is performed. If a seek operation is already in
591     * progress, that seek is first aborted. The seek operation will proceed
592     * asynchronously.
593     *
594     * @param {Number} position
595     *     The position within the recording to seek to, in milliseconds.
596     *
597     * @param {function} [callback]
598     *     The callback to invoke once the seek operation has completed.
599     */
600    this.seek = function seek(position, callback) {
601
602        // Do not seek if no frames exist
603        if (frames.length === 0)
604            return;
605
606        // Pause playback, preserving playback state
607        var originallyPlaying = recording.isPlaying();
608        recording.pause();
609
610        // Perform seek
611        seekToFrame(findFrame(0, frames.length - 1, position), function restorePlaybackState() {
612
613            // Restore playback state
614            if (originallyPlaying)
615                recording.play();
616
617            // Notify that seek has completed
618            if (callback)
619                callback();
620
621        });
622
623    };
624
625    /**
626     * Pauses playback of the recording, if playback is currently in progress.
627     * If playback is not in progress, this function has no effect. If a seek
628     * operation is in progress, the seek is aborted. Playback is initially
629     * paused when a Guacamole.SessionRecording is created, and must be
630     * explicitly started through a call to play().
631     */
632    this.pause = function pause() {
633
634        // Abort any in-progress seek / playback
635        abortSeek();
636
637        // Stop playback only if playback is in progress
638        if (recording.isPlaying()) {
639
640            // Notify that playback is stopping
641            if (recording.onpause)
642                recording.onpause();
643
644            // Playback is stopped
645            startVideoTimestamp = null;
646            startRealTimestamp = null;
647
648        }
649
650    };
651
652};
653
654/**
655 * A single frame of Guacamole session data. Each frame is made up of the set
656 * of instructions used to generate that frame, and the timestamp as dictated
657 * by the "sync" instruction terminating the frame. Optionally, a frame may
658 * also be associated with a snapshot of Guacamole client state, such that the
659 * frame can be rendered without replaying all previous frames.
660 *
661 * @private
662 * @constructor
663 * @param {Number} timestamp
664 *     The timestamp of this frame, as dictated by the "sync" instruction which
665 *     terminates the frame.
666 *
667 * @param {Guacamole.SessionRecording._Frame.Instruction[]} instructions
668 *     All instructions which are necessary to generate this frame relative to
669 *     the previous frame in the Guacamole session.
670 */
671Guacamole.SessionRecording._Frame = function _Frame(timestamp, instructions) {
672
673    /**
674     * Whether this frame should be used as a keyframe if possible. This value
675     * is purely advisory. The stored clientState must eventually be manually
676     * set for the frame to be used as a keyframe. By default, frames are not
677     * keyframes.
678     *
679     * @type {Boolean}
680     * @default false
681     */
682    this.keyframe = false;
683
684    /**
685     * The timestamp of this frame, as dictated by the "sync" instruction which
686     * terminates the frame.
687     *
688     * @type {Number}
689     */
690    this.timestamp = timestamp;
691
692    /**
693     * All instructions which are necessary to generate this frame relative to
694     * the previous frame in the Guacamole session.
695     *
696     * @type {Guacamole.SessionRecording._Frame.Instruction[]}
697     */
698    this.instructions = instructions;
699
700    /**
701     * A snapshot of client state after this frame was rendered, as returned by
702     * a call to exportState(). If no such snapshot has been taken, this will
703     * be null.
704     *
705     * @type {Object}
706     * @default null
707     */
708    this.clientState = null;
709
710};
711
712/**
713 * A Guacamole protocol instruction. Each Guacamole protocol instruction is
714 * made up of an opcode and set of arguments.
715 *
716 * @private
717 * @constructor
718 * @param {String} opcode
719 *     The opcode of this Guacamole instruction.
720 *
721 * @param {String[]} args
722 *     All arguments associated with this Guacamole instruction.
723 */
724Guacamole.SessionRecording._Frame.Instruction = function Instruction(opcode, args) {
725
726    /**
727     * Reference to this Guacamole.SessionRecording._Frame.Instruction.
728     *
729     * @private
730     * @type {Guacamole.SessionRecording._Frame.Instruction}
731     */
732    var instruction = this;
733
734    /**
735     * The opcode of this Guacamole instruction.
736     *
737     * @type {String}
738     */
739    this.opcode = opcode;
740
741    /**
742     * All arguments associated with this Guacamole instruction.
743     *
744     * @type {String[]}
745     */
746    this.args = args;
747
748    /**
749     * Returns the approximate number of characters which make up this
750     * instruction. This value is only approximate as it excludes the length
751     * prefixes and various delimiters used by the Guacamole protocol; only
752     * the content of the opcode and each argument is taken into account.
753     *
754     * @returns {Number}
755     *     The approximate size of this instruction, in characters.
756     */
757    this.getSize = function getSize() {
758
759        // Init with length of opcode
760        var size = instruction.opcode.length;
761
762        // Add length of all arguments
763        for (var i = 0; i < instruction.args.length; i++)
764            size += instruction.args[i].length;
765
766        return size;
767
768    };
769
770};
771
772/**
773 * A read-only Guacamole.Tunnel implementation which streams instructions
774 * received through explicit calls to its receiveInstruction() function.
775 *
776 * @private
777 * @constructor
778 * @augments {Guacamole.Tunnel}
779 */
780Guacamole.SessionRecording._PlaybackTunnel = function _PlaybackTunnel() {
781
782    /**
783     * Reference to this Guacamole.SessionRecording._PlaybackTunnel.
784     *
785     * @private
786     * @type {Guacamole.SessionRecording._PlaybackTunnel}
787     */
788    var tunnel = this;
789
790    this.connect = function connect(data) {
791        // Do nothing
792    };
793
794    this.sendMessage = function sendMessage(elements) {
795        // Do nothing
796    };
797
798    this.disconnect = function disconnect() {
799        // Do nothing
800    };
801
802    /**
803     * Invokes this tunnel's oninstruction handler, notifying users of this
804     * tunnel (such as a Guacamole.Client instance) that an instruction has
805     * been received. If the oninstruction handler has not been set, this
806     * function has no effect.
807     *
808     * @param {String} opcode
809     *     The opcode of the Guacamole instruction.
810     *
811     * @param {String[]} args
812     *     All arguments associated with this Guacamole instruction.
813     */
814    this.receiveInstruction = function receiveInstruction(opcode, args) {
815        if (tunnel.oninstruction)
816            tunnel.oninstruction(opcode, args);
817    };
818
819};
Note: See TracBrowser for help on using the repository browser.