guacamole-common.js (509771B)
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 reader which automatically handles the given input stream, returning 24 * strictly received packets as array buffers. Note that this object will 25 * overwrite any installed event handlers on the given Guacamole.InputStream. 26 * 27 * @constructor 28 * @param {!Guacamole.InputStream} stream 29 * The stream that data will be read from. 30 */ 31Guacamole.ArrayBufferReader = function(stream) { 32 33 /** 34 * Reference to this Guacamole.InputStream. 35 * @private 36 */ 37 var guac_reader = this; 38 39 // Receive blobs as array buffers 40 stream.onblob = function(data) { 41 42 // Convert to ArrayBuffer 43 var binary = window.atob(data); 44 var arrayBuffer = new ArrayBuffer(binary.length); 45 var bufferView = new Uint8Array(arrayBuffer); 46 47 for (var i=0; i<binary.length; i++) 48 bufferView[i] = binary.charCodeAt(i); 49 50 // Call handler, if present 51 if (guac_reader.ondata) 52 guac_reader.ondata(arrayBuffer); 53 54 }; 55 56 // Simply call onend when end received 57 stream.onend = function() { 58 if (guac_reader.onend) 59 guac_reader.onend(); 60 }; 61 62 /** 63 * Fired once for every blob of data received. 64 * 65 * @event 66 * @param {!ArrayBuffer} buffer 67 * The data packet received. 68 */ 69 this.ondata = null; 70 71 /** 72 * Fired once this stream is finished and no further data will be written. 73 * @event 74 */ 75 this.onend = null; 76 77};/* 78 * Licensed to the Apache Software Foundation (ASF) under one 79 * or more contributor license agreements. See the NOTICE file 80 * distributed with this work for additional information 81 * regarding copyright ownership. The ASF licenses this file 82 * to you under the Apache License, Version 2.0 (the 83 * "License"); you may not use this file except in compliance 84 * with the License. You may obtain a copy of the License at 85 * 86 * http://www.apache.org/licenses/LICENSE-2.0 87 * 88 * Unless required by applicable law or agreed to in writing, 89 * software distributed under the License is distributed on an 90 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 91 * KIND, either express or implied. See the License for the 92 * specific language governing permissions and limitations 93 * under the License. 94 */ 95 96var Guacamole = Guacamole || {}; 97 98/** 99 * A writer which automatically writes to the given output stream with arbitrary 100 * binary data, supplied as ArrayBuffers. 101 * 102 * @constructor 103 * @param {!Guacamole.OutputStream} stream 104 * The stream that data will be written to. 105 */ 106Guacamole.ArrayBufferWriter = function(stream) { 107 108 /** 109 * Reference to this Guacamole.StringWriter. 110 * 111 * @private 112 * @type {!Guacamole.ArrayBufferWriter} 113 */ 114 var guac_writer = this; 115 116 // Simply call onack for acknowledgements 117 stream.onack = function(status) { 118 if (guac_writer.onack) 119 guac_writer.onack(status); 120 }; 121 122 /** 123 * Encodes the given data as base64, sending it as a blob. The data must 124 * be small enough to fit into a single blob instruction. 125 * 126 * @private 127 * @param {!Uint8Array} bytes 128 * The data to send. 129 */ 130 function __send_blob(bytes) { 131 132 var binary = ""; 133 134 // Produce binary string from bytes in buffer 135 for (var i=0; i<bytes.byteLength; i++) 136 binary += String.fromCharCode(bytes[i]); 137 138 // Send as base64 139 stream.sendBlob(window.btoa(binary)); 140 141 } 142 143 /** 144 * The maximum length of any blob sent by this Guacamole.ArrayBufferWriter, 145 * in bytes. Data sent via 146 * [sendData()]{@link Guacamole.ArrayBufferWriter#sendData} which exceeds 147 * this length will be split into multiple blobs. As the Guacamole protocol 148 * limits the maximum size of any instruction or instruction element to 149 * 8192 bytes, and the contents of blobs will be base64-encoded, this value 150 * should only be increased with extreme caution. 151 * 152 * @type {!number} 153 * @default {@link Guacamole.ArrayBufferWriter.DEFAULT_BLOB_LENGTH} 154 */ 155 this.blobLength = Guacamole.ArrayBufferWriter.DEFAULT_BLOB_LENGTH; 156 157 /** 158 * Sends the given data. 159 * 160 * @param {!(ArrayBuffer|TypedArray)} data 161 * The data to send. 162 */ 163 this.sendData = function(data) { 164 165 var bytes = new Uint8Array(data); 166 167 // If small enough to fit into single instruction, send as-is 168 if (bytes.length <= guac_writer.blobLength) 169 __send_blob(bytes); 170 171 // Otherwise, send as multiple instructions 172 else { 173 for (var offset=0; offset<bytes.length; offset += guac_writer.blobLength) 174 __send_blob(bytes.subarray(offset, offset + guac_writer.blobLength)); 175 } 176 177 }; 178 179 /** 180 * Signals that no further text will be sent, effectively closing the 181 * stream. 182 */ 183 this.sendEnd = function() { 184 stream.sendEnd(); 185 }; 186 187 /** 188 * Fired for received data, if acknowledged by the server. 189 * @event 190 * @param {!Guacamole.Status} status 191 * The status of the operation. 192 */ 193 this.onack = null; 194 195}; 196 197/** 198 * The default maximum blob length for new Guacamole.ArrayBufferWriter 199 * instances. 200 * 201 * @constant 202 * @type {!number} 203 */ 204Guacamole.ArrayBufferWriter.DEFAULT_BLOB_LENGTH = 6048; 205/* 206 * Licensed to the Apache Software Foundation (ASF) under one 207 * or more contributor license agreements. See the NOTICE file 208 * distributed with this work for additional information 209 * regarding copyright ownership. The ASF licenses this file 210 * to you under the Apache License, Version 2.0 (the 211 * "License"); you may not use this file except in compliance 212 * with the License. You may obtain a copy of the License at 213 * 214 * http://www.apache.org/licenses/LICENSE-2.0 215 * 216 * Unless required by applicable law or agreed to in writing, 217 * software distributed under the License is distributed on an 218 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 219 * KIND, either express or implied. See the License for the 220 * specific language governing permissions and limitations 221 * under the License. 222 */ 223 224var Guacamole = Guacamole || {}; 225 226/** 227 * Maintains a singleton instance of the Web Audio API AudioContext class, 228 * instantiating the AudioContext only in response to the first call to 229 * getAudioContext(), and only if no existing AudioContext instance has been 230 * provided via the singleton property. Subsequent calls to getAudioContext() 231 * will return the same instance. 232 * 233 * @namespace 234 */ 235Guacamole.AudioContextFactory = { 236 237 /** 238 * A singleton instance of a Web Audio API AudioContext object, or null if 239 * no instance has yes been created. This property may be manually set if 240 * you wish to supply your own AudioContext instance, but care must be 241 * taken to do so as early as possible. Assignments to this property will 242 * not retroactively affect the value returned by previous calls to 243 * getAudioContext(). 244 * 245 * @type {AudioContext} 246 */ 247 'singleton' : null, 248 249 /** 250 * Returns a singleton instance of a Web Audio API AudioContext object. 251 * 252 * @return {AudioContext} 253 * A singleton instance of a Web Audio API AudioContext object, or null 254 * if the Web Audio API is not supported. 255 */ 256 'getAudioContext' : function getAudioContext() { 257 258 // Fallback to Webkit-specific AudioContext implementation 259 var AudioContext = window.AudioContext || window.webkitAudioContext; 260 261 // Get new AudioContext instance if Web Audio API is supported 262 if (AudioContext) { 263 try { 264 265 // Create new instance if none yet exists 266 if (!Guacamole.AudioContextFactory.singleton) 267 Guacamole.AudioContextFactory.singleton = new AudioContext(); 268 269 // Return singleton instance 270 return Guacamole.AudioContextFactory.singleton; 271 272 } 273 catch (e) { 274 // Do not use Web Audio API if not allowed by browser 275 } 276 } 277 278 // Web Audio API not supported 279 return null; 280 281 } 282 283}; 284/* 285 * Licensed to the Apache Software Foundation (ASF) under one 286 * or more contributor license agreements. See the NOTICE file 287 * distributed with this work for additional information 288 * regarding copyright ownership. The ASF licenses this file 289 * to you under the Apache License, Version 2.0 (the 290 * "License"); you may not use this file except in compliance 291 * with the License. You may obtain a copy of the License at 292 * 293 * http://www.apache.org/licenses/LICENSE-2.0 294 * 295 * Unless required by applicable law or agreed to in writing, 296 * software distributed under the License is distributed on an 297 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 298 * KIND, either express or implied. See the License for the 299 * specific language governing permissions and limitations 300 * under the License. 301 */ 302 303var Guacamole = Guacamole || {}; 304 305/** 306 * Abstract audio player which accepts, queues and plays back arbitrary audio 307 * data. It is up to implementations of this class to provide some means of 308 * handling a provided Guacamole.InputStream. Data received along the provided 309 * stream is to be played back immediately. 310 * 311 * @constructor 312 */ 313Guacamole.AudioPlayer = function AudioPlayer() { 314 315 /** 316 * Notifies this Guacamole.AudioPlayer that all audio up to the current 317 * point in time has been given via the underlying stream, and that any 318 * difference in time between queued audio data and the current time can be 319 * considered latency. 320 */ 321 this.sync = function sync() { 322 // Default implementation - do nothing 323 }; 324 325}; 326 327/** 328 * Determines whether the given mimetype is supported by any built-in 329 * implementation of Guacamole.AudioPlayer, and thus will be properly handled 330 * by Guacamole.AudioPlayer.getInstance(). 331 * 332 * @param {!string} mimetype 333 * The mimetype to check. 334 * 335 * @returns {!boolean} 336 * true if the given mimetype is supported by any built-in 337 * Guacamole.AudioPlayer, false otherwise. 338 */ 339Guacamole.AudioPlayer.isSupportedType = function isSupportedType(mimetype) { 340 341 return Guacamole.RawAudioPlayer.isSupportedType(mimetype); 342 343}; 344 345/** 346 * Returns a list of all mimetypes supported by any built-in 347 * Guacamole.AudioPlayer, in rough order of priority. Beware that only the core 348 * mimetypes themselves will be listed. Any mimetype parameters, even required 349 * ones, will not be included in the list. For example, "audio/L8" is a 350 * supported raw audio mimetype that is supported, but it is invalid without 351 * additional parameters. Something like "audio/L8;rate=44100" would be valid, 352 * however (see https://tools.ietf.org/html/rfc4856). 353 * 354 * @returns {!string[]} 355 * A list of all mimetypes supported by any built-in Guacamole.AudioPlayer, 356 * excluding any parameters. 357 */ 358Guacamole.AudioPlayer.getSupportedTypes = function getSupportedTypes() { 359 360 return Guacamole.RawAudioPlayer.getSupportedTypes(); 361 362}; 363 364/** 365 * Returns an instance of Guacamole.AudioPlayer providing support for the given 366 * audio format. If support for the given audio format is not available, null 367 * is returned. 368 * 369 * @param {!Guacamole.InputStream} stream 370 * The Guacamole.InputStream to read audio data from. 371 * 372 * @param {!string} mimetype 373 * The mimetype of the audio data in the provided stream. 374 * 375 * @return {Guacamole.AudioPlayer} 376 * A Guacamole.AudioPlayer instance supporting the given mimetype and 377 * reading from the given stream, or null if support for the given mimetype 378 * is absent. 379 */ 380Guacamole.AudioPlayer.getInstance = function getInstance(stream, mimetype) { 381 382 // Use raw audio player if possible 383 if (Guacamole.RawAudioPlayer.isSupportedType(mimetype)) 384 return new Guacamole.RawAudioPlayer(stream, mimetype); 385 386 // No support for given mimetype 387 return null; 388 389}; 390 391/** 392 * Implementation of Guacamole.AudioPlayer providing support for raw PCM format 393 * audio. This player relies only on the Web Audio API and does not require any 394 * browser-level support for its audio formats. 395 * 396 * @constructor 397 * @augments Guacamole.AudioPlayer 398 * @param {!Guacamole.InputStream} stream 399 * The Guacamole.InputStream to read audio data from. 400 * 401 * @param {!string} mimetype 402 * The mimetype of the audio data in the provided stream, which must be a 403 * "audio/L8" or "audio/L16" mimetype with necessary parameters, such as: 404 * "audio/L16;rate=44100,channels=2". 405 */ 406Guacamole.RawAudioPlayer = function RawAudioPlayer(stream, mimetype) { 407 408 /** 409 * The format of audio this player will decode. 410 * 411 * @private 412 * @type {Guacamole.RawAudioFormat} 413 */ 414 var format = Guacamole.RawAudioFormat.parse(mimetype); 415 416 /** 417 * An instance of a Web Audio API AudioContext object, or null if the 418 * Web Audio API is not supported. 419 * 420 * @private 421 * @type {AudioContext} 422 */ 423 var context = Guacamole.AudioContextFactory.getAudioContext(); 424 425 /** 426 * The earliest possible time that the next packet could play without 427 * overlapping an already-playing packet, in seconds. Note that while this 428 * value is in seconds, it is not an integer value and has microsecond 429 * resolution. 430 * 431 * @private 432 * @type {!number} 433 */ 434 var nextPacketTime = context.currentTime; 435 436 /** 437 * Guacamole.ArrayBufferReader wrapped around the audio input stream 438 * provided with this Guacamole.RawAudioPlayer was created. 439 * 440 * @private 441 * @type {!Guacamole.ArrayBufferReader} 442 */ 443 var reader = new Guacamole.ArrayBufferReader(stream); 444 445 /** 446 * The minimum size of an audio packet split by splitAudioPacket(), in 447 * seconds. Audio packets smaller than this will not be split, nor will the 448 * split result of a larger packet ever be smaller in size than this 449 * minimum. 450 * 451 * @private 452 * @constant 453 * @type {!number} 454 */ 455 var MIN_SPLIT_SIZE = 0.02; 456 457 /** 458 * The maximum amount of latency to allow between the buffered data stream 459 * and the playback position, in seconds. Initially, this is set to 460 * roughly one third of a second. 461 * 462 * @private 463 * @type {!number} 464 */ 465 var maxLatency = 0.3; 466 467 /** 468 * The type of typed array that will be used to represent each audio packet 469 * internally. This will be either Int8Array or Int16Array, depending on 470 * whether the raw audio format is 8-bit or 16-bit. 471 * 472 * @private 473 * @constructor 474 */ 475 var SampleArray = (format.bytesPerSample === 1) ? window.Int8Array : window.Int16Array; 476 477 /** 478 * The maximum absolute value of any sample within a raw audio packet 479 * received by this audio player. This depends only on the size of each 480 * sample, and will be 128 for 8-bit audio and 32768 for 16-bit audio. 481 * 482 * @private 483 * @type {!number} 484 */ 485 var maxSampleValue = (format.bytesPerSample === 1) ? 128 : 32768; 486 487 /** 488 * The queue of all pending audio packets, as an array of sample arrays. 489 * Audio packets which are pending playback will be added to this queue for 490 * further manipulation prior to scheduling via the Web Audio API. Once an 491 * audio packet leaves this queue and is scheduled via the Web Audio API, 492 * no further modifications can be made to that packet. 493 * 494 * @private 495 * @type {!SampleArray[]} 496 */ 497 var packetQueue = []; 498 499 /** 500 * Given an array of audio packets, returns a single audio packet 501 * containing the concatenation of those packets. 502 * 503 * @private 504 * @param {!SampleArray[]} packets 505 * The array of audio packets to concatenate. 506 * 507 * @returns {SampleArray} 508 * A single audio packet containing the concatenation of all given 509 * audio packets. If no packets are provided, this will be undefined. 510 */ 511 var joinAudioPackets = function joinAudioPackets(packets) { 512 513 // Do not bother joining if one or fewer packets are in the queue 514 if (packets.length <= 1) 515 return packets[0]; 516 517 // Determine total sample length of the entire queue 518 var totalLength = 0; 519 packets.forEach(function addPacketLengths(packet) { 520 totalLength += packet.length; 521 }); 522 523 // Append each packet within queue 524 var offset = 0; 525 var joined = new SampleArray(totalLength); 526 packets.forEach(function appendPacket(packet) { 527 joined.set(packet, offset); 528 offset += packet.length; 529 }); 530 531 return joined; 532 533 }; 534 535 /** 536 * Given a single packet of audio data, splits off an arbitrary length of 537 * audio data from the beginning of that packet, returning the split result 538 * as an array of two packets. The split location is determined through an 539 * algorithm intended to minimize the liklihood of audible clicking between 540 * packets. If no such split location is possible, an array containing only 541 * the originally-provided audio packet is returned. 542 * 543 * @private 544 * @param {!SampleArray} data 545 * The audio packet to split. 546 * 547 * @returns {!SampleArray[]} 548 * An array of audio packets containing the result of splitting the 549 * provided audio packet. If splitting is possible, this array will 550 * contain two packets. If splitting is not possible, this array will 551 * contain only the originally-provided packet. 552 */ 553 var splitAudioPacket = function splitAudioPacket(data) { 554 555 var minValue = Number.MAX_VALUE; 556 var optimalSplitLength = data.length; 557 558 // Calculate number of whole samples in the provided audio packet AND 559 // in the minimum possible split packet 560 var samples = Math.floor(data.length / format.channels); 561 var minSplitSamples = Math.floor(format.rate * MIN_SPLIT_SIZE); 562 563 // Calculate the beginning of the "end" of the audio packet 564 var start = Math.max( 565 format.channels * minSplitSamples, 566 format.channels * (samples - minSplitSamples) 567 ); 568 569 // For all samples at the end of the given packet, find a point where 570 // the perceptible volume across all channels is lowest (and thus is 571 // the optimal point to split) 572 for (var offset = start; offset < data.length; offset += format.channels) { 573 574 // Calculate the sum of all values across all channels (the result 575 // will be proportional to the average volume of a sample) 576 var totalValue = 0; 577 for (var channel = 0; channel < format.channels; channel++) { 578 totalValue += Math.abs(data[offset + channel]); 579 } 580 581 // If this is the smallest average value thus far, set the split 582 // length such that the first packet ends with the current sample 583 if (totalValue <= minValue) { 584 optimalSplitLength = offset + format.channels; 585 minValue = totalValue; 586 } 587 588 } 589 590 // If packet is not split, return the supplied packet untouched 591 if (optimalSplitLength === data.length) 592 return [data]; 593 594 // Otherwise, split the packet into two new packets according to the 595 // calculated optimal split length 596 return [ 597 new SampleArray(data.buffer.slice(0, optimalSplitLength * format.bytesPerSample)), 598 new SampleArray(data.buffer.slice(optimalSplitLength * format.bytesPerSample)) 599 ]; 600 601 }; 602 603 /** 604 * Pushes the given packet of audio data onto the playback queue. Unlike 605 * other private functions within Guacamole.RawAudioPlayer, the type of the 606 * ArrayBuffer packet of audio data here need not be specific to the type 607 * of audio (as with SampleArray). The ArrayBuffer type provided by a 608 * Guacamole.ArrayBufferReader, for example, is sufficient. Any necessary 609 * conversions will be performed automatically internally. 610 * 611 * @private 612 * @param {!ArrayBuffer} data 613 * A raw packet of audio data that should be pushed onto the audio 614 * playback queue. 615 */ 616 var pushAudioPacket = function pushAudioPacket(data) { 617 packetQueue.push(new SampleArray(data)); 618 }; 619 620 /** 621 * Shifts off and returns a packet of audio data from the beginning of the 622 * playback queue. The length of this audio packet is determined 623 * dynamically according to the click-reduction algorithm implemented by 624 * splitAudioPacket(). 625 * 626 * @private 627 * @returns {SampleArray} 628 * A packet of audio data pulled from the beginning of the playback 629 * queue. If there is no audio currently in the playback queue, this 630 * will be null. 631 */ 632 var shiftAudioPacket = function shiftAudioPacket() { 633 634 // Flatten data in packet queue 635 var data = joinAudioPackets(packetQueue); 636 if (!data) 637 return null; 638 639 // Pull an appropriate amount of data from the front of the queue 640 packetQueue = splitAudioPacket(data); 641 data = packetQueue.shift(); 642 643 return data; 644 645 }; 646 647 /** 648 * Converts the given audio packet into an AudioBuffer, ready for playback 649 * by the Web Audio API. Unlike the raw audio packets received by this 650 * audio player, AudioBuffers require floating point samples and are split 651 * into isolated planes of channel-specific data. 652 * 653 * @private 654 * @param {!SampleArray} data 655 * The raw audio packet that should be converted into a Web Audio API 656 * AudioBuffer. 657 * 658 * @returns {!AudioBuffer} 659 * A new Web Audio API AudioBuffer containing the provided audio data, 660 * converted to the format used by the Web Audio API. 661 */ 662 var toAudioBuffer = function toAudioBuffer(data) { 663 664 // Calculate total number of samples 665 var samples = data.length / format.channels; 666 667 // Determine exactly when packet CAN play 668 var packetTime = context.currentTime; 669 if (nextPacketTime < packetTime) 670 nextPacketTime = packetTime; 671 672 // Get audio buffer for specified format 673 var audioBuffer = context.createBuffer(format.channels, samples, format.rate); 674 675 // Convert each channel 676 for (var channel = 0; channel < format.channels; channel++) { 677 678 var audioData = audioBuffer.getChannelData(channel); 679 680 // Fill audio buffer with data for channel 681 var offset = channel; 682 for (var i = 0; i < samples; i++) { 683 audioData[i] = data[offset] / maxSampleValue; 684 offset += format.channels; 685 } 686 687 } 688 689 return audioBuffer; 690 691 }; 692 693 // Defer playback of received audio packets slightly 694 reader.ondata = function playReceivedAudio(data) { 695 696 // Push received samples onto queue 697 pushAudioPacket(new SampleArray(data)); 698 699 // Shift off an arbitrary packet of audio data from the queue (this may 700 // be different in size from the packet just pushed) 701 var packet = shiftAudioPacket(); 702 if (!packet) 703 return; 704 705 // Determine exactly when packet CAN play 706 var packetTime = context.currentTime; 707 if (nextPacketTime < packetTime) 708 nextPacketTime = packetTime; 709 710 // Set up buffer source 711 var source = context.createBufferSource(); 712 source.connect(context.destination); 713 714 // Use noteOn() instead of start() if necessary 715 if (!source.start) 716 source.start = source.noteOn; 717 718 // Schedule packet 719 source.buffer = toAudioBuffer(packet); 720 source.start(nextPacketTime); 721 722 // Update timeline by duration of scheduled packet 723 nextPacketTime += packet.length / format.channels / format.rate; 724 725 }; 726 727 /** @override */ 728 this.sync = function sync() { 729 730 // Calculate elapsed time since last sync 731 var now = context.currentTime; 732 733 // Reschedule future playback time such that playback latency is 734 // bounded within a reasonable latency threshold 735 nextPacketTime = Math.min(nextPacketTime, now + maxLatency); 736 737 }; 738 739}; 740 741Guacamole.RawAudioPlayer.prototype = new Guacamole.AudioPlayer(); 742 743/** 744 * Determines whether the given mimetype is supported by 745 * Guacamole.RawAudioPlayer. 746 * 747 * @param {!string} mimetype 748 * The mimetype to check. 749 * 750 * @returns {!boolean} 751 * true if the given mimetype is supported by Guacamole.RawAudioPlayer, 752 * false otherwise. 753 */ 754Guacamole.RawAudioPlayer.isSupportedType = function isSupportedType(mimetype) { 755 756 // No supported types if no Web Audio API 757 if (!Guacamole.AudioContextFactory.getAudioContext()) 758 return false; 759 760 return Guacamole.RawAudioFormat.parse(mimetype) !== null; 761 762}; 763 764/** 765 * Returns a list of all mimetypes supported by Guacamole.RawAudioPlayer. Only 766 * the core mimetypes themselves will be listed. Any mimetype parameters, even 767 * required ones, will not be included in the list. For example, "audio/L8" is 768 * a raw audio mimetype that may be supported, but it is invalid without 769 * additional parameters. Something like "audio/L8;rate=44100" would be valid, 770 * however (see https://tools.ietf.org/html/rfc4856). 771 * 772 * @returns {!string[]} 773 * A list of all mimetypes supported by Guacamole.RawAudioPlayer, excluding 774 * any parameters. If the necessary JavaScript APIs for playing raw audio 775 * are absent, this list will be empty. 776 */ 777Guacamole.RawAudioPlayer.getSupportedTypes = function getSupportedTypes() { 778 779 // No supported types if no Web Audio API 780 if (!Guacamole.AudioContextFactory.getAudioContext()) 781 return []; 782 783 // We support 8-bit and 16-bit raw PCM 784 return [ 785 'audio/L8', 786 'audio/L16' 787 ]; 788 789}; 790/* 791 * Licensed to the Apache Software Foundation (ASF) under one 792 * or more contributor license agreements. See the NOTICE file 793 * distributed with this work for additional information 794 * regarding copyright ownership. The ASF licenses this file 795 * to you under the Apache License, Version 2.0 (the 796 * "License"); you may not use this file except in compliance 797 * with the License. You may obtain a copy of the License at 798 * 799 * http://www.apache.org/licenses/LICENSE-2.0 800 * 801 * Unless required by applicable law or agreed to in writing, 802 * software distributed under the License is distributed on an 803 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 804 * KIND, either express or implied. See the License for the 805 * specific language governing permissions and limitations 806 * under the License. 807 */ 808 809var Guacamole = Guacamole || {}; 810 811/** 812 * Abstract audio recorder which streams arbitrary audio data to an underlying 813 * Guacamole.OutputStream. It is up to implementations of this class to provide 814 * some means of handling this Guacamole.OutputStream. Data produced by the 815 * recorder is to be sent along the provided stream immediately. 816 * 817 * @constructor 818 */ 819Guacamole.AudioRecorder = function AudioRecorder() { 820 821 /** 822 * Callback which is invoked when the audio recording process has stopped 823 * and the underlying Guacamole stream has been closed normally. Audio will 824 * only resume recording if a new Guacamole.AudioRecorder is started. This 825 * Guacamole.AudioRecorder instance MAY NOT be reused. 826 * 827 * @event 828 */ 829 this.onclose = null; 830 831 /** 832 * Callback which is invoked when the audio recording process cannot 833 * continue due to an error, if it has started at all. The underlying 834 * Guacamole stream is automatically closed. Future attempts to record 835 * audio should not be made, and this Guacamole.AudioRecorder instance 836 * MAY NOT be reused. 837 * 838 * @event 839 */ 840 this.onerror = null; 841 842}; 843 844/** 845 * Determines whether the given mimetype is supported by any built-in 846 * implementation of Guacamole.AudioRecorder, and thus will be properly handled 847 * by Guacamole.AudioRecorder.getInstance(). 848 * 849 * @param {!string} mimetype 850 * The mimetype to check. 851 * 852 * @returns {!boolean} 853 * true if the given mimetype is supported by any built-in 854 * Guacamole.AudioRecorder, false otherwise. 855 */ 856Guacamole.AudioRecorder.isSupportedType = function isSupportedType(mimetype) { 857 858 return Guacamole.RawAudioRecorder.isSupportedType(mimetype); 859 860}; 861 862/** 863 * Returns a list of all mimetypes supported by any built-in 864 * Guacamole.AudioRecorder, in rough order of priority. Beware that only the 865 * core mimetypes themselves will be listed. Any mimetype parameters, even 866 * required ones, will not be included in the list. For example, "audio/L8" is 867 * a supported raw audio mimetype that is supported, but it is invalid without 868 * additional parameters. Something like "audio/L8;rate=44100" would be valid, 869 * however (see https://tools.ietf.org/html/rfc4856). 870 * 871 * @returns {!string[]} 872 * A list of all mimetypes supported by any built-in 873 * Guacamole.AudioRecorder, excluding any parameters. 874 */ 875Guacamole.AudioRecorder.getSupportedTypes = function getSupportedTypes() { 876 877 return Guacamole.RawAudioRecorder.getSupportedTypes(); 878 879}; 880 881/** 882 * Returns an instance of Guacamole.AudioRecorder providing support for the 883 * given audio format. If support for the given audio format is not available, 884 * null is returned. 885 * 886 * @param {!Guacamole.OutputStream} stream 887 * The Guacamole.OutputStream to send audio data through. 888 * 889 * @param {!string} mimetype 890 * The mimetype of the audio data to be sent along the provided stream. 891 * 892 * @return {Guacamole.AudioRecorder} 893 * A Guacamole.AudioRecorder instance supporting the given mimetype and 894 * writing to the given stream, or null if support for the given mimetype 895 * is absent. 896 */ 897Guacamole.AudioRecorder.getInstance = function getInstance(stream, mimetype) { 898 899 // Use raw audio recorder if possible 900 if (Guacamole.RawAudioRecorder.isSupportedType(mimetype)) 901 return new Guacamole.RawAudioRecorder(stream, mimetype); 902 903 // No support for given mimetype 904 return null; 905 906}; 907 908/** 909 * Implementation of Guacamole.AudioRecorder providing support for raw PCM 910 * format audio. This recorder relies only on the Web Audio API and does not 911 * require any browser-level support for its audio formats. 912 * 913 * @constructor 914 * @augments Guacamole.AudioRecorder 915 * @param {!Guacamole.OutputStream} stream 916 * The Guacamole.OutputStream to write audio data to. 917 * 918 * @param {!string} mimetype 919 * The mimetype of the audio data to send along the provided stream, which 920 * must be a "audio/L8" or "audio/L16" mimetype with necessary parameters, 921 * such as: "audio/L16;rate=44100,channels=2". 922 */ 923Guacamole.RawAudioRecorder = function RawAudioRecorder(stream, mimetype) { 924 925 /** 926 * Reference to this RawAudioRecorder. 927 * 928 * @private 929 * @type {!Guacamole.RawAudioRecorder} 930 */ 931 var recorder = this; 932 933 /** 934 * The size of audio buffer to request from the Web Audio API when 935 * recording or processing audio, in sample-frames. This must be a power of 936 * two between 256 and 16384 inclusive, as required by 937 * AudioContext.createScriptProcessor(). 938 * 939 * @private 940 * @constant 941 * @type {!number} 942 */ 943 var BUFFER_SIZE = 2048; 944 945 /** 946 * The window size to use when applying Lanczos interpolation, commonly 947 * denoted by the variable "a". 948 * See: https://en.wikipedia.org/wiki/Lanczos_resampling 949 * 950 * @private 951 * @contant 952 * @type {!number} 953 */ 954 var LANCZOS_WINDOW_SIZE = 3; 955 956 /** 957 * The format of audio this recorder will encode. 958 * 959 * @private 960 * @type {Guacamole.RawAudioFormat} 961 */ 962 var format = Guacamole.RawAudioFormat.parse(mimetype); 963 964 /** 965 * An instance of a Web Audio API AudioContext object, or null if the 966 * Web Audio API is not supported. 967 * 968 * @private 969 * @type {AudioContext} 970 */ 971 var context = Guacamole.AudioContextFactory.getAudioContext(); 972 973 // Some browsers do not implement navigator.mediaDevices - this 974 // shims in this functionality to ensure code compatibility. 975 if (!navigator.mediaDevices) 976 navigator.mediaDevices = {}; 977 978 // Browsers that either do not implement navigator.mediaDevices 979 // at all or do not implement it completely need the getUserMedia 980 // method defined. This shims in this function by detecting 981 // one of the supported legacy methods. 982 if (!navigator.mediaDevices.getUserMedia) 983 navigator.mediaDevices.getUserMedia = (navigator.getUserMedia 984 || navigator.webkitGetUserMedia 985 || navigator.mozGetUserMedia 986 || navigator.msGetUserMedia).bind(navigator); 987 988 /** 989 * Guacamole.ArrayBufferWriter wrapped around the audio output stream 990 * provided when this Guacamole.RawAudioRecorder was created. 991 * 992 * @private 993 * @type {!Guacamole.ArrayBufferWriter} 994 */ 995 var writer = new Guacamole.ArrayBufferWriter(stream); 996 997 /** 998 * The type of typed array that will be used to represent each audio packet 999 * internally. This will be either Int8Array or Int16Array, depending on 1000 * whether the raw audio format is 8-bit or 16-bit. 1001 * 1002 * @private 1003 * @constructor 1004 */ 1005 var SampleArray = (format.bytesPerSample === 1) ? window.Int8Array : window.Int16Array; 1006 1007 /** 1008 * The maximum absolute value of any sample within a raw audio packet sent 1009 * by this audio recorder. This depends only on the size of each sample, 1010 * and will be 128 for 8-bit audio and 32768 for 16-bit audio. 1011 * 1012 * @private 1013 * @type {!number} 1014 */ 1015 var maxSampleValue = (format.bytesPerSample === 1) ? 128 : 32768; 1016 1017 /** 1018 * The total number of audio samples read from the local audio input device 1019 * over the life of this audio recorder. 1020 * 1021 * @private 1022 * @type {!number} 1023 */ 1024 var readSamples = 0; 1025 1026 /** 1027 * The total number of audio samples written to the underlying Guacamole 1028 * connection over the life of this audio recorder. 1029 * 1030 * @private 1031 * @type {!number} 1032 */ 1033 var writtenSamples = 0; 1034 1035 /** 1036 * The audio stream provided by the browser, if allowed. If no stream has 1037 * yet been received, this will be null. 1038 * 1039 * @private 1040 * @type {MediaStream} 1041 */ 1042 var mediaStream = null; 1043 1044 /** 1045 * The source node providing access to the local audio input device. 1046 * 1047 * @private 1048 * @type {MediaStreamAudioSourceNode} 1049 */ 1050 var source = null; 1051 1052 /** 1053 * The script processing node which receives audio input from the media 1054 * stream source node as individual audio buffers. 1055 * 1056 * @private 1057 * @type {ScriptProcessorNode} 1058 */ 1059 var processor = null; 1060 1061 /** 1062 * The normalized sinc function. The normalized sinc function is defined as 1063 * 1 for x=0 and sin(PI * x) / (PI * x) for all other values of x. 1064 * 1065 * See: https://en.wikipedia.org/wiki/Sinc_function 1066 * 1067 * @private 1068 * @param {!number} x 1069 * The point at which the normalized sinc function should be computed. 1070 * 1071 * @returns {!number} 1072 * The value of the normalized sinc function at x. 1073 */ 1074 var sinc = function sinc(x) { 1075 1076 // The value of sinc(0) is defined as 1 1077 if (x === 0) 1078 return 1; 1079 1080 // Otherwise, normlized sinc(x) is sin(PI * x) / (PI * x) 1081 var piX = Math.PI * x; 1082 return Math.sin(piX) / piX; 1083 1084 }; 1085 1086 /** 1087 * Calculates the value of the Lanczos kernal at point x for a given window 1088 * size. See: https://en.wikipedia.org/wiki/Lanczos_resampling 1089 * 1090 * @private 1091 * @param {!number} x 1092 * The point at which the value of the Lanczos kernel should be 1093 * computed. 1094 * 1095 * @param {!number} a 1096 * The window size to use for the Lanczos kernel. 1097 * 1098 * @returns {!number} 1099 * The value of the Lanczos kernel at the given point for the given 1100 * window size. 1101 */ 1102 var lanczos = function lanczos(x, a) { 1103 1104 // Lanczos is sinc(x) * sinc(x / a) for -a < x < a ... 1105 if (-a < x && x < a) 1106 return sinc(x) * sinc(x / a); 1107 1108 // ... and 0 otherwise 1109 return 0; 1110 1111 }; 1112 1113 /** 1114 * Determines the value of the waveform represented by the audio data at 1115 * the given location. If the value cannot be determined exactly as it does 1116 * not correspond to an exact sample within the audio data, the value will 1117 * be derived through interpolating nearby samples. 1118 * 1119 * @private 1120 * @param {!Float32Array} audioData 1121 * An array of audio data, as returned by AudioBuffer.getChannelData(). 1122 * 1123 * @param {!number} t 1124 * The relative location within the waveform from which the value 1125 * should be retrieved, represented as a floating point number between 1126 * 0 and 1 inclusive, where 0 represents the earliest point in time and 1127 * 1 represents the latest. 1128 * 1129 * @returns {!number} 1130 * The value of the waveform at the given location. 1131 */ 1132 var interpolateSample = function getValueAt(audioData, t) { 1133 1134 // Convert [0, 1] range to [0, audioData.length - 1] 1135 var index = (audioData.length - 1) * t; 1136 1137 // Determine the start and end points for the summation used by the 1138 // Lanczos interpolation algorithm (see: https://en.wikipedia.org/wiki/Lanczos_resampling) 1139 var start = Math.floor(index) - LANCZOS_WINDOW_SIZE + 1; 1140 var end = Math.floor(index) + LANCZOS_WINDOW_SIZE; 1141 1142 // Calculate the value of the Lanczos interpolation function for the 1143 // required range 1144 var sum = 0; 1145 for (var i = start; i <= end; i++) { 1146 sum += (audioData[i] || 0) * lanczos(index - i, LANCZOS_WINDOW_SIZE); 1147 } 1148 1149 return sum; 1150 1151 }; 1152 1153 /** 1154 * Converts the given AudioBuffer into an audio packet, ready for streaming 1155 * along the underlying output stream. Unlike the raw audio packets used by 1156 * this audio recorder, AudioBuffers require floating point samples and are 1157 * split into isolated planes of channel-specific data. 1158 * 1159 * @private 1160 * @param {!AudioBuffer} audioBuffer 1161 * The Web Audio API AudioBuffer that should be converted to a raw 1162 * audio packet. 1163 * 1164 * @returns {!SampleArray} 1165 * A new raw audio packet containing the audio data from the provided 1166 * AudioBuffer. 1167 */ 1168 var toSampleArray = function toSampleArray(audioBuffer) { 1169 1170 // Track overall amount of data read 1171 var inSamples = audioBuffer.length; 1172 readSamples += inSamples; 1173 1174 // Calculate the total number of samples that should be written as of 1175 // the audio data just received and adjust the size of the output 1176 // packet accordingly 1177 var expectedWrittenSamples = Math.round(readSamples * format.rate / audioBuffer.sampleRate); 1178 var outSamples = expectedWrittenSamples - writtenSamples; 1179 1180 // Update number of samples written 1181 writtenSamples += outSamples; 1182 1183 // Get array for raw PCM storage 1184 var data = new SampleArray(outSamples * format.channels); 1185 1186 // Convert each channel 1187 for (var channel = 0; channel < format.channels; channel++) { 1188 1189 var audioData = audioBuffer.getChannelData(channel); 1190 1191 // Fill array with data from audio buffer channel 1192 var offset = channel; 1193 for (var i = 0; i < outSamples; i++) { 1194 data[offset] = interpolateSample(audioData, i / (outSamples - 1)) * maxSampleValue; 1195 offset += format.channels; 1196 } 1197 1198 } 1199 1200 return data; 1201 1202 }; 1203 1204 /** 1205 * getUserMedia() callback which handles successful retrieval of an 1206 * audio stream (successful start of recording). 1207 * 1208 * @private 1209 * @param {!MediaStream} stream 1210 * A MediaStream which provides access to audio data read from the 1211 * user's local audio input device. 1212 */ 1213 var streamReceived = function streamReceived(stream) { 1214 1215 // Create processing node which receives appropriately-sized audio buffers 1216 processor = context.createScriptProcessor(BUFFER_SIZE, format.channels, format.channels); 1217 processor.connect(context.destination); 1218 1219 // Send blobs when audio buffers are received 1220 processor.onaudioprocess = function processAudio(e) { 1221 writer.sendData(toSampleArray(e.inputBuffer).buffer); 1222 }; 1223 1224 // Connect processing node to user's audio input source 1225 source = context.createMediaStreamSource(stream); 1226 source.connect(processor); 1227 1228 // Attempt to explicitly resume AudioContext, as it may be paused 1229 // by default 1230 if (context.state === 'suspended') 1231 context.resume(); 1232 1233 // Save stream for later cleanup 1234 mediaStream = stream; 1235 1236 }; 1237 1238 /** 1239 * getUserMedia() callback which handles audio recording denial. The 1240 * underlying Guacamole output stream is closed, and the failure to 1241 * record is noted using onerror. 1242 * 1243 * @private 1244 */ 1245 var streamDenied = function streamDenied() { 1246 1247 // Simply end stream if audio access is not allowed 1248 writer.sendEnd(); 1249 1250 // Notify of closure 1251 if (recorder.onerror) 1252 recorder.onerror(); 1253 1254 }; 1255 1256 /** 1257 * Requests access to the user's microphone and begins capturing audio. All 1258 * received audio data is resampled as necessary and forwarded to the 1259 * Guacamole stream underlying this Guacamole.RawAudioRecorder. This 1260 * function must be invoked ONLY ONCE per instance of 1261 * Guacamole.RawAudioRecorder. 1262 * 1263 * @private 1264 */ 1265 var beginAudioCapture = function beginAudioCapture() { 1266 1267 // Attempt to retrieve an audio input stream from the browser 1268 var promise = navigator.mediaDevices.getUserMedia({ 1269 'audio' : true 1270 }, streamReceived, streamDenied); 1271 1272 // Handle stream creation/rejection via Promise for newer versions of 1273 // getUserMedia() 1274 if (promise && promise.then) 1275 promise.then(streamReceived, streamDenied); 1276 1277 }; 1278 1279 /** 1280 * Stops capturing audio, if the capture has started, freeing all associated 1281 * resources. If the capture has not started, this function simply ends the 1282 * underlying Guacamole stream. 1283 * 1284 * @private 1285 */ 1286 var stopAudioCapture = function stopAudioCapture() { 1287 1288 // Disconnect media source node from script processor 1289 if (source) 1290 source.disconnect(); 1291 1292 // Disconnect associated script processor node 1293 if (processor) 1294 processor.disconnect(); 1295 1296 // Stop capture 1297 if (mediaStream) { 1298 var tracks = mediaStream.getTracks(); 1299 for (var i = 0; i < tracks.length; i++) 1300 tracks[i].stop(); 1301 } 1302 1303 // Remove references to now-unneeded components 1304 processor = null; 1305 source = null; 1306 mediaStream = null; 1307 1308 // End stream 1309 writer.sendEnd(); 1310 1311 }; 1312 1313 // Once audio stream is successfully open, request and begin reading audio 1314 writer.onack = function audioStreamAcknowledged(status) { 1315 1316 // Begin capture if successful response and not yet started 1317 if (status.code === Guacamole.Status.Code.SUCCESS && !mediaStream) 1318 beginAudioCapture(); 1319 1320 // Otherwise stop capture and cease handling any further acks 1321 else { 1322 1323 // Stop capturing audio 1324 stopAudioCapture(); 1325 writer.onack = null; 1326 1327 // Notify if stream has closed normally 1328 if (status.code === Guacamole.Status.Code.RESOURCE_CLOSED) { 1329 if (recorder.onclose) 1330 recorder.onclose(); 1331 } 1332 1333 // Otherwise notify of closure due to error 1334 else { 1335 if (recorder.onerror) 1336 recorder.onerror(); 1337 } 1338 1339 } 1340 1341 }; 1342 1343}; 1344 1345Guacamole.RawAudioRecorder.prototype = new Guacamole.AudioRecorder(); 1346 1347/** 1348 * Determines whether the given mimetype is supported by 1349 * Guacamole.RawAudioRecorder. 1350 * 1351 * @param {!string} mimetype 1352 * The mimetype to check. 1353 * 1354 * @returns {!boolean} 1355 * true if the given mimetype is supported by Guacamole.RawAudioRecorder, 1356 * false otherwise. 1357 */ 1358Guacamole.RawAudioRecorder.isSupportedType = function isSupportedType(mimetype) { 1359 1360 // No supported types if no Web Audio API 1361 if (!Guacamole.AudioContextFactory.getAudioContext()) 1362 return false; 1363 1364 return Guacamole.RawAudioFormat.parse(mimetype) !== null; 1365 1366}; 1367 1368/** 1369 * Returns a list of all mimetypes supported by Guacamole.RawAudioRecorder. Only 1370 * the core mimetypes themselves will be listed. Any mimetype parameters, even 1371 * required ones, will not be included in the list. For example, "audio/L8" is 1372 * a raw audio mimetype that may be supported, but it is invalid without 1373 * additional parameters. Something like "audio/L8;rate=44100" would be valid, 1374 * however (see https://tools.ietf.org/html/rfc4856). 1375 * 1376 * @returns {!string[]} 1377 * A list of all mimetypes supported by Guacamole.RawAudioRecorder, 1378 * excluding any parameters. If the necessary JavaScript APIs for recording 1379 * raw audio are absent, this list will be empty. 1380 */ 1381Guacamole.RawAudioRecorder.getSupportedTypes = function getSupportedTypes() { 1382 1383 // No supported types if no Web Audio API 1384 if (!Guacamole.AudioContextFactory.getAudioContext()) 1385 return []; 1386 1387 // We support 8-bit and 16-bit raw PCM 1388 return [ 1389 'audio/L8', 1390 'audio/L16' 1391 ]; 1392 1393}; 1394/* 1395 * Licensed to the Apache Software Foundation (ASF) under one 1396 * or more contributor license agreements. See the NOTICE file 1397 * distributed with this work for additional information 1398 * regarding copyright ownership. The ASF licenses this file 1399 * to you under the Apache License, Version 2.0 (the 1400 * "License"); you may not use this file except in compliance 1401 * with the License. You may obtain a copy of the License at 1402 * 1403 * http://www.apache.org/licenses/LICENSE-2.0 1404 * 1405 * Unless required by applicable law or agreed to in writing, 1406 * software distributed under the License is distributed on an 1407 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 1408 * KIND, either express or implied. See the License for the 1409 * specific language governing permissions and limitations 1410 * under the License. 1411 */ 1412 1413var Guacamole = Guacamole || {}; 1414 1415/** 1416 * A reader which automatically handles the given input stream, assembling all 1417 * received blobs into a single blob by appending them to each other in order. 1418 * Note that this object will overwrite any installed event handlers on the 1419 * given Guacamole.InputStream. 1420 * 1421 * @constructor 1422 * @param {!Guacamole.InputStream} stream 1423 * The stream that data will be read from. 1424 * 1425 * @param {!string} mimetype 1426 * The mimetype of the blob being built. 1427 */ 1428Guacamole.BlobReader = function(stream, mimetype) { 1429 1430 /** 1431 * Reference to this Guacamole.InputStream. 1432 * 1433 * @private 1434 * @type {!Guacamole.BlobReader} 1435 */ 1436 var guac_reader = this; 1437 1438 /** 1439 * The length of this Guacamole.InputStream in bytes. 1440 * 1441 * @private 1442 * @type {!number} 1443 */ 1444 var length = 0; 1445 1446 // Get blob builder 1447 var blob_builder; 1448 if (window.BlobBuilder) blob_builder = new BlobBuilder(); 1449 else if (window.WebKitBlobBuilder) blob_builder = new WebKitBlobBuilder(); 1450 else if (window.MozBlobBuilder) blob_builder = new MozBlobBuilder(); 1451 else 1452 blob_builder = new (function() { 1453 1454 var blobs = []; 1455 1456 /** @ignore */ 1457 this.append = function(data) { 1458 blobs.push(new Blob([data], {"type": mimetype})); 1459 }; 1460 1461 /** @ignore */ 1462 this.getBlob = function() { 1463 return new Blob(blobs, {"type": mimetype}); 1464 }; 1465 1466 })(); 1467 1468 // Append received blobs 1469 stream.onblob = function(data) { 1470 1471 // Convert to ArrayBuffer 1472 var binary = window.atob(data); 1473 var arrayBuffer = new ArrayBuffer(binary.length); 1474 var bufferView = new Uint8Array(arrayBuffer); 1475 1476 for (var i=0; i<binary.length; i++) 1477 bufferView[i] = binary.charCodeAt(i); 1478 1479 blob_builder.append(arrayBuffer); 1480 length += arrayBuffer.byteLength; 1481 1482 // Call handler, if present 1483 if (guac_reader.onprogress) 1484 guac_reader.onprogress(arrayBuffer.byteLength); 1485 1486 // Send success response 1487 stream.sendAck("OK", 0x0000); 1488 1489 }; 1490 1491 // Simply call onend when end received 1492 stream.onend = function() { 1493 if (guac_reader.onend) 1494 guac_reader.onend(); 1495 }; 1496 1497 /** 1498 * Returns the current length of this Guacamole.InputStream, in bytes. 1499 * 1500 * @return {!number} 1501 * The current length of this Guacamole.InputStream. 1502 */ 1503 this.getLength = function() { 1504 return length; 1505 }; 1506 1507 /** 1508 * Returns the contents of this Guacamole.BlobReader as a Blob. 1509 * 1510 * @return {!Blob} 1511 * The contents of this Guacamole.BlobReader. 1512 */ 1513 this.getBlob = function() { 1514 return blob_builder.getBlob(); 1515 }; 1516 1517 /** 1518 * Fired once for every blob of data received. 1519 * 1520 * @event 1521 * @param {!number} length 1522 * The number of bytes received. 1523 */ 1524 this.onprogress = null; 1525 1526 /** 1527 * Fired once this stream is finished and no further data will be written. 1528 * @event 1529 */ 1530 this.onend = null; 1531 1532};/* 1533 * Licensed to the Apache Software Foundation (ASF) under one 1534 * or more contributor license agreements. See the NOTICE file 1535 * distributed with this work for additional information 1536 * regarding copyright ownership. The ASF licenses this file 1537 * to you under the Apache License, Version 2.0 (the 1538 * "License"); you may not use this file except in compliance 1539 * with the License. You may obtain a copy of the License at 1540 * 1541 * http://www.apache.org/licenses/LICENSE-2.0 1542 * 1543 * Unless required by applicable law or agreed to in writing, 1544 * software distributed under the License is distributed on an 1545 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 1546 * KIND, either express or implied. See the License for the 1547 * specific language governing permissions and limitations 1548 * under the License. 1549 */ 1550 1551var Guacamole = Guacamole || {}; 1552 1553/** 1554 * A writer which automatically writes to the given output stream with the 1555 * contents of provided Blob objects. 1556 * 1557 * @constructor 1558 * @param {!Guacamole.OutputStream} stream 1559 * The stream that data will be written to. 1560 */ 1561Guacamole.BlobWriter = function BlobWriter(stream) { 1562 1563 /** 1564 * Reference to this Guacamole.BlobWriter. 1565 * 1566 * @private 1567 * @type {!Guacamole.BlobWriter} 1568 */ 1569 var guacWriter = this; 1570 1571 /** 1572 * Wrapped Guacamole.ArrayBufferWriter which will be used to send any 1573 * provided file data. 1574 * 1575 * @private 1576 * @type {!Guacamole.ArrayBufferWriter} 1577 */ 1578 var arrayBufferWriter = new Guacamole.ArrayBufferWriter(stream); 1579 1580 // Initially, simply call onack for acknowledgements 1581 arrayBufferWriter.onack = function(status) { 1582 if (guacWriter.onack) 1583 guacWriter.onack(status); 1584 }; 1585 1586 /** 1587 * Browser-independent implementation of Blob.slice() which uses an end 1588 * offset to determine the span of the resulting slice, rather than a 1589 * length. 1590 * 1591 * @private 1592 * @param {!Blob} blob 1593 * The Blob to slice. 1594 * 1595 * @param {!number} start 1596 * The starting offset of the slice, in bytes, inclusive. 1597 * 1598 * @param {!number} end 1599 * The ending offset of the slice, in bytes, exclusive. 1600 * 1601 * @returns {!Blob} 1602 * A Blob containing the data within the given Blob starting at 1603 * <code>start</code> and ending at <code>end - 1</code>. 1604 */ 1605 var slice = function slice(blob, start, end) { 1606 1607 // Use prefixed implementations if necessary 1608 var sliceImplementation = ( 1609 blob.slice 1610 || blob.webkitSlice 1611 || blob.mozSlice 1612 ).bind(blob); 1613 1614 var length = end - start; 1615 1616 // The old Blob.slice() was length-based (not end-based). Try the 1617 // length version first, if the two calls are not equivalent. 1618 if (length !== end) { 1619 1620 // If the result of the slice() call matches the expected length, 1621 // trust that result. It must be correct. 1622 var sliceResult = sliceImplementation(start, length); 1623 if (sliceResult.size === length) 1624 return sliceResult; 1625 1626 } 1627 1628 // Otherwise, use the most-recent standard: end-based slice() 1629 return sliceImplementation(start, end); 1630 1631 }; 1632 1633 /** 1634 * Sends the contents of the given blob over the underlying stream. 1635 * 1636 * @param {!Blob} blob 1637 * The blob to send. 1638 */ 1639 this.sendBlob = function sendBlob(blob) { 1640 1641 var offset = 0; 1642 var reader = new FileReader(); 1643 1644 /** 1645 * Reads the next chunk of the blob provided to 1646 * [sendBlob()]{@link Guacamole.BlobWriter#sendBlob}. The chunk itself 1647 * is read asynchronously, and will not be available until 1648 * reader.onload fires. 1649 * 1650 * @private 1651 */ 1652 var readNextChunk = function readNextChunk() { 1653 1654 // If no further chunks remain, inform of completion and stop 1655 if (offset >= blob.size) { 1656 1657 // Fire completion event for completed blob 1658 if (guacWriter.oncomplete) 1659 guacWriter.oncomplete(blob); 1660 1661 // No further chunks to read 1662 return; 1663 1664 } 1665 1666 // Obtain reference to next chunk as a new blob 1667 var chunk = slice(blob, offset, offset + arrayBufferWriter.blobLength); 1668 offset += arrayBufferWriter.blobLength; 1669 1670 // Attempt to read the blob contents represented by the blob into 1671 // a new array buffer 1672 reader.readAsArrayBuffer(chunk); 1673 1674 }; 1675 1676 // Send each chunk over the stream, continue reading the next chunk 1677 reader.onload = function chunkLoadComplete() { 1678 1679 // Send the successfully-read chunk 1680 arrayBufferWriter.sendData(reader.result); 1681 1682 // Continue sending more chunks after the latest chunk is 1683 // acknowledged 1684 arrayBufferWriter.onack = function sendMoreChunks(status) { 1685 1686 if (guacWriter.onack) 1687 guacWriter.onack(status); 1688 1689 // Abort transfer if an error occurs 1690 if (status.isError()) 1691 return; 1692 1693 // Inform of blob upload progress via progress events 1694 if (guacWriter.onprogress) 1695 guacWriter.onprogress(blob, offset - arrayBufferWriter.blobLength); 1696 1697 // Queue the next chunk for reading 1698 readNextChunk(); 1699 1700 }; 1701 1702 }; 1703 1704 // If an error prevents further reading, inform of error and stop 1705 reader.onerror = function chunkLoadFailed() { 1706 1707 // Fire error event, including the context of the error 1708 if (guacWriter.onerror) 1709 guacWriter.onerror(blob, offset, reader.error); 1710 1711 }; 1712 1713 // Begin reading the first chunk 1714 readNextChunk(); 1715 1716 }; 1717 1718 /** 1719 * Signals that no further text will be sent, effectively closing the 1720 * stream. 1721 */ 1722 this.sendEnd = function sendEnd() { 1723 arrayBufferWriter.sendEnd(); 1724 }; 1725 1726 /** 1727 * Fired for received data, if acknowledged by the server. 1728 * 1729 * @event 1730 * @param {!Guacamole.Status} status 1731 * The status of the operation. 1732 */ 1733 this.onack = null; 1734 1735 /** 1736 * Fired when an error occurs reading a blob passed to 1737 * [sendBlob()]{@link Guacamole.BlobWriter#sendBlob}. The transfer for the 1738 * the given blob will cease, but the stream will remain open. 1739 * 1740 * @event 1741 * @param {!Blob} blob 1742 * The blob that was being read when the error occurred. 1743 * 1744 * @param {!number} offset 1745 * The offset of the failed read attempt within the blob, in bytes. 1746 * 1747 * @param {!DOMError} error 1748 * The error that occurred. 1749 */ 1750 this.onerror = null; 1751 1752 /** 1753 * Fired for each successfully-read chunk of data as a blob is being sent 1754 * via [sendBlob()]{@link Guacamole.BlobWriter#sendBlob}. 1755 * 1756 * @event 1757 * @param {!Blob} blob 1758 * The blob that is being read. 1759 * 1760 * @param {!number} offset 1761 * The offset of the read that just succeeded. 1762 */ 1763 this.onprogress = null; 1764 1765 /** 1766 * Fired when a blob passed to 1767 * [sendBlob()]{@link Guacamole.BlobWriter#sendBlob} has finished being 1768 * sent. 1769 * 1770 * @event 1771 * @param {!Blob} blob 1772 * The blob that was sent. 1773 */ 1774 this.oncomplete = null; 1775 1776}; 1777/* 1778 * Licensed to the Apache Software Foundation (ASF) under one 1779 * or more contributor license agreements. See the NOTICE file 1780 * distributed with this work for additional information 1781 * regarding copyright ownership. The ASF licenses this file 1782 * to you under the Apache License, Version 2.0 (the 1783 * "License"); you may not use this file except in compliance 1784 * with the License. You may obtain a copy of the License at 1785 * 1786 * http://www.apache.org/licenses/LICENSE-2.0 1787 * 1788 * Unless required by applicable law or agreed to in writing, 1789 * software distributed under the License is distributed on an 1790 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 1791 * KIND, either express or implied. See the License for the 1792 * specific language governing permissions and limitations 1793 * under the License. 1794 */ 1795 1796var Guacamole = Guacamole || {}; 1797 1798/** 1799 * Guacamole protocol client. Given a {@link Guacamole.Tunnel}, 1800 * automatically handles incoming and outgoing Guacamole instructions via the 1801 * provided tunnel, updating its display using one or more canvas elements. 1802 * 1803 * @constructor 1804 * @param {!Guacamole.Tunnel} tunnel 1805 * The tunnel to use to send and receive Guacamole instructions. 1806 */ 1807Guacamole.Client = function(tunnel) { 1808 1809 var guac_client = this; 1810 1811 var currentState = Guacamole.Client.State.IDLE; 1812 1813 var currentTimestamp = 0; 1814 1815 /** 1816 * The rough number of milliseconds to wait between sending keep-alive 1817 * pings. This may vary depending on how frequently the browser allows 1818 * timers to run, as well as how frequently the client receives messages 1819 * from the server. 1820 * 1821 * @private 1822 * @constant 1823 * @type {!number} 1824 */ 1825 var KEEP_ALIVE_FREQUENCY = 5000; 1826 1827 /** 1828 * The current keep-alive ping timeout ID, if any. This will only be set 1829 * upon connecting. 1830 * 1831 * @private 1832 * @type {number} 1833 */ 1834 var keepAliveTimeout = null; 1835 1836 /** 1837 * The timestamp of the point in time that the last keep-live ping was 1838 * sent, in milliseconds elapsed since midnight of January 1, 1970 UTC. 1839 * 1840 * @private 1841 * @type {!number} 1842 */ 1843 var lastSentKeepAlive = 0; 1844 1845 /** 1846 * Translation from Guacamole protocol line caps to Layer line caps. 1847 * 1848 * @private 1849 * @type {!Object.<number, string>} 1850 */ 1851 var lineCap = { 1852 0: "butt", 1853 1: "round", 1854 2: "square" 1855 }; 1856 1857 /** 1858 * Translation from Guacamole protocol line caps to Layer line caps. 1859 * 1860 * @private 1861 * @type {!Object.<number, string>} 1862 */ 1863 var lineJoin = { 1864 0: "bevel", 1865 1: "miter", 1866 2: "round" 1867 }; 1868 1869 /** 1870 * The underlying Guacamole display. 1871 * 1872 * @private 1873 * @type {!Guacamole.Display} 1874 */ 1875 var display = new Guacamole.Display(); 1876 1877 /** 1878 * All available layers and buffers 1879 * 1880 * @private 1881 * @type {!Object.<number, (Guacamole.Display.VisibleLayer|Guacamole.Layer)>} 1882 */ 1883 var layers = {}; 1884 1885 /** 1886 * All audio players currently in use by the client. Initially, this will 1887 * be empty, but audio players may be allocated by the server upon request. 1888 * 1889 * @private 1890 * @type {!Object.<number, Guacamole.AudioPlayer>} 1891 */ 1892 var audioPlayers = {}; 1893 1894 /** 1895 * All video players currently in use by the client. Initially, this will 1896 * be empty, but video players may be allocated by the server upon request. 1897 * 1898 * @private 1899 * @type {!Object.<number, Guacamole.VideoPlayer>} 1900 */ 1901 var videoPlayers = {}; 1902 1903 // No initial parsers 1904 var parsers = []; 1905 1906 // No initial streams 1907 var streams = []; 1908 1909 /** 1910 * All current objects. The index of each object is dictated by the 1911 * Guacamole server. 1912 * 1913 * @private 1914 * @type {!Guacamole.Object[]} 1915 */ 1916 var objects = []; 1917 1918 // Pool of available stream indices 1919 var stream_indices = new Guacamole.IntegerPool(); 1920 1921 // Array of allocated output streams by index 1922 var output_streams = []; 1923 1924 function setState(state) { 1925 if (state != currentState) { 1926 currentState = state; 1927 if (guac_client.onstatechange) 1928 guac_client.onstatechange(currentState); 1929 } 1930 } 1931 1932 function isConnected() { 1933 return currentState == Guacamole.Client.State.CONNECTED 1934 || currentState == Guacamole.Client.State.WAITING; 1935 } 1936 1937 /** 1938 * Produces an opaque representation of Guacamole.Client state which can be 1939 * later imported through a call to importState(). This object is 1940 * effectively an independent, compressed snapshot of protocol and display 1941 * state. Invoking this function implicitly flushes the display. 1942 * 1943 * @param {!function} callback 1944 * Callback which should be invoked once the state object is ready. The 1945 * state object will be passed to the callback as the sole parameter. 1946 * This callback may be invoked immediately, or later as the display 1947 * finishes rendering and becomes ready. 1948 */ 1949 this.exportState = function exportState(callback) { 1950 1951 // Start with empty state 1952 var state = { 1953 'currentState' : currentState, 1954 'currentTimestamp' : currentTimestamp, 1955 'layers' : {} 1956 }; 1957 1958 var layersSnapshot = {}; 1959 1960 // Make a copy of all current layers (protocol state) 1961 for (var key in layers) { 1962 layersSnapshot[key] = layers[key]; 1963 } 1964 1965 // Populate layers once data is available (display state, requires flush) 1966 display.flush(function populateLayers() { 1967 1968 // Export each defined layer/buffer 1969 for (var key in layersSnapshot) { 1970 1971 var index = parseInt(key); 1972 var layer = layersSnapshot[key]; 1973 var canvas = layer.toCanvas(); 1974 1975 // Store layer/buffer dimensions 1976 var exportLayer = { 1977 'width' : layer.width, 1978 'height' : layer.height 1979 }; 1980 1981 // Store layer/buffer image data, if it can be generated 1982 if (layer.width && layer.height) 1983 exportLayer.url = canvas.toDataURL('image/png'); 1984 1985 // Add layer properties if not a buffer nor the default layer 1986 if (index > 0) { 1987 exportLayer.x = layer.x; 1988 exportLayer.y = layer.y; 1989 exportLayer.z = layer.z; 1990 exportLayer.alpha = layer.alpha; 1991 exportLayer.matrix = layer.matrix; 1992 exportLayer.parent = getLayerIndex(layer.parent); 1993 } 1994 1995 // Store exported layer 1996 state.layers[key] = exportLayer; 1997 1998 } 1999 2000 // Invoke callback now that the state is ready 2001 callback(state); 2002 2003 }); 2004 2005 }; 2006 2007 /** 2008 * Restores Guacamole.Client protocol and display state based on an opaque 2009 * object from a prior call to exportState(). The Guacamole.Client instance 2010 * used to export that state need not be the same as this instance. 2011 * 2012 * @param {!object} state 2013 * An opaque representation of Guacamole.Client state from a prior call 2014 * to exportState(). 2015 * 2016 * @param {function} [callback] 2017 * The function to invoke when state has finished being imported. This 2018 * may happen immediately, or later as images within the provided state 2019 * object are loaded. 2020 */ 2021 this.importState = function importState(state, callback) { 2022 2023 var key; 2024 var index; 2025 2026 currentState = state.currentState; 2027 currentTimestamp = state.currentTimestamp; 2028 2029 // Cancel any pending display operations/frames 2030 display.cancel(); 2031 2032 // Dispose of all layers 2033 for (key in layers) { 2034 index = parseInt(key); 2035 if (index > 0) 2036 layers[key].dispose(); 2037 } 2038 2039 layers = {}; 2040 2041 // Import state of each layer/buffer 2042 for (key in state.layers) { 2043 2044 index = parseInt(key); 2045 2046 var importLayer = state.layers[key]; 2047 var layer = getLayer(index); 2048 2049 // Reset layer size 2050 display.resize(layer, importLayer.width, importLayer.height); 2051 2052 // Initialize new layer if it has associated data 2053 if (importLayer.url) { 2054 display.setChannelMask(layer, Guacamole.Layer.SRC); 2055 display.draw(layer, 0, 0, importLayer.url); 2056 } 2057 2058 // Set layer-specific properties if not a buffer nor the default layer 2059 if (index > 0 && importLayer.parent >= 0) { 2060 2061 // Apply layer position and set parent 2062 var parent = getLayer(importLayer.parent); 2063 display.move(layer, parent, importLayer.x, importLayer.y, importLayer.z); 2064 2065 // Set layer transparency 2066 display.shade(layer, importLayer.alpha); 2067 2068 // Apply matrix transform 2069 var matrix = importLayer.matrix; 2070 display.distort(layer, 2071 matrix[0], matrix[1], matrix[2], 2072 matrix[3], matrix[4], matrix[5]); 2073 2074 } 2075 2076 } 2077 2078 // Flush changes to display 2079 display.flush(callback); 2080 2081 }; 2082 2083 /** 2084 * Returns the underlying display of this Guacamole.Client. The display 2085 * contains an Element which can be added to the DOM, causing the 2086 * display to become visible. 2087 * 2088 * @return {!Guacamole.Display} 2089 * The underlying display of this Guacamole.Client. 2090 */ 2091 this.getDisplay = function() { 2092 return display; 2093 }; 2094 2095 /** 2096 * Sends the current size of the screen. 2097 * 2098 * @param {!number} width 2099 * The width of the screen. 2100 * 2101 * @param {!number} height 2102 * The height of the screen. 2103 */ 2104 this.sendSize = function(width, height) { 2105 2106 // Do not send requests if not connected 2107 if (!isConnected()) 2108 return; 2109 2110 tunnel.sendMessage("size", width, height); 2111 2112 }; 2113 2114 /** 2115 * Sends a key event having the given properties as if the user 2116 * pressed or released a key. 2117 * 2118 * @param {!boolean} pressed 2119 * Whether the key is pressed (true) or released (false). 2120 * 2121 * @param {!number} keysym 2122 * The keysym of the key being pressed or released. 2123 */ 2124 this.sendKeyEvent = function(pressed, keysym) { 2125 // Do not send requests if not connected 2126 if (!isConnected()) 2127 return; 2128 2129 tunnel.sendMessage("key", keysym, pressed); 2130 }; 2131 2132 /** 2133 * Sends a mouse event having the properties provided by the given mouse 2134 * state. 2135 * 2136 * @param {!Guacamole.Mouse.State} mouseState 2137 * The state of the mouse to send in the mouse event. 2138 * 2139 * @param {boolean} [applyDisplayScale=false] 2140 * Whether the provided mouse state uses local display units, rather 2141 * than remote display units, and should be scaled to match the 2142 * {@link Guacamole.Display}. 2143 */ 2144 this.sendMouseState = function sendMouseState(mouseState, applyDisplayScale) { 2145 2146 // Do not send requests if not connected 2147 if (!isConnected()) 2148 return; 2149 2150 var x = mouseState.x; 2151 var y = mouseState.y; 2152 2153 // Translate for display units if requested 2154 if (applyDisplayScale) { 2155 x /= display.getScale(); 2156 y /= display.getScale(); 2157 } 2158 2159 // Update client-side cursor 2160 display.moveCursor( 2161 Math.floor(x), 2162 Math.floor(y) 2163 ); 2164 2165 // Build mask 2166 var buttonMask = 0; 2167 if (mouseState.left) buttonMask |= 1; 2168 if (mouseState.middle) buttonMask |= 2; 2169 if (mouseState.right) buttonMask |= 4; 2170 if (mouseState.up) buttonMask |= 8; 2171 if (mouseState.down) buttonMask |= 16; 2172 2173 // Send message 2174 tunnel.sendMessage("mouse", Math.floor(x), Math.floor(y), buttonMask); 2175 }; 2176 2177 /** 2178 * Sends a touch event having the properties provided by the given touch 2179 * state. 2180 * 2181 * @param {!Guacamole.Touch.State} touchState 2182 * The state of the touch contact to send in the touch event. 2183 * 2184 * @param {boolean} [applyDisplayScale=false] 2185 * Whether the provided touch state uses local display units, rather 2186 * than remote display units, and should be scaled to match the 2187 * {@link Guacamole.Display}. 2188 */ 2189 this.sendTouchState = function sendTouchState(touchState, applyDisplayScale) { 2190 2191 // Do not send requests if not connected 2192 if (!isConnected()) 2193 return; 2194 2195 var x = touchState.x; 2196 var y = touchState.y; 2197 2198 // Translate for display units if requested 2199 if (applyDisplayScale) { 2200 x /= display.getScale(); 2201 y /= display.getScale(); 2202 } 2203 2204 tunnel.sendMessage('touch', touchState.id, Math.floor(x), Math.floor(y), 2205 Math.floor(touchState.radiusX), Math.floor(touchState.radiusY), 2206 touchState.angle, touchState.force); 2207 2208 }; 2209 2210 /** 2211 * Allocates an available stream index and creates a new 2212 * Guacamole.OutputStream using that index, associating the resulting 2213 * stream with this Guacamole.Client. Note that this stream will not yet 2214 * exist as far as the other end of the Guacamole connection is concerned. 2215 * Streams exist within the Guacamole protocol only when referenced by an 2216 * instruction which creates the stream, such as a "clipboard", "file", or 2217 * "pipe" instruction. 2218 * 2219 * @returns {!Guacamole.OutputStream} 2220 * A new Guacamole.OutputStream with a newly-allocated index and 2221 * associated with this Guacamole.Client. 2222 */ 2223 this.createOutputStream = function createOutputStream() { 2224 2225 // Allocate index 2226 var index = stream_indices.next(); 2227 2228 // Return new stream 2229 var stream = output_streams[index] = new Guacamole.OutputStream(guac_client, index); 2230 return stream; 2231 2232 }; 2233 2234 /** 2235 * Opens a new audio stream for writing, where audio data having the give 2236 * mimetype will be sent along the returned stream. The instruction 2237 * necessary to create this stream will automatically be sent. 2238 * 2239 * @param {!string} mimetype 2240 * The mimetype of the audio data that will be sent along the returned 2241 * stream. 2242 * 2243 * @return {!Guacamole.OutputStream} 2244 * The created audio stream. 2245 */ 2246 this.createAudioStream = function(mimetype) { 2247 2248 // Allocate and associate stream with audio metadata 2249 var stream = guac_client.createOutputStream(); 2250 tunnel.sendMessage("audio", stream.index, mimetype); 2251 return stream; 2252 2253 }; 2254 2255 /** 2256 * Opens a new file for writing, having the given index, mimetype and 2257 * filename. The instruction necessary to create this stream will 2258 * automatically be sent. 2259 * 2260 * @param {!string} mimetype 2261 * The mimetype of the file being sent. 2262 * 2263 * @param {!string} filename 2264 * The filename of the file being sent. 2265 * 2266 * @return {!Guacamole.OutputStream} 2267 * The created file stream. 2268 */ 2269 this.createFileStream = function(mimetype, filename) { 2270 2271 // Allocate and associate stream with file metadata 2272 var stream = guac_client.createOutputStream(); 2273 tunnel.sendMessage("file", stream.index, mimetype, filename); 2274 return stream; 2275 2276 }; 2277 2278 /** 2279 * Opens a new pipe for writing, having the given name and mimetype. The 2280 * instruction necessary to create this stream will automatically be sent. 2281 * 2282 * @param {!string} mimetype 2283 * The mimetype of the data being sent. 2284 * 2285 * @param {!string} name 2286 * The name of the pipe. 2287 * 2288 * @return {!Guacamole.OutputStream} 2289 * The created file stream. 2290 */ 2291 this.createPipeStream = function(mimetype, name) { 2292 2293 // Allocate and associate stream with pipe metadata 2294 var stream = guac_client.createOutputStream(); 2295 tunnel.sendMessage("pipe", stream.index, mimetype, name); 2296 return stream; 2297 2298 }; 2299 2300 /** 2301 * Opens a new clipboard object for writing, having the given mimetype. The 2302 * instruction necessary to create this stream will automatically be sent. 2303 * 2304 * @param {!string} mimetype 2305 * The mimetype of the data being sent. 2306 * 2307 * @param {!string} name 2308 * The name of the pipe. 2309 * 2310 * @return {!Guacamole.OutputStream} 2311 * The created file stream. 2312 */ 2313 this.createClipboardStream = function(mimetype) { 2314 2315 // Allocate and associate stream with clipboard metadata 2316 var stream = guac_client.createOutputStream(); 2317 tunnel.sendMessage("clipboard", stream.index, mimetype); 2318 return stream; 2319 2320 }; 2321 2322 /** 2323 * Opens a new argument value stream for writing, having the given 2324 * parameter name and mimetype, requesting that the connection parameter 2325 * with the given name be updated to the value described by the contents 2326 * of the following stream. The instruction necessary to create this stream 2327 * will automatically be sent. 2328 * 2329 * @param {!string} mimetype 2330 * The mimetype of the data being sent. 2331 * 2332 * @param {!string} name 2333 * The name of the connection parameter to attempt to update. 2334 * 2335 * @return {!Guacamole.OutputStream} 2336 * The created argument value stream. 2337 */ 2338 this.createArgumentValueStream = function createArgumentValueStream(mimetype, name) { 2339 2340 // Allocate and associate stream with argument value metadata 2341 var stream = guac_client.createOutputStream(); 2342 tunnel.sendMessage("argv", stream.index, mimetype, name); 2343 return stream; 2344 2345 }; 2346 2347 /** 2348 * Creates a new output stream associated with the given object and having 2349 * the given mimetype and name. The legality of a mimetype and name is 2350 * dictated by the object itself. The instruction necessary to create this 2351 * stream will automatically be sent. 2352 * 2353 * @param {!number} index 2354 * The index of the object for which the output stream is being 2355 * created. 2356 * 2357 * @param {!string} mimetype 2358 * The mimetype of the data which will be sent to the output stream. 2359 * 2360 * @param {!string} name 2361 * The defined name of an output stream within the given object. 2362 * 2363 * @returns {!Guacamole.OutputStream} 2364 * An output stream which will write blobs to the named output stream 2365 * of the given object. 2366 */ 2367 this.createObjectOutputStream = function createObjectOutputStream(index, mimetype, name) { 2368 2369 // Allocate and associate stream with object metadata 2370 var stream = guac_client.createOutputStream(); 2371 tunnel.sendMessage("put", index, stream.index, mimetype, name); 2372 return stream; 2373 2374 }; 2375 2376 /** 2377 * Requests read access to the input stream having the given name. If 2378 * successful, a new input stream will be created. 2379 * 2380 * @param {!number} index 2381 * The index of the object from which the input stream is being 2382 * requested. 2383 * 2384 * @param {!string} name 2385 * The name of the input stream to request. 2386 */ 2387 this.requestObjectInputStream = function requestObjectInputStream(index, name) { 2388 2389 // Do not send requests if not connected 2390 if (!isConnected()) 2391 return; 2392 2393 tunnel.sendMessage("get", index, name); 2394 }; 2395 2396 /** 2397 * Acknowledge receipt of a blob on the stream with the given index. 2398 * 2399 * @param {!number} index 2400 * The index of the stream associated with the received blob. 2401 * 2402 * @param {!string} message 2403 * A human-readable message describing the error or status. 2404 * 2405 * @param {!number} code 2406 * The error code, if any, or 0 for success. 2407 */ 2408 this.sendAck = function(index, message, code) { 2409 2410 // Do not send requests if not connected 2411 if (!isConnected()) 2412 return; 2413 2414 tunnel.sendMessage("ack", index, message, code); 2415 }; 2416 2417 /** 2418 * Given the index of a file, writes a blob of data to that file. 2419 * 2420 * @param {!number} index 2421 * The index of the file to write to. 2422 * 2423 * @param {!string} data 2424 * Base64-encoded data to write to the file. 2425 */ 2426 this.sendBlob = function(index, data) { 2427 2428 // Do not send requests if not connected 2429 if (!isConnected()) 2430 return; 2431 2432 tunnel.sendMessage("blob", index, data); 2433 }; 2434 2435 /** 2436 * Marks a currently-open stream as complete. The other end of the 2437 * Guacamole connection will be notified via an "end" instruction that the 2438 * stream is closed, and the index will be made available for reuse in 2439 * future streams. 2440 * 2441 * @param {!number} index 2442 * The index of the stream to end. 2443 */ 2444 this.endStream = function(index) { 2445 2446 // Do not send requests if not connected 2447 if (!isConnected()) 2448 return; 2449 2450 // Explicitly close stream by sending "end" instruction 2451 tunnel.sendMessage("end", index); 2452 2453 // Free associated index and stream if they exist 2454 if (output_streams[index]) { 2455 stream_indices.free(index); 2456 delete output_streams[index]; 2457 } 2458 2459 }; 2460 2461 /** 2462 * Fired whenever the state of this Guacamole.Client changes. 2463 * 2464 * @event 2465 * @param {!number} state 2466 * The new state of the client. 2467 */ 2468 this.onstatechange = null; 2469 2470 /** 2471 * Fired when the remote client sends a name update. 2472 * 2473 * @event 2474 * @param {!string} name 2475 * The new name of this client. 2476 */ 2477 this.onname = null; 2478 2479 /** 2480 * Fired when an error is reported by the remote client, and the connection 2481 * is being closed. 2482 * 2483 * @event 2484 * @param {!Guacamole.Status} status 2485 * A status object which describes the error. 2486 */ 2487 this.onerror = null; 2488 2489 /** 2490 * Fired when an arbitrary message is received from the tunnel that should 2491 * be processed by the client. By default, additional message-specific 2492 * events such as "onjoin" and "onleave" will fire for the received message 2493 * after this event has been processed. An event handler for "onmsg" need 2494 * not be supplied if "onjoin" and/or "onleave" will be used. 2495 * 2496 * @event 2497 * @param {!number} msgcode 2498 * A status code sent by the remote server that indicates the nature of 2499 * the message that is being sent to the client. 2500 * 2501 * @param {string[]} args 2502 * An array of arguments to be processed with the message sent to the 2503 * client. 2504 * 2505 * @return {boolean} 2506 * true if message-specific events such as "onjoin" and 2507 * "onleave" should be fired for this message, false otherwise. If 2508 * no value is returned, message-specific events will be allowed to 2509 * fire. 2510 */ 2511 this.onmsg = null; 2512 2513 /** 2514 * Fired when a user joins a shared connection. 2515 * 2516 * @event 2517 * @param {!string} userID 2518 * A unique value representing this specific user's connection to the 2519 * shared connection. This value is generated by the server and is 2520 * guaranteed to be unique relative to other users of the connection. 2521 * 2522 * @param {!string} name 2523 * A human-readable name representing the user that joined, such as 2524 * their username. This value is provided by the web application during 2525 * the connection handshake and is not necessarily unique relative to 2526 * other users of the connection. 2527 */ 2528 this.onjoin = null; 2529 2530 /** 2531 * Fired when a user leaves a shared connection. 2532 * 2533 * @event 2534 * @param {!string} userID 2535 * A unique value representing this specific user's connection to the 2536 * shared connection. This value is generated by the server and is 2537 * guaranteed to be unique relative to other users of the connection. 2538 * 2539 * @param {!string} name 2540 * A human-readable name representing the user that left, such as their 2541 * username. This value is provided by the web application during the 2542 * connection handshake and is not necessarily unique relative to other 2543 * users of the connection. 2544 */ 2545 this.onleave = null; 2546 2547 /** 2548 * Fired when a audio stream is created. The stream provided to this event 2549 * handler will contain its own event handlers for received data. 2550 * 2551 * @event 2552 * @param {!Guacamole.InputStream} stream 2553 * The stream that will receive audio data from the server. 2554 * 2555 * @param {!string} mimetype 2556 * The mimetype of the audio data which will be received. 2557 * 2558 * @return {Guacamole.AudioPlayer} 2559 * An object which implements the Guacamole.AudioPlayer interface and 2560 * has been initialized to play the data in the provided stream, or null 2561 * if the built-in audio players of the Guacamole client should be 2562 * used. 2563 */ 2564 this.onaudio = null; 2565 2566 /** 2567 * Fired when a video stream is created. The stream provided to this event 2568 * handler will contain its own event handlers for received data. 2569 * 2570 * @event 2571 * @param {!Guacamole.InputStream} stream 2572 * The stream that will receive video data from the server. 2573 * 2574 * @param {!Guacamole.Display.VisibleLayer} layer 2575 * The destination layer on which the received video data should be 2576 * played. It is the responsibility of the Guacamole.VideoPlayer 2577 * implementation to play the received data within this layer. 2578 * 2579 * @param {!string} mimetype 2580 * The mimetype of the video data which will be received. 2581 * 2582 * @return {Guacamole.VideoPlayer} 2583 * An object which implements the Guacamole.VideoPlayer interface and 2584 * has been initialized to play the data in the provided stream, or null 2585 * if the built-in video players of the Guacamole client should be 2586 * used. 2587 */ 2588 this.onvideo = null; 2589 2590 /** 2591 * Fired when the remote client is explicitly declaring the level of 2592 * multi-touch support provided by a particular display layer. 2593 * 2594 * @event 2595 * @param {!Guacamole.Display.VisibleLayer} layer 2596 * The layer whose multi-touch support level is being declared. 2597 * 2598 * @param {!number} touches 2599 * The maximum number of simultaneous touches supported by the given 2600 * layer, where 0 indicates that touch events are not supported at all. 2601 */ 2602 this.onmultitouch = null; 2603 2604 /** 2605 * Fired when the current value of a connection parameter is being exposed 2606 * by the server. 2607 * 2608 * @event 2609 * @param {!Guacamole.InputStream} stream 2610 * The stream that will receive connection parameter data from the 2611 * server. 2612 * 2613 * @param {!string} mimetype 2614 * The mimetype of the data which will be received. 2615 * 2616 * @param {!string} name 2617 * The name of the connection parameter whose value is being exposed. 2618 */ 2619 this.onargv = null; 2620 2621 /** 2622 * Fired when the clipboard of the remote client is changing. 2623 * 2624 * @event 2625 * @param {!Guacamole.InputStream} stream 2626 * The stream that will receive clipboard data from the server. 2627 * 2628 * @param {!string} mimetype 2629 * The mimetype of the data which will be received. 2630 */ 2631 this.onclipboard = null; 2632 2633 /** 2634 * Fired when a file stream is created. The stream provided to this event 2635 * handler will contain its own event handlers for received data. 2636 * 2637 * @event 2638 * @param {!Guacamole.InputStream} stream 2639 * The stream that will receive data from the server. 2640 * 2641 * @param {!string} mimetype 2642 * The mimetype of the file received. 2643 * 2644 * @param {!string} filename 2645 * The name of the file received. 2646 */ 2647 this.onfile = null; 2648 2649 /** 2650 * Fired when a filesystem object is created. The object provided to this 2651 * event handler will contain its own event handlers and functions for 2652 * requesting and handling data. 2653 * 2654 * @event 2655 * @param {!Guacamole.Object} object 2656 * The created filesystem object. 2657 * 2658 * @param {!string} name 2659 * The name of the filesystem. 2660 */ 2661 this.onfilesystem = null; 2662 2663 /** 2664 * Fired when a pipe stream is created. The stream provided to this event 2665 * handler will contain its own event handlers for received data; 2666 * 2667 * @event 2668 * @param {!Guacamole.InputStream} stream 2669 * The stream that will receive data from the server. 2670 * 2671 * @param {!string} mimetype 2672 * The mimetype of the data which will be received. 2673 * 2674 * @param {!string} name 2675 * The name of the pipe. 2676 */ 2677 this.onpipe = null; 2678 2679 /** 2680 * Fired when a "required" instruction is received. A required instruction 2681 * indicates that additional parameters are required for the connection to 2682 * continue, such as user credentials. 2683 * 2684 * @event 2685 * @param {!string[]} parameters 2686 * The names of the connection parameters that are required to be 2687 * provided for the connection to continue. 2688 */ 2689 this.onrequired = null; 2690 2691 /** 2692 * Fired whenever a sync instruction is received from the server, indicating 2693 * that the server is finished processing any input from the client and 2694 * has sent any results. 2695 * 2696 * @event 2697 * @param {!number} timestamp 2698 * The timestamp associated with the sync instruction. 2699 * 2700 * @param {!number} frames 2701 * The number of frames that were considered or combined to produce the 2702 * frame associated with this sync instruction, or zero if this value 2703 * is not known or the remote desktop server provides no concept of 2704 * frames. 2705 */ 2706 this.onsync = null; 2707 2708 /** 2709 * Returns the layer with the given index, creating it if necessary. 2710 * Positive indices refer to visible layers, an index of zero refers to 2711 * the default layer, and negative indices refer to buffers. 2712 * 2713 * @private 2714 * @param {!number} index 2715 * The index of the layer to retrieve. 2716 * 2717 * @return {!(Guacamole.Display.VisibleLayer|Guacamole.Layer)} 2718 * The layer having the given index. 2719 */ 2720 var getLayer = function getLayer(index) { 2721 2722 // Get layer, create if necessary 2723 var layer = layers[index]; 2724 if (!layer) { 2725 2726 // Create layer based on index 2727 if (index === 0) 2728 layer = display.getDefaultLayer(); 2729 else if (index > 0) 2730 layer = display.createLayer(); 2731 else 2732 layer = display.createBuffer(); 2733 2734 // Add new layer 2735 layers[index] = layer; 2736 2737 } 2738 2739 return layer; 2740 2741 }; 2742 2743 /** 2744 * Returns the index passed to getLayer() when the given layer was created. 2745 * Positive indices refer to visible layers, an index of zero refers to the 2746 * default layer, and negative indices refer to buffers. 2747 * 2748 * @param {!(Guacamole.Display.VisibleLayer|Guacamole.Layer)} layer 2749 * The layer whose index should be determined. 2750 * 2751 * @returns {number} 2752 * The index of the given layer, or null if no such layer is associated 2753 * with this client. 2754 */ 2755 var getLayerIndex = function getLayerIndex(layer) { 2756 2757 // Avoid searching if there clearly is no such layer 2758 if (!layer) 2759 return null; 2760 2761 // Search through each layer, returning the index of the given layer 2762 // once found 2763 for (var key in layers) { 2764 if (layer === layers[key]) 2765 return parseInt(key); 2766 } 2767 2768 // Otherwise, no such index 2769 return null; 2770 2771 }; 2772 2773 function getParser(index) { 2774 2775 var parser = parsers[index]; 2776 2777 // If parser not yet created, create it, and tie to the 2778 // oninstruction handler of the tunnel. 2779 if (parser == null) { 2780 parser = parsers[index] = new Guacamole.Parser(); 2781 parser.oninstruction = tunnel.oninstruction; 2782 } 2783 2784 return parser; 2785 2786 } 2787 2788 /** 2789 * Handlers for all defined layer properties. 2790 * 2791 * @private 2792 * @type {!Object.<string, function>} 2793 */ 2794 var layerPropertyHandlers = { 2795 2796 "miter-limit": function(layer, value) { 2797 display.setMiterLimit(layer, parseFloat(value)); 2798 }, 2799 2800 "multi-touch" : function layerSupportsMultiTouch(layer, value) { 2801 2802 // Process "multi-touch" property only for true visible layers (not off-screen buffers) 2803 if (guac_client.onmultitouch && layer instanceof Guacamole.Display.VisibleLayer) 2804 guac_client.onmultitouch(layer, parseInt(value)); 2805 2806 } 2807 2808 }; 2809 2810 /** 2811 * Handlers for all instruction opcodes receivable by a Guacamole protocol 2812 * client. 2813 * 2814 * @private 2815 * @type {!Object.<string, function>} 2816 */ 2817 var instructionHandlers = { 2818 2819 "ack": function(parameters) { 2820 2821 var stream_index = parseInt(parameters[0]); 2822 var reason = parameters[1]; 2823 var code = parseInt(parameters[2]); 2824 2825 // Get stream 2826 var stream = output_streams[stream_index]; 2827 if (stream) { 2828 2829 // Signal ack if handler defined 2830 if (stream.onack) 2831 stream.onack(new Guacamole.Status(code, reason)); 2832 2833 // If code is an error, invalidate stream if not already 2834 // invalidated by onack handler 2835 if (code >= 0x0100 && output_streams[stream_index] === stream) { 2836 stream_indices.free(stream_index); 2837 delete output_streams[stream_index]; 2838 } 2839 2840 } 2841 2842 }, 2843 2844 "arc": function(parameters) { 2845 2846 var layer = getLayer(parseInt(parameters[0])); 2847 var x = parseInt(parameters[1]); 2848 var y = parseInt(parameters[2]); 2849 var radius = parseInt(parameters[3]); 2850 var startAngle = parseFloat(parameters[4]); 2851 var endAngle = parseFloat(parameters[5]); 2852 var negative = parseInt(parameters[6]); 2853 2854 display.arc(layer, x, y, radius, startAngle, endAngle, negative != 0); 2855 2856 }, 2857 2858 "argv": function(parameters) { 2859 2860 var stream_index = parseInt(parameters[0]); 2861 var mimetype = parameters[1]; 2862 var name = parameters[2]; 2863 2864 // Create stream 2865 if (guac_client.onargv) { 2866 var stream = streams[stream_index] = new Guacamole.InputStream(guac_client, stream_index); 2867 guac_client.onargv(stream, mimetype, name); 2868 } 2869 2870 // Otherwise, unsupported 2871 else 2872 guac_client.sendAck(stream_index, "Receiving argument values unsupported", 0x0100); 2873 2874 }, 2875 2876 "audio": function(parameters) { 2877 2878 var stream_index = parseInt(parameters[0]); 2879 var mimetype = parameters[1]; 2880 2881 // Create stream 2882 var stream = streams[stream_index] = 2883 new Guacamole.InputStream(guac_client, stream_index); 2884 2885 // Get player instance via callback 2886 var audioPlayer = null; 2887 if (guac_client.onaudio) 2888 audioPlayer = guac_client.onaudio(stream, mimetype); 2889 2890 // If unsuccessful, try to use a default implementation 2891 if (!audioPlayer) 2892 audioPlayer = Guacamole.AudioPlayer.getInstance(stream, mimetype); 2893 2894 // If we have successfully retrieved an audio player, send success response 2895 if (audioPlayer) { 2896 audioPlayers[stream_index] = audioPlayer; 2897 guac_client.sendAck(stream_index, "OK", 0x0000); 2898 } 2899 2900 // Otherwise, mimetype must be unsupported 2901 else 2902 guac_client.sendAck(stream_index, "BAD TYPE", 0x030F); 2903 2904 }, 2905 2906 "blob": function(parameters) { 2907 2908 // Get stream 2909 var stream_index = parseInt(parameters[0]); 2910 var data = parameters[1]; 2911 var stream = streams[stream_index]; 2912 2913 // Write data 2914 if (stream && stream.onblob) 2915 stream.onblob(data); 2916 2917 }, 2918 2919 "body" : function handleBody(parameters) { 2920 2921 // Get object 2922 var objectIndex = parseInt(parameters[0]); 2923 var object = objects[objectIndex]; 2924 2925 var streamIndex = parseInt(parameters[1]); 2926 var mimetype = parameters[2]; 2927 var name = parameters[3]; 2928 2929 // Create stream if handler defined 2930 if (object && object.onbody) { 2931 var stream = streams[streamIndex] = new Guacamole.InputStream(guac_client, streamIndex); 2932 object.onbody(stream, mimetype, name); 2933 } 2934 2935 // Otherwise, unsupported 2936 else 2937 guac_client.sendAck(streamIndex, "Receipt of body unsupported", 0x0100); 2938 2939 }, 2940 2941 "cfill": function(parameters) { 2942 2943 var channelMask = parseInt(parameters[0]); 2944 var layer = getLayer(parseInt(parameters[1])); 2945 var r = parseInt(parameters[2]); 2946 var g = parseInt(parameters[3]); 2947 var b = parseInt(parameters[4]); 2948 var a = parseInt(parameters[5]); 2949 2950 display.setChannelMask(layer, channelMask); 2951 display.fillColor(layer, r, g, b, a); 2952 2953 }, 2954 2955 "clip": function(parameters) { 2956 2957 var layer = getLayer(parseInt(parameters[0])); 2958 2959 display.clip(layer); 2960 2961 }, 2962 2963 "clipboard": function(parameters) { 2964 2965 var stream_index = parseInt(parameters[0]); 2966 var mimetype = parameters[1]; 2967 2968 // Create stream 2969 if (guac_client.onclipboard) { 2970 var stream = streams[stream_index] = new Guacamole.InputStream(guac_client, stream_index); 2971 guac_client.onclipboard(stream, mimetype); 2972 } 2973 2974 // Otherwise, unsupported 2975 else 2976 guac_client.sendAck(stream_index, "Clipboard unsupported", 0x0100); 2977 2978 }, 2979 2980 "close": function(parameters) { 2981 2982 var layer = getLayer(parseInt(parameters[0])); 2983 2984 display.close(layer); 2985 2986 }, 2987 2988 "copy": function(parameters) { 2989 2990 var srcL = getLayer(parseInt(parameters[0])); 2991 var srcX = parseInt(parameters[1]); 2992 var srcY = parseInt(parameters[2]); 2993 var srcWidth = parseInt(parameters[3]); 2994 var srcHeight = parseInt(parameters[4]); 2995 var channelMask = parseInt(parameters[5]); 2996 var dstL = getLayer(parseInt(parameters[6])); 2997 var dstX = parseInt(parameters[7]); 2998 var dstY = parseInt(parameters[8]); 2999 3000 display.setChannelMask(dstL, channelMask); 3001 display.copy(srcL, srcX, srcY, srcWidth, srcHeight, 3002 dstL, dstX, dstY); 3003 3004 }, 3005 3006 "cstroke": function(parameters) { 3007 3008 var channelMask = parseInt(parameters[0]); 3009 var layer = getLayer(parseInt(parameters[1])); 3010 var cap = lineCap[parseInt(parameters[2])]; 3011 var join = lineJoin[parseInt(parameters[3])]; 3012 var thickness = parseInt(parameters[4]); 3013 var r = parseInt(parameters[5]); 3014 var g = parseInt(parameters[6]); 3015 var b = parseInt(parameters[7]); 3016 var a = parseInt(parameters[8]); 3017 3018 display.setChannelMask(layer, channelMask); 3019 display.strokeColor(layer, cap, join, thickness, r, g, b, a); 3020 3021 }, 3022 3023 "cursor": function(parameters) { 3024 3025 var cursorHotspotX = parseInt(parameters[0]); 3026 var cursorHotspotY = parseInt(parameters[1]); 3027 var srcL = getLayer(parseInt(parameters[2])); 3028 var srcX = parseInt(parameters[3]); 3029 var srcY = parseInt(parameters[4]); 3030 var srcWidth = parseInt(parameters[5]); 3031 var srcHeight = parseInt(parameters[6]); 3032 3033 display.setCursor(cursorHotspotX, cursorHotspotY, 3034 srcL, srcX, srcY, srcWidth, srcHeight); 3035 3036 }, 3037 3038 "curve": function(parameters) { 3039 3040 var layer = getLayer(parseInt(parameters[0])); 3041 var cp1x = parseInt(parameters[1]); 3042 var cp1y = parseInt(parameters[2]); 3043 var cp2x = parseInt(parameters[3]); 3044 var cp2y = parseInt(parameters[4]); 3045 var x = parseInt(parameters[5]); 3046 var y = parseInt(parameters[6]); 3047 3048 display.curveTo(layer, cp1x, cp1y, cp2x, cp2y, x, y); 3049 3050 }, 3051 3052 "disconnect" : function handleDisconnect(parameters) { 3053 3054 // Explicitly tear down connection 3055 guac_client.disconnect(); 3056 3057 }, 3058 3059 "dispose": function(parameters) { 3060 3061 var layer_index = parseInt(parameters[0]); 3062 3063 // If visible layer, remove from parent 3064 if (layer_index > 0) { 3065 3066 // Remove from parent 3067 var layer = getLayer(layer_index); 3068 display.dispose(layer); 3069 3070 // Delete reference 3071 delete layers[layer_index]; 3072 3073 } 3074 3075 // If buffer, just delete reference 3076 else if (layer_index < 0) 3077 delete layers[layer_index]; 3078 3079 // Attempting to dispose the root layer currently has no effect. 3080 3081 }, 3082 3083 "distort": function(parameters) { 3084 3085 var layer_index = parseInt(parameters[0]); 3086 var a = parseFloat(parameters[1]); 3087 var b = parseFloat(parameters[2]); 3088 var c = parseFloat(parameters[3]); 3089 var d = parseFloat(parameters[4]); 3090 var e = parseFloat(parameters[5]); 3091 var f = parseFloat(parameters[6]); 3092 3093 // Only valid for visible layers (not buffers) 3094 if (layer_index >= 0) { 3095 var layer = getLayer(layer_index); 3096 display.distort(layer, a, b, c, d, e, f); 3097 } 3098 3099 }, 3100 3101 "error": function(parameters) { 3102 3103 var reason = parameters[0]; 3104 var code = parseInt(parameters[1]); 3105 3106 // Call handler if defined 3107 if (guac_client.onerror) 3108 guac_client.onerror(new Guacamole.Status(code, reason)); 3109 3110 guac_client.disconnect(); 3111 3112 }, 3113 3114 "end": function(parameters) { 3115 3116 var stream_index = parseInt(parameters[0]); 3117 3118 // Get stream 3119 var stream = streams[stream_index]; 3120 if (stream) { 3121 3122 // Signal end of stream if handler defined 3123 if (stream.onend) 3124 stream.onend(); 3125 3126 // Invalidate stream 3127 delete streams[stream_index]; 3128 3129 } 3130 3131 }, 3132 3133 "file": function(parameters) { 3134 3135 var stream_index = parseInt(parameters[0]); 3136 var mimetype = parameters[1]; 3137 var filename = parameters[2]; 3138 3139 // Create stream 3140 if (guac_client.onfile) { 3141 var stream = streams[stream_index] = new Guacamole.InputStream(guac_client, stream_index); 3142 guac_client.onfile(stream, mimetype, filename); 3143 } 3144 3145 // Otherwise, unsupported 3146 else 3147 guac_client.sendAck(stream_index, "File transfer unsupported", 0x0100); 3148 3149 }, 3150 3151 "filesystem" : function handleFilesystem(parameters) { 3152 3153 var objectIndex = parseInt(parameters[0]); 3154 var name = parameters[1]; 3155 3156 // Create object, if supported 3157 if (guac_client.onfilesystem) { 3158 var object = objects[objectIndex] = new Guacamole.Object(guac_client, objectIndex); 3159 guac_client.onfilesystem(object, name); 3160 } 3161 3162 // If unsupported, simply ignore the availability of the filesystem 3163 3164 }, 3165 3166 "identity": function(parameters) { 3167 3168 var layer = getLayer(parseInt(parameters[0])); 3169 3170 display.setTransform(layer, 1, 0, 0, 1, 0, 0); 3171 3172 }, 3173 3174 "img": function(parameters) { 3175 3176 var stream_index = parseInt(parameters[0]); 3177 var channelMask = parseInt(parameters[1]); 3178 var layer = getLayer(parseInt(parameters[2])); 3179 var mimetype = parameters[3]; 3180 var x = parseInt(parameters[4]); 3181 var y = parseInt(parameters[5]); 3182 3183 // Create stream 3184 var stream = streams[stream_index] = new Guacamole.InputStream(guac_client, stream_index); 3185 3186 // Draw received contents once decoded 3187 display.setChannelMask(layer, channelMask); 3188 display.drawStream(layer, x, y, stream, mimetype); 3189 3190 }, 3191 3192 "jpeg": function(parameters) { 3193 3194 var channelMask = parseInt(parameters[0]); 3195 var layer = getLayer(parseInt(parameters[1])); 3196 var x = parseInt(parameters[2]); 3197 var y = parseInt(parameters[3]); 3198 var data = parameters[4]; 3199 3200 display.setChannelMask(layer, channelMask); 3201 display.draw(layer, x, y, "data:image/jpeg;base64," + data); 3202 3203 }, 3204 3205 "lfill": function(parameters) { 3206 3207 var channelMask = parseInt(parameters[0]); 3208 var layer = getLayer(parseInt(parameters[1])); 3209 var srcLayer = getLayer(parseInt(parameters[2])); 3210 3211 display.setChannelMask(layer, channelMask); 3212 display.fillLayer(layer, srcLayer); 3213 3214 }, 3215 3216 "line": function(parameters) { 3217 3218 var layer = getLayer(parseInt(parameters[0])); 3219 var x = parseInt(parameters[1]); 3220 var y = parseInt(parameters[2]); 3221 3222 display.lineTo(layer, x, y); 3223 3224 }, 3225 3226 "lstroke": function(parameters) { 3227 3228 var channelMask = parseInt(parameters[0]); 3229 var layer = getLayer(parseInt(parameters[1])); 3230 var srcLayer = getLayer(parseInt(parameters[2])); 3231 3232 display.setChannelMask(layer, channelMask); 3233 display.strokeLayer(layer, srcLayer); 3234 3235 }, 3236 3237 "mouse" : function handleMouse(parameters) { 3238 3239 var x = parseInt(parameters[0]); 3240 var y = parseInt(parameters[1]); 3241 3242 // Display and move software cursor to received coordinates 3243 display.showCursor(true); 3244 display.moveCursor(x, y); 3245 3246 }, 3247 3248 "move": function(parameters) { 3249 3250 var layer_index = parseInt(parameters[0]); 3251 var parent_index = parseInt(parameters[1]); 3252 var x = parseInt(parameters[2]); 3253 var y = parseInt(parameters[3]); 3254 var z = parseInt(parameters[4]); 3255 3256 // Only valid for non-default layers 3257 if (layer_index > 0 && parent_index >= 0) { 3258 var layer = getLayer(layer_index); 3259 var parent = getLayer(parent_index); 3260 display.move(layer, parent, x, y, z); 3261 } 3262 3263 }, 3264 3265 "msg" : function(parameters) { 3266 3267 var userID; 3268 var username; 3269 3270 // Fire general message handling event first 3271 var allowDefault = true; 3272 var msgid = parseInt(parameters[0]); 3273 if (guac_client.onmsg) { 3274 allowDefault = guac_client.onmsg(msgid, parameters.slice(1)); 3275 if (allowDefault === undefined) 3276 allowDefault = true; 3277 } 3278 3279 // Fire message-specific convenience events if not prevented by the 3280 // "onmsg" handler 3281 if (allowDefault) { 3282 switch (msgid) { 3283 3284 case Guacamole.Client.Message.USER_JOINED: 3285 userID = parameters[1]; 3286 username = parameters[2]; 3287 if (guac_client.onjoin) 3288 guac_client.onjoin(userID, username); 3289 break; 3290 3291 case Guacamole.Client.Message.USER_LEFT: 3292 userID = parameters[1]; 3293 username = parameters[2]; 3294 if (guac_client.onleave) 3295 guac_client.onleave(userID, username); 3296 break; 3297 3298 } 3299 } 3300 3301 }, 3302 3303 "name": function(parameters) { 3304 if (guac_client.onname) guac_client.onname(parameters[0]); 3305 }, 3306 3307 "nest": function(parameters) { 3308 var parser = getParser(parseInt(parameters[0])); 3309 parser.receive(parameters[1]); 3310 }, 3311 3312 "pipe": function(parameters) { 3313 3314 var stream_index = parseInt(parameters[0]); 3315 var mimetype = parameters[1]; 3316 var name = parameters[2]; 3317 3318 // Create stream 3319 if (guac_client.onpipe) { 3320 var stream = streams[stream_index] = new Guacamole.InputStream(guac_client, stream_index); 3321 guac_client.onpipe(stream, mimetype, name); 3322 } 3323 3324 // Otherwise, unsupported 3325 else 3326 guac_client.sendAck(stream_index, "Named pipes unsupported", 0x0100); 3327 3328 }, 3329 3330 "png": function(parameters) { 3331 3332 var channelMask = parseInt(parameters[0]); 3333 var layer = getLayer(parseInt(parameters[1])); 3334 var x = parseInt(parameters[2]); 3335 var y = parseInt(parameters[3]); 3336 var data = parameters[4]; 3337 3338 display.setChannelMask(layer, channelMask); 3339 display.draw(layer, x, y, "data:image/png;base64," + data); 3340 3341 }, 3342 3343 "pop": function(parameters) { 3344 3345 var layer = getLayer(parseInt(parameters[0])); 3346 3347 display.pop(layer); 3348 3349 }, 3350 3351 "push": function(parameters) { 3352 3353 var layer = getLayer(parseInt(parameters[0])); 3354 3355 display.push(layer); 3356 3357 }, 3358 3359 "rect": function(parameters) { 3360 3361 var layer = getLayer(parseInt(parameters[0])); 3362 var x = parseInt(parameters[1]); 3363 var y = parseInt(parameters[2]); 3364 var w = parseInt(parameters[3]); 3365 var h = parseInt(parameters[4]); 3366 3367 display.rect(layer, x, y, w, h); 3368 3369 }, 3370 3371 "required": function required(parameters) { 3372 if (guac_client.onrequired) guac_client.onrequired(parameters); 3373 }, 3374 3375 "reset": function(parameters) { 3376 3377 var layer = getLayer(parseInt(parameters[0])); 3378 3379 display.reset(layer); 3380 3381 }, 3382 3383 "set": function(parameters) { 3384 3385 var layer = getLayer(parseInt(parameters[0])); 3386 var name = parameters[1]; 3387 var value = parameters[2]; 3388 3389 // Call property handler if defined 3390 var handler = layerPropertyHandlers[name]; 3391 if (handler) 3392 handler(layer, value); 3393 3394 }, 3395 3396 "shade": function(parameters) { 3397 3398 var layer_index = parseInt(parameters[0]); 3399 var a = parseInt(parameters[1]); 3400 3401 // Only valid for visible layers (not buffers) 3402 if (layer_index >= 0) { 3403 var layer = getLayer(layer_index); 3404 display.shade(layer, a); 3405 } 3406 3407 }, 3408 3409 "size": function(parameters) { 3410 3411 var layer_index = parseInt(parameters[0]); 3412 var layer = getLayer(layer_index); 3413 var width = parseInt(parameters[1]); 3414 var height = parseInt(parameters[2]); 3415 3416 display.resize(layer, width, height); 3417 3418 }, 3419 3420 "start": function(parameters) { 3421 3422 var layer = getLayer(parseInt(parameters[0])); 3423 var x = parseInt(parameters[1]); 3424 var y = parseInt(parameters[2]); 3425 3426 display.moveTo(layer, x, y); 3427 3428 }, 3429 3430 "sync": function(parameters) { 3431 3432 var timestamp = parseInt(parameters[0]); 3433 var frames = parameters[1] ? parseInt(parameters[1]) : 0; 3434 3435 // Flush display, send sync when done 3436 display.flush(function displaySyncComplete() { 3437 3438 // Synchronize all audio players 3439 for (var index in audioPlayers) { 3440 var audioPlayer = audioPlayers[index]; 3441 if (audioPlayer) 3442 audioPlayer.sync(); 3443 } 3444 3445 // Send sync response to server 3446 if (timestamp !== currentTimestamp) { 3447 tunnel.sendMessage("sync", timestamp); 3448 currentTimestamp = timestamp; 3449 } 3450 3451 }, timestamp, frames); 3452 3453 // If received first update, no longer waiting. 3454 if (currentState === Guacamole.Client.State.WAITING) 3455 setState(Guacamole.Client.State.CONNECTED); 3456 3457 // Call sync handler if defined 3458 if (guac_client.onsync) 3459 guac_client.onsync(timestamp, frames); 3460 3461 }, 3462 3463 "transfer": function(parameters) { 3464 3465 var srcL = getLayer(parseInt(parameters[0])); 3466 var srcX = parseInt(parameters[1]); 3467 var srcY = parseInt(parameters[2]); 3468 var srcWidth = parseInt(parameters[3]); 3469 var srcHeight = parseInt(parameters[4]); 3470 var function_index = parseInt(parameters[5]); 3471 var dstL = getLayer(parseInt(parameters[6])); 3472 var dstX = parseInt(parameters[7]); 3473 var dstY = parseInt(parameters[8]); 3474 3475 /* SRC */ 3476 if (function_index === 0x3) 3477 display.put(srcL, srcX, srcY, srcWidth, srcHeight, 3478 dstL, dstX, dstY); 3479 3480 /* Anything else that isn't a NO-OP */ 3481 else if (function_index !== 0x5) 3482 display.transfer(srcL, srcX, srcY, srcWidth, srcHeight, 3483 dstL, dstX, dstY, Guacamole.Client.DefaultTransferFunction[function_index]); 3484 3485 }, 3486 3487 "transform": function(parameters) { 3488 3489 var layer = getLayer(parseInt(parameters[0])); 3490 var a = parseFloat(parameters[1]); 3491 var b = parseFloat(parameters[2]); 3492 var c = parseFloat(parameters[3]); 3493 var d = parseFloat(parameters[4]); 3494 var e = parseFloat(parameters[5]); 3495 var f = parseFloat(parameters[6]); 3496 3497 display.transform(layer, a, b, c, d, e, f); 3498 3499 }, 3500 3501 "undefine" : function handleUndefine(parameters) { 3502 3503 // Get object 3504 var objectIndex = parseInt(parameters[0]); 3505 var object = objects[objectIndex]; 3506 3507 // Signal end of object definition 3508 if (object && object.onundefine) 3509 object.onundefine(); 3510 3511 }, 3512 3513 "video": function(parameters) { 3514 3515 var stream_index = parseInt(parameters[0]); 3516 var layer = getLayer(parseInt(parameters[1])); 3517 var mimetype = parameters[2]; 3518 3519 // Create stream 3520 var stream = streams[stream_index] = 3521 new Guacamole.InputStream(guac_client, stream_index); 3522 3523 // Get player instance via callback 3524 var videoPlayer = null; 3525 if (guac_client.onvideo) 3526 videoPlayer = guac_client.onvideo(stream, layer, mimetype); 3527 3528 // If unsuccessful, try to use a default implementation 3529 if (!videoPlayer) 3530 videoPlayer = Guacamole.VideoPlayer.getInstance(stream, layer, mimetype); 3531 3532 // If we have successfully retrieved an video player, send success response 3533 if (videoPlayer) { 3534 videoPlayers[stream_index] = videoPlayer; 3535 guac_client.sendAck(stream_index, "OK", 0x0000); 3536 } 3537 3538 // Otherwise, mimetype must be unsupported 3539 else 3540 guac_client.sendAck(stream_index, "BAD TYPE", 0x030F); 3541 3542 } 3543 3544 }; 3545 3546 /** 3547 * Sends a keep-alive ping to the Guacamole server, advising the server 3548 * that the client is still connected and responding. The lastSentKeepAlive 3549 * timestamp is automatically updated as a result of calling this function. 3550 * 3551 * @private 3552 */ 3553 var sendKeepAlive = function sendKeepAlive() { 3554 tunnel.sendMessage('nop'); 3555 lastSentKeepAlive = new Date().getTime(); 3556 }; 3557 3558 /** 3559 * Schedules the next keep-alive ping based on the KEEP_ALIVE_FREQUENCY and 3560 * the time that the last ping was sent, if ever. If enough time has 3561 * elapsed that a ping should have already been sent, calling this function 3562 * will send that ping immediately. 3563 * 3564 * @private 3565 */ 3566 var scheduleKeepAlive = function scheduleKeepAlive() { 3567 3568 window.clearTimeout(keepAliveTimeout); 3569 3570 var currentTime = new Date().getTime(); 3571 var keepAliveDelay = Math.max(lastSentKeepAlive + KEEP_ALIVE_FREQUENCY - currentTime, 0); 3572 3573 // Ping server regularly to keep connection alive, but send the ping 3574 // immediately if enough time has elapsed that it should have already 3575 // been sent 3576 if (keepAliveDelay > 0) 3577 keepAliveTimeout = window.setTimeout(sendKeepAlive, keepAliveDelay); 3578 else 3579 sendKeepAlive(); 3580 3581 }; 3582 3583 /** 3584 * Stops sending any further keep-alive pings. If a keep-alive ping was 3585 * scheduled to be sent, that ping is cancelled. 3586 * 3587 * @private 3588 */ 3589 var stopKeepAlive = function stopKeepAlive() { 3590 window.clearTimeout(keepAliveTimeout); 3591 }; 3592 3593 tunnel.oninstruction = function(opcode, parameters) { 3594 3595 var handler = instructionHandlers[opcode]; 3596 if (handler) 3597 handler(parameters); 3598 3599 // Leverage network activity to ensure the next keep-alive ping is 3600 // sent, even if the browser is currently throttling timers 3601 scheduleKeepAlive(); 3602 3603 }; 3604 3605 /** 3606 * Sends a disconnect instruction to the server and closes the tunnel. 3607 */ 3608 this.disconnect = function() { 3609 3610 // Only attempt disconnection not disconnected. 3611 if (currentState != Guacamole.Client.State.DISCONNECTED 3612 && currentState != Guacamole.Client.State.DISCONNECTING) { 3613 3614 setState(Guacamole.Client.State.DISCONNECTING); 3615 3616 // Stop sending keep-alive messages 3617 stopKeepAlive(); 3618 3619 // Send disconnect message and disconnect 3620 tunnel.sendMessage("disconnect"); 3621 tunnel.disconnect(); 3622 setState(Guacamole.Client.State.DISCONNECTED); 3623 3624 } 3625 3626 }; 3627 3628 /** 3629 * Connects the underlying tunnel of this Guacamole.Client, passing the 3630 * given arbitrary data to the tunnel during the connection process. 3631 * 3632 * @param {string} data 3633 * Arbitrary connection data to be sent to the underlying tunnel during 3634 * the connection process. 3635 * 3636 * @throws {!Guacamole.Status} 3637 * If an error occurs during connection. 3638 */ 3639 this.connect = function(data) { 3640 3641 setState(Guacamole.Client.State.CONNECTING); 3642 3643 try { 3644 tunnel.connect(data); 3645 } 3646 catch (status) { 3647 setState(Guacamole.Client.State.IDLE); 3648 throw status; 3649 } 3650 3651 // Regularly send keep-alive ping to ensure the server knows we're 3652 // still here, even if not active 3653 scheduleKeepAlive(); 3654 3655 setState(Guacamole.Client.State.WAITING); 3656 }; 3657 3658}; 3659 3660/** 3661 * All possible Guacamole Client states. 3662 * 3663 * @type {!Object.<string, number>} 3664 */ 3665Guacamole.Client.State = { 3666 3667 /** 3668 * The client is idle, with no active connection. 3669 * 3670 * @type number 3671 */ 3672 "IDLE" : 0, 3673 3674 /** 3675 * The client is in the process of establishing a connection. 3676 * 3677 * @type {!number} 3678 */ 3679 "CONNECTING" : 1, 3680 3681 /** 3682 * The client is waiting on further information or a remote server to 3683 * establish the connection. 3684 * 3685 * @type {!number} 3686 */ 3687 "WAITING" : 2, 3688 3689 /** 3690 * The client is actively connected to a remote server. 3691 * 3692 * @type {!number} 3693 */ 3694 "CONNECTED" : 3, 3695 3696 /** 3697 * The client is in the process of disconnecting from the remote server. 3698 * 3699 * @type {!number} 3700 */ 3701 "DISCONNECTING" : 4, 3702 3703 /** 3704 * The client has completed the connection and is no longer connected. 3705 * 3706 * @type {!number} 3707 */ 3708 "DISCONNECTED" : 5 3709 3710}; 3711 3712/** 3713 * Map of all Guacamole binary raster operations to transfer functions. 3714 * 3715 * @private 3716 * @type {!Object.<number, function>} 3717 */ 3718Guacamole.Client.DefaultTransferFunction = { 3719 3720 /* BLACK */ 3721 0x0: function (src, dst) { 3722 dst.red = dst.green = dst.blue = 0x00; 3723 }, 3724 3725 /* WHITE */ 3726 0xF: function (src, dst) { 3727 dst.red = dst.green = dst.blue = 0xFF; 3728 }, 3729 3730 /* SRC */ 3731 0x3: function (src, dst) { 3732 dst.red = src.red; 3733 dst.green = src.green; 3734 dst.blue = src.blue; 3735 dst.alpha = src.alpha; 3736 }, 3737 3738 /* DEST (no-op) */ 3739 0x5: function (src, dst) { 3740 // Do nothing 3741 }, 3742 3743 /* Invert SRC */ 3744 0xC: function (src, dst) { 3745 dst.red = 0xFF & ~src.red; 3746 dst.green = 0xFF & ~src.green; 3747 dst.blue = 0xFF & ~src.blue; 3748 dst.alpha = src.alpha; 3749 }, 3750 3751 /* Invert DEST */ 3752 0xA: function (src, dst) { 3753 dst.red = 0xFF & ~dst.red; 3754 dst.green = 0xFF & ~dst.green; 3755 dst.blue = 0xFF & ~dst.blue; 3756 }, 3757 3758 /* AND */ 3759 0x1: function (src, dst) { 3760 dst.red = ( src.red & dst.red); 3761 dst.green = ( src.green & dst.green); 3762 dst.blue = ( src.blue & dst.blue); 3763 }, 3764 3765 /* NAND */ 3766 0xE: function (src, dst) { 3767 dst.red = 0xFF & ~( src.red & dst.red); 3768 dst.green = 0xFF & ~( src.green & dst.green); 3769 dst.blue = 0xFF & ~( src.blue & dst.blue); 3770 }, 3771 3772 /* OR */ 3773 0x7: function (src, dst) { 3774 dst.red = ( src.red | dst.red); 3775 dst.green = ( src.green | dst.green); 3776 dst.blue = ( src.blue | dst.blue); 3777 }, 3778 3779 /* NOR */ 3780 0x8: function (src, dst) { 3781 dst.red = 0xFF & ~( src.red | dst.red); 3782 dst.green = 0xFF & ~( src.green | dst.green); 3783 dst.blue = 0xFF & ~( src.blue | dst.blue); 3784 }, 3785 3786 /* XOR */ 3787 0x6: function (src, dst) { 3788 dst.red = ( src.red ^ dst.red); 3789 dst.green = ( src.green ^ dst.green); 3790 dst.blue = ( src.blue ^ dst.blue); 3791 }, 3792 3793 /* XNOR */ 3794 0x9: function (src, dst) { 3795 dst.red = 0xFF & ~( src.red ^ dst.red); 3796 dst.green = 0xFF & ~( src.green ^ dst.green); 3797 dst.blue = 0xFF & ~( src.blue ^ dst.blue); 3798 }, 3799 3800 /* AND inverted source */ 3801 0x4: function (src, dst) { 3802 dst.red = 0xFF & (~src.red & dst.red); 3803 dst.green = 0xFF & (~src.green & dst.green); 3804 dst.blue = 0xFF & (~src.blue & dst.blue); 3805 }, 3806 3807 /* OR inverted source */ 3808 0xD: function (src, dst) { 3809 dst.red = 0xFF & (~src.red | dst.red); 3810 dst.green = 0xFF & (~src.green | dst.green); 3811 dst.blue = 0xFF & (~src.blue | dst.blue); 3812 }, 3813 3814 /* AND inverted destination */ 3815 0x2: function (src, dst) { 3816 dst.red = 0xFF & ( src.red & ~dst.red); 3817 dst.green = 0xFF & ( src.green & ~dst.green); 3818 dst.blue = 0xFF & ( src.blue & ~dst.blue); 3819 }, 3820 3821 /* OR inverted destination */ 3822 0xB: function (src, dst) { 3823 dst.red = 0xFF & ( src.red | ~dst.red); 3824 dst.green = 0xFF & ( src.green | ~dst.green); 3825 dst.blue = 0xFF & ( src.blue | ~dst.blue); 3826 } 3827 3828}; 3829 3830/** 3831 * A list of possible messages that can be sent by the server for processing 3832 * by the client. 3833 * 3834 * @type {!Object.<string, number>} 3835 */ 3836Guacamole.Client.Message = { 3837 3838 /** 3839 * A client message that indicates that a user has joined an existing 3840 * connection. This message expects a single additional argument - the 3841 * name of the user who has joined the connection. 3842 * 3843 * @type {!number} 3844 */ 3845 "USER_JOINED": 0x0001, 3846 3847 /** 3848 * A client message that indicates that a user has left an existing 3849 * connection. This message expects a single additional argument - the 3850 * name of the user who has left the connection. 3851 * 3852 * @type {!number} 3853 */ 3854 "USER_LEFT": 0x0002 3855 3856}; 3857/* 3858 * Licensed to the Apache Software Foundation (ASF) under one 3859 * or more contributor license agreements. See the NOTICE file 3860 * distributed with this work for additional information 3861 * regarding copyright ownership. The ASF licenses this file 3862 * to you under the Apache License, Version 2.0 (the 3863 * "License"); you may not use this file except in compliance 3864 * with the License. You may obtain a copy of the License at 3865 * 3866 * http://www.apache.org/licenses/LICENSE-2.0 3867 * 3868 * Unless required by applicable law or agreed to in writing, 3869 * software distributed under the License is distributed on an 3870 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 3871 * KIND, either express or implied. See the License for the 3872 * specific language governing permissions and limitations 3873 * under the License. 3874 */ 3875 3876var Guacamole = Guacamole || {}; 3877 3878/** 3879 * A reader which automatically handles the given input stream, returning 3880 * received blobs as a single data URI built over the course of the stream. 3881 * Note that this object will overwrite any installed event handlers on the 3882 * given Guacamole.InputStream. 3883 * 3884 * @constructor 3885 * @param {!Guacamole.InputStream} stream 3886 * The stream that data will be read from. 3887 * 3888 * @param {!string} mimetype 3889 * The mimetype of the data being received. 3890 */ 3891Guacamole.DataURIReader = function(stream, mimetype) { 3892 3893 /** 3894 * Reference to this Guacamole.DataURIReader. 3895 * 3896 * @private 3897 * @type {!Guacamole.DataURIReader} 3898 */ 3899 var guac_reader = this; 3900 3901 /** 3902 * Current data URI. 3903 * 3904 * @private 3905 * @type {!string} 3906 */ 3907 var uri = 'data:' + mimetype + ';base64,'; 3908 3909 // Receive blobs as array buffers 3910 stream.onblob = function dataURIReaderBlob(data) { 3911 3912 // Currently assuming data will ALWAYS be safe to simply append. This 3913 // will not be true if the received base64 data encodes a number of 3914 // bytes that isn't a multiple of three (as base64 expands in a ratio 3915 // of exactly 3:4). 3916 uri += data; 3917 3918 }; 3919 3920 // Simply call onend when end received 3921 stream.onend = function dataURIReaderEnd() { 3922 if (guac_reader.onend) 3923 guac_reader.onend(); 3924 }; 3925 3926 /** 3927 * Returns the data URI of all data received through the underlying stream 3928 * thus far. 3929 * 3930 * @returns {!string} 3931 * The data URI of all data received through the underlying stream thus 3932 * far. 3933 */ 3934 this.getURI = function getURI() { 3935 return uri; 3936 }; 3937 3938 /** 3939 * Fired once this stream is finished and no further data will be written. 3940 * 3941 * @event 3942 */ 3943 this.onend = null; 3944 3945};/* 3946 * Licensed to the Apache Software Foundation (ASF) under one 3947 * or more contributor license agreements. See the NOTICE file 3948 * distributed with this work for additional information 3949 * regarding copyright ownership. The ASF licenses this file 3950 * to you under the Apache License, Version 2.0 (the 3951 * "License"); you may not use this file except in compliance 3952 * with the License. You may obtain a copy of the License at 3953 * 3954 * http://www.apache.org/licenses/LICENSE-2.0 3955 * 3956 * Unless required by applicable law or agreed to in writing, 3957 * software distributed under the License is distributed on an 3958 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 3959 * KIND, either express or implied. See the License for the 3960 * specific language governing permissions and limitations 3961 * under the License. 3962 */ 3963 3964var Guacamole = Guacamole || {}; 3965 3966/** 3967 * The Guacamole display. The display does not deal with the Guacamole 3968 * protocol, and instead implements a set of graphical operations which 3969 * embody the set of operations present in the protocol. The order operations 3970 * are executed is guaranteed to be in the same order as their corresponding 3971 * functions are called. 3972 * 3973 * @constructor 3974 */ 3975Guacamole.Display = function() { 3976 3977 /** 3978 * Reference to this Guacamole.Display. 3979 * @private 3980 */ 3981 var guac_display = this; 3982 3983 var displayWidth = 0; 3984 var displayHeight = 0; 3985 var displayScale = 1; 3986 3987 // Create display 3988 var display = document.createElement("div"); 3989 display.style.position = "relative"; 3990 display.style.width = displayWidth + "px"; 3991 display.style.height = displayHeight + "px"; 3992 3993 // Ensure transformations on display originate at 0,0 3994 display.style.transformOrigin = 3995 display.style.webkitTransformOrigin = 3996 display.style.MozTransformOrigin = 3997 display.style.OTransformOrigin = 3998 display.style.msTransformOrigin = 3999 "0 0"; 4000 4001 // Create default layer 4002 var default_layer = new Guacamole.Display.VisibleLayer(displayWidth, displayHeight); 4003 4004 // Create cursor layer 4005 var cursor = new Guacamole.Display.VisibleLayer(0, 0); 4006 cursor.setChannelMask(Guacamole.Layer.SRC); 4007 4008 // Add default layer and cursor to display 4009 display.appendChild(default_layer.getElement()); 4010 display.appendChild(cursor.getElement()); 4011 4012 // Create bounding div 4013 var bounds = document.createElement("div"); 4014 bounds.style.position = "relative"; 4015 bounds.style.width = (displayWidth*displayScale) + "px"; 4016 bounds.style.height = (displayHeight*displayScale) + "px"; 4017 4018 // Add display to bounds 4019 bounds.appendChild(display); 4020 4021 /** 4022 * The X coordinate of the hotspot of the mouse cursor. The hotspot is 4023 * the relative location within the image of the mouse cursor at which 4024 * each click occurs. 4025 * 4026 * @type {!number} 4027 */ 4028 this.cursorHotspotX = 0; 4029 4030 /** 4031 * The Y coordinate of the hotspot of the mouse cursor. The hotspot is 4032 * the relative location within the image of the mouse cursor at which 4033 * each click occurs. 4034 * 4035 * @type {!number} 4036 */ 4037 this.cursorHotspotY = 0; 4038 4039 /** 4040 * The current X coordinate of the local mouse cursor. This is not 4041 * necessarily the location of the actual mouse - it refers only to 4042 * the location of the cursor image within the Guacamole display, as 4043 * last set by moveCursor(). 4044 * 4045 * @type {!number} 4046 */ 4047 this.cursorX = 0; 4048 4049 /** 4050 * The current X coordinate of the local mouse cursor. This is not 4051 * necessarily the location of the actual mouse - it refers only to 4052 * the location of the cursor image within the Guacamole display, as 4053 * last set by moveCursor(). 4054 * 4055 * @type {!number} 4056 */ 4057 this.cursorY = 0; 4058 4059 /** 4060 * The number of milliseconds over which display rendering statistics 4061 * should be gathered, dispatching {@link #onstatistics} events as those 4062 * statistics are available. If set to zero, no statistics will be 4063 * gathered. 4064 * 4065 * @default 0 4066 * @type {!number} 4067 */ 4068 this.statisticWindow = 0; 4069 4070 /** 4071 * Fired when the default layer (and thus the entire Guacamole display) 4072 * is resized. 4073 * 4074 * @event 4075 * @param {!number} width 4076 * The new width of the Guacamole display. 4077 * 4078 * @param {!number} height 4079 * The new height of the Guacamole display. 4080 */ 4081 this.onresize = null; 4082 4083 /** 4084 * Fired whenever the local cursor image is changed. This can be used to 4085 * implement special handling of the client-side cursor, or to override 4086 * the default use of a software cursor layer. 4087 * 4088 * @event 4089 * @param {!HTMLCanvasElement} canvas 4090 * The cursor image. 4091 * 4092 * @param {!number} x 4093 * The X-coordinate of the cursor hotspot. 4094 * 4095 * @param {!number} y 4096 * The Y-coordinate of the cursor hotspot. 4097 */ 4098 this.oncursor = null; 4099 4100 /** 4101 * Fired whenever performance statistics are available for recently- 4102 * rendered frames. This event will fire only if {@link #statisticWindow} 4103 * is non-zero. 4104 * 4105 * @event 4106 * @param {!Guacamole.Display.Statistics} stats 4107 * An object containing general rendering performance statistics for 4108 * the remote desktop, Guacamole server, and Guacamole client. 4109 */ 4110 this.onstatistics = null; 4111 4112 /** 4113 * The queue of all pending Tasks. Tasks will be run in order, with new 4114 * tasks added at the end of the queue and old tasks removed from the 4115 * front of the queue (FIFO). These tasks will eventually be grouped 4116 * into a Frame. 4117 * 4118 * @private 4119 * @type {!Task[]} 4120 */ 4121 var tasks = []; 4122 4123 /** 4124 * The queue of all frames. Each frame is a pairing of an array of tasks 4125 * and a callback which must be called when the frame is rendered. 4126 * 4127 * @private 4128 * @type {!Frame[]} 4129 */ 4130 var frames = []; 4131 4132 /** 4133 * The ID of the animation frame request returned by the last call to 4134 * requestAnimationFrame(). This value will only be set if the browser 4135 * supports requestAnimationFrame(), if a frame render is currently 4136 * pending, and if the current browser tab is currently focused (likely to 4137 * handle requests for animation frames). In all other cases, this will be 4138 * null. 4139 * 4140 * @private 4141 * @type {number} 4142 */ 4143 var inProgressFrame = null; 4144 4145 /** 4146 * Flushes all pending frames synchronously. This function will block until 4147 * all pending frames have rendered. If a frame is currently blocked by an 4148 * asynchronous operation like an image load, this function will return 4149 * after reaching that operation and the flush operation will 4150 * automamtically resume after that operation completes. 4151 * 4152 * @private 4153 */ 4154 var syncFlush = function syncFlush() { 4155 4156 var localTimestamp = 0; 4157 var remoteTimestamp = 0; 4158 4159 var renderedLogicalFrames = 0; 4160 var rendered_frames = 0; 4161 4162 // Draw all pending frames, if ready 4163 while (rendered_frames < frames.length) { 4164 4165 var frame = frames[rendered_frames]; 4166 if (!frame.isReady()) 4167 break; 4168 4169 frame.flush(); 4170 4171 localTimestamp = frame.localTimestamp; 4172 remoteTimestamp = frame.remoteTimestamp; 4173 renderedLogicalFrames += frame.logicalFrames; 4174 rendered_frames++; 4175 4176 } 4177 4178 // Remove rendered frames from array 4179 frames.splice(0, rendered_frames); 4180 4181 if (rendered_frames) 4182 notifyFlushed(localTimestamp, remoteTimestamp, renderedLogicalFrames); 4183 4184 }; 4185 4186 /** 4187 * Flushes all pending frames asynchronously. This function returns 4188 * immediately, relying on requestAnimationFrame() to dictate when each 4189 * frame should be flushed. 4190 * 4191 * @private 4192 */ 4193 var asyncFlush = function asyncFlush() { 4194 4195 var continueFlush = function continueFlush() { 4196 4197 // We're no longer waiting to render a frame 4198 inProgressFrame = null; 4199 4200 // Nothing to do if there are no frames remaining 4201 if (!frames.length) 4202 return; 4203 4204 // Flush the next frame only if it is ready (not awaiting 4205 // completion of some asynchronous operation like an image load) 4206 if (frames[0].isReady()) { 4207 var frame = frames.shift(); 4208 frame.flush(); 4209 notifyFlushed(frame.localTimestamp, frame.remoteTimestamp, frame.logicalFrames); 4210 } 4211 4212 // Request yet another animation frame if frames remain to be 4213 // flushed 4214 if (frames.length) 4215 inProgressFrame = window.requestAnimationFrame(continueFlush); 4216 4217 }; 4218 4219 // Begin flushing frames if not already waiting to render a frame 4220 if (!inProgressFrame) 4221 inProgressFrame = window.requestAnimationFrame(continueFlush); 4222 4223 }; 4224 4225 /** 4226 * Recently-gathered display render statistics, as made available by calls 4227 * to notifyFlushed(). The contents of this array will be trimmed to 4228 * contain only up to {@link #statisticWindow} milliseconds of statistics. 4229 * 4230 * @private 4231 * @type {Guacamole.Display.Statistics[]} 4232 */ 4233 var statistics = []; 4234 4235 /** 4236 * Notifies that one or more frames have been successfully rendered 4237 * (flushed) to the display. 4238 * 4239 * @private 4240 * @param {!number} localTimestamp 4241 * The local timestamp of the point in time at which the most recent, 4242 * flushed frame was received by the display, in milliseconds since the 4243 * Unix Epoch. 4244 * 4245 * @param {!number} remoteTimestamp 4246 * The remote timestamp of sync instruction associated with the most 4247 * recent, flushed frame received by the display. This timestamp is in 4248 * milliseconds, but is arbitrary, having meaning only relative to 4249 * other timestamps in the same connection. 4250 * 4251 * @param {!number} logicalFrames 4252 * The number of remote desktop frames that were flushed. 4253 */ 4254 var notifyFlushed = function notifyFlushed(localTimestamp, remoteTimestamp, logicalFrames) { 4255 4256 // Ignore if statistics are not being gathered 4257 if (!guac_display.statisticWindow) 4258 return; 4259 4260 var current = new Date().getTime(); 4261 4262 // Find the first statistic that is still within the configured time 4263 // window 4264 for (var first = 0; first < statistics.length; first++) { 4265 if (current - statistics[first].timestamp <= guac_display.statisticWindow) 4266 break; 4267 } 4268 4269 // Remove all statistics except those within the time window 4270 statistics.splice(0, first - 1); 4271 4272 // Record statistics for latest frame 4273 statistics.push({ 4274 localTimestamp : localTimestamp, 4275 remoteTimestamp : remoteTimestamp, 4276 timestamp : current, 4277 frames : logicalFrames 4278 }); 4279 4280 // Determine the actual time interval of the available statistics (this 4281 // will not perfectly match the configured interval, which is an upper 4282 // bound) 4283 var statDuration = (statistics[statistics.length - 1].timestamp - statistics[0].timestamp) / 1000; 4284 4285 // Determine the amount of time that elapsed remotely (within the 4286 // remote desktop) 4287 var remoteDuration = (statistics[statistics.length - 1].remoteTimestamp - statistics[0].remoteTimestamp) / 1000; 4288 4289 // Calculate the number of frames that have been rendered locally 4290 // within the configured time interval 4291 var localFrames = statistics.length; 4292 4293 // Calculate the number of frames actually received from the remote 4294 // desktop by the Guacamole server 4295 var remoteFrames = statistics.reduce(function sumFrames(prev, stat) { 4296 return prev + stat.frames; 4297 }, 0); 4298 4299 // Calculate the number of frames that the Guacamole server had to 4300 // drop or combine with other frames 4301 var drops = statistics.reduce(function sumDrops(prev, stat) { 4302 return prev + Math.max(0, stat.frames - 1); 4303 }, 0); 4304 4305 // Produce lag and FPS statistics from above raw measurements 4306 var stats = new Guacamole.Display.Statistics({ 4307 processingLag : current - localTimestamp, 4308 desktopFps : (remoteDuration && remoteFrames) ? remoteFrames / remoteDuration : null, 4309 clientFps : statDuration ? localFrames / statDuration : null, 4310 serverFps : remoteDuration ? localFrames / remoteDuration : null, 4311 dropRate : remoteDuration ? drops / remoteDuration : null 4312 }); 4313 4314 // Notify of availability of new statistics 4315 if (guac_display.onstatistics) 4316 guac_display.onstatistics(stats); 4317 4318 }; 4319 4320 // Switch from asynchronous frame handling to synchronous frame handling if 4321 // requestAnimationFrame() is unlikely to be usable (browsers may not 4322 // invoke the animation frame callback if the relevant tab is not focused) 4323 window.addEventListener('blur', function switchToSyncFlush() { 4324 if (inProgressFrame && !document.hasFocus()) { 4325 4326 // Cancel pending asynchronous processing of frame ... 4327 window.cancelAnimationFrame(inProgressFrame); 4328 inProgressFrame = null; 4329 4330 // ... and instead process it synchronously 4331 syncFlush(); 4332 4333 } 4334 }, true); 4335 4336 /** 4337 * Flushes all pending frames. 4338 * @private 4339 */ 4340 function __flush_frames() { 4341 4342 if (window.requestAnimationFrame && document.hasFocus()) 4343 asyncFlush(); 4344 else 4345 syncFlush(); 4346 4347 } 4348 4349 /** 4350 * An ordered list of tasks which must be executed atomically. Once 4351 * executed, an associated (and optional) callback will be called. 4352 * 4353 * @private 4354 * @constructor 4355 * @param {function} [callback] 4356 * The function to call when this frame is rendered. 4357 * 4358 * @param {!Task[]} tasks 4359 * The set of tasks which must be executed to render this frame. 4360 * 4361 * @param {number} [timestamp] 4362 * The remote timestamp of sync instruction associated with this frame. 4363 * This timestamp is in milliseconds, but is arbitrary, having meaning 4364 * only relative to other remote timestamps in the same connection. If 4365 * omitted, a compatible but local timestamp will be used instead. 4366 * 4367 * @param {number} [logicalFrames=0] 4368 * The number of remote desktop frames that were combined to produce 4369 * this frame, or zero if this value is unknown or inapplicable. 4370 */ 4371 var Frame = function Frame(callback, tasks, timestamp, logicalFrames) { 4372 4373 /** 4374 * The local timestamp of the point in time at which this frame was 4375 * received by the display, in milliseconds since the Unix Epoch. 4376 * 4377 * @type {!number} 4378 */ 4379 this.localTimestamp = new Date().getTime(); 4380 4381 /** 4382 * The remote timestamp of sync instruction associated with this frame. 4383 * This timestamp is in milliseconds, but is arbitrary, having meaning 4384 * only relative to other remote timestamps in the same connection. 4385 * 4386 * @type {!number} 4387 */ 4388 this.remoteTimestamp = timestamp || this.localTimestamp; 4389 4390 /** 4391 * The number of remote desktop frames that were combined to produce 4392 * this frame. If unknown or not applicable, this will be zero. 4393 * 4394 * @type {!number} 4395 */ 4396 this.logicalFrames = logicalFrames || 0; 4397 4398 /** 4399 * Cancels rendering of this frame and all associated tasks. The 4400 * callback provided at construction time, if any, is not invoked. 4401 */ 4402 this.cancel = function cancel() { 4403 4404 callback = null; 4405 4406 tasks.forEach(function cancelTask(task) { 4407 task.cancel(); 4408 }); 4409 4410 tasks = []; 4411 4412 }; 4413 4414 /** 4415 * Returns whether this frame is ready to be rendered. This function 4416 * returns true if and only if ALL underlying tasks are unblocked. 4417 * 4418 * @returns {!boolean} 4419 * true if all underlying tasks are unblocked, false otherwise. 4420 */ 4421 this.isReady = function() { 4422 4423 // Search for blocked tasks 4424 for (var i=0; i < tasks.length; i++) { 4425 if (tasks[i].blocked) 4426 return false; 4427 } 4428 4429 // If no blocked tasks, the frame is ready 4430 return true; 4431 4432 }; 4433 4434 /** 4435 * Renders this frame, calling the associated callback, if any, after 4436 * the frame is complete. This function MUST only be called when no 4437 * blocked tasks exist. Calling this function with blocked tasks 4438 * will result in undefined behavior. 4439 */ 4440 this.flush = function() { 4441 4442 // Draw all pending tasks. 4443 for (var i=0; i < tasks.length; i++) 4444 tasks[i].execute(); 4445 4446 // Call callback 4447 if (callback) callback(); 4448 4449 }; 4450 4451 }; 4452 4453 /** 4454 * A container for an task handler. Each operation which must be ordered 4455 * is associated with a Task that goes into a task queue. Tasks in this 4456 * queue are executed in order once their handlers are set, while Tasks 4457 * without handlers block themselves and any following Tasks from running. 4458 * 4459 * @constructor 4460 * @private 4461 * @param {function} [taskHandler] 4462 * The function to call when this task runs, if any. 4463 * 4464 * @param {boolean} [blocked] 4465 * Whether this task should start blocked. 4466 */ 4467 function Task(taskHandler, blocked) { 4468 4469 /** 4470 * Reference to this Task. 4471 * 4472 * @private 4473 * @type {!Guacamole.Display.Task} 4474 */ 4475 var task = this; 4476 4477 /** 4478 * Whether this Task is blocked. 4479 * 4480 * @type {boolean} 4481 */ 4482 this.blocked = blocked; 4483 4484 /** 4485 * Cancels this task such that it will not run. The task handler 4486 * provided at construction time, if any, is not invoked. Calling 4487 * execute() after calling this function has no effect. 4488 */ 4489 this.cancel = function cancel() { 4490 task.blocked = false; 4491 taskHandler = null; 4492 }; 4493 4494 /** 4495 * Unblocks this Task, allowing it to run. 4496 */ 4497 this.unblock = function() { 4498 if (task.blocked) { 4499 task.blocked = false; 4500 __flush_frames(); 4501 } 4502 }; 4503 4504 /** 4505 * Calls the handler associated with this task IMMEDIATELY. This 4506 * function does not track whether this task is marked as blocked. 4507 * Enforcing the blocked status of tasks is up to the caller. 4508 */ 4509 this.execute = function() { 4510 if (taskHandler) taskHandler(); 4511 }; 4512 4513 } 4514 4515 /** 4516 * Schedules a task for future execution. The given handler will execute 4517 * immediately after all previous tasks upon frame flush, unless this 4518 * task is blocked. If any tasks is blocked, the entire frame will not 4519 * render (and no tasks within will execute) until all tasks are unblocked. 4520 * 4521 * @private 4522 * @param {function} [handler] 4523 * The function to call when possible, if any. 4524 * 4525 * @param {boolean} [blocked] 4526 * Whether the task should start blocked. 4527 * 4528 * @returns {!Task} 4529 * The Task created and added to the queue for future running. 4530 */ 4531 function scheduleTask(handler, blocked) { 4532 var task = new Task(handler, blocked); 4533 tasks.push(task); 4534 return task; 4535 } 4536 4537 /** 4538 * Returns the element which contains the Guacamole display. 4539 * 4540 * @return {!Element} 4541 * The element containing the Guacamole display. 4542 */ 4543 this.getElement = function() { 4544 return bounds; 4545 }; 4546 4547 /** 4548 * Returns the width of this display. 4549 * 4550 * @return {!number} 4551 * The width of this display; 4552 */ 4553 this.getWidth = function() { 4554 return displayWidth; 4555 }; 4556 4557 /** 4558 * Returns the height of this display. 4559 * 4560 * @return {!number} 4561 * The height of this display; 4562 */ 4563 this.getHeight = function() { 4564 return displayHeight; 4565 }; 4566 4567 /** 4568 * Returns the default layer of this display. Each Guacamole display always 4569 * has at least one layer. Other layers can optionally be created within 4570 * this layer, but the default layer cannot be removed and is the absolute 4571 * ancestor of all other layers. 4572 * 4573 * @return {!Guacamole.Display.VisibleLayer} 4574 * The default layer. 4575 */ 4576 this.getDefaultLayer = function() { 4577 return default_layer; 4578 }; 4579 4580 /** 4581 * Returns the cursor layer of this display. Each Guacamole display contains 4582 * a layer for the image of the mouse cursor. This layer is a special case 4583 * and exists above all other layers, similar to the hardware mouse cursor. 4584 * 4585 * @return {!Guacamole.Display.VisibleLayer} 4586 * The cursor layer. 4587 */ 4588 this.getCursorLayer = function() { 4589 return cursor; 4590 }; 4591 4592 /** 4593 * Creates a new layer. The new layer will be a direct child of the default 4594 * layer, but can be moved to be a child of any other layer. Layers returned 4595 * by this function are visible. 4596 * 4597 * @return {!Guacamole.Display.VisibleLayer} 4598 * The newly-created layer. 4599 */ 4600 this.createLayer = function() { 4601 var layer = new Guacamole.Display.VisibleLayer(displayWidth, displayHeight); 4602 layer.move(default_layer, 0, 0, 0); 4603 return layer; 4604 }; 4605 4606 /** 4607 * Creates a new buffer. Buffers are invisible, off-screen surfaces. They 4608 * are implemented in the same manner as layers, but do not provide the 4609 * same nesting semantics. 4610 * 4611 * @return {!Guacamole.Layer} 4612 * The newly-created buffer. 4613 */ 4614 this.createBuffer = function() { 4615 var buffer = new Guacamole.Layer(0, 0); 4616 buffer.autosize = 1; 4617 return buffer; 4618 }; 4619 4620 /** 4621 * Flush all pending draw tasks, if possible, as a new frame. If the entire 4622 * frame is not ready, the flush will wait until all required tasks are 4623 * unblocked. 4624 * 4625 * @param {function} [callback] 4626 * The function to call when this frame is flushed. This may happen 4627 * immediately, or later when blocked tasks become unblocked. 4628 * 4629 * @param {number} timestamp 4630 * The remote timestamp of sync instruction associated with this frame. 4631 * This timestamp is in milliseconds, but is arbitrary, having meaning 4632 * only relative to other remote timestamps in the same connection. 4633 * 4634 * @param {number} logicalFrames 4635 * The number of remote desktop frames that were combined to produce 4636 * this frame. 4637 */ 4638 this.flush = function(callback, timestamp, logicalFrames) { 4639 4640 // Add frame, reset tasks 4641 frames.push(new Frame(callback, tasks, timestamp, logicalFrames)); 4642 tasks = []; 4643 4644 // Attempt flush 4645 __flush_frames(); 4646 4647 }; 4648 4649 /** 4650 * Cancels rendering of all pending frames and associated rendering 4651 * operations. The callbacks provided to outstanding past calls to flush(), 4652 * if any, are not invoked. 4653 */ 4654 this.cancel = function cancel() { 4655 4656 frames.forEach(function cancelFrame(frame) { 4657 frame.cancel(); 4658 }); 4659 4660 frames = []; 4661 4662 tasks.forEach(function cancelTask(task) { 4663 task.cancel(); 4664 }); 4665 4666 tasks = []; 4667 4668 }; 4669 4670 /** 4671 * Sets the hotspot and image of the mouse cursor displayed within the 4672 * Guacamole display. 4673 * 4674 * @param {!number} hotspotX 4675 * The X coordinate of the cursor hotspot. 4676 * 4677 * @param {!number} hotspotY 4678 * The Y coordinate of the cursor hotspot. 4679 * 4680 * @param {!Guacamole.Layer} layer 4681 * The source layer containing the data which should be used as the 4682 * mouse cursor image. 4683 * 4684 * @param {!number} srcx 4685 * The X coordinate of the upper-left corner of the rectangle within 4686 * the source layer's coordinate space to copy data from. 4687 * 4688 * @param {!number} srcy 4689 * The Y coordinate of the upper-left corner of the rectangle within 4690 * the source layer's coordinate space to copy data from. 4691 * 4692 * @param {!number} srcw 4693 * The width of the rectangle within the source layer's coordinate 4694 * space to copy data from. 4695 * 4696 * @param {!number} srch 4697 * The height of the rectangle within the source layer's coordinate 4698 * space to copy data from. 4699 */ 4700 this.setCursor = function(hotspotX, hotspotY, layer, srcx, srcy, srcw, srch) { 4701 scheduleTask(function __display_set_cursor() { 4702 4703 // Set hotspot 4704 guac_display.cursorHotspotX = hotspotX; 4705 guac_display.cursorHotspotY = hotspotY; 4706 4707 // Reset cursor size 4708 cursor.resize(srcw, srch); 4709 4710 // Draw cursor to cursor layer 4711 cursor.copy(layer, srcx, srcy, srcw, srch, 0, 0); 4712 guac_display.moveCursor(guac_display.cursorX, guac_display.cursorY); 4713 4714 // Fire cursor change event 4715 if (guac_display.oncursor) 4716 guac_display.oncursor(cursor.toCanvas(), hotspotX, hotspotY); 4717 4718 }); 4719 }; 4720 4721 /** 4722 * Sets whether the software-rendered cursor is shown. This cursor differs 4723 * from the hardware cursor in that it is built into the Guacamole.Display, 4724 * and relies on its own Guacamole layer to render. 4725 * 4726 * @param {boolean} [shown=true] 4727 * Whether to show the software cursor. 4728 */ 4729 this.showCursor = function(shown) { 4730 4731 var element = cursor.getElement(); 4732 var parent = element.parentNode; 4733 4734 // Remove from DOM if hidden 4735 if (shown === false) { 4736 if (parent) 4737 parent.removeChild(element); 4738 } 4739 4740 // Otherwise, ensure cursor is child of display 4741 else if (parent !== display) 4742 display.appendChild(element); 4743 4744 }; 4745 4746 /** 4747 * Sets the location of the local cursor to the given coordinates. For the 4748 * sake of responsiveness, this function performs its action immediately. 4749 * Cursor motion is not maintained within atomic frames. 4750 * 4751 * @param {!number} x 4752 * The X coordinate to move the cursor to. 4753 * 4754 * @param {!number} y 4755 * The Y coordinate to move the cursor to. 4756 */ 4757 this.moveCursor = function(x, y) { 4758 4759 // Move cursor layer 4760 cursor.translate(x - guac_display.cursorHotspotX, 4761 y - guac_display.cursorHotspotY); 4762 4763 // Update stored position 4764 guac_display.cursorX = x; 4765 guac_display.cursorY = y; 4766 4767 }; 4768 4769 /** 4770 * Changes the size of the given Layer to the given width and height. 4771 * Resizing is only attempted if the new size provided is actually different 4772 * from the current size. 4773 * 4774 * @param {!Guacamole.Layer} layer 4775 * The layer to resize. 4776 * 4777 * @param {!number} width 4778 * The new width. 4779 * 4780 * @param {!number} height 4781 * The new height. 4782 */ 4783 this.resize = function(layer, width, height) { 4784 scheduleTask(function __display_resize() { 4785 4786 layer.resize(width, height); 4787 4788 // Resize display if default layer is resized 4789 if (layer === default_layer) { 4790 4791 // Update (set) display size 4792 displayWidth = width; 4793 displayHeight = height; 4794 display.style.width = displayWidth + "px"; 4795 display.style.height = displayHeight + "px"; 4796 4797 // Update bounds size 4798 bounds.style.width = (displayWidth*displayScale) + "px"; 4799 bounds.style.height = (displayHeight*displayScale) + "px"; 4800 4801 // Notify of resize 4802 if (guac_display.onresize) 4803 guac_display.onresize(width, height); 4804 4805 } 4806 4807 }); 4808 }; 4809 4810 /** 4811 * Draws the specified image at the given coordinates. The image specified 4812 * must already be loaded. 4813 * 4814 * @param {!Guacamole.Layer} layer 4815 * The layer to draw upon. 4816 * 4817 * @param {!number} x 4818 * The destination X coordinate. 4819 * 4820 * @param {!number} y 4821 * The destination Y coordinate. 4822 * 4823 * @param {!CanvasImageSource} image 4824 * The image to draw. Note that this not a URL. 4825 */ 4826 this.drawImage = function(layer, x, y, image) { 4827 scheduleTask(function __display_drawImage() { 4828 layer.drawImage(x, y, image); 4829 }); 4830 }; 4831 4832 /** 4833 * Draws the image contained within the specified Blob at the given 4834 * coordinates. The Blob specified must already be populated with image 4835 * data. 4836 * 4837 * @param {!Guacamole.Layer} layer 4838 * The layer to draw upon. 4839 * 4840 * @param {!number} x 4841 * The destination X coordinate. 4842 * 4843 * @param {!number} y 4844 * The destination Y coordinate. 4845 * 4846 * @param {!Blob} blob 4847 * The Blob containing the image data to draw. 4848 */ 4849 this.drawBlob = function(layer, x, y, blob) { 4850 4851 var task; 4852 4853 // Prefer createImageBitmap() over blob URLs if available 4854 if (window.createImageBitmap) { 4855 4856 var bitmap; 4857 4858 // Draw image once loaded 4859 task = scheduleTask(function drawImageBitmap() { 4860 layer.drawImage(x, y, bitmap); 4861 }, true); 4862 4863 // Load image from provided blob 4864 window.createImageBitmap(blob).then(function bitmapLoaded(decoded) { 4865 bitmap = decoded; 4866 task.unblock(); 4867 }); 4868 4869 } 4870 4871 // Use blob URLs and the Image object if createImageBitmap() is 4872 // unavailable 4873 else { 4874 4875 // Create URL for blob 4876 var url = URL.createObjectURL(blob); 4877 4878 // Draw and free blob URL when ready 4879 task = scheduleTask(function __display_drawBlob() { 4880 4881 // Draw the image only if it loaded without errors 4882 if (image.width && image.height) 4883 layer.drawImage(x, y, image); 4884 4885 // Blob URL no longer needed 4886 URL.revokeObjectURL(url); 4887 4888 }, true); 4889 4890 // Load image from URL 4891 var image = new Image(); 4892 image.onload = task.unblock; 4893 image.onerror = task.unblock; 4894 image.src = url; 4895 4896 } 4897 4898 }; 4899 4900 /** 4901 * Draws the image within the given stream at the given coordinates. The 4902 * image will be loaded automatically, and this and any future operations 4903 * will wait for the image to finish loading. This function will 4904 * automatically choose an appropriate method for reading and decoding the 4905 * given image stream, and should be preferred for received streams except 4906 * where manual decoding of the stream is unavoidable. 4907 * 4908 * @param {!Guacamole.Layer} layer 4909 * The layer to draw upon. 4910 * 4911 * @param {!number} x 4912 * The destination X coordinate. 4913 * 4914 * @param {!number} y 4915 * The destination Y coordinate. 4916 * 4917 * @param {!Guacamole.InputStream} stream 4918 * The stream along which image data will be received. 4919 * 4920 * @param {!string} mimetype 4921 * The mimetype of the image within the stream. 4922 */ 4923 this.drawStream = function drawStream(layer, x, y, stream, mimetype) { 4924 4925 // If createImageBitmap() is available, load the image as a blob so 4926 // that function can be used 4927 if (window.createImageBitmap) { 4928 var reader = new Guacamole.BlobReader(stream, mimetype); 4929 reader.onend = function drawImageBlob() { 4930 guac_display.drawBlob(layer, x, y, reader.getBlob()); 4931 }; 4932 } 4933 4934 // Lacking createImageBitmap(), fall back to data URIs and the Image 4935 // object 4936 else { 4937 var reader = new Guacamole.DataURIReader(stream, mimetype); 4938 reader.onend = function drawImageDataURI() { 4939 guac_display.draw(layer, x, y, reader.getURI()); 4940 }; 4941 } 4942 4943 }; 4944 4945 /** 4946 * Draws the image at the specified URL at the given coordinates. The image 4947 * will be loaded automatically, and this and any future operations will 4948 * wait for the image to finish loading. 4949 * 4950 * @param {!Guacamole.Layer} layer 4951 * The layer to draw upon. 4952 * 4953 * @param {!number} x 4954 * The destination X coordinate. 4955 * 4956 * @param {!number} y 4957 * The destination Y coordinate. 4958 * 4959 * @param {!string} url 4960 * The URL of the image to draw. 4961 */ 4962 this.draw = function(layer, x, y, url) { 4963 4964 var task = scheduleTask(function __display_draw() { 4965 4966 // Draw the image only if it loaded without errors 4967 if (image.width && image.height) 4968 layer.drawImage(x, y, image); 4969 4970 }, true); 4971 4972 var image = new Image(); 4973 image.onload = task.unblock; 4974 image.onerror = task.unblock; 4975 image.src = url; 4976 4977 }; 4978 4979 /** 4980 * Plays the video at the specified URL within this layer. The video 4981 * will be loaded automatically, and this and any future operations will 4982 * wait for the video to finish loading. Future operations will not be 4983 * executed until the video finishes playing. 4984 * 4985 * @param {!Guacamole.Layer} layer 4986 * The layer to draw upon. 4987 * 4988 * @param {!string} mimetype 4989 * The mimetype of the video to play. 4990 * 4991 * @param {!number} duration 4992 * The duration of the video in milliseconds. 4993 * 4994 * @param {!string} url 4995 * The URL of the video to play. 4996 */ 4997 this.play = function(layer, mimetype, duration, url) { 4998 4999 // Start loading the video 5000 var video = document.createElement("video"); 5001 video.type = mimetype; 5002 video.src = url; 5003 5004 // Start copying frames when playing 5005 video.addEventListener("play", function() { 5006 5007 function render_callback() { 5008 layer.drawImage(0, 0, video); 5009 if (!video.ended) 5010 window.setTimeout(render_callback, 20); 5011 } 5012 5013 render_callback(); 5014 5015 }, false); 5016 5017 scheduleTask(video.play); 5018 5019 }; 5020 5021 /** 5022 * Transfer a rectangle of image data from one Layer to this Layer using the 5023 * specified transfer function. 5024 * 5025 * @param {!Guacamole.Layer} srcLayer 5026 * The Layer to copy image data from. 5027 * 5028 * @param {!number} srcx 5029 * The X coordinate of the upper-left corner of the rectangle within 5030 * the source Layer's coordinate space to copy data from. 5031 * 5032 * @param {!number} srcy 5033 * The Y coordinate of the upper-left corner of the rectangle within 5034 * the source Layer's coordinate space to copy data from. 5035 * 5036 * @param {!number} srcw 5037 * The width of the rectangle within the source Layer's coordinate 5038 * space to copy data from. 5039 * 5040 * @param {!number} srch 5041 * The height of the rectangle within the source Layer's coordinate 5042 * space to copy data from. 5043 * 5044 * @param {!Guacamole.Layer} dstLayer 5045 * The layer to draw upon. 5046 * 5047 * @param {!number} x 5048 * The destination X coordinate. 5049 * 5050 * @param {!number} y 5051 * The destination Y coordinate. 5052 * 5053 * @param {!function} transferFunction 5054 * The transfer function to use to transfer data from source to 5055 * destination. 5056 */ 5057 this.transfer = function(srcLayer, srcx, srcy, srcw, srch, dstLayer, x, y, transferFunction) { 5058 scheduleTask(function __display_transfer() { 5059 dstLayer.transfer(srcLayer, srcx, srcy, srcw, srch, x, y, transferFunction); 5060 }); 5061 }; 5062 5063 /** 5064 * Put a rectangle of image data from one Layer to this Layer directly 5065 * without performing any alpha blending. Simply copy the data. 5066 * 5067 * @param {!Guacamole.Layer} srcLayer 5068 * The Layer to copy image data from. 5069 * 5070 * @param {!number} srcx 5071 * The X coordinate of the upper-left corner of the rectangle within 5072 * the source Layer's coordinate space to copy data from. 5073 * 5074 * @param {!number} srcy 5075 * The Y coordinate of the upper-left corner of the rectangle within 5076 * the source Layer's coordinate space to copy data from. 5077 * 5078 * @param {!number} srcw 5079 * The width of the rectangle within the source Layer's coordinate 5080 * space to copy data from. 5081 * 5082 * @param {!number} srch 5083 * The height of the rectangle within the source Layer's coordinate 5084 * space to copy data from. 5085 * 5086 * @param {!Guacamole.Layer} dstLayer 5087 * The layer to draw upon. 5088 * 5089 * @param {!number} x 5090 * The destination X coordinate. 5091 * 5092 * @param {!number} y 5093 * The destination Y coordinate. 5094 */ 5095 this.put = function(srcLayer, srcx, srcy, srcw, srch, dstLayer, x, y) { 5096 scheduleTask(function __display_put() { 5097 dstLayer.put(srcLayer, srcx, srcy, srcw, srch, x, y); 5098 }); 5099 }; 5100 5101 /** 5102 * Copy a rectangle of image data from one Layer to this Layer. This 5103 * operation will copy exactly the image data that will be drawn once all 5104 * operations of the source Layer that were pending at the time this 5105 * function was called are complete. This operation will not alter the 5106 * size of the source Layer even if its autosize property is set to true. 5107 * 5108 * @param {!Guacamole.Layer} srcLayer 5109 * The Layer to copy image data from. 5110 * 5111 * @param {!number} srcx 5112 * The X coordinate of the upper-left corner of the rectangle within 5113 * the source Layer's coordinate space to copy data from. 5114 * 5115 * @param {!number} srcy 5116 * The Y coordinate of the upper-left corner of the rectangle within 5117 * the source Layer's coordinate space to copy data from. 5118 * 5119 * @param {!number} srcw 5120 * The width of the rectangle within the source Layer's coordinate 5121 * space to copy data from. 5122 * 5123 * @param {!number} srch 5124 * The height of the rectangle within the source Layer's coordinate space to copy data from. 5125 * 5126 * @param {!Guacamole.Layer} dstLayer 5127 * The layer to draw upon. 5128 * 5129 * @param {!number} x 5130 * The destination X coordinate. 5131 * 5132 * @param {!number} y 5133 * The destination Y coordinate. 5134 */ 5135 this.copy = function(srcLayer, srcx, srcy, srcw, srch, dstLayer, x, y) { 5136 scheduleTask(function __display_copy() { 5137 dstLayer.copy(srcLayer, srcx, srcy, srcw, srch, x, y); 5138 }); 5139 }; 5140 5141 /** 5142 * Starts a new path at the specified point. 5143 * 5144 * @param {!Guacamole.Layer} layer 5145 * The layer to draw upon. 5146 * 5147 * @param {!number} x 5148 * The X coordinate of the point to draw. 5149 * 5150 * @param {!number} y 5151 * The Y coordinate of the point to draw. 5152 */ 5153 this.moveTo = function(layer, x, y) { 5154 scheduleTask(function __display_moveTo() { 5155 layer.moveTo(x, y); 5156 }); 5157 }; 5158 5159 /** 5160 * Add the specified line to the current path. 5161 * 5162 * @param {!Guacamole.Layer} layer 5163 * The layer to draw upon. 5164 * 5165 * @param {!number} x 5166 * The X coordinate of the endpoint of the line to draw. 5167 * 5168 * @param {!number} y 5169 * The Y coordinate of the endpoint of the line to draw. 5170 */ 5171 this.lineTo = function(layer, x, y) { 5172 scheduleTask(function __display_lineTo() { 5173 layer.lineTo(x, y); 5174 }); 5175 }; 5176 5177 /** 5178 * Add the specified arc to the current path. 5179 * 5180 * @param {!Guacamole.Layer} layer 5181 * The layer to draw upon. 5182 * 5183 * @param {!number} x 5184 * The X coordinate of the center of the circle which will contain the 5185 * arc. 5186 * 5187 * @param {!number} y 5188 * The Y coordinate of the center of the circle which will contain the 5189 * arc. 5190 * 5191 * @param {!number} radius 5192 * The radius of the circle. 5193 * 5194 * @param {!number} startAngle 5195 * The starting angle of the arc, in radians. 5196 * 5197 * @param {!number} endAngle 5198 * The ending angle of the arc, in radians. 5199 * 5200 * @param {!boolean} negative 5201 * Whether the arc should be drawn in order of decreasing angle. 5202 */ 5203 this.arc = function(layer, x, y, radius, startAngle, endAngle, negative) { 5204 scheduleTask(function __display_arc() { 5205 layer.arc(x, y, radius, startAngle, endAngle, negative); 5206 }); 5207 }; 5208 5209 /** 5210 * Starts a new path at the specified point. 5211 * 5212 * @param {!Guacamole.Layer} layer 5213 * The layer to draw upon. 5214 * 5215 * @param {!number} cp1x 5216 * The X coordinate of the first control point. 5217 * 5218 * @param {!number} cp1y 5219 * The Y coordinate of the first control point. 5220 * 5221 * @param {!number} cp2x 5222 * The X coordinate of the second control point. 5223 * 5224 * @param {!number} cp2y 5225 * The Y coordinate of the second control point. 5226 * 5227 * @param {!number} x 5228 * The X coordinate of the endpoint of the curve. 5229 * 5230 * @param {!number} y 5231 * The Y coordinate of the endpoint of the curve. 5232 */ 5233 this.curveTo = function(layer, cp1x, cp1y, cp2x, cp2y, x, y) { 5234 scheduleTask(function __display_curveTo() { 5235 layer.curveTo(cp1x, cp1y, cp2x, cp2y, x, y); 5236 }); 5237 }; 5238 5239 /** 5240 * Closes the current path by connecting the end point with the start 5241 * point (if any) with a straight line. 5242 * 5243 * @param {!Guacamole.Layer} layer 5244 * The layer to draw upon. 5245 */ 5246 this.close = function(layer) { 5247 scheduleTask(function __display_close() { 5248 layer.close(); 5249 }); 5250 }; 5251 5252 /** 5253 * Add the specified rectangle to the current path. 5254 * 5255 * @param {!Guacamole.Layer} layer 5256 * The layer to draw upon. 5257 * 5258 * @param {!number} x 5259 * The X coordinate of the upper-left corner of the rectangle to draw. 5260 * 5261 * @param {!number} y 5262 * The Y coordinate of the upper-left corner of the rectangle to draw. 5263 * 5264 * @param {!number} w 5265 * The width of the rectangle to draw. 5266 * 5267 * @param {!number} h 5268 * The height of the rectangle to draw. 5269 */ 5270 this.rect = function(layer, x, y, w, h) { 5271 scheduleTask(function __display_rect() { 5272 layer.rect(x, y, w, h); 5273 }); 5274 }; 5275 5276 /** 5277 * Clip all future drawing operations by the current path. The current path 5278 * is implicitly closed. The current path can continue to be reused 5279 * for other operations (such as fillColor()) but a new path will be started 5280 * once a path drawing operation (path() or rect()) is used. 5281 * 5282 * @param {!Guacamole.Layer} layer 5283 * The layer to affect. 5284 */ 5285 this.clip = function(layer) { 5286 scheduleTask(function __display_clip() { 5287 layer.clip(); 5288 }); 5289 }; 5290 5291 /** 5292 * Stroke the current path with the specified color. The current path 5293 * is implicitly closed. The current path can continue to be reused 5294 * for other operations (such as clip()) but a new path will be started 5295 * once a path drawing operation (path() or rect()) is used. 5296 * 5297 * @param {!Guacamole.Layer} layer 5298 * The layer to draw upon. 5299 * 5300 * @param {!string} cap 5301 * The line cap style. Can be "round", "square", or "butt". 5302 * 5303 * @param {!string} join 5304 * The line join style. Can be "round", "bevel", or "miter". 5305 * 5306 * @param {!number} thickness 5307 * The line thickness in pixels. 5308 * 5309 * @param {!number} r 5310 * The red component of the color to fill. 5311 * 5312 * @param {!number} g 5313 * The green component of the color to fill. 5314 * 5315 * @param {!number} b 5316 * The blue component of the color to fill. 5317 * 5318 * @param {!number} a 5319 * The alpha component of the color to fill. 5320 */ 5321 this.strokeColor = function(layer, cap, join, thickness, r, g, b, a) { 5322 scheduleTask(function __display_strokeColor() { 5323 layer.strokeColor(cap, join, thickness, r, g, b, a); 5324 }); 5325 }; 5326 5327 /** 5328 * Fills the current path with the specified color. The current path 5329 * is implicitly closed. The current path can continue to be reused 5330 * for other operations (such as clip()) but a new path will be started 5331 * once a path drawing operation (path() or rect()) is used. 5332 * 5333 * @param {!Guacamole.Layer} layer 5334 * The layer to draw upon. 5335 * 5336 * @param {!number} r 5337 * The red component of the color to fill. 5338 * 5339 * @param {!number} g 5340 * The green component of the color to fill. 5341 * 5342 * @param {!number} b 5343 * The blue component of the color to fill. 5344 * 5345 * @param {!number} a 5346 * The alpha component of the color to fill. 5347 */ 5348 this.fillColor = function(layer, r, g, b, a) { 5349 scheduleTask(function __display_fillColor() { 5350 layer.fillColor(r, g, b, a); 5351 }); 5352 }; 5353 5354 /** 5355 * Stroke the current path with the image within the specified layer. The 5356 * image data will be tiled infinitely within the stroke. The current path 5357 * is implicitly closed. The current path can continue to be reused 5358 * for other operations (such as clip()) but a new path will be started 5359 * once a path drawing operation (path() or rect()) is used. 5360 * 5361 * @param {!Guacamole.Layer} layer 5362 * The layer to draw upon. 5363 * 5364 * @param {!string} cap 5365 * The line cap style. Can be "round", "square", or "butt". 5366 * 5367 * @param {!string} join 5368 * The line join style. Can be "round", "bevel", or "miter". 5369 * 5370 * @param {!number} thickness 5371 * The line thickness in pixels. 5372 * 5373 * @param {!Guacamole.Layer} srcLayer 5374 * The layer to use as a repeating pattern within the stroke. 5375 */ 5376 this.strokeLayer = function(layer, cap, join, thickness, srcLayer) { 5377 scheduleTask(function __display_strokeLayer() { 5378 layer.strokeLayer(cap, join, thickness, srcLayer); 5379 }); 5380 }; 5381 5382 /** 5383 * Fills the current path with the image within the specified layer. The 5384 * image data will be tiled infinitely within the stroke. The current path 5385 * is implicitly closed. The current path can continue to be reused 5386 * for other operations (such as clip()) but a new path will be started 5387 * once a path drawing operation (path() or rect()) is used. 5388 * 5389 * @param {!Guacamole.Layer} layer 5390 * The layer to draw upon. 5391 * 5392 * @param {!Guacamole.Layer} srcLayer 5393 * The layer to use as a repeating pattern within the fill. 5394 */ 5395 this.fillLayer = function(layer, srcLayer) { 5396 scheduleTask(function __display_fillLayer() { 5397 layer.fillLayer(srcLayer); 5398 }); 5399 }; 5400 5401 /** 5402 * Push current layer state onto stack. 5403 * 5404 * @param {!Guacamole.Layer} layer 5405 * The layer to draw upon. 5406 */ 5407 this.push = function(layer) { 5408 scheduleTask(function __display_push() { 5409 layer.push(); 5410 }); 5411 }; 5412 5413 /** 5414 * Pop layer state off stack. 5415 * 5416 * @param {!Guacamole.Layer} layer 5417 * The layer to draw upon. 5418 */ 5419 this.pop = function(layer) { 5420 scheduleTask(function __display_pop() { 5421 layer.pop(); 5422 }); 5423 }; 5424 5425 /** 5426 * Reset the layer, clearing the stack, the current path, and any transform 5427 * matrix. 5428 * 5429 * @param {!Guacamole.Layer} layer 5430 * The layer to draw upon. 5431 */ 5432 this.reset = function(layer) { 5433 scheduleTask(function __display_reset() { 5434 layer.reset(); 5435 }); 5436 }; 5437 5438 /** 5439 * Sets the given affine transform (defined with six values from the 5440 * transform's matrix). 5441 * 5442 * @param {!Guacamole.Layer} layer 5443 * The layer to modify. 5444 * 5445 * @param {!number} a 5446 * The first value in the affine transform's matrix. 5447 * 5448 * @param {!number} b 5449 * The second value in the affine transform's matrix. 5450 * 5451 * @param {!number} c 5452 * The third value in the affine transform's matrix. 5453 * 5454 * @param {!number} d 5455 * The fourth value in the affine transform's matrix. 5456 * 5457 * @param {!number} e 5458 * The fifth value in the affine transform's matrix. 5459 * 5460 * @param {!number} f 5461 * The sixth value in the affine transform's matrix. 5462 */ 5463 this.setTransform = function(layer, a, b, c, d, e, f) { 5464 scheduleTask(function __display_setTransform() { 5465 layer.setTransform(a, b, c, d, e, f); 5466 }); 5467 }; 5468 5469 /** 5470 * Applies the given affine transform (defined with six values from the 5471 * transform's matrix). 5472 * 5473 * @param {!Guacamole.Layer} layer 5474 * The layer to modify. 5475 * 5476 * @param {!number} a 5477 * The first value in the affine transform's matrix. 5478 * 5479 * @param {!number} b 5480 * The second value in the affine transform's matrix. 5481 * 5482 * @param {!number} c 5483 * The third value in the affine transform's matrix. 5484 * 5485 * @param {!number} d 5486 * The fourth value in the affine transform's matrix. 5487 * 5488 * @param {!number} e 5489 * The fifth value in the affine transform's matrix. 5490 * 5491 * @param {!number} f 5492 * The sixth value in the affine transform's matrix. 5493 * 5494 */ 5495 this.transform = function(layer, a, b, c, d, e, f) { 5496 scheduleTask(function __display_transform() { 5497 layer.transform(a, b, c, d, e, f); 5498 }); 5499 }; 5500 5501 /** 5502 * Sets the channel mask for future operations on this Layer. 5503 * 5504 * The channel mask is a Guacamole-specific compositing operation identifier 5505 * with a single bit representing each of four channels (in order): source 5506 * image where destination transparent, source where destination opaque, 5507 * destination where source transparent, and destination where source 5508 * opaque. 5509 * 5510 * @param {!Guacamole.Layer} layer 5511 * The layer to modify. 5512 * 5513 * @param {!number} mask 5514 * The channel mask for future operations on this Layer. 5515 */ 5516 this.setChannelMask = function(layer, mask) { 5517 scheduleTask(function __display_setChannelMask() { 5518 layer.setChannelMask(mask); 5519 }); 5520 }; 5521 5522 /** 5523 * Sets the miter limit for stroke operations using the miter join. This 5524 * limit is the maximum ratio of the size of the miter join to the stroke 5525 * width. If this ratio is exceeded, the miter will not be drawn for that 5526 * joint of the path. 5527 * 5528 * @param {!Guacamole.Layer} layer 5529 * The layer to modify. 5530 * 5531 * @param {!number} limit 5532 * The miter limit for stroke operations using the miter join. 5533 */ 5534 this.setMiterLimit = function(layer, limit) { 5535 scheduleTask(function __display_setMiterLimit() { 5536 layer.setMiterLimit(limit); 5537 }); 5538 }; 5539 5540 /** 5541 * Removes the given layer container entirely, such that it is no longer 5542 * contained within its parent layer, if any. 5543 * 5544 * @param {!Guacamole.Display.VisibleLayer} layer 5545 * The layer being removed from its parent. 5546 */ 5547 this.dispose = function dispose(layer) { 5548 scheduleTask(function disposeLayer() { 5549 layer.dispose(); 5550 }); 5551 }; 5552 5553 /** 5554 * Applies the given affine transform (defined with six values from the 5555 * transform's matrix) to the given layer. 5556 * 5557 * @param {!Guacamole.Display.VisibleLayer} layer 5558 * The layer being distorted. 5559 * 5560 * @param {!number} a 5561 * The first value in the affine transform's matrix. 5562 * 5563 * @param {!number} b 5564 * The second value in the affine transform's matrix. 5565 * 5566 * @param {!number} c 5567 * The third value in the affine transform's matrix. 5568 * 5569 * @param {!number} d 5570 * The fourth value in the affine transform's matrix. 5571 * 5572 * @param {!number} e 5573 * The fifth value in the affine transform's matrix. 5574 * 5575 * @param {!number} f 5576 * The sixth value in the affine transform's matrix. 5577 */ 5578 this.distort = function distort(layer, a, b, c, d, e, f) { 5579 scheduleTask(function distortLayer() { 5580 layer.distort(a, b, c, d, e, f); 5581 }); 5582 }; 5583 5584 /** 5585 * Moves the upper-left corner of the given layer to the given X and Y 5586 * coordinate, sets the Z stacking order, and reparents the layer 5587 * to the given parent layer. 5588 * 5589 * @param {!Guacamole.Display.VisibleLayer} layer 5590 * The layer being moved. 5591 * 5592 * @param {!Guacamole.Display.VisibleLayer} parent 5593 * The parent to set. 5594 * 5595 * @param {!number} x 5596 * The X coordinate to move to. 5597 * 5598 * @param {!number} y 5599 * The Y coordinate to move to. 5600 * 5601 * @param {!number} z 5602 * The Z coordinate to move to. 5603 */ 5604 this.move = function move(layer, parent, x, y, z) { 5605 scheduleTask(function moveLayer() { 5606 layer.move(parent, x, y, z); 5607 }); 5608 }; 5609 5610 /** 5611 * Sets the opacity of the given layer to the given value, where 255 is 5612 * fully opaque and 0 is fully transparent. 5613 * 5614 * @param {!Guacamole.Display.VisibleLayer} layer 5615 * The layer whose opacity should be set. 5616 * 5617 * @param {!number} alpha 5618 * The opacity to set. 5619 */ 5620 this.shade = function shade(layer, alpha) { 5621 scheduleTask(function shadeLayer() { 5622 layer.shade(alpha); 5623 }); 5624 }; 5625 5626 /** 5627 * Sets the scale of the client display element such that it renders at 5628 * a relatively smaller or larger size, without affecting the true 5629 * resolution of the display. 5630 * 5631 * @param {!number} scale 5632 * The scale to resize to, where 1.0 is normal size (1:1 scale). 5633 */ 5634 this.scale = function(scale) { 5635 5636 display.style.transform = 5637 display.style.WebkitTransform = 5638 display.style.MozTransform = 5639 display.style.OTransform = 5640 display.style.msTransform = 5641 5642 "scale(" + scale + "," + scale + ")"; 5643 5644 displayScale = scale; 5645 5646 // Update bounds size 5647 bounds.style.width = (displayWidth*displayScale) + "px"; 5648 bounds.style.height = (displayHeight*displayScale) + "px"; 5649 5650 }; 5651 5652 /** 5653 * Returns the scale of the display. 5654 * 5655 * @return {!number} 5656 * The scale of the display. 5657 */ 5658 this.getScale = function() { 5659 return displayScale; 5660 }; 5661 5662 /** 5663 * Returns a canvas element containing the entire display, with all child 5664 * layers composited within. 5665 * 5666 * @return {!HTMLCanvasElement} 5667 * A new canvas element containing a copy of the display. 5668 */ 5669 this.flatten = function() { 5670 5671 // Get destination canvas 5672 var canvas = document.createElement("canvas"); 5673 canvas.width = default_layer.width; 5674 canvas.height = default_layer.height; 5675 5676 var context = canvas.getContext("2d"); 5677 5678 // Returns sorted array of children 5679 function get_children(layer) { 5680 5681 // Build array of children 5682 var children = []; 5683 for (var index in layer.children) 5684 children.push(layer.children[index]); 5685 5686 // Sort 5687 children.sort(function children_comparator(a, b) { 5688 5689 // Compare based on Z order 5690 var diff = a.z - b.z; 5691 if (diff !== 0) 5692 return diff; 5693 5694 // If Z order identical, use document order 5695 var a_element = a.getElement(); 5696 var b_element = b.getElement(); 5697 var position = b_element.compareDocumentPosition(a_element); 5698 5699 if (position & Node.DOCUMENT_POSITION_PRECEDING) return -1; 5700 if (position & Node.DOCUMENT_POSITION_FOLLOWING) return 1; 5701 5702 // Otherwise, assume same 5703 return 0; 5704 5705 }); 5706 5707 // Done 5708 return children; 5709 5710 } 5711 5712 // Draws the contents of the given layer at the given coordinates 5713 function draw_layer(layer, x, y) { 5714 5715 // Draw layer 5716 if (layer.width > 0 && layer.height > 0) { 5717 5718 // Save and update alpha 5719 var initial_alpha = context.globalAlpha; 5720 context.globalAlpha *= layer.alpha / 255.0; 5721 5722 // Copy data 5723 context.drawImage(layer.getCanvas(), x, y); 5724 5725 // Draw all children 5726 var children = get_children(layer); 5727 for (var i=0; i<children.length; i++) { 5728 var child = children[i]; 5729 draw_layer(child, x + child.x, y + child.y); 5730 } 5731 5732 // Restore alpha 5733 context.globalAlpha = initial_alpha; 5734 5735 } 5736 5737 } 5738 5739 // Draw default layer and all children 5740 draw_layer(default_layer, 0, 0); 5741 5742 // Return new canvas copy 5743 return canvas; 5744 5745 }; 5746 5747}; 5748 5749/** 5750 * Simple container for Guacamole.Layer, allowing layers to be easily 5751 * repositioned and nested. This allows certain operations to be accelerated 5752 * through DOM manipulation, rather than raster operations. 5753 * 5754 * @constructor 5755 * @augments Guacamole.Layer 5756 * @param {!number} width 5757 * The width of the Layer, in pixels. The canvas element backing this Layer 5758 * will be given this width. 5759 * 5760 * @param {!number} height 5761 * The height of the Layer, in pixels. The canvas element backing this 5762 * Layer will be given this height. 5763 */ 5764Guacamole.Display.VisibleLayer = function(width, height) { 5765 5766 Guacamole.Layer.apply(this, [width, height]); 5767 5768 /** 5769 * Reference to this layer. 5770 * 5771 * @private 5772 * @type {!Guacamole.Display.Layer} 5773 */ 5774 var layer = this; 5775 5776 /** 5777 * Identifier which uniquely identifies this layer. This is COMPLETELY 5778 * UNRELATED to the index of the underlying layer, which is specific 5779 * to the Guacamole protocol, and not relevant at this level. 5780 * 5781 * @private 5782 * @type {!number} 5783 */ 5784 this.__unique_id = Guacamole.Display.VisibleLayer.__next_id++; 5785 5786 /** 5787 * The opacity of the layer container, where 255 is fully opaque and 0 is 5788 * fully transparent. 5789 * 5790 * @type {!number} 5791 */ 5792 this.alpha = 0xFF; 5793 5794 /** 5795 * X coordinate of the upper-left corner of this layer container within 5796 * its parent, in pixels. 5797 * 5798 * @type {!number} 5799 */ 5800 this.x = 0; 5801 5802 /** 5803 * Y coordinate of the upper-left corner of this layer container within 5804 * its parent, in pixels. 5805 * 5806 * @type {!number} 5807 */ 5808 this.y = 0; 5809 5810 /** 5811 * Z stacking order of this layer relative to other sibling layers. 5812 * 5813 * @type {!number} 5814 */ 5815 this.z = 0; 5816 5817 /** 5818 * The affine transformation applied to this layer container. Each element 5819 * corresponds to a value from the transformation matrix, with the first 5820 * three values being the first row, and the last three values being the 5821 * second row. There are six values total. 5822 * 5823 * @type {!number[]} 5824 */ 5825 this.matrix = [1, 0, 0, 1, 0, 0]; 5826 5827 /** 5828 * The parent layer container of this layer, if any. 5829 * @type {Guacamole.Display.VisibleLayer} 5830 */ 5831 this.parent = null; 5832 5833 /** 5834 * Set of all children of this layer, indexed by layer index. This object 5835 * will have one property per child. 5836 * 5837 * @type {!Object.<number, Guacamole.Display.VisibleLayer>} 5838 */ 5839 this.children = {}; 5840 5841 // Set layer position 5842 var canvas = layer.getCanvas(); 5843 canvas.style.position = "absolute"; 5844 canvas.style.left = "0px"; 5845 canvas.style.top = "0px"; 5846 5847 // Create div with given size 5848 var div = document.createElement("div"); 5849 div.appendChild(canvas); 5850 div.style.width = width + "px"; 5851 div.style.height = height + "px"; 5852 div.style.position = "absolute"; 5853 div.style.left = "0px"; 5854 div.style.top = "0px"; 5855 div.style.overflow = "hidden"; 5856 5857 /** 5858 * Superclass resize() function. 5859 * @private 5860 */ 5861 var __super_resize = this.resize; 5862 5863 this.resize = function(width, height) { 5864 5865 // Resize containing div 5866 div.style.width = width + "px"; 5867 div.style.height = height + "px"; 5868 5869 __super_resize(width, height); 5870 5871 }; 5872 5873 /** 5874 * Returns the element containing the canvas and any other elements 5875 * associated with this layer. 5876 * 5877 * @returns {!Element} 5878 * The element containing this layer's canvas. 5879 */ 5880 this.getElement = function() { 5881 return div; 5882 }; 5883 5884 /** 5885 * The translation component of this layer's transform. 5886 * 5887 * @private 5888 * @type {!string} 5889 */ 5890 var translate = "translate(0px, 0px)"; // (0, 0) 5891 5892 /** 5893 * The arbitrary matrix component of this layer's transform. 5894 * 5895 * @private 5896 * @type {!string} 5897 */ 5898 var matrix = "matrix(1, 0, 0, 1, 0, 0)"; // Identity 5899 5900 /** 5901 * Moves the upper-left corner of this layer to the given X and Y 5902 * coordinate. 5903 * 5904 * @param {!number} x 5905 * The X coordinate to move to. 5906 * 5907 * @param {!number} y 5908 * The Y coordinate to move to. 5909 */ 5910 this.translate = function(x, y) { 5911 5912 layer.x = x; 5913 layer.y = y; 5914 5915 // Generate translation 5916 translate = "translate(" 5917 + x + "px," 5918 + y + "px)"; 5919 5920 // Set layer transform 5921 div.style.transform = 5922 div.style.WebkitTransform = 5923 div.style.MozTransform = 5924 div.style.OTransform = 5925 div.style.msTransform = 5926 5927 translate + " " + matrix; 5928 5929 }; 5930 5931 /** 5932 * Moves the upper-left corner of this VisibleLayer to the given X and Y 5933 * coordinate, sets the Z stacking order, and reparents this VisibleLayer 5934 * to the given VisibleLayer. 5935 * 5936 * @param {!Guacamole.Display.VisibleLayer} parent 5937 * The parent to set. 5938 * 5939 * @param {!number} x 5940 * The X coordinate to move to. 5941 * 5942 * @param {!number} y 5943 * The Y coordinate to move to. 5944 * 5945 * @param {!number} z 5946 * The Z coordinate to move to. 5947 */ 5948 this.move = function(parent, x, y, z) { 5949 5950 // Set parent if necessary 5951 if (layer.parent !== parent) { 5952 5953 // Maintain relationship 5954 if (layer.parent) 5955 delete layer.parent.children[layer.__unique_id]; 5956 layer.parent = parent; 5957 parent.children[layer.__unique_id] = layer; 5958 5959 // Reparent element 5960 var parent_element = parent.getElement(); 5961 parent_element.appendChild(div); 5962 5963 } 5964 5965 // Set location 5966 layer.translate(x, y); 5967 layer.z = z; 5968 div.style.zIndex = z; 5969 5970 }; 5971 5972 /** 5973 * Sets the opacity of this layer to the given value, where 255 is fully 5974 * opaque and 0 is fully transparent. 5975 * 5976 * @param {!number} a 5977 * The opacity to set. 5978 */ 5979 this.shade = function(a) { 5980 layer.alpha = a; 5981 div.style.opacity = a/255.0; 5982 }; 5983 5984 /** 5985 * Removes this layer container entirely, such that it is no longer 5986 * contained within its parent layer, if any. 5987 */ 5988 this.dispose = function() { 5989 5990 // Remove from parent container 5991 if (layer.parent) { 5992 delete layer.parent.children[layer.__unique_id]; 5993 layer.parent = null; 5994 } 5995 5996 // Remove from parent element 5997 if (div.parentNode) 5998 div.parentNode.removeChild(div); 5999 6000 }; 6001 6002 /** 6003 * Applies the given affine transform (defined with six values from the 6004 * transform's matrix). 6005 * 6006 * @param {!number} a 6007 * The first value in the affine transform's matrix. 6008 * 6009 * @param {!number} b 6010 * The second value in the affine transform's matrix. 6011 * 6012 * @param {!number} c 6013 * The third value in the affine transform's matrix. 6014 * 6015 * @param {!number} d 6016 * The fourth value in the affine transform's matrix. 6017 * 6018 * @param {!number} e 6019 * The fifth value in the affine transform's matrix. 6020 * 6021 * @param {!number} f 6022 * The sixth value in the affine transform's matrix. 6023 */ 6024 this.distort = function(a, b, c, d, e, f) { 6025 6026 // Store matrix 6027 layer.matrix = [a, b, c, d, e, f]; 6028 6029 // Generate matrix transformation 6030 matrix = 6031 6032 /* a c e 6033 * b d f 6034 * 0 0 1 6035 */ 6036 6037 "matrix(" + a + "," + b + "," + c + "," + d + "," + e + "," + f + ")"; 6038 6039 // Set layer transform 6040 div.style.transform = 6041 div.style.WebkitTransform = 6042 div.style.MozTransform = 6043 div.style.OTransform = 6044 div.style.msTransform = 6045 6046 translate + " " + matrix; 6047 6048 }; 6049 6050}; 6051 6052/** 6053 * The next identifier to be assigned to the layer container. This identifier 6054 * uniquely identifies each VisibleLayer, but is unrelated to the index of 6055 * the layer, which exists at the protocol/client level only. 6056 * 6057 * @private 6058 * @type {!number} 6059 */ 6060Guacamole.Display.VisibleLayer.__next_id = 0; 6061 6062/** 6063 * A set of Guacamole display performance statistics, describing the speed at 6064 * which the remote desktop, Guacamole server, and Guacamole client are 6065 * rendering frames. 6066 * 6067 * @constructor 6068 * @param {Guacamole.Display.Statistics|Object} [template={}] 6069 * The object whose properties should be copied within the new 6070 * Guacamole.Display.Statistics. 6071 */ 6072Guacamole.Display.Statistics = function Statistics(template) { 6073 6074 template = template || {}; 6075 6076 /** 6077 * The amount of time that the Guacamole client is taking to render 6078 * individual frames, in milliseconds, if known. If this value is unknown, 6079 * such as if the there are insufficient frame statistics recorded to 6080 * calculate this value, this will be null. 6081 * 6082 * @type {?number} 6083 */ 6084 this.processingLag = template.processingLag; 6085 6086 /** 6087 * The framerate of the remote desktop currently being viewed within the 6088 * relevant Gucamole.Display, independent of Guacamole, in frames per 6089 * second. This represents the speed at which the remote desktop is 6090 * producing frame data for the Guacamole server to consume. If this 6091 * value is unknown, such as if the remote desktop server does not actually 6092 * define frame boundaries, this will be null. 6093 * 6094 * @type {?number} 6095 */ 6096 this.desktopFps = template.desktopFps; 6097 6098 /** 6099 * The rate at which the Guacamole server is generating frames for the 6100 * Guacamole client to consume, in frames per second. If the Guacamole 6101 * server is correctly adjusting for variance in client/browser processing 6102 * power, this rate should closely match the client rate, and should remain 6103 * independent of any network latency. If this value is unknown, such as if 6104 * the there are insufficient frame statistics recorded to calculate this 6105 * value, this will be null. 6106 * 6107 * @type {?number} 6108 */ 6109 this.serverFps = template.serverFps; 6110 6111 /** 6112 * The rate at which the Guacamole client is consuming frames generated by 6113 * the Guacamole server, in frames per second. If the Guacamole server is 6114 * correctly adjusting for variance in client/browser processing power, 6115 * this rate should closely match the server rate, regardless of any 6116 * latency on the network between the server and client. If this value is 6117 * unknown, such as if the there are insufficient frame statistics recorded 6118 * to calculate this value, this will be null. 6119 * 6120 * @type {?number} 6121 */ 6122 this.clientFps = template.clientFps; 6123 6124 /** 6125 * The rate at which the Guacamole server is dropping or combining frames 6126 * received from the remote desktop server to compensate for variance in 6127 * client/browser processing power, in frames per second. This value may 6128 * also be non-zero if the server is compensating for variances in its own 6129 * processing power, or relative slowness in image compression vs. the rate 6130 * that inbound frames are received. If this value is unknown, such as if 6131 * the remote desktop server does not actually define frame boundaries, 6132 * this will be null. 6133 */ 6134 this.dropRate = template.dropRate; 6135 6136}; 6137/* 6138 * Licensed to the Apache Software Foundation (ASF) under one 6139 * or more contributor license agreements. See the NOTICE file 6140 * distributed with this work for additional information 6141 * regarding copyright ownership. The ASF licenses this file 6142 * to you under the Apache License, Version 2.0 (the 6143 * "License"); you may not use this file except in compliance 6144 * with the License. You may obtain a copy of the License at 6145 * 6146 * http://www.apache.org/licenses/LICENSE-2.0 6147 * 6148 * Unless required by applicable law or agreed to in writing, 6149 * software distributed under the License is distributed on an 6150 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 6151 * KIND, either express or implied. See the License for the 6152 * specific language governing permissions and limitations 6153 * under the License. 6154 */ 6155 6156var Guacamole = Guacamole || {}; 6157 6158/** 6159 * An arbitrary event, emitted by a {@link Guacamole.Event.Target}. This object 6160 * should normally serve as the base class for a different object that is more 6161 * specific to the event type. 6162 * 6163 * @constructor 6164 * @param {!string} type 6165 * The unique name of this event type. 6166 */ 6167Guacamole.Event = function Event(type) { 6168 6169 /** 6170 * The unique name of this event type. 6171 * 6172 * @type {!string} 6173 */ 6174 this.type = type; 6175 6176 /** 6177 * An arbitrary timestamp in milliseconds, indicating this event's 6178 * position in time relative to other events. 6179 * 6180 * @type {!number} 6181 */ 6182 this.timestamp = new Date().getTime(); 6183 6184 /** 6185 * Returns the number of milliseconds elapsed since this event was created. 6186 * 6187 * @return {!number} 6188 * The number of milliseconds elapsed since this event was created. 6189 */ 6190 this.getAge = function getAge() { 6191 return new Date().getTime() - this.timestamp; 6192 }; 6193 6194 /** 6195 * Requests that the legacy event handler associated with this event be 6196 * invoked on the given event target. This function will be invoked 6197 * automatically by implementations of {@link Guacamole.Event.Target} 6198 * whenever {@link Guacamole.Event.Target#emit emit()} is invoked. 6199 * <p> 6200 * Older versions of Guacamole relied on single event handlers with the 6201 * prefix "on", such as "onmousedown" or "onkeyup". If a Guacamole.Event 6202 * implementation is replacing the event previously represented by one of 6203 * these handlers, this function gives the implementation the opportunity 6204 * to provide backward compatibility with the old handler. 6205 * <p> 6206 * Unless overridden, this function does nothing. 6207 * 6208 * @param {!Guacamole.Event.Target} eventTarget 6209 * The {@link Guacamole.Event.Target} that emitted this event. 6210 */ 6211 this.invokeLegacyHandler = function invokeLegacyHandler(eventTarget) { 6212 // Do nothing 6213 }; 6214 6215}; 6216 6217/** 6218 * A {@link Guacamole.Event} that may relate to one or more DOM events. 6219 * Continued propagation and default behavior of the related DOM events may be 6220 * prevented with {@link Guacamole.Event.DOMEvent#stopPropagation stopPropagation()} 6221 * and {@link Guacamole.Event.DOMEvent#preventDefault preventDefault()} 6222 * respectively. 6223 * 6224 * @constructor 6225 * @augments Guacamole.Event 6226 * 6227 * @param {!string} type 6228 * The unique name of this event type. 6229 * 6230 * @param {Event|Event[]} [events=[]] 6231 * The DOM events that are related to this event, if any. Future calls to 6232 * {@link Guacamole.Event.DOMEvent#preventDefault preventDefault()} and 6233 * {@link Guacamole.Event.DOMEvent#stopPropagation stopPropagation()} will 6234 * affect these events. 6235 */ 6236Guacamole.Event.DOMEvent = function DOMEvent(type, events) { 6237 6238 Guacamole.Event.call(this, type); 6239 6240 // Default to empty array 6241 events = events || []; 6242 6243 // Automatically wrap non-array single Event in an array 6244 if (!Array.isArray(events)) 6245 events = [ events ]; 6246 6247 /** 6248 * Requests that the default behavior of related DOM events be prevented. 6249 * Whether this request will be honored by the browser depends on the 6250 * nature of those events and the timing of the request. 6251 */ 6252 this.preventDefault = function preventDefault() { 6253 events.forEach(function applyPreventDefault(event) { 6254 if (event.preventDefault) event.preventDefault(); 6255 event.returnValue = false; 6256 }); 6257 }; 6258 6259 /** 6260 * Stops further propagation of related events through the DOM. Only events 6261 * that are directly related to this event will be stopped. 6262 */ 6263 this.stopPropagation = function stopPropagation() { 6264 events.forEach(function applyStopPropagation(event) { 6265 event.stopPropagation(); 6266 }); 6267 }; 6268 6269}; 6270 6271/** 6272 * Convenience function for cancelling all further processing of a given DOM 6273 * event. Invoking this function prevents the default behavior of the event and 6274 * stops any further propagation. 6275 * 6276 * @param {!Event} event 6277 * The DOM event to cancel. 6278 */ 6279Guacamole.Event.DOMEvent.cancelEvent = function cancelEvent(event) { 6280 event.stopPropagation(); 6281 if (event.preventDefault) event.preventDefault(); 6282 event.returnValue = false; 6283}; 6284 6285/** 6286 * An object which can dispatch {@link Guacamole.Event} objects. Listeners 6287 * registered with {@link Guacamole.Event.Target#on on()} will automatically 6288 * be invoked based on the type of {@link Guacamole.Event} passed to 6289 * {@link Guacamole.Event.Target#dispatch dispatch()}. It is normally 6290 * subclasses of Guacamole.Event.Target that will dispatch events, and usages 6291 * of those subclasses that will catch dispatched events with on(). 6292 * 6293 * @constructor 6294 */ 6295Guacamole.Event.Target = function Target() { 6296 6297 /** 6298 * A callback function which handles an event dispatched by an event 6299 * target. 6300 * 6301 * @callback Guacamole.Event.Target~listener 6302 * @param {!Guacamole.Event} event 6303 * The event that was dispatched. 6304 * 6305 * @param {!Guacamole.Event.Target} target 6306 * The object that dispatched the event. 6307 */ 6308 6309 /** 6310 * All listeners (callback functions) registered for each event type passed 6311 * to {@link Guacamole.Event.Targer#on on()}. 6312 * 6313 * @private 6314 * @type {!Object.<string, Guacamole.Event.Target~listener[]>} 6315 */ 6316 var listeners = {}; 6317 6318 /** 6319 * Registers a listener for events having the given type, as dictated by 6320 * the {@link Guacamole.Event#type type} property of {@link Guacamole.Event} 6321 * provided to {@link Guacamole.Event.Target#dispatch dispatch()}. 6322 * 6323 * @param {!string} type 6324 * The unique name of this event type. 6325 * 6326 * @param {!Guacamole.Event.Target~listener} listener 6327 * The function to invoke when an event having the given type is 6328 * dispatched. The {@link Guacamole.Event} object provided to 6329 * {@link Guacamole.Event.Target#dispatch dispatch()} will be passed to 6330 * this function, along with the dispatching Guacamole.Event.Target. 6331 */ 6332 this.on = function on(type, listener) { 6333 6334 var relevantListeners = listeners[type]; 6335 if (!relevantListeners) 6336 listeners[type] = relevantListeners = []; 6337 6338 relevantListeners.push(listener); 6339 6340 }; 6341 6342 /** 6343 * Registers a listener for events having the given types, as dictated by 6344 * the {@link Guacamole.Event#type type} property of {@link Guacamole.Event} 6345 * provided to {@link Guacamole.Event.Target#dispatch dispatch()}. 6346 * <p> 6347 * Invoking this function is equivalent to manually invoking 6348 * {@link Guacamole.Event.Target#on on()} for each of the provided types. 6349 * 6350 * @param {!string[]} types 6351 * The unique names of the event types to associate with the given 6352 * listener. 6353 * 6354 * @param {!Guacamole.Event.Target~listener} listener 6355 * The function to invoke when an event having any of the given types 6356 * is dispatched. The {@link Guacamole.Event} object provided to 6357 * {@link Guacamole.Event.Target#dispatch dispatch()} will be passed to 6358 * this function, along with the dispatching Guacamole.Event.Target. 6359 */ 6360 this.onEach = function onEach(types, listener) { 6361 types.forEach(function addListener(type) { 6362 this.on(type, listener); 6363 }, this); 6364 }; 6365 6366 /** 6367 * Dispatches the given event, invoking all event handlers registered with 6368 * this Guacamole.Event.Target for that event's 6369 * {@link Guacamole.Event#type type}. 6370 * 6371 * @param {!Guacamole.Event} event 6372 * The event to dispatch. 6373 */ 6374 this.dispatch = function dispatch(event) { 6375 6376 // Invoke any relevant legacy handler for the event 6377 event.invokeLegacyHandler(this); 6378 6379 // Invoke all registered listeners 6380 var relevantListeners = listeners[event.type]; 6381 if (relevantListeners) { 6382 for (var i = 0; i < relevantListeners.length; i++) { 6383 relevantListeners[i](event, this); 6384 } 6385 } 6386 6387 }; 6388 6389 /** 6390 * Unregisters a listener that was previously registered with 6391 * {@link Guacamole.Event.Target#on on()} or 6392 * {@link Guacamole.Event.Target#onEach onEach()}. If no such listener was 6393 * registered, this function has no effect. If multiple copies of the same 6394 * listener were registered, the first listener still registered will be 6395 * removed. 6396 * 6397 * @param {!string} type 6398 * The unique name of the event type handled by the listener being 6399 * removed. 6400 * 6401 * @param {!Guacamole.Event.Target~listener} listener 6402 * The listener function previously provided to 6403 * {@link Guacamole.Event.Target#on on()}or 6404 * {@link Guacamole.Event.Target#onEach onEach()}. 6405 * 6406 * @returns {!boolean} 6407 * true if the specified listener was removed, false otherwise. 6408 */ 6409 this.off = function off(type, listener) { 6410 6411 var relevantListeners = listeners[type]; 6412 if (!relevantListeners) 6413 return false; 6414 6415 for (var i = 0; i < relevantListeners.length; i++) { 6416 if (relevantListeners[i] === listener) { 6417 relevantListeners.splice(i, 1); 6418 return true; 6419 } 6420 } 6421 6422 return false; 6423 6424 }; 6425 6426 /** 6427 * Unregisters listeners that were previously registered with 6428 * {@link Guacamole.Event.Target#on on()} or 6429 * {@link Guacamole.Event.Target#onEach onEach()}. If no such listeners 6430 * were registered, this function has no effect. If multiple copies of the 6431 * same listener were registered for the same event type, the first 6432 * listener still registered will be removed. 6433 * <p> 6434 * Invoking this function is equivalent to manually invoking 6435 * {@link Guacamole.Event.Target#off off()} for each of the provided types. 6436 * 6437 * @param {!string[]} types 6438 * The unique names of the event types handled by the listeners being 6439 * removed. 6440 * 6441 * @param {!Guacamole.Event.Target~listener} listener 6442 * The listener function previously provided to 6443 * {@link Guacamole.Event.Target#on on()} or 6444 * {@link Guacamole.Event.Target#onEach onEach()}. 6445 * 6446 * @returns {!boolean} 6447 * true if any of the specified listeners were removed, false 6448 * otherwise. 6449 */ 6450 this.offEach = function offEach(types, listener) { 6451 6452 var changed = false; 6453 6454 types.forEach(function removeListener(type) { 6455 changed |= this.off(type, listener); 6456 }, this); 6457 6458 return changed; 6459 6460 }; 6461 6462}; 6463/* 6464 * Licensed to the Apache Software Foundation (ASF) under one 6465 * or more contributor license agreements. See the NOTICE file 6466 * distributed with this work for additional information 6467 * regarding copyright ownership. The ASF licenses this file 6468 * to you under the Apache License, Version 2.0 (the 6469 * "License"); you may not use this file except in compliance 6470 * with the License. You may obtain a copy of the License at 6471 * 6472 * http://www.apache.org/licenses/LICENSE-2.0 6473 * 6474 * Unless required by applicable law or agreed to in writing, 6475 * software distributed under the License is distributed on an 6476 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 6477 * KIND, either express or implied. See the License for the 6478 * specific language governing permissions and limitations 6479 * under the License. 6480 */ 6481 6482var Guacamole = Guacamole || {}; 6483 6484/** 6485 * A hidden input field which attempts to keep itself focused at all times, 6486 * except when another input field has been intentionally focused, whether 6487 * programatically or by the user. The actual underlying input field, returned 6488 * by getElement(), may be used as a reliable source of keyboard-related events, 6489 * particularly composition and input events which may require a focused input 6490 * field to be dispatched at all. 6491 * 6492 * @constructor 6493 */ 6494Guacamole.InputSink = function InputSink() { 6495 6496 /** 6497 * Reference to this instance of Guacamole.InputSink. 6498 * 6499 * @private 6500 * @type {!Guacamole.InputSink} 6501 */ 6502 var sink = this; 6503 6504 /** 6505 * The underlying input field, styled to be invisible. 6506 * 6507 * @private 6508 * @type {!Element} 6509 */ 6510 var field = document.createElement('textarea'); 6511 field.style.position = 'fixed'; 6512 field.style.outline = 'none'; 6513 field.style.border = 'none'; 6514 field.style.margin = '0'; 6515 field.style.padding = '0'; 6516 field.style.height = '0'; 6517 field.style.width = '0'; 6518 field.style.left = '0'; 6519 field.style.bottom = '0'; 6520 field.style.resize = 'none'; 6521 field.style.background = 'transparent'; 6522 field.style.color = 'transparent'; 6523 6524 // Keep field clear when modified via normal keypresses 6525 field.addEventListener("keypress", function clearKeypress(e) { 6526 field.value = ''; 6527 }, false); 6528 6529 // Keep field clear when modofied via composition events 6530 field.addEventListener("compositionend", function clearCompletedComposition(e) { 6531 if (e.data) 6532 field.value = ''; 6533 }, false); 6534 6535 // Keep field clear when modofied via input events 6536 field.addEventListener("input", function clearCompletedInput(e) { 6537 if (e.data && !e.isComposing) 6538 field.value = ''; 6539 }, false); 6540 6541 // Whenever focus is gained, automatically click to ensure cursor is 6542 // actually placed within the field (the field may simply be highlighted or 6543 // outlined otherwise) 6544 field.addEventListener("focus", function focusReceived() { 6545 window.setTimeout(function deferRefocus() { 6546 field.click(); 6547 field.select(); 6548 }, 0); 6549 }, true); 6550 6551 /** 6552 * Attempts to focus the underlying input field. The focus attempt occurs 6553 * asynchronously, and may silently fail depending on browser restrictions. 6554 */ 6555 this.focus = function focus() { 6556 window.setTimeout(function deferRefocus() { 6557 field.focus(); // Focus must be deferred to work reliably across browsers 6558 }, 0); 6559 }; 6560 6561 /** 6562 * Returns the underlying input field. This input field MUST be manually 6563 * added to the DOM for the Guacamole.InputSink to have any effect. 6564 * 6565 * @returns {!Element} 6566 * The underlying input field. 6567 */ 6568 this.getElement = function getElement() { 6569 return field; 6570 }; 6571 6572 // Automatically refocus input sink if part of DOM 6573 document.addEventListener("keydown", function refocusSink(e) { 6574 6575 // Do not refocus if focus is on an input field 6576 var focused = document.activeElement; 6577 if (focused && focused !== document.body) { 6578 6579 // Only consider focused input fields which are actually visible 6580 var rect = focused.getBoundingClientRect(); 6581 if (rect.left + rect.width > 0 && rect.top + rect.height > 0) 6582 return; 6583 6584 } 6585 6586 // Refocus input sink instead of handling click 6587 sink.focus(); 6588 6589 }, true); 6590 6591}; 6592/* 6593 * Licensed to the Apache Software Foundation (ASF) under one 6594 * or more contributor license agreements. See the NOTICE file 6595 * distributed with this work for additional information 6596 * regarding copyright ownership. The ASF licenses this file 6597 * to you under the Apache License, Version 2.0 (the 6598 * "License"); you may not use this file except in compliance 6599 * with the License. You may obtain a copy of the License at 6600 * 6601 * http://www.apache.org/licenses/LICENSE-2.0 6602 * 6603 * Unless required by applicable law or agreed to in writing, 6604 * software distributed under the License is distributed on an 6605 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 6606 * KIND, either express or implied. See the License for the 6607 * specific language governing permissions and limitations 6608 * under the License. 6609 */ 6610 6611var Guacamole = Guacamole || {}; 6612 6613/** 6614 * An input stream abstraction used by the Guacamole client to facilitate 6615 * transfer of files or other binary data. 6616 * 6617 * @constructor 6618 * @param {!Guacamole.Client} client 6619 * The client owning this stream. 6620 * 6621 * @param {!number} index 6622 * The index of this stream. 6623 */ 6624Guacamole.InputStream = function(client, index) { 6625 6626 /** 6627 * Reference to this stream. 6628 * 6629 * @private 6630 * @type {!Guacamole.InputStream} 6631 */ 6632 var guac_stream = this; 6633 6634 /** 6635 * The index of this stream. 6636 * 6637 * @type {!number} 6638 */ 6639 this.index = index; 6640 6641 /** 6642 * Called when a blob of data is received. 6643 * 6644 * @event 6645 * @param {!string} data 6646 * The received base64 data. 6647 */ 6648 this.onblob = null; 6649 6650 /** 6651 * Called when this stream is closed. 6652 * 6653 * @event 6654 */ 6655 this.onend = null; 6656 6657 /** 6658 * Acknowledges the receipt of a blob. 6659 * 6660 * @param {!string} message 6661 * A human-readable message describing the error or status. 6662 * 6663 * @param {!number} code 6664 * The error code, if any, or 0 for success. 6665 */ 6666 this.sendAck = function(message, code) { 6667 client.sendAck(guac_stream.index, message, code); 6668 }; 6669 6670}; 6671/* 6672 * Licensed to the Apache Software Foundation (ASF) under one 6673 * or more contributor license agreements. See the NOTICE file 6674 * distributed with this work for additional information 6675 * regarding copyright ownership. The ASF licenses this file 6676 * to you under the Apache License, Version 2.0 (the 6677 * "License"); you may not use this file except in compliance 6678 * with the License. You may obtain a copy of the License at 6679 * 6680 * http://www.apache.org/licenses/LICENSE-2.0 6681 * 6682 * Unless required by applicable law or agreed to in writing, 6683 * software distributed under the License is distributed on an 6684 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 6685 * KIND, either express or implied. See the License for the 6686 * specific language governing permissions and limitations 6687 * under the License. 6688 */ 6689 6690var Guacamole = Guacamole || {}; 6691 6692/** 6693 * Integer pool which returns consistently increasing integers while integers 6694 * are in use, and previously-used integers when possible. 6695 * @constructor 6696 */ 6697Guacamole.IntegerPool = function() { 6698 6699 /** 6700 * Reference to this integer pool. 6701 * 6702 * @private 6703 */ 6704 var guac_pool = this; 6705 6706 /** 6707 * Array of available integers. 6708 * 6709 * @private 6710 * @type {!number[]} 6711 */ 6712 var pool = []; 6713 6714 /** 6715 * The next integer to return if no more integers remain. 6716 * 6717 * @type {!number} 6718 */ 6719 this.next_int = 0; 6720 6721 /** 6722 * Returns the next available integer in the pool. If possible, a previously 6723 * used integer will be returned. 6724 * 6725 * @return {!number} 6726 * The next available integer. 6727 */ 6728 this.next = function() { 6729 6730 // If free'd integers exist, return one of those 6731 if (pool.length > 0) 6732 return pool.shift(); 6733 6734 // Otherwise, return a new integer 6735 return guac_pool.next_int++; 6736 6737 }; 6738 6739 /** 6740 * Frees the given integer, allowing it to be reused. 6741 * 6742 * @param {!number} integer 6743 * The integer to free. 6744 */ 6745 this.free = function(integer) { 6746 pool.push(integer); 6747 }; 6748 6749}; 6750/* 6751 * Licensed to the Apache Software Foundation (ASF) under one 6752 * or more contributor license agreements. See the NOTICE file 6753 * distributed with this work for additional information 6754 * regarding copyright ownership. The ASF licenses this file 6755 * to you under the Apache License, Version 2.0 (the 6756 * "License"); you may not use this file except in compliance 6757 * with the License. You may obtain a copy of the License at 6758 * 6759 * http://www.apache.org/licenses/LICENSE-2.0 6760 * 6761 * Unless required by applicable law or agreed to in writing, 6762 * software distributed under the License is distributed on an 6763 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 6764 * KIND, either express or implied. See the License for the 6765 * specific language governing permissions and limitations 6766 * under the License. 6767 */ 6768 6769var Guacamole = Guacamole || {}; 6770 6771/** 6772 * A reader which automatically handles the given input stream, assembling all 6773 * received blobs into a JavaScript object by appending them to each other, in 6774 * order, and decoding the result as JSON. Note that this object will overwrite 6775 * any installed event handlers on the given Guacamole.InputStream. 6776 * 6777 * @constructor 6778 * @param {Guacamole.InputStream} stream 6779 * The stream that JSON will be read from. 6780 */ 6781Guacamole.JSONReader = function guacamoleJSONReader(stream) { 6782 6783 /** 6784 * Reference to this Guacamole.JSONReader. 6785 * 6786 * @private 6787 * @type {!Guacamole.JSONReader} 6788 */ 6789 var guacReader = this; 6790 6791 /** 6792 * Wrapped Guacamole.StringReader. 6793 * 6794 * @private 6795 * @type {!Guacamole.StringReader} 6796 */ 6797 var stringReader = new Guacamole.StringReader(stream); 6798 6799 /** 6800 * All JSON read thus far. 6801 * 6802 * @private 6803 * @type {!string} 6804 */ 6805 var json = ''; 6806 6807 /** 6808 * Returns the current length of this Guacamole.JSONReader, in characters. 6809 * 6810 * @return {!number} 6811 * The current length of this Guacamole.JSONReader. 6812 */ 6813 this.getLength = function getLength() { 6814 return json.length; 6815 }; 6816 6817 /** 6818 * Returns the contents of this Guacamole.JSONReader as a JavaScript 6819 * object. 6820 * 6821 * @return {object} 6822 * The contents of this Guacamole.JSONReader, as parsed from the JSON 6823 * contents of the input stream. 6824 */ 6825 this.getJSON = function getJSON() { 6826 return JSON.parse(json); 6827 }; 6828 6829 // Append all received text 6830 stringReader.ontext = function ontext(text) { 6831 6832 // Append received text 6833 json += text; 6834 6835 // Call handler, if present 6836 if (guacReader.onprogress) 6837 guacReader.onprogress(text.length); 6838 6839 }; 6840 6841 // Simply call onend when end received 6842 stringReader.onend = function onend() { 6843 if (guacReader.onend) 6844 guacReader.onend(); 6845 }; 6846 6847 /** 6848 * Fired once for every blob of data received. 6849 * 6850 * @event 6851 * @param {!number} length 6852 * The number of characters received. 6853 */ 6854 this.onprogress = null; 6855 6856 /** 6857 * Fired once this stream is finished and no further data will be written. 6858 * 6859 * @event 6860 */ 6861 this.onend = null; 6862 6863}; 6864/* 6865 * Licensed to the Apache Software Foundation (ASF) under one 6866 * or more contributor license agreements. See the NOTICE file 6867 * distributed with this work for additional information 6868 * regarding copyright ownership. The ASF licenses this file 6869 * to you under the Apache License, Version 2.0 (the 6870 * "License"); you may not use this file except in compliance 6871 * with the License. You may obtain a copy of the License at 6872 * 6873 * http://www.apache.org/licenses/LICENSE-2.0 6874 * 6875 * Unless required by applicable law or agreed to in writing, 6876 * software distributed under the License is distributed on an 6877 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 6878 * KIND, either express or implied. See the License for the 6879 * specific language governing permissions and limitations 6880 * under the License. 6881 */ 6882 6883var Guacamole = Guacamole || {}; 6884 6885/** 6886 * Provides cross-browser and cross-keyboard keyboard for a specific element. 6887 * Browser and keyboard layout variation is abstracted away, providing events 6888 * which represent keys as their corresponding X11 keysym. 6889 * 6890 * @constructor 6891 * @param {Element|Document} [element] 6892 * The Element to use to provide keyboard events. If omitted, at least one 6893 * Element must be manually provided through the listenTo() function for 6894 * the Guacamole.Keyboard instance to have any effect. 6895 */ 6896Guacamole.Keyboard = function Keyboard(element) { 6897 6898 /** 6899 * Reference to this Guacamole.Keyboard. 6900 * 6901 * @private 6902 * @type {!Guacamole.Keyboard} 6903 */ 6904 var guac_keyboard = this; 6905 6906 /** 6907 * An integer value which uniquely identifies this Guacamole.Keyboard 6908 * instance with respect to other Guacamole.Keyboard instances. 6909 * 6910 * @private 6911 * @type {!number} 6912 */ 6913 var guacKeyboardID = Guacamole.Keyboard._nextID++; 6914 6915 /** 6916 * The name of the property which is added to event objects via markEvent() 6917 * to note that they have already been handled by this Guacamole.Keyboard. 6918 * 6919 * @private 6920 * @constant 6921 * @type {!string} 6922 */ 6923 var EVENT_MARKER = '_GUAC_KEYBOARD_HANDLED_BY_' + guacKeyboardID; 6924 6925 /** 6926 * Fired whenever the user presses a key with the element associated 6927 * with this Guacamole.Keyboard in focus. 6928 * 6929 * @event 6930 * @param {!number} keysym 6931 * The keysym of the key being pressed. 6932 * 6933 * @return {!boolean} 6934 * true if the key event should be allowed through to the browser, 6935 * false otherwise. 6936 */ 6937 this.onkeydown = null; 6938 6939 /** 6940 * Fired whenever the user releases a key with the element associated 6941 * with this Guacamole.Keyboard in focus. 6942 * 6943 * @event 6944 * @param {!number} keysym 6945 * The keysym of the key being released. 6946 */ 6947 this.onkeyup = null; 6948 6949 /** 6950 * Set of known platform-specific or browser-specific quirks which must be 6951 * accounted for to properly interpret key events, even if the only way to 6952 * reliably detect that quirk is to platform/browser-sniff. 6953 * 6954 * @private 6955 * @type {!Object.<string, boolean>} 6956 */ 6957 var quirks = { 6958 6959 /** 6960 * Whether keyup events are universally unreliable. 6961 * 6962 * @type {!boolean} 6963 */ 6964 keyupUnreliable: false, 6965 6966 /** 6967 * Whether the Alt key is actually a modifier for typable keys and is 6968 * thus never used for keyboard shortcuts. 6969 * 6970 * @type {!boolean} 6971 */ 6972 altIsTypableOnly: false, 6973 6974 /** 6975 * Whether we can rely on receiving a keyup event for the Caps Lock 6976 * key. 6977 * 6978 * @type {!boolean} 6979 */ 6980 capsLockKeyupUnreliable: false 6981 6982 }; 6983 6984 // Set quirk flags depending on platform/browser, if such information is 6985 // available 6986 if (navigator && navigator.platform) { 6987 6988 // All keyup events are unreliable on iOS (sadly) 6989 if (navigator.platform.match(/ipad|iphone|ipod/i)) 6990 quirks.keyupUnreliable = true; 6991 6992 // The Alt key on Mac is never used for keyboard shortcuts, and the 6993 // Caps Lock key never dispatches keyup events 6994 else if (navigator.platform.match(/^mac/i)) { 6995 quirks.altIsTypableOnly = true; 6996 quirks.capsLockKeyupUnreliable = true; 6997 } 6998 6999 } 7000 7001 /** 7002 * A key event having a corresponding timestamp. This event is non-specific. 7003 * Its subclasses should be used instead when recording specific key 7004 * events. 7005 * 7006 * @private 7007 * @constructor 7008 * @param {KeyboardEvent} [orig] 7009 * The relevant DOM keyboard event. 7010 */ 7011 var KeyEvent = function KeyEvent(orig) { 7012 7013 /** 7014 * Reference to this key event. 7015 * 7016 * @private 7017 * @type {!KeyEvent} 7018 */ 7019 var key_event = this; 7020 7021 /** 7022 * The JavaScript key code of the key pressed. For most events (keydown 7023 * and keyup), this is a scancode-like value related to the position of 7024 * the key on the US English "Qwerty" keyboard. For keypress events, 7025 * this is the Unicode codepoint of the character that would be typed 7026 * by the key pressed. 7027 * 7028 * @type {!number} 7029 */ 7030 this.keyCode = orig ? (orig.which || orig.keyCode) : 0; 7031 7032 /** 7033 * The legacy DOM3 "keyIdentifier" of the key pressed, as defined at: 7034 * http://www.w3.org/TR/2009/WD-DOM-Level-3-Events-20090908/#events-Events-KeyboardEvent 7035 * 7036 * @type {!string} 7037 */ 7038 this.keyIdentifier = orig && orig.keyIdentifier; 7039 7040 /** 7041 * The standard name of the key pressed, as defined at: 7042 * http://www.w3.org/TR/DOM-Level-3-Events/#events-KeyboardEvent 7043 * 7044 * @type {!string} 7045 */ 7046 this.key = orig && orig.key; 7047 7048 /** 7049 * The location on the keyboard corresponding to the key pressed, as 7050 * defined at: 7051 * http://www.w3.org/TR/DOM-Level-3-Events/#events-KeyboardEvent 7052 * 7053 * @type {!number} 7054 */ 7055 this.location = orig ? getEventLocation(orig) : 0; 7056 7057 /** 7058 * The state of all local keyboard modifiers at the time this event was 7059 * received. 7060 * 7061 * @type {!Guacamole.Keyboard.ModifierState} 7062 */ 7063 this.modifiers = orig ? Guacamole.Keyboard.ModifierState.fromKeyboardEvent(orig) : new Guacamole.Keyboard.ModifierState(); 7064 7065 /** 7066 * An arbitrary timestamp in milliseconds, indicating this event's 7067 * position in time relative to other events. 7068 * 7069 * @type {!number} 7070 */ 7071 this.timestamp = new Date().getTime(); 7072 7073 /** 7074 * Whether the default action of this key event should be prevented. 7075 * 7076 * @type {!boolean} 7077 */ 7078 this.defaultPrevented = false; 7079 7080 /** 7081 * The keysym of the key associated with this key event, as determined 7082 * by a best-effort guess using available event properties and keyboard 7083 * state. 7084 * 7085 * @type {number} 7086 */ 7087 this.keysym = null; 7088 7089 /** 7090 * Whether the keysym value of this key event is known to be reliable. 7091 * If false, the keysym may still be valid, but it's only a best guess, 7092 * and future key events may be a better source of information. 7093 * 7094 * @type {!boolean} 7095 */ 7096 this.reliable = false; 7097 7098 /** 7099 * Returns the number of milliseconds elapsed since this event was 7100 * received. 7101 * 7102 * @return {!number} 7103 * The number of milliseconds elapsed since this event was 7104 * received. 7105 */ 7106 this.getAge = function() { 7107 return new Date().getTime() - key_event.timestamp; 7108 }; 7109 7110 }; 7111 7112 /** 7113 * Information related to the pressing of a key, which need not be a key 7114 * associated with a printable character. The presence or absence of any 7115 * information within this object is browser-dependent. 7116 * 7117 * @private 7118 * @constructor 7119 * @augments Guacamole.Keyboard.KeyEvent 7120 * @param {!KeyboardEvent} orig 7121 * The relevant DOM "keydown" event. 7122 */ 7123 var KeydownEvent = function KeydownEvent(orig) { 7124 7125 // We extend KeyEvent 7126 KeyEvent.call(this, orig); 7127 7128 // If key is known from keyCode or DOM3 alone, use that 7129 this.keysym = keysym_from_key_identifier(this.key, this.location) 7130 || keysym_from_keycode(this.keyCode, this.location); 7131 7132 /** 7133 * Whether the keyup following this keydown event is known to be 7134 * reliable. If false, we cannot rely on the keyup event to occur. 7135 * 7136 * @type {!boolean} 7137 */ 7138 this.keyupReliable = !quirks.keyupUnreliable; 7139 7140 // DOM3 and keyCode are reliable sources if the corresponding key is 7141 // not a printable key 7142 if (this.keysym && !isPrintable(this.keysym)) 7143 this.reliable = true; 7144 7145 // Use legacy keyIdentifier as a last resort, if it looks sane 7146 if (!this.keysym && key_identifier_sane(this.keyCode, this.keyIdentifier)) 7147 this.keysym = keysym_from_key_identifier(this.keyIdentifier, this.location, this.modifiers.shift); 7148 7149 // If a key is pressed while meta is held down, the keyup will 7150 // never be sent in Chrome (bug #108404) 7151 if (this.modifiers.meta && this.keysym !== 0xFFE7 && this.keysym !== 0xFFE8) 7152 this.keyupReliable = false; 7153 7154 // We cannot rely on receiving keyup for Caps Lock on certain platforms 7155 else if (this.keysym === 0xFFE5 && quirks.capsLockKeyupUnreliable) 7156 this.keyupReliable = false; 7157 7158 // Determine whether default action for Alt+combinations must be prevented 7159 var prevent_alt = !this.modifiers.ctrl && !quirks.altIsTypableOnly; 7160 7161 // Determine whether default action for Ctrl+combinations must be prevented 7162 var prevent_ctrl = !this.modifiers.alt; 7163 7164 // We must rely on the (potentially buggy) keyIdentifier if preventing 7165 // the default action is important 7166 if ((prevent_ctrl && this.modifiers.ctrl) 7167 || (prevent_alt && this.modifiers.alt) 7168 || this.modifiers.meta 7169 || this.modifiers.hyper) 7170 this.reliable = true; 7171 7172 // Record most recently known keysym by associated key code 7173 recentKeysym[this.keyCode] = this.keysym; 7174 7175 }; 7176 7177 KeydownEvent.prototype = new KeyEvent(); 7178 7179 /** 7180 * Information related to the pressing of a key, which MUST be 7181 * associated with a printable character. The presence or absence of any 7182 * information within this object is browser-dependent. 7183 * 7184 * @private 7185 * @constructor 7186 * @augments Guacamole.Keyboard.KeyEvent 7187 * @param {!KeyboardEvent} orig 7188 * The relevant DOM "keypress" event. 7189 */ 7190 var KeypressEvent = function KeypressEvent(orig) { 7191 7192 // We extend KeyEvent 7193 KeyEvent.call(this, orig); 7194 7195 // Pull keysym from char code 7196 this.keysym = keysym_from_charcode(this.keyCode); 7197 7198 // Keypress is always reliable 7199 this.reliable = true; 7200 7201 }; 7202 7203 KeypressEvent.prototype = new KeyEvent(); 7204 7205 /** 7206 * Information related to the releasing of a key, which need not be a key 7207 * associated with a printable character. The presence or absence of any 7208 * information within this object is browser-dependent. 7209 * 7210 * @private 7211 * @constructor 7212 * @augments Guacamole.Keyboard.KeyEvent 7213 * @param {!KeyboardEvent} orig 7214 * The relevant DOM "keyup" event. 7215 */ 7216 var KeyupEvent = function KeyupEvent(orig) { 7217 7218 // We extend KeyEvent 7219 KeyEvent.call(this, orig); 7220 7221 // If key is known from keyCode or DOM3 alone, use that (keyCode is 7222 // still more reliable for keyup when dead keys are in use) 7223 this.keysym = keysym_from_keycode(this.keyCode, this.location) 7224 || keysym_from_key_identifier(this.key, this.location); 7225 7226 // Fall back to the most recently pressed keysym associated with the 7227 // keyCode if the inferred key doesn't seem to actually be pressed 7228 if (!guac_keyboard.pressed[this.keysym]) 7229 this.keysym = recentKeysym[this.keyCode] || this.keysym; 7230 7231 // Keyup is as reliable as it will ever be 7232 this.reliable = true; 7233 7234 }; 7235 7236 KeyupEvent.prototype = new KeyEvent(); 7237 7238 /** 7239 * An array of recorded events, which can be instances of the private 7240 * KeydownEvent, KeypressEvent, and KeyupEvent classes. 7241 * 7242 * @private 7243 * @type {!KeyEvent[]} 7244 */ 7245 var eventLog = []; 7246 7247 /** 7248 * Map of known JavaScript keycodes which do not map to typable characters 7249 * to their X11 keysym equivalents. 7250 * 7251 * @private 7252 * @type {!Object.<number, number[]>} 7253 */ 7254 var keycodeKeysyms = { 7255 8: [0xFF08], // backspace 7256 9: [0xFF09], // tab 7257 12: [0xFF0B, 0xFF0B, 0xFF0B, 0xFFB5], // clear / KP 5 7258 13: [0xFF0D], // enter 7259 16: [0xFFE1, 0xFFE1, 0xFFE2], // shift 7260 17: [0xFFE3, 0xFFE3, 0xFFE4], // ctrl 7261 18: [0xFFE9, 0xFFE9, 0xFE03], // alt 7262 19: [0xFF13], // pause/break 7263 20: [0xFFE5], // caps lock 7264 27: [0xFF1B], // escape 7265 32: [0x0020], // space 7266 33: [0xFF55, 0xFF55, 0xFF55, 0xFFB9], // page up / KP 9 7267 34: [0xFF56, 0xFF56, 0xFF56, 0xFFB3], // page down / KP 3 7268 35: [0xFF57, 0xFF57, 0xFF57, 0xFFB1], // end / KP 1 7269 36: [0xFF50, 0xFF50, 0xFF50, 0xFFB7], // home / KP 7 7270 37: [0xFF51, 0xFF51, 0xFF51, 0xFFB4], // left arrow / KP 4 7271 38: [0xFF52, 0xFF52, 0xFF52, 0xFFB8], // up arrow / KP 8 7272 39: [0xFF53, 0xFF53, 0xFF53, 0xFFB6], // right arrow / KP 6 7273 40: [0xFF54, 0xFF54, 0xFF54, 0xFFB2], // down arrow / KP 2 7274 45: [0xFF63, 0xFF63, 0xFF63, 0xFFB0], // insert / KP 0 7275 46: [0xFFFF, 0xFFFF, 0xFFFF, 0xFFAE], // delete / KP decimal 7276 91: [0xFFE7], // left windows/command key (meta_l) 7277 92: [0xFFE8], // right window/command key (meta_r) 7278 93: [0xFF67], // menu key 7279 96: [0xFFB0], // KP 0 7280 97: [0xFFB1], // KP 1 7281 98: [0xFFB2], // KP 2 7282 99: [0xFFB3], // KP 3 7283 100: [0xFFB4], // KP 4 7284 101: [0xFFB5], // KP 5 7285 102: [0xFFB6], // KP 6 7286 103: [0xFFB7], // KP 7 7287 104: [0xFFB8], // KP 8 7288 105: [0xFFB9], // KP 9 7289 106: [0xFFAA], // KP multiply 7290 107: [0xFFAB], // KP add 7291 109: [0xFFAD], // KP subtract 7292 110: [0xFFAE], // KP decimal 7293 111: [0xFFAF], // KP divide 7294 112: [0xFFBE], // f1 7295 113: [0xFFBF], // f2 7296 114: [0xFFC0], // f3 7297 115: [0xFFC1], // f4 7298 116: [0xFFC2], // f5 7299 117: [0xFFC3], // f6 7300 118: [0xFFC4], // f7 7301 119: [0xFFC5], // f8 7302 120: [0xFFC6], // f9 7303 121: [0xFFC7], // f10 7304 122: [0xFFC8], // f11 7305 123: [0xFFC9], // f12 7306 144: [0xFF7F], // num lock 7307 145: [0xFF14], // scroll lock 7308 225: [0xFE03] // altgraph (iso_level3_shift) 7309 }; 7310 7311 /** 7312 * Map of known JavaScript keyidentifiers which do not map to typable 7313 * characters to their unshifted X11 keysym equivalents. 7314 * 7315 * @private 7316 * @type {!Object.<string, number[]>} 7317 */ 7318 var keyidentifier_keysym = { 7319 "Again": [0xFF66], 7320 "AllCandidates": [0xFF3D], 7321 "Alphanumeric": [0xFF30], 7322 "Alt": [0xFFE9, 0xFFE9, 0xFE03], 7323 "Attn": [0xFD0E], 7324 "AltGraph": [0xFE03], 7325 "ArrowDown": [0xFF54], 7326 "ArrowLeft": [0xFF51], 7327 "ArrowRight": [0xFF53], 7328 "ArrowUp": [0xFF52], 7329 "Backspace": [0xFF08], 7330 "CapsLock": [0xFFE5], 7331 "Cancel": [0xFF69], 7332 "Clear": [0xFF0B], 7333 "Convert": [0xFF21], 7334 "Copy": [0xFD15], 7335 "Crsel": [0xFD1C], 7336 "CrSel": [0xFD1C], 7337 "CodeInput": [0xFF37], 7338 "Compose": [0xFF20], 7339 "Control": [0xFFE3, 0xFFE3, 0xFFE4], 7340 "ContextMenu": [0xFF67], 7341 "Delete": [0xFFFF], 7342 "Down": [0xFF54], 7343 "End": [0xFF57], 7344 "Enter": [0xFF0D], 7345 "EraseEof": [0xFD06], 7346 "Escape": [0xFF1B], 7347 "Execute": [0xFF62], 7348 "Exsel": [0xFD1D], 7349 "ExSel": [0xFD1D], 7350 "F1": [0xFFBE], 7351 "F2": [0xFFBF], 7352 "F3": [0xFFC0], 7353 "F4": [0xFFC1], 7354 "F5": [0xFFC2], 7355 "F6": [0xFFC3], 7356 "F7": [0xFFC4], 7357 "F8": [0xFFC5], 7358 "F9": [0xFFC6], 7359 "F10": [0xFFC7], 7360 "F11": [0xFFC8], 7361 "F12": [0xFFC9], 7362 "F13": [0xFFCA], 7363 "F14": [0xFFCB], 7364 "F15": [0xFFCC], 7365 "F16": [0xFFCD], 7366 "F17": [0xFFCE], 7367 "F18": [0xFFCF], 7368 "F19": [0xFFD0], 7369 "F20": [0xFFD1], 7370 "F21": [0xFFD2], 7371 "F22": [0xFFD3], 7372 "F23": [0xFFD4], 7373 "F24": [0xFFD5], 7374 "Find": [0xFF68], 7375 "GroupFirst": [0xFE0C], 7376 "GroupLast": [0xFE0E], 7377 "GroupNext": [0xFE08], 7378 "GroupPrevious": [0xFE0A], 7379 "FullWidth": null, 7380 "HalfWidth": null, 7381 "HangulMode": [0xFF31], 7382 "Hankaku": [0xFF29], 7383 "HanjaMode": [0xFF34], 7384 "Help": [0xFF6A], 7385 "Hiragana": [0xFF25], 7386 "HiraganaKatakana": [0xFF27], 7387 "Home": [0xFF50], 7388 "Hyper": [0xFFED, 0xFFED, 0xFFEE], 7389 "Insert": [0xFF63], 7390 "JapaneseHiragana": [0xFF25], 7391 "JapaneseKatakana": [0xFF26], 7392 "JapaneseRomaji": [0xFF24], 7393 "JunjaMode": [0xFF38], 7394 "KanaMode": [0xFF2D], 7395 "KanjiMode": [0xFF21], 7396 "Katakana": [0xFF26], 7397 "Left": [0xFF51], 7398 "Meta": [0xFFE7, 0xFFE7, 0xFFE8], 7399 "ModeChange": [0xFF7E], 7400 "NumLock": [0xFF7F], 7401 "PageDown": [0xFF56], 7402 "PageUp": [0xFF55], 7403 "Pause": [0xFF13], 7404 "Play": [0xFD16], 7405 "PreviousCandidate": [0xFF3E], 7406 "PrintScreen": [0xFF61], 7407 "Redo": [0xFF66], 7408 "Right": [0xFF53], 7409 "RomanCharacters": null, 7410 "Scroll": [0xFF14], 7411 "Select": [0xFF60], 7412 "Separator": [0xFFAC], 7413 "Shift": [0xFFE1, 0xFFE1, 0xFFE2], 7414 "SingleCandidate": [0xFF3C], 7415 "Super": [0xFFEB, 0xFFEB, 0xFFEC], 7416 "Tab": [0xFF09], 7417 "UIKeyInputDownArrow": [0xFF54], 7418 "UIKeyInputEscape": [0xFF1B], 7419 "UIKeyInputLeftArrow": [0xFF51], 7420 "UIKeyInputRightArrow": [0xFF53], 7421 "UIKeyInputUpArrow": [0xFF52], 7422 "Up": [0xFF52], 7423 "Undo": [0xFF65], 7424 "Win": [0xFFE7, 0xFFE7, 0xFFE8], 7425 "Zenkaku": [0xFF28], 7426 "ZenkakuHankaku": [0xFF2A] 7427 }; 7428 7429 /** 7430 * All keysyms which should not repeat when held down. 7431 * 7432 * @private 7433 * @type {!Object.<number, boolean>} 7434 */ 7435 var no_repeat = { 7436 0xFE03: true, // ISO Level 3 Shift (AltGr) 7437 0xFFE1: true, // Left shift 7438 0xFFE2: true, // Right shift 7439 0xFFE3: true, // Left ctrl 7440 0xFFE4: true, // Right ctrl 7441 0xFFE5: true, // Caps Lock 7442 0xFFE7: true, // Left meta 7443 0xFFE8: true, // Right meta 7444 0xFFE9: true, // Left alt 7445 0xFFEA: true, // Right alt 7446 0xFFEB: true, // Left super/hyper 7447 0xFFEC: true // Right super/hyper 7448 }; 7449 7450 /** 7451 * All modifiers and their states. 7452 * 7453 * @type {!Guacamole.Keyboard.ModifierState} 7454 */ 7455 this.modifiers = new Guacamole.Keyboard.ModifierState(); 7456 7457 /** 7458 * The state of every key, indexed by keysym. If a particular key is 7459 * pressed, the value of pressed for that keysym will be true. If a key 7460 * is not currently pressed, it will not be defined. 7461 * 7462 * @type {!Object.<number, boolean>} 7463 */ 7464 this.pressed = {}; 7465 7466 /** 7467 * The state of every key, indexed by keysym, for strictly those keys whose 7468 * status has been indirectly determined thorugh observation of other key 7469 * events. If a particular key is implicitly pressed, the value of 7470 * implicitlyPressed for that keysym will be true. If a key 7471 * is not currently implicitly pressed (the key is not pressed OR the state 7472 * of the key is explicitly known), it will not be defined. 7473 * 7474 * @private 7475 * @type {!Object.<number, boolean>} 7476 */ 7477 var implicitlyPressed = {}; 7478 7479 /** 7480 * The last result of calling the onkeydown handler for each key, indexed 7481 * by keysym. This is used to prevent/allow default actions for key events, 7482 * even when the onkeydown handler cannot be called again because the key 7483 * is (theoretically) still pressed. 7484 * 7485 * @private 7486 * @type {!Object.<number, boolean>} 7487 */ 7488 var last_keydown_result = {}; 7489 7490 /** 7491 * The keysym most recently associated with a given keycode when keydown 7492 * fired. This object maps keycodes to keysyms. 7493 * 7494 * @private 7495 * @type {!Object.<number, number>} 7496 */ 7497 var recentKeysym = {}; 7498 7499 /** 7500 * Timeout before key repeat starts. 7501 * 7502 * @private 7503 * @type {number} 7504 */ 7505 var key_repeat_timeout = null; 7506 7507 /** 7508 * Interval which presses and releases the last key pressed while that 7509 * key is still being held down. 7510 * 7511 * @private 7512 * @type {number} 7513 */ 7514 var key_repeat_interval = null; 7515 7516 /** 7517 * Given an array of keysyms indexed by location, returns the keysym 7518 * for the given location, or the keysym for the standard location if 7519 * undefined. 7520 * 7521 * @private 7522 * @param {number[]} keysyms 7523 * An array of keysyms, where the index of the keysym in the array is 7524 * the location value. 7525 * 7526 * @param {!number} location 7527 * The location on the keyboard corresponding to the key pressed, as 7528 * defined at: http://www.w3.org/TR/DOM-Level-3-Events/#events-KeyboardEvent 7529 */ 7530 var get_keysym = function get_keysym(keysyms, location) { 7531 7532 if (!keysyms) 7533 return null; 7534 7535 return keysyms[location] || keysyms[0]; 7536 }; 7537 7538 /** 7539 * Returns true if the given keysym corresponds to a printable character, 7540 * false otherwise. 7541 * 7542 * @param {!number} keysym 7543 * The keysym to check. 7544 * 7545 * @returns {!boolean} 7546 * true if the given keysym corresponds to a printable character, 7547 * false otherwise. 7548 */ 7549 var isPrintable = function isPrintable(keysym) { 7550 7551 // Keysyms with Unicode equivalents are printable 7552 return (keysym >= 0x00 && keysym <= 0xFF) 7553 || (keysym & 0xFFFF0000) === 0x01000000; 7554 7555 }; 7556 7557 function keysym_from_key_identifier(identifier, location, shifted) { 7558 7559 if (!identifier) 7560 return null; 7561 7562 var typedCharacter; 7563 7564 // If identifier is U+xxxx, decode Unicode character 7565 var unicodePrefixLocation = identifier.indexOf("U+"); 7566 if (unicodePrefixLocation >= 0) { 7567 var hex = identifier.substring(unicodePrefixLocation+2); 7568 typedCharacter = String.fromCharCode(parseInt(hex, 16)); 7569 } 7570 7571 // If single character and not keypad, use that as typed character 7572 else if (identifier.length === 1 && location !== 3) 7573 typedCharacter = identifier; 7574 7575 // Otherwise, look up corresponding keysym 7576 else 7577 return get_keysym(keyidentifier_keysym[identifier], location); 7578 7579 // Alter case if necessary 7580 if (shifted === true) 7581 typedCharacter = typedCharacter.toUpperCase(); 7582 else if (shifted === false) 7583 typedCharacter = typedCharacter.toLowerCase(); 7584 7585 // Get codepoint 7586 var codepoint = typedCharacter.charCodeAt(0); 7587 return keysym_from_charcode(codepoint); 7588 7589 } 7590 7591 function isControlCharacter(codepoint) { 7592 return codepoint <= 0x1F || (codepoint >= 0x7F && codepoint <= 0x9F); 7593 } 7594 7595 function keysym_from_charcode(codepoint) { 7596 7597 // Keysyms for control characters 7598 if (isControlCharacter(codepoint)) return 0xFF00 | codepoint; 7599 7600 // Keysyms for ASCII chars 7601 if (codepoint >= 0x0000 && codepoint <= 0x00FF) 7602 return codepoint; 7603 7604 // Keysyms for Unicode 7605 if (codepoint >= 0x0100 && codepoint <= 0x10FFFF) 7606 return 0x01000000 | codepoint; 7607 7608 return null; 7609 7610 } 7611 7612 function keysym_from_keycode(keyCode, location) { 7613 return get_keysym(keycodeKeysyms[keyCode], location); 7614 } 7615 7616 /** 7617 * Heuristically detects if the legacy keyIdentifier property of 7618 * a keydown/keyup event looks incorrectly derived. Chrome, and 7619 * presumably others, will produce the keyIdentifier by assuming 7620 * the keyCode is the Unicode codepoint for that key. This is not 7621 * correct in all cases. 7622 * 7623 * @private 7624 * @param {!number} keyCode 7625 * The keyCode from a browser keydown/keyup event. 7626 * 7627 * @param {string} keyIdentifier 7628 * The legacy keyIdentifier from a browser keydown/keyup event. 7629 * 7630 * @returns {!boolean} 7631 * true if the keyIdentifier looks sane, false if the keyIdentifier 7632 * appears incorrectly derived or is missing entirely. 7633 */ 7634 var key_identifier_sane = function key_identifier_sane(keyCode, keyIdentifier) { 7635 7636 // Missing identifier is not sane 7637 if (!keyIdentifier) 7638 return false; 7639 7640 // Assume non-Unicode keyIdentifier values are sane 7641 var unicodePrefixLocation = keyIdentifier.indexOf("U+"); 7642 if (unicodePrefixLocation === -1) 7643 return true; 7644 7645 // If the Unicode codepoint isn't identical to the keyCode, 7646 // then the identifier is likely correct 7647 var codepoint = parseInt(keyIdentifier.substring(unicodePrefixLocation+2), 16); 7648 if (keyCode !== codepoint) 7649 return true; 7650 7651 // The keyCodes for A-Z and 0-9 are actually identical to their 7652 // Unicode codepoints 7653 if ((keyCode >= 65 && keyCode <= 90) || (keyCode >= 48 && keyCode <= 57)) 7654 return true; 7655 7656 // The keyIdentifier does NOT appear sane 7657 return false; 7658 7659 }; 7660 7661 /** 7662 * Marks a key as pressed, firing the keydown event if registered. Key 7663 * repeat for the pressed key will start after a delay if that key is 7664 * not a modifier. The return value of this function depends on the 7665 * return value of the keydown event handler, if any. 7666 * 7667 * @param {number} keysym 7668 * The keysym of the key to press. 7669 * 7670 * @return {boolean} 7671 * true if event should NOT be canceled, false otherwise. 7672 */ 7673 this.press = function(keysym) { 7674 7675 // Don't bother with pressing the key if the key is unknown 7676 if (keysym === null) return; 7677 7678 // Only press if released 7679 if (!guac_keyboard.pressed[keysym]) { 7680 7681 // Mark key as pressed 7682 guac_keyboard.pressed[keysym] = true; 7683 7684 // Send key event 7685 if (guac_keyboard.onkeydown) { 7686 var result = guac_keyboard.onkeydown(keysym); 7687 last_keydown_result[keysym] = result; 7688 7689 // Stop any current repeat 7690 window.clearTimeout(key_repeat_timeout); 7691 window.clearInterval(key_repeat_interval); 7692 7693 // Repeat after a delay as long as pressed 7694 if (!no_repeat[keysym]) 7695 key_repeat_timeout = window.setTimeout(function() { 7696 key_repeat_interval = window.setInterval(function() { 7697 guac_keyboard.onkeyup(keysym); 7698 guac_keyboard.onkeydown(keysym); 7699 }, 50); 7700 }, 500); 7701 7702 return result; 7703 } 7704 } 7705 7706 // Return the last keydown result by default, resort to false if unknown 7707 return last_keydown_result[keysym] || false; 7708 7709 }; 7710 7711 /** 7712 * Marks a key as released, firing the keyup event if registered. 7713 * 7714 * @param {number} keysym 7715 * The keysym of the key to release. 7716 */ 7717 this.release = function(keysym) { 7718 7719 // Only release if pressed 7720 if (guac_keyboard.pressed[keysym]) { 7721 7722 // Mark key as released 7723 delete guac_keyboard.pressed[keysym]; 7724 delete implicitlyPressed[keysym]; 7725 7726 // Stop repeat 7727 window.clearTimeout(key_repeat_timeout); 7728 window.clearInterval(key_repeat_interval); 7729 7730 // Send key event 7731 if (keysym !== null && guac_keyboard.onkeyup) 7732 guac_keyboard.onkeyup(keysym); 7733 7734 } 7735 7736 }; 7737 7738 /** 7739 * Presses and releases the keys necessary to type the given string of 7740 * text. 7741 * 7742 * @param {!string} str 7743 * The string to type. 7744 */ 7745 this.type = function type(str) { 7746 7747 // Press/release the key corresponding to each character in the string 7748 for (var i = 0; i < str.length; i++) { 7749 7750 // Determine keysym of current character 7751 var codepoint = str.codePointAt ? str.codePointAt(i) : str.charCodeAt(i); 7752 var keysym = keysym_from_charcode(codepoint); 7753 7754 // Press and release key for current character 7755 guac_keyboard.press(keysym); 7756 guac_keyboard.release(keysym); 7757 7758 } 7759 7760 }; 7761 7762 /** 7763 * Resets the state of this keyboard, releasing all keys, and firing keyup 7764 * events for each released key. 7765 */ 7766 this.reset = function() { 7767 7768 // Release all pressed keys 7769 for (var keysym in guac_keyboard.pressed) 7770 guac_keyboard.release(parseInt(keysym)); 7771 7772 // Clear event log 7773 eventLog = []; 7774 7775 }; 7776 7777 /** 7778 * Resynchronizes the remote state of the given modifier with its 7779 * corresponding local modifier state, as dictated by 7780 * {@link KeyEvent#modifiers} within the given key event, by pressing or 7781 * releasing keysyms. 7782 * 7783 * @private 7784 * @param {!string} modifier 7785 * The name of the {@link Guacamole.Keyboard.ModifierState} property 7786 * being updated. 7787 * 7788 * @param {!number[]} keysyms 7789 * The keysyms which represent the modifier being updated. 7790 * 7791 * @param {!KeyEvent} keyEvent 7792 * Guacamole's current best interpretation of the key event being 7793 * processed. 7794 */ 7795 var updateModifierState = function updateModifierState(modifier, 7796 keysyms, keyEvent) { 7797 7798 var localState = keyEvent.modifiers[modifier]; 7799 var remoteState = guac_keyboard.modifiers[modifier]; 7800 7801 var i; 7802 7803 // Do not trust changes in modifier state for events directly involving 7804 // that modifier: (1) the flag may erroneously be cleared despite 7805 // another version of the same key still being held and (2) the change 7806 // in flag may be due to the current event being processed, thus 7807 // updating things here is at best redundant and at worst incorrect 7808 if (keysyms.indexOf(keyEvent.keysym) !== -1) 7809 return; 7810 7811 // Release all related keys if modifier is implicitly released 7812 if (remoteState && localState === false) { 7813 for (i = 0; i < keysyms.length; i++) { 7814 guac_keyboard.release(keysyms[i]); 7815 } 7816 } 7817 7818 // Press if modifier is implicitly pressed 7819 else if (!remoteState && localState) { 7820 7821 // Verify that modifier flag isn't already pressed or already set 7822 // due to another version of the same key being held down 7823 for (i = 0; i < keysyms.length; i++) { 7824 if (guac_keyboard.pressed[keysyms[i]]) 7825 return; 7826 } 7827 7828 // Mark as implicitly pressed only if there is other information 7829 // within the key event relating to a different key. Some 7830 // platforms, such as iOS, will send essentially empty key events 7831 // for modifier keys, using only the modifier flags to signal the 7832 // identity of the key. 7833 var keysym = keysyms[0]; 7834 if (keyEvent.keysym) 7835 implicitlyPressed[keysym] = true; 7836 7837 guac_keyboard.press(keysym); 7838 7839 } 7840 7841 }; 7842 7843 /** 7844 * Given a keyboard event, updates the remote key state to match the local 7845 * modifier state and remote based on the modifier flags within the event. 7846 * This function pays no attention to keycodes. 7847 * 7848 * @private 7849 * @param {!KeyEvent} keyEvent 7850 * Guacamole's current best interpretation of the key event being 7851 * processed. 7852 */ 7853 var syncModifierStates = function syncModifierStates(keyEvent) { 7854 7855 // Resync state of alt 7856 updateModifierState('alt', [ 7857 0xFFE9, // Left alt 7858 0xFFEA, // Right alt 7859 0xFE03 // AltGr 7860 ], keyEvent); 7861 7862 // Resync state of shift 7863 updateModifierState('shift', [ 7864 0xFFE1, // Left shift 7865 0xFFE2 // Right shift 7866 ], keyEvent); 7867 7868 // Resync state of ctrl 7869 updateModifierState('ctrl', [ 7870 0xFFE3, // Left ctrl 7871 0xFFE4 // Right ctrl 7872 ], keyEvent); 7873 7874 // Resync state of meta 7875 updateModifierState('meta', [ 7876 0xFFE7, // Left meta 7877 0xFFE8 // Right meta 7878 ], keyEvent); 7879 7880 // Resync state of hyper 7881 updateModifierState('hyper', [ 7882 0xFFEB, // Left super/hyper 7883 0xFFEC // Right super/hyper 7884 ], keyEvent); 7885 7886 // Update state 7887 guac_keyboard.modifiers = keyEvent.modifiers; 7888 7889 }; 7890 7891 /** 7892 * Returns whether all currently pressed keys were implicitly pressed. A 7893 * key is implicitly pressed if its status was inferred indirectly from 7894 * inspection of other key events. 7895 * 7896 * @private 7897 * @returns {!boolean} 7898 * true if all currently pressed keys were implicitly pressed, false 7899 * otherwise. 7900 */ 7901 var isStateImplicit = function isStateImplicit() { 7902 7903 for (var keysym in guac_keyboard.pressed) { 7904 if (!implicitlyPressed[keysym]) 7905 return false; 7906 } 7907 7908 return true; 7909 7910 }; 7911 7912 /** 7913 * Reads through the event log, removing events from the head of the log 7914 * when the corresponding true key presses are known (or as known as they 7915 * can be). 7916 * 7917 * @private 7918 * @return {boolean} 7919 * Whether the default action of the latest event should be prevented. 7920 */ 7921 function interpret_events() { 7922 7923 // Do not prevent default if no event could be interpreted 7924 var handled_event = interpret_event(); 7925 if (!handled_event) 7926 return false; 7927 7928 // Interpret as much as possible 7929 var last_event; 7930 do { 7931 last_event = handled_event; 7932 handled_event = interpret_event(); 7933 } while (handled_event !== null); 7934 7935 // Reset keyboard state if we cannot expect to receive any further 7936 // keyup events 7937 if (isStateImplicit()) 7938 guac_keyboard.reset(); 7939 7940 return last_event.defaultPrevented; 7941 7942 } 7943 7944 /** 7945 * Releases Ctrl+Alt, if both are currently pressed and the given keysym 7946 * looks like a key that may require AltGr. 7947 * 7948 * @private 7949 * @param {!number} keysym 7950 * The key that was just pressed. 7951 */ 7952 var release_simulated_altgr = function release_simulated_altgr(keysym) { 7953 7954 // Both Ctrl+Alt must be pressed if simulated AltGr is in use 7955 if (!guac_keyboard.modifiers.ctrl || !guac_keyboard.modifiers.alt) 7956 return; 7957 7958 // Assume [A-Z] never require AltGr 7959 if (keysym >= 0x0041 && keysym <= 0x005A) 7960 return; 7961 7962 // Assume [a-z] never require AltGr 7963 if (keysym >= 0x0061 && keysym <= 0x007A) 7964 return; 7965 7966 // Release Ctrl+Alt if the keysym is printable 7967 if (keysym <= 0xFF || (keysym & 0xFF000000) === 0x01000000) { 7968 guac_keyboard.release(0xFFE3); // Left ctrl 7969 guac_keyboard.release(0xFFE4); // Right ctrl 7970 guac_keyboard.release(0xFFE9); // Left alt 7971 guac_keyboard.release(0xFFEA); // Right alt 7972 } 7973 7974 }; 7975 7976 /** 7977 * Reads through the event log, interpreting the first event, if possible, 7978 * and returning that event. If no events can be interpreted, due to a 7979 * total lack of events or the need for more events, null is returned. Any 7980 * interpreted events are automatically removed from the log. 7981 * 7982 * @private 7983 * @return {KeyEvent} 7984 * The first key event in the log, if it can be interpreted, or null 7985 * otherwise. 7986 */ 7987 var interpret_event = function interpret_event() { 7988 7989 // Peek at first event in log 7990 var first = eventLog[0]; 7991 if (!first) 7992 return null; 7993 7994 // Keydown event 7995 if (first instanceof KeydownEvent) { 7996 7997 var keysym = null; 7998 var accepted_events = []; 7999 8000 // Defer handling of Meta until it is known to be functioning as a 8001 // modifier (it may otherwise actually be an alternative method for 8002 // pressing a single key, such as Meta+Left for Home on ChromeOS) 8003 if (first.keysym === 0xFFE7 || first.keysym === 0xFFE8) { 8004 8005 // Defer handling until further events exist to provide context 8006 if (eventLog.length === 1) 8007 return null; 8008 8009 // Drop keydown if it turns out Meta does not actually apply 8010 if (eventLog[1].keysym !== first.keysym) { 8011 if (!eventLog[1].modifiers.meta) 8012 return eventLog.shift(); 8013 } 8014 8015 // Drop duplicate keydown events while waiting to determine 8016 // whether to acknowledge Meta (browser may repeat keydown 8017 // while the key is held) 8018 else if (eventLog[1] instanceof KeydownEvent) 8019 return eventLog.shift(); 8020 8021 } 8022 8023 // If event itself is reliable, no need to wait for other events 8024 if (first.reliable) { 8025 keysym = first.keysym; 8026 accepted_events = eventLog.splice(0, 1); 8027 } 8028 8029 // If keydown is immediately followed by a keypress, use the indicated character 8030 else if (eventLog[1] instanceof KeypressEvent) { 8031 keysym = eventLog[1].keysym; 8032 accepted_events = eventLog.splice(0, 2); 8033 } 8034 8035 // If keydown is immediately followed by anything else, then no 8036 // keypress can possibly occur to clarify this event, and we must 8037 // handle it now 8038 else if (eventLog[1]) { 8039 keysym = first.keysym; 8040 accepted_events = eventLog.splice(0, 1); 8041 } 8042 8043 // Fire a key press if valid events were found 8044 if (accepted_events.length > 0) { 8045 8046 syncModifierStates(first); 8047 8048 if (keysym) { 8049 8050 // Fire event 8051 release_simulated_altgr(keysym); 8052 var defaultPrevented = !guac_keyboard.press(keysym); 8053 recentKeysym[first.keyCode] = keysym; 8054 8055 // Release the key now if we cannot rely on the associated 8056 // keyup event 8057 if (!first.keyupReliable) 8058 guac_keyboard.release(keysym); 8059 8060 // Record whether default was prevented 8061 for (var i=0; i<accepted_events.length; i++) 8062 accepted_events[i].defaultPrevented = defaultPrevented; 8063 8064 } 8065 8066 return first; 8067 8068 } 8069 8070 } // end if keydown 8071 8072 // Keyup event 8073 else if (first instanceof KeyupEvent && !quirks.keyupUnreliable) { 8074 8075 // Release specific key if known 8076 var keysym = first.keysym; 8077 if (keysym) { 8078 guac_keyboard.release(keysym); 8079 delete recentKeysym[first.keyCode]; 8080 first.defaultPrevented = true; 8081 } 8082 8083 // Otherwise, fall back to releasing all keys 8084 else { 8085 guac_keyboard.reset(); 8086 return first; 8087 } 8088 8089 syncModifierStates(first); 8090 return eventLog.shift(); 8091 8092 } // end if keyup 8093 8094 // Ignore any other type of event (keypress by itself is invalid, and 8095 // unreliable keyup events should simply be dumped) 8096 else 8097 return eventLog.shift(); 8098 8099 // No event interpreted 8100 return null; 8101 8102 }; 8103 8104 /** 8105 * Returns the keyboard location of the key associated with the given 8106 * keyboard event. The location differentiates key events which otherwise 8107 * have the same keycode, such as left shift vs. right shift. 8108 * 8109 * @private 8110 * @param {!KeyboardEvent} e 8111 * A JavaScript keyboard event, as received through the DOM via a 8112 * "keydown", "keyup", or "keypress" handler. 8113 * 8114 * @returns {!number} 8115 * The location of the key event on the keyboard, as defined at: 8116 * http://www.w3.org/TR/DOM-Level-3-Events/#events-KeyboardEvent 8117 */ 8118 var getEventLocation = function getEventLocation(e) { 8119 8120 // Use standard location, if possible 8121 if ('location' in e) 8122 return e.location; 8123 8124 // Failing that, attempt to use deprecated keyLocation 8125 if ('keyLocation' in e) 8126 return e.keyLocation; 8127 8128 // If no location is available, assume left side 8129 return 0; 8130 8131 }; 8132 8133 /** 8134 * Attempts to mark the given Event as having been handled by this 8135 * Guacamole.Keyboard. If the Event has already been marked as handled, 8136 * false is returned. 8137 * 8138 * @param {!Event} e 8139 * The Event to mark. 8140 * 8141 * @returns {!boolean} 8142 * true if the given Event was successfully marked, false if the given 8143 * Event was already marked. 8144 */ 8145 var markEvent = function markEvent(e) { 8146 8147 // Fail if event is already marked 8148 if (e[EVENT_MARKER]) 8149 return false; 8150 8151 // Mark event otherwise 8152 e[EVENT_MARKER] = true; 8153 return true; 8154 8155 }; 8156 8157 /** 8158 * Attaches event listeners to the given Element, automatically translating 8159 * received key, input, and composition events into simple keydown/keyup 8160 * events signalled through this Guacamole.Keyboard's onkeydown and 8161 * onkeyup handlers. 8162 * 8163 * @param {!(Element|Document)} element 8164 * The Element to attach event listeners to for the sake of handling 8165 * key or input events. 8166 */ 8167 this.listenTo = function listenTo(element) { 8168 8169 // When key pressed 8170 element.addEventListener("keydown", function(e) { 8171 8172 // Only intercept if handler set 8173 if (!guac_keyboard.onkeydown) return; 8174 8175 // Ignore events which have already been handled 8176 if (!markEvent(e)) return; 8177 8178 var keydownEvent = new KeydownEvent(e); 8179 8180 // Ignore (but do not prevent) the "composition" keycode sent by some 8181 // browsers when an IME is in use (see: http://lists.w3.org/Archives/Public/www-dom/2010JulSep/att-0182/keyCode-spec.html) 8182 if (keydownEvent.keyCode === 229) 8183 return; 8184 8185 // Log event 8186 eventLog.push(keydownEvent); 8187 8188 // Interpret as many events as possible, prevent default if indicated 8189 if (interpret_events()) 8190 e.preventDefault(); 8191 8192 }, true); 8193 8194 // When key pressed 8195 element.addEventListener("keypress", function(e) { 8196 8197 // Only intercept if handler set 8198 if (!guac_keyboard.onkeydown && !guac_keyboard.onkeyup) return; 8199 8200 // Ignore events which have already been handled 8201 if (!markEvent(e)) return; 8202 8203 // Log event 8204 eventLog.push(new KeypressEvent(e)); 8205 8206 // Interpret as many events as possible, prevent default if indicated 8207 if (interpret_events()) 8208 e.preventDefault(); 8209 8210 }, true); 8211 8212 // When key released 8213 element.addEventListener("keyup", function(e) { 8214 8215 // Only intercept if handler set 8216 if (!guac_keyboard.onkeyup) return; 8217 8218 // Ignore events which have already been handled 8219 if (!markEvent(e)) return; 8220 8221 e.preventDefault(); 8222 8223 // Log event, call for interpretation 8224 eventLog.push(new KeyupEvent(e)); 8225 interpret_events(); 8226 8227 }, true); 8228 8229 /** 8230 * Handles the given "input" event, typing the data within the input text. 8231 * If the event is complete (text is provided), handling of "compositionend" 8232 * events is suspended, as such events may conflict with input events. 8233 * 8234 * @private 8235 * @param {!InputEvent} e 8236 * The "input" event to handle. 8237 */ 8238 var handleInput = function handleInput(e) { 8239 8240 // Only intercept if handler set 8241 if (!guac_keyboard.onkeydown && !guac_keyboard.onkeyup) return; 8242 8243 // Ignore events which have already been handled 8244 if (!markEvent(e)) return; 8245 8246 // Type all content written 8247 if (e.data && !e.isComposing) { 8248 element.removeEventListener("compositionend", handleComposition, false); 8249 guac_keyboard.type(e.data); 8250 } 8251 8252 }; 8253 8254 /** 8255 * Handles the given "compositionend" event, typing the data within the 8256 * composed text. If the event is complete (composed text is provided), 8257 * handling of "input" events is suspended, as such events may conflict 8258 * with composition events. 8259 * 8260 * @private 8261 * @param {!CompositionEvent} e 8262 * The "compositionend" event to handle. 8263 */ 8264 var handleComposition = function handleComposition(e) { 8265 8266 // Only intercept if handler set 8267 if (!guac_keyboard.onkeydown && !guac_keyboard.onkeyup) return; 8268 8269 // Ignore events which have already been handled 8270 if (!markEvent(e)) return; 8271 8272 // Type all content written 8273 if (e.data) { 8274 element.removeEventListener("input", handleInput, false); 8275 guac_keyboard.type(e.data); 8276 } 8277 8278 }; 8279 8280 // Automatically type text entered into the wrapped field 8281 element.addEventListener("input", handleInput, false); 8282 element.addEventListener("compositionend", handleComposition, false); 8283 8284 }; 8285 8286 // Listen to given element, if any 8287 if (element) 8288 guac_keyboard.listenTo(element); 8289 8290}; 8291 8292/** 8293 * The unique numerical identifier to assign to the next Guacamole.Keyboard 8294 * instance. 8295 * 8296 * @private 8297 * @type {!number} 8298 */ 8299Guacamole.Keyboard._nextID = 0; 8300 8301/** 8302 * The state of all supported keyboard modifiers. 8303 * @constructor 8304 */ 8305Guacamole.Keyboard.ModifierState = function() { 8306 8307 /** 8308 * Whether shift is currently pressed. 8309 * 8310 * @type {!boolean} 8311 */ 8312 this.shift = false; 8313 8314 /** 8315 * Whether ctrl is currently pressed. 8316 * 8317 * @type {!boolean} 8318 */ 8319 this.ctrl = false; 8320 8321 /** 8322 * Whether alt is currently pressed. 8323 * 8324 * @type {!boolean} 8325 */ 8326 this.alt = false; 8327 8328 /** 8329 * Whether meta (apple key) is currently pressed. 8330 * 8331 * @type {!boolean} 8332 */ 8333 this.meta = false; 8334 8335 /** 8336 * Whether hyper (windows key) is currently pressed. 8337 * 8338 * @type {!boolean} 8339 */ 8340 this.hyper = false; 8341 8342}; 8343 8344/** 8345 * Returns the modifier state applicable to the keyboard event given. 8346 * 8347 * @param {!KeyboardEvent} e 8348 * The keyboard event to read. 8349 * 8350 * @returns {!Guacamole.Keyboard.ModifierState} 8351 * The current state of keyboard modifiers. 8352 */ 8353Guacamole.Keyboard.ModifierState.fromKeyboardEvent = function(e) { 8354 8355 var state = new Guacamole.Keyboard.ModifierState(); 8356 8357 // Assign states from old flags 8358 state.shift = e.shiftKey; 8359 state.ctrl = e.ctrlKey; 8360 state.alt = e.altKey; 8361 state.meta = e.metaKey; 8362 8363 // Use DOM3 getModifierState() for others 8364 if (e.getModifierState) { 8365 state.hyper = e.getModifierState("OS") 8366 || e.getModifierState("Super") 8367 || e.getModifierState("Hyper") 8368 || e.getModifierState("Win"); 8369 } 8370 8371 return state; 8372 8373}; 8374/* 8375 * Licensed to the Apache Software Foundation (ASF) under one 8376 * or more contributor license agreements. See the NOTICE file 8377 * distributed with this work for additional information 8378 * regarding copyright ownership. The ASF licenses this file 8379 * to you under the Apache License, Version 2.0 (the 8380 * "License"); you may not use this file except in compliance 8381 * with the License. You may obtain a copy of the License at 8382 * 8383 * http://www.apache.org/licenses/LICENSE-2.0 8384 * 8385 * Unless required by applicable law or agreed to in writing, 8386 * software distributed under the License is distributed on an 8387 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 8388 * KIND, either express or implied. See the License for the 8389 * specific language governing permissions and limitations 8390 * under the License. 8391 */ 8392 8393var Guacamole = Guacamole || {}; 8394 8395/** 8396 * Abstract ordered drawing surface. Each Layer contains a canvas element and 8397 * provides simple drawing instructions for drawing to that canvas element, 8398 * however unlike the canvas element itself, drawing operations on a Layer are 8399 * guaranteed to run in order, even if such an operation must wait for an image 8400 * to load before completing. 8401 * 8402 * @constructor 8403 * 8404 * @param {!number} width 8405 * The width of the Layer, in pixels. The canvas element backing this Layer 8406 * will be given this width. 8407 * 8408 * @param {!number} height 8409 * The height of the Layer, in pixels. The canvas element backing this 8410 * Layer will be given this height. 8411 */ 8412Guacamole.Layer = function(width, height) { 8413 8414 /** 8415 * Reference to this Layer. 8416 * 8417 * @private 8418 * @type {!Guacamole.Layer} 8419 */ 8420 var layer = this; 8421 8422 /** 8423 * The number of pixels the width or height of a layer must change before 8424 * the underlying canvas is resized. The underlying canvas will be kept at 8425 * dimensions which are integer multiples of this factor. 8426 * 8427 * @private 8428 * @constant 8429 * @type {!number} 8430 */ 8431 var CANVAS_SIZE_FACTOR = 64; 8432 8433 /** 8434 * The canvas element backing this Layer. 8435 * 8436 * @private 8437 * @type {!HTMLCanvasElement} 8438 */ 8439 var canvas = document.createElement("canvas"); 8440 8441 /** 8442 * The 2D display context of the canvas element backing this Layer. 8443 * 8444 * @private 8445 * @type {!CanvasRenderingContext2D} 8446 */ 8447 var context = canvas.getContext("2d"); 8448 context.save(); 8449 8450 /** 8451 * Whether the layer has not yet been drawn to. Once any draw operation 8452 * which affects the underlying canvas is invoked, this flag will be set to 8453 * false. 8454 * 8455 * @private 8456 * @type {!boolean} 8457 */ 8458 var empty = true; 8459 8460 /** 8461 * Whether a new path should be started with the next path drawing 8462 * operations. 8463 * 8464 * @private 8465 * @type {!boolean} 8466 */ 8467 var pathClosed = true; 8468 8469 /** 8470 * The number of states on the state stack. 8471 * 8472 * Note that there will ALWAYS be one element on the stack, but that 8473 * element is not exposed. It is only used to reset the layer to its 8474 * initial state. 8475 * 8476 * @private 8477 * @type {!number} 8478 */ 8479 var stackSize = 0; 8480 8481 /** 8482 * Map of all Guacamole channel masks to HTML5 canvas composite operation 8483 * names. Not all channel mask combinations are currently implemented. 8484 * 8485 * @private 8486 * @type {!Object.<number, string>} 8487 */ 8488 var compositeOperation = { 8489 /* 0x0 NOT IMPLEMENTED */ 8490 0x1: "destination-in", 8491 0x2: "destination-out", 8492 /* 0x3 NOT IMPLEMENTED */ 8493 0x4: "source-in", 8494 /* 0x5 NOT IMPLEMENTED */ 8495 0x6: "source-atop", 8496 /* 0x7 NOT IMPLEMENTED */ 8497 0x8: "source-out", 8498 0x9: "destination-atop", 8499 0xA: "xor", 8500 0xB: "destination-over", 8501 0xC: "copy", 8502 /* 0xD NOT IMPLEMENTED */ 8503 0xE: "source-over", 8504 0xF: "lighter" 8505 }; 8506 8507 /** 8508 * Resizes the canvas element backing this Layer. This function should only 8509 * be used internally. 8510 * 8511 * @private 8512 * @param {number} [newWidth=0] 8513 * The new width to assign to this Layer. 8514 * 8515 * @param {number} [newHeight=0] 8516 * The new height to assign to this Layer. 8517 */ 8518 var resize = function resize(newWidth, newHeight) { 8519 8520 // Default size to zero 8521 newWidth = newWidth || 0; 8522 newHeight = newHeight || 0; 8523 8524 // Calculate new dimensions of internal canvas 8525 var canvasWidth = Math.ceil(newWidth / CANVAS_SIZE_FACTOR) * CANVAS_SIZE_FACTOR; 8526 var canvasHeight = Math.ceil(newHeight / CANVAS_SIZE_FACTOR) * CANVAS_SIZE_FACTOR; 8527 8528 // Resize only if canvas dimensions are actually changing 8529 if (canvas.width !== canvasWidth || canvas.height !== canvasHeight) { 8530 8531 // Copy old data only if relevant and non-empty 8532 var oldData = null; 8533 if (!empty && canvas.width !== 0 && canvas.height !== 0) { 8534 8535 // Create canvas and context for holding old data 8536 oldData = document.createElement("canvas"); 8537 oldData.width = Math.min(layer.width, newWidth); 8538 oldData.height = Math.min(layer.height, newHeight); 8539 8540 var oldDataContext = oldData.getContext("2d"); 8541 8542 // Copy image data from current 8543 oldDataContext.drawImage(canvas, 8544 0, 0, oldData.width, oldData.height, 8545 0, 0, oldData.width, oldData.height); 8546 8547 } 8548 8549 // Preserve composite operation 8550 var oldCompositeOperation = context.globalCompositeOperation; 8551 8552 // Resize canvas 8553 canvas.width = canvasWidth; 8554 canvas.height = canvasHeight; 8555 8556 // Redraw old data, if any 8557 if (oldData) 8558 context.drawImage(oldData, 8559 0, 0, oldData.width, oldData.height, 8560 0, 0, oldData.width, oldData.height); 8561 8562 // Restore composite operation 8563 context.globalCompositeOperation = oldCompositeOperation; 8564 8565 // Acknowledge reset of stack (happens on resize of canvas) 8566 stackSize = 0; 8567 context.save(); 8568 8569 } 8570 8571 // If the canvas size is not changing, manually force state reset 8572 else 8573 layer.reset(); 8574 8575 // Assign new layer dimensions 8576 layer.width = newWidth; 8577 layer.height = newHeight; 8578 8579 }; 8580 8581 /** 8582 * Given the X and Y coordinates of the upper-left corner of a rectangle 8583 * and the rectangle's width and height, resize the backing canvas element 8584 * as necessary to ensure that the rectangle fits within the canvas 8585 * element's coordinate space. This function will only make the canvas 8586 * larger. If the rectangle already fits within the canvas element's 8587 * coordinate space, the canvas is left unchanged. 8588 * 8589 * @private 8590 * @param {!number} x 8591 * The X coordinate of the upper-left corner of the rectangle to fit. 8592 * 8593 * @param {!number} y 8594 * The Y coordinate of the upper-left corner of the rectangle to fit. 8595 * 8596 * @param {!number} w 8597 * The width of the rectangle to fit. 8598 * 8599 * @param {!number} h 8600 * The height of the rectangle to fit. 8601 */ 8602 function fitRect(x, y, w, h) { 8603 8604 // Calculate bounds 8605 var opBoundX = w + x; 8606 var opBoundY = h + y; 8607 8608 // Determine max width 8609 var resizeWidth; 8610 if (opBoundX > layer.width) 8611 resizeWidth = opBoundX; 8612 else 8613 resizeWidth = layer.width; 8614 8615 // Determine max height 8616 var resizeHeight; 8617 if (opBoundY > layer.height) 8618 resizeHeight = opBoundY; 8619 else 8620 resizeHeight = layer.height; 8621 8622 // Resize if necessary 8623 layer.resize(resizeWidth, resizeHeight); 8624 8625 } 8626 8627 /** 8628 * Set to true if this Layer should resize itself to accommodate the 8629 * dimensions of any drawing operation, and false (the default) otherwise. 8630 * 8631 * Note that setting this property takes effect immediately, and thus may 8632 * take effect on operations that were started in the past but have not 8633 * yet completed. If you wish the setting of this flag to only modify 8634 * future operations, you will need to make the setting of this flag an 8635 * operation with sync(). 8636 * 8637 * @example 8638 * // Set autosize to true for all future operations 8639 * layer.sync(function() { 8640 * layer.autosize = true; 8641 * }); 8642 * 8643 * @type {!boolean} 8644 * @default false 8645 */ 8646 this.autosize = false; 8647 8648 /** 8649 * The current width of this layer. 8650 * 8651 * @type {!number} 8652 */ 8653 this.width = width; 8654 8655 /** 8656 * The current height of this layer. 8657 * 8658 * @type {!number} 8659 */ 8660 this.height = height; 8661 8662 /** 8663 * Returns the canvas element backing this Layer. Note that the dimensions 8664 * of the canvas may not exactly match those of the Layer, as resizing a 8665 * canvas while maintaining its state is an expensive operation. 8666 * 8667 * @returns {!HTMLCanvasElement} 8668 * The canvas element backing this Layer. 8669 */ 8670 this.getCanvas = function getCanvas() { 8671 return canvas; 8672 }; 8673 8674 /** 8675 * Returns a new canvas element containing the same image as this Layer. 8676 * Unlike getCanvas(), the canvas element returned is guaranteed to have 8677 * the exact same dimensions as the Layer. 8678 * 8679 * @returns {!HTMLCanvasElement} 8680 * A new canvas element containing a copy of the image content this 8681 * Layer. 8682 */ 8683 this.toCanvas = function toCanvas() { 8684 8685 // Create new canvas having same dimensions 8686 var canvas = document.createElement('canvas'); 8687 canvas.width = layer.width; 8688 canvas.height = layer.height; 8689 8690 // Copy image contents to new canvas 8691 var context = canvas.getContext('2d'); 8692 context.drawImage(layer.getCanvas(), 0, 0); 8693 8694 return canvas; 8695 8696 }; 8697 8698 /** 8699 * Changes the size of this Layer to the given width and height. Resizing 8700 * is only attempted if the new size provided is actually different from 8701 * the current size. 8702 * 8703 * @param {!number} newWidth 8704 * The new width to assign to this Layer. 8705 * 8706 * @param {!number} newHeight 8707 * The new height to assign to this Layer. 8708 */ 8709 this.resize = function(newWidth, newHeight) { 8710 if (newWidth !== layer.width || newHeight !== layer.height) 8711 resize(newWidth, newHeight); 8712 }; 8713 8714 /** 8715 * Draws the specified image at the given coordinates. The image specified 8716 * must already be loaded. 8717 * 8718 * @param {!number} x 8719 * The destination X coordinate. 8720 * 8721 * @param {!number} y 8722 * The destination Y coordinate. 8723 * 8724 * @param {!CanvasImageSource} image 8725 * The image to draw. Note that this is not a URL. 8726 */ 8727 this.drawImage = function(x, y, image) { 8728 if (layer.autosize) fitRect(x, y, image.width, image.height); 8729 context.drawImage(image, x, y); 8730 empty = false; 8731 }; 8732 8733 /** 8734 * Transfer a rectangle of image data from one Layer to this Layer using the 8735 * specified transfer function. 8736 * 8737 * @param {!Guacamole.Layer} srcLayer 8738 * The Layer to copy image data from. 8739 * 8740 * @param {!number} srcx 8741 * The X coordinate of the upper-left corner of the rectangle within 8742 * the source Layer's coordinate space to copy data from. 8743 * 8744 * @param {!number} srcy 8745 * The Y coordinate of the upper-left corner of the rectangle within 8746 * the source Layer's coordinate space to copy data from. 8747 * 8748 * @param {!number} srcw 8749 * The width of the rectangle within the source Layer's coordinate 8750 * space to copy data from. 8751 * 8752 * @param {!number} srch 8753 * The height of the rectangle within the source Layer's coordinate 8754 * space to copy data from. 8755 * 8756 * @param {!number} x 8757 * The destination X coordinate. 8758 * 8759 * @param {!number} y 8760 * The destination Y coordinate. 8761 * 8762 * @param {!function} transferFunction 8763 * The transfer function to use to transfer data from source to 8764 * destination. 8765 */ 8766 this.transfer = function(srcLayer, srcx, srcy, srcw, srch, x, y, transferFunction) { 8767 8768 var srcCanvas = srcLayer.getCanvas(); 8769 8770 // If entire rectangle outside source canvas, stop 8771 if (srcx >= srcCanvas.width || srcy >= srcCanvas.height) return; 8772 8773 // Otherwise, clip rectangle to area 8774 if (srcx + srcw > srcCanvas.width) 8775 srcw = srcCanvas.width - srcx; 8776 8777 if (srcy + srch > srcCanvas.height) 8778 srch = srcCanvas.height - srcy; 8779 8780 // Stop if nothing to draw. 8781 if (srcw === 0 || srch === 0) return; 8782 8783 if (layer.autosize) fitRect(x, y, srcw, srch); 8784 8785 // Get image data from src and dst 8786 var src = srcLayer.getCanvas().getContext("2d").getImageData(srcx, srcy, srcw, srch); 8787 var dst = context.getImageData(x , y, srcw, srch); 8788 8789 // Apply transfer for each pixel 8790 for (var i=0; i<srcw*srch*4; i+=4) { 8791 8792 // Get source pixel environment 8793 var src_pixel = new Guacamole.Layer.Pixel( 8794 src.data[i], 8795 src.data[i+1], 8796 src.data[i+2], 8797 src.data[i+3] 8798 ); 8799 8800 // Get destination pixel environment 8801 var dst_pixel = new Guacamole.Layer.Pixel( 8802 dst.data[i], 8803 dst.data[i+1], 8804 dst.data[i+2], 8805 dst.data[i+3] 8806 ); 8807 8808 // Apply transfer function 8809 transferFunction(src_pixel, dst_pixel); 8810 8811 // Save pixel data 8812 dst.data[i ] = dst_pixel.red; 8813 dst.data[i+1] = dst_pixel.green; 8814 dst.data[i+2] = dst_pixel.blue; 8815 dst.data[i+3] = dst_pixel.alpha; 8816 8817 } 8818 8819 // Draw image data 8820 context.putImageData(dst, x, y); 8821 empty = false; 8822 8823 }; 8824 8825 /** 8826 * Put a rectangle of image data from one Layer to this Layer directly 8827 * without performing any alpha blending. Simply copy the data. 8828 * 8829 * @param {!Guacamole.Layer} srcLayer 8830 * The Layer to copy image data from. 8831 * 8832 * @param {!number} srcx 8833 * The X coordinate of the upper-left corner of the rectangle within 8834 * the source Layer's coordinate space to copy data from. 8835 * 8836 * @param {!number} srcy 8837 * The Y coordinate of the upper-left corner of the rectangle within 8838 * the source Layer's coordinate space to copy data from. 8839 * 8840 * @param {!number} srcw 8841 * The width of the rectangle within the source Layer's coordinate 8842 * space to copy data from. 8843 * 8844 * @param {!number} srch 8845 * The height of the rectangle within the source Layer's coordinate 8846 * space to copy data from. 8847 * 8848 * @param {!number} x 8849 * The destination X coordinate. 8850 * 8851 * @param {!number} y 8852 * The destination Y coordinate. 8853 */ 8854 this.put = function(srcLayer, srcx, srcy, srcw, srch, x, y) { 8855 8856 var srcCanvas = srcLayer.getCanvas(); 8857 8858 // If entire rectangle outside source canvas, stop 8859 if (srcx >= srcCanvas.width || srcy >= srcCanvas.height) return; 8860 8861 // Otherwise, clip rectangle to area 8862 if (srcx + srcw > srcCanvas.width) 8863 srcw = srcCanvas.width - srcx; 8864 8865 if (srcy + srch > srcCanvas.height) 8866 srch = srcCanvas.height - srcy; 8867 8868 // Stop if nothing to draw. 8869 if (srcw === 0 || srch === 0) return; 8870 8871 if (layer.autosize) fitRect(x, y, srcw, srch); 8872 8873 // Get image data from src and dst 8874 var src = srcLayer.getCanvas().getContext("2d").getImageData(srcx, srcy, srcw, srch); 8875 context.putImageData(src, x, y); 8876 empty = false; 8877 8878 }; 8879 8880 /** 8881 * Copy a rectangle of image data from one Layer to this Layer. This 8882 * operation will copy exactly the image data that will be drawn once all 8883 * operations of the source Layer that were pending at the time this 8884 * function was called are complete. This operation will not alter the 8885 * size of the source Layer even if its autosize property is set to true. 8886 * 8887 * @param {!Guacamole.Layer} srcLayer 8888 * The Layer to copy image data from. 8889 * 8890 * @param {!number} srcx 8891 * The X coordinate of the upper-left corner of the rectangle within 8892 * the source Layer's coordinate space to copy data from. 8893 * 8894 * @param {!number} srcy 8895 * The Y coordinate of the upper-left corner of the rectangle within 8896 * the source Layer's coordinate space to copy data from. 8897 * 8898 * @param {!number} srcw 8899 * The width of the rectangle within the source Layer's coordinate 8900 * space to copy data from. 8901 * 8902 * @param {!number} srch 8903 * The height of the rectangle within the source Layer's coordinate 8904 * space to copy data from. 8905 * 8906 * @param {!number} x 8907 * The destination X coordinate. 8908 * 8909 * @param {!number} y 8910 * The destination Y coordinate. 8911 */ 8912 this.copy = function(srcLayer, srcx, srcy, srcw, srch, x, y) { 8913 8914 var srcCanvas = srcLayer.getCanvas(); 8915 8916 // If entire rectangle outside source canvas, stop 8917 if (srcx >= srcCanvas.width || srcy >= srcCanvas.height) return; 8918 8919 // Otherwise, clip rectangle to area 8920 if (srcx + srcw > srcCanvas.width) 8921 srcw = srcCanvas.width - srcx; 8922 8923 if (srcy + srch > srcCanvas.height) 8924 srch = srcCanvas.height - srcy; 8925 8926 // Stop if nothing to draw. 8927 if (srcw === 0 || srch === 0) return; 8928 8929 if (layer.autosize) fitRect(x, y, srcw, srch); 8930 context.drawImage(srcCanvas, srcx, srcy, srcw, srch, x, y, srcw, srch); 8931 empty = false; 8932 8933 }; 8934 8935 /** 8936 * Starts a new path at the specified point. 8937 * 8938 * @param {!number} x 8939 * The X coordinate of the point to draw. 8940 * 8941 * @param {!number} y 8942 * The Y coordinate of the point to draw. 8943 */ 8944 this.moveTo = function(x, y) { 8945 8946 // Start a new path if current path is closed 8947 if (pathClosed) { 8948 context.beginPath(); 8949 pathClosed = false; 8950 } 8951 8952 if (layer.autosize) fitRect(x, y, 0, 0); 8953 context.moveTo(x, y); 8954 8955 }; 8956 8957 /** 8958 * Add the specified line to the current path. 8959 * 8960 * @param {!number} x 8961 * The X coordinate of the endpoint of the line to draw. 8962 * 8963 * @param {!number} y 8964 * The Y coordinate of the endpoint of the line to draw. 8965 */ 8966 this.lineTo = function(x, y) { 8967 8968 // Start a new path if current path is closed 8969 if (pathClosed) { 8970 context.beginPath(); 8971 pathClosed = false; 8972 } 8973 8974 if (layer.autosize) fitRect(x, y, 0, 0); 8975 context.lineTo(x, y); 8976 8977 }; 8978 8979 /** 8980 * Add the specified arc to the current path. 8981 * 8982 * @param {!number} x 8983 * The X coordinate of the center of the circle which will contain the 8984 * arc. 8985 * 8986 * @param {!number} y 8987 * The Y coordinate of the center of the circle which will contain the 8988 * arc. 8989 * 8990 * @param {!number} radius 8991 * The radius of the circle. 8992 * 8993 * @param {!number} startAngle 8994 * The starting angle of the arc, in radians. 8995 * 8996 * @param {!number} endAngle 8997 * The ending angle of the arc, in radians. 8998 * 8999 * @param {!boolean} negative 9000 * Whether the arc should be drawn in order of decreasing angle. 9001 */ 9002 this.arc = function(x, y, radius, startAngle, endAngle, negative) { 9003 9004 // Start a new path if current path is closed 9005 if (pathClosed) { 9006 context.beginPath(); 9007 pathClosed = false; 9008 } 9009 9010 if (layer.autosize) fitRect(x, y, 0, 0); 9011 context.arc(x, y, radius, startAngle, endAngle, negative); 9012 9013 }; 9014 9015 /** 9016 * Starts a new path at the specified point. 9017 * 9018 * @param {!number} cp1x 9019 * The X coordinate of the first control point. 9020 * 9021 * @param {!number} cp1y 9022 * The Y coordinate of the first control point. 9023 * 9024 * @param {!number} cp2x 9025 * The X coordinate of the second control point. 9026 * 9027 * @param {!number} cp2y 9028 * The Y coordinate of the second control point. 9029 * 9030 * @param {!number} x 9031 * The X coordinate of the endpoint of the curve. 9032 * 9033 * @param {!number} y 9034 * The Y coordinate of the endpoint of the curve. 9035 */ 9036 this.curveTo = function(cp1x, cp1y, cp2x, cp2y, x, y) { 9037 9038 // Start a new path if current path is closed 9039 if (pathClosed) { 9040 context.beginPath(); 9041 pathClosed = false; 9042 } 9043 9044 if (layer.autosize) fitRect(x, y, 0, 0); 9045 context.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y); 9046 9047 }; 9048 9049 /** 9050 * Closes the current path by connecting the end point with the start 9051 * point (if any) with a straight line. 9052 */ 9053 this.close = function() { 9054 context.closePath(); 9055 pathClosed = true; 9056 }; 9057 9058 /** 9059 * Add the specified rectangle to the current path. 9060 * 9061 * @param {!number} x 9062 * The X coordinate of the upper-left corner of the rectangle to draw. 9063 * 9064 * @param {!number} y 9065 * The Y coordinate of the upper-left corner of the rectangle to draw. 9066 * 9067 * @param {!number} w 9068 * The width of the rectangle to draw. 9069 * 9070 * @param {!number} h 9071 * The height of the rectangle to draw. 9072 */ 9073 this.rect = function(x, y, w, h) { 9074 9075 // Start a new path if current path is closed 9076 if (pathClosed) { 9077 context.beginPath(); 9078 pathClosed = false; 9079 } 9080 9081 if (layer.autosize) fitRect(x, y, w, h); 9082 context.rect(x, y, w, h); 9083 9084 }; 9085 9086 /** 9087 * Clip all future drawing operations by the current path. The current path 9088 * is implicitly closed. The current path can continue to be reused 9089 * for other operations (such as fillColor()) but a new path will be started 9090 * once a path drawing operation (path() or rect()) is used. 9091 */ 9092 this.clip = function() { 9093 9094 // Set new clipping region 9095 context.clip(); 9096 9097 // Path now implicitly closed 9098 pathClosed = true; 9099 9100 }; 9101 9102 /** 9103 * Stroke the current path with the specified color. The current path 9104 * is implicitly closed. The current path can continue to be reused 9105 * for other operations (such as clip()) but a new path will be started 9106 * once a path drawing operation (path() or rect()) is used. 9107 * 9108 * @param {!string} cap 9109 * The line cap style. Can be "round", "square", or "butt". 9110 * 9111 * @param {!string} join 9112 * The line join style. Can be "round", "bevel", or "miter". 9113 * 9114 * @param {!number} thickness 9115 * The line thickness in pixels. 9116 * 9117 * @param {!number} r 9118 * The red component of the color to fill. 9119 * 9120 * @param {!number} g 9121 * The green component of the color to fill. 9122 * 9123 * @param {!number} b 9124 * The blue component of the color to fill. 9125 * 9126 * @param {!number} a 9127 * The alpha component of the color to fill. 9128 */ 9129 this.strokeColor = function(cap, join, thickness, r, g, b, a) { 9130 9131 // Stroke with color 9132 context.lineCap = cap; 9133 context.lineJoin = join; 9134 context.lineWidth = thickness; 9135 context.strokeStyle = "rgba(" + r + "," + g + "," + b + "," + a/255.0 + ")"; 9136 context.stroke(); 9137 empty = false; 9138 9139 // Path now implicitly closed 9140 pathClosed = true; 9141 9142 }; 9143 9144 /** 9145 * Fills the current path with the specified color. The current path 9146 * is implicitly closed. The current path can continue to be reused 9147 * for other operations (such as clip()) but a new path will be started 9148 * once a path drawing operation (path() or rect()) is used. 9149 * 9150 * @param {!number} r 9151 * The red component of the color to fill. 9152 * 9153 * @param {!number} g 9154 * The green component of the color to fill. 9155 * 9156 * @param {!number} b 9157 * The blue component of the color to fill. 9158 * 9159 * @param {!number} a 9160 * The alpha component of the color to fill. 9161 */ 9162 this.fillColor = function(r, g, b, a) { 9163 9164 // Fill with color 9165 context.fillStyle = "rgba(" + r + "," + g + "," + b + "," + a/255.0 + ")"; 9166 context.fill(); 9167 empty = false; 9168 9169 // Path now implicitly closed 9170 pathClosed = true; 9171 9172 }; 9173 9174 /** 9175 * Stroke the current path with the image within the specified layer. The 9176 * image data will be tiled infinitely within the stroke. The current path 9177 * is implicitly closed. The current path can continue to be reused 9178 * for other operations (such as clip()) but a new path will be started 9179 * once a path drawing operation (path() or rect()) is used. 9180 * 9181 * @param {!string} cap 9182 * The line cap style. Can be "round", "square", or "butt". 9183 * 9184 * @param {!string} join 9185 * The line join style. Can be "round", "bevel", or "miter". 9186 * 9187 * @param {!number} thickness 9188 * The line thickness in pixels. 9189 * 9190 * @param {!Guacamole.Layer} srcLayer 9191 * The layer to use as a repeating pattern within the stroke. 9192 */ 9193 this.strokeLayer = function(cap, join, thickness, srcLayer) { 9194 9195 // Stroke with image data 9196 context.lineCap = cap; 9197 context.lineJoin = join; 9198 context.lineWidth = thickness; 9199 context.strokeStyle = context.createPattern( 9200 srcLayer.getCanvas(), 9201 "repeat" 9202 ); 9203 context.stroke(); 9204 empty = false; 9205 9206 // Path now implicitly closed 9207 pathClosed = true; 9208 9209 }; 9210 9211 /** 9212 * Fills the current path with the image within the specified layer. The 9213 * image data will be tiled infinitely within the stroke. The current path 9214 * is implicitly closed. The current path can continue to be reused 9215 * for other operations (such as clip()) but a new path will be started 9216 * once a path drawing operation (path() or rect()) is used. 9217 * 9218 * @param {!Guacamole.Layer} srcLayer 9219 * The layer to use as a repeating pattern within the fill. 9220 */ 9221 this.fillLayer = function(srcLayer) { 9222 9223 // Fill with image data 9224 context.fillStyle = context.createPattern( 9225 srcLayer.getCanvas(), 9226 "repeat" 9227 ); 9228 context.fill(); 9229 empty = false; 9230 9231 // Path now implicitly closed 9232 pathClosed = true; 9233 9234 }; 9235 9236 /** 9237 * Push current layer state onto stack. 9238 */ 9239 this.push = function() { 9240 9241 // Save current state onto stack 9242 context.save(); 9243 stackSize++; 9244 9245 }; 9246 9247 /** 9248 * Pop layer state off stack. 9249 */ 9250 this.pop = function() { 9251 9252 // Restore current state from stack 9253 if (stackSize > 0) { 9254 context.restore(); 9255 stackSize--; 9256 } 9257 9258 }; 9259 9260 /** 9261 * Reset the layer, clearing the stack, the current path, and any transform 9262 * matrix. 9263 */ 9264 this.reset = function() { 9265 9266 // Clear stack 9267 while (stackSize > 0) { 9268 context.restore(); 9269 stackSize--; 9270 } 9271 9272 // Restore to initial state 9273 context.restore(); 9274 context.save(); 9275 9276 // Clear path 9277 context.beginPath(); 9278 pathClosed = false; 9279 9280 }; 9281 9282 /** 9283 * Sets the given affine transform (defined with six values from the 9284 * transform's matrix). 9285 * 9286 * @param {!number} a 9287 * The first value in the affine transform's matrix. 9288 * 9289 * @param {!number} b 9290 * The second value in the affine transform's matrix. 9291 * 9292 * @param {!number} c 9293 * The third value in the affine transform's matrix. 9294 * 9295 * @param {!number} d 9296 * The fourth value in the affine transform's matrix. 9297 * 9298 * @param {!number} e 9299 * The fifth value in the affine transform's matrix. 9300 * 9301 * @param {!number} f 9302 * The sixth value in the affine transform's matrix. 9303 */ 9304 this.setTransform = function(a, b, c, d, e, f) { 9305 context.setTransform( 9306 a, b, c, 9307 d, e, f 9308 /*0, 0, 1*/ 9309 ); 9310 }; 9311 9312 /** 9313 * Applies the given affine transform (defined with six values from the 9314 * transform's matrix). 9315 * 9316 * @param {!number} a 9317 * The first value in the affine transform's matrix. 9318 * 9319 * @param {!number} b 9320 * The second value in the affine transform's matrix. 9321 * 9322 * @param {!number} c 9323 * The third value in the affine transform's matrix. 9324 * 9325 * @param {!number} d 9326 * The fourth value in the affine transform's matrix. 9327 * 9328 * @param {!number} e 9329 * The fifth value in the affine transform's matrix. 9330 * 9331 * @param {!number} f 9332 * The sixth value in the affine transform's matrix. 9333 */ 9334 this.transform = function(a, b, c, d, e, f) { 9335 context.transform( 9336 a, b, c, 9337 d, e, f 9338 /*0, 0, 1*/ 9339 ); 9340 }; 9341 9342 /** 9343 * Sets the channel mask for future operations on this Layer. 9344 * 9345 * The channel mask is a Guacamole-specific compositing operation identifier 9346 * with a single bit representing each of four channels (in order): source 9347 * image where destination transparent, source where destination opaque, 9348 * destination where source transparent, and destination where source 9349 * opaque. 9350 * 9351 * @param {!number} mask 9352 * The channel mask for future operations on this Layer. 9353 */ 9354 this.setChannelMask = function(mask) { 9355 context.globalCompositeOperation = compositeOperation[mask]; 9356 }; 9357 9358 /** 9359 * Sets the miter limit for stroke operations using the miter join. This 9360 * limit is the maximum ratio of the size of the miter join to the stroke 9361 * width. If this ratio is exceeded, the miter will not be drawn for that 9362 * joint of the path. 9363 * 9364 * @param {!number} limit 9365 * The miter limit for stroke operations using the miter join. 9366 */ 9367 this.setMiterLimit = function(limit) { 9368 context.miterLimit = limit; 9369 }; 9370 9371 // Initialize canvas dimensions 9372 resize(width, height); 9373 9374 // Explicitly render canvas below other elements in the layer (such as 9375 // child layers). Chrome and others may fail to render layers properly 9376 // without this. 9377 canvas.style.zIndex = -1; 9378 9379}; 9380 9381/** 9382 * Channel mask for the composite operation "rout". 9383 * 9384 * @type {!number} 9385 */ 9386Guacamole.Layer.ROUT = 0x2; 9387 9388/** 9389 * Channel mask for the composite operation "atop". 9390 * 9391 * @type {!number} 9392 */ 9393Guacamole.Layer.ATOP = 0x6; 9394 9395/** 9396 * Channel mask for the composite operation "xor". 9397 * 9398 * @type {!number} 9399 */ 9400Guacamole.Layer.XOR = 0xA; 9401 9402/** 9403 * Channel mask for the composite operation "rover". 9404 * 9405 * @type {!number} 9406 */ 9407Guacamole.Layer.ROVER = 0xB; 9408 9409/** 9410 * Channel mask for the composite operation "over". 9411 * 9412 * @type {!number} 9413 */ 9414Guacamole.Layer.OVER = 0xE; 9415 9416/** 9417 * Channel mask for the composite operation "plus". 9418 * 9419 * @type {!number} 9420 */ 9421Guacamole.Layer.PLUS = 0xF; 9422 9423/** 9424 * Channel mask for the composite operation "rin". 9425 * Beware that WebKit-based browsers may leave the contents of the destination 9426 * layer where the source layer is transparent, despite the definition of this 9427 * operation. 9428 * 9429 * @type {!number} 9430 */ 9431Guacamole.Layer.RIN = 0x1; 9432 9433/** 9434 * Channel mask for the composite operation "in". 9435 * Beware that WebKit-based browsers may leave the contents of the destination 9436 * layer where the source layer is transparent, despite the definition of this 9437 * operation. 9438 * 9439 * @type {!number} 9440 */ 9441Guacamole.Layer.IN = 0x4; 9442 9443/** 9444 * Channel mask for the composite operation "out". 9445 * Beware that WebKit-based browsers may leave the contents of the destination 9446 * layer where the source layer is transparent, despite the definition of this 9447 * operation. 9448 * 9449 * @type {!number} 9450 */ 9451Guacamole.Layer.OUT = 0x8; 9452 9453/** 9454 * Channel mask for the composite operation "ratop". 9455 * Beware that WebKit-based browsers may leave the contents of the destination 9456 * layer where the source layer is transparent, despite the definition of this 9457 * operation. 9458 * 9459 * @type {!number} 9460 */ 9461Guacamole.Layer.RATOP = 0x9; 9462 9463/** 9464 * Channel mask for the composite operation "src". 9465 * Beware that WebKit-based browsers may leave the contents of the destination 9466 * layer where the source layer is transparent, despite the definition of this 9467 * operation. 9468 * 9469 * @type {!number} 9470 */ 9471Guacamole.Layer.SRC = 0xC; 9472 9473/** 9474 * Represents a single pixel of image data. All components have a minimum value 9475 * of 0 and a maximum value of 255. 9476 * 9477 * @constructor 9478 * 9479 * @param {!number} r 9480 * The red component of this pixel. 9481 * 9482 * @param {!number} g 9483 * The green component of this pixel. 9484 * 9485 * @param {!number} b 9486 * The blue component of this pixel. 9487 * 9488 * @param {!number} a 9489 * The alpha component of this pixel. 9490 */ 9491Guacamole.Layer.Pixel = function(r, g, b, a) { 9492 9493 /** 9494 * The red component of this pixel, where 0 is the minimum value, 9495 * and 255 is the maximum. 9496 * 9497 * @type {!number} 9498 */ 9499 this.red = r; 9500 9501 /** 9502 * The green component of this pixel, where 0 is the minimum value, 9503 * and 255 is the maximum. 9504 * 9505 * @type {!number} 9506 */ 9507 this.green = g; 9508 9509 /** 9510 * The blue component of this pixel, where 0 is the minimum value, 9511 * and 255 is the maximum. 9512 * 9513 * @type {!number} 9514 */ 9515 this.blue = b; 9516 9517 /** 9518 * The alpha component of this pixel, where 0 is the minimum value, 9519 * and 255 is the maximum. 9520 * 9521 * @type {!number} 9522 */ 9523 this.alpha = a; 9524 9525}; 9526/* 9527 * Licensed to the Apache Software Foundation (ASF) under one 9528 * or more contributor license agreements. See the NOTICE file 9529 * distributed with this work for additional information 9530 * regarding copyright ownership. The ASF licenses this file 9531 * to you under the Apache License, Version 2.0 (the 9532 * "License"); you may not use this file except in compliance 9533 * with the License. You may obtain a copy of the License at 9534 * 9535 * http://www.apache.org/licenses/LICENSE-2.0 9536 * 9537 * Unless required by applicable law or agreed to in writing, 9538 * software distributed under the License is distributed on an 9539 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 9540 * KIND, either express or implied. See the License for the 9541 * specific language governing permissions and limitations 9542 * under the License. 9543 */ 9544 9545var Guacamole = Guacamole || {}; 9546 9547/** 9548 * Provides cross-browser mouse events for a given element. The events of 9549 * the given element are automatically populated with handlers that translate 9550 * mouse events into a non-browser-specific event provided by the 9551 * Guacamole.Mouse instance. 9552 * 9553 * @example 9554 * var mouse = new Guacamole.Mouse(client.getDisplay().getElement()); 9555 * 9556 * // Forward all mouse interaction over Guacamole connection 9557 * mouse.onEach(['mousedown', 'mousemove', 'mouseup'], function sendMouseEvent(e) { 9558 * client.sendMouseState(e.state, true); 9559 * }); 9560 * 9561 * @example 9562 * // Hide software cursor when mouse leaves display 9563 * mouse.on('mouseout', function hideCursor() { 9564 * client.getDisplay().showCursor(false); 9565 * }); 9566 * 9567 * @constructor 9568 * @augments Guacamole.Mouse.Event.Target 9569 * @param {!Element} element 9570 * The Element to use to provide mouse events. 9571 */ 9572Guacamole.Mouse = function Mouse(element) { 9573 9574 Guacamole.Mouse.Event.Target.call(this); 9575 9576 /** 9577 * Reference to this Guacamole.Mouse. 9578 * 9579 * @private 9580 * @type {!Guacamole.Mouse} 9581 */ 9582 var guac_mouse = this; 9583 9584 /** 9585 * The number of mousemove events to require before re-enabling mouse 9586 * event handling after receiving a touch event. 9587 * 9588 * @type {!number} 9589 */ 9590 this.touchMouseThreshold = 3; 9591 9592 /** 9593 * The minimum amount of pixels scrolled required for a single scroll button 9594 * click. 9595 * 9596 * @type {!number} 9597 */ 9598 this.scrollThreshold = 53; 9599 9600 /** 9601 * The number of pixels to scroll per line. 9602 * 9603 * @type {!number} 9604 */ 9605 this.PIXELS_PER_LINE = 18; 9606 9607 /** 9608 * The number of pixels to scroll per page. 9609 * 9610 * @type {!number} 9611 */ 9612 this.PIXELS_PER_PAGE = this.PIXELS_PER_LINE * 16; 9613 9614 /** 9615 * Array of {@link Guacamole.Mouse.State} button names corresponding to the 9616 * mouse button indices used by DOM mouse events. 9617 * 9618 * @private 9619 * @type {!string[]} 9620 */ 9621 var MOUSE_BUTTONS = [ 9622 Guacamole.Mouse.State.Buttons.LEFT, 9623 Guacamole.Mouse.State.Buttons.MIDDLE, 9624 Guacamole.Mouse.State.Buttons.RIGHT 9625 ]; 9626 9627 /** 9628 * Counter of mouse events to ignore. This decremented by mousemove, and 9629 * while non-zero, mouse events will have no effect. 9630 * 9631 * @private 9632 * @type {!number} 9633 */ 9634 var ignore_mouse = 0; 9635 9636 /** 9637 * Cumulative scroll delta amount. This value is accumulated through scroll 9638 * events and results in scroll button clicks if it exceeds a certain 9639 * threshold. 9640 * 9641 * @private 9642 * @type {!number} 9643 */ 9644 var scroll_delta = 0; 9645 9646 // Block context menu so right-click gets sent properly 9647 element.addEventListener("contextmenu", function(e) { 9648 Guacamole.Event.DOMEvent.cancelEvent(e); 9649 }, false); 9650 9651 element.addEventListener("mousemove", function(e) { 9652 9653 // If ignoring events, decrement counter 9654 if (ignore_mouse) { 9655 Guacamole.Event.DOMEvent.cancelEvent(e); 9656 ignore_mouse--; 9657 return; 9658 } 9659 9660 guac_mouse.move(Guacamole.Position.fromClientPosition(element, e.clientX, e.clientY), e); 9661 9662 }, false); 9663 9664 element.addEventListener("mousedown", function(e) { 9665 9666 // Do not handle if ignoring events 9667 if (ignore_mouse) { 9668 Guacamole.Event.DOMEvent.cancelEvent(e); 9669 return; 9670 } 9671 9672 var button = MOUSE_BUTTONS[e.button]; 9673 if (button) 9674 guac_mouse.press(button, e); 9675 9676 }, false); 9677 9678 element.addEventListener("mouseup", function(e) { 9679 9680 // Do not handle if ignoring events 9681 if (ignore_mouse) { 9682 Guacamole.Event.DOMEvent.cancelEvent(e); 9683 return; 9684 } 9685 9686 var button = MOUSE_BUTTONS[e.button]; 9687 if (button) 9688 guac_mouse.release(button, e); 9689 9690 }, false); 9691 9692 element.addEventListener("mouseout", function(e) { 9693 9694 // Get parent of the element the mouse pointer is leaving 9695 if (!e) e = window.event; 9696 9697 // Check that mouseout is due to actually LEAVING the element 9698 var target = e.relatedTarget || e.toElement; 9699 while (target) { 9700 if (target === element) 9701 return; 9702 target = target.parentNode; 9703 } 9704 9705 // Release all buttons and fire mouseout 9706 guac_mouse.reset(e); 9707 guac_mouse.out(e); 9708 9709 }, false); 9710 9711 // Override selection on mouse event element. 9712 element.addEventListener("selectstart", function(e) { 9713 Guacamole.Event.DOMEvent.cancelEvent(e); 9714 }, false); 9715 9716 // Ignore all pending mouse events when touch events are the apparent source 9717 function ignorePendingMouseEvents() { ignore_mouse = guac_mouse.touchMouseThreshold; } 9718 9719 element.addEventListener("touchmove", ignorePendingMouseEvents, false); 9720 element.addEventListener("touchstart", ignorePendingMouseEvents, false); 9721 element.addEventListener("touchend", ignorePendingMouseEvents, false); 9722 9723 // Scroll wheel support 9724 function mousewheel_handler(e) { 9725 9726 // Determine approximate scroll amount (in pixels) 9727 var delta = e.deltaY || -e.wheelDeltaY || -e.wheelDelta; 9728 9729 // If successfully retrieved scroll amount, convert to pixels if not 9730 // already in pixels 9731 if (delta) { 9732 9733 // Convert to pixels if delta was lines 9734 if (e.deltaMode === 1) 9735 delta = e.deltaY * guac_mouse.PIXELS_PER_LINE; 9736 9737 // Convert to pixels if delta was pages 9738 else if (e.deltaMode === 2) 9739 delta = e.deltaY * guac_mouse.PIXELS_PER_PAGE; 9740 9741 } 9742 9743 // Otherwise, assume legacy mousewheel event and line scrolling 9744 else 9745 delta = e.detail * guac_mouse.PIXELS_PER_LINE; 9746 9747 // Update overall delta 9748 scroll_delta += delta; 9749 9750 // Up 9751 if (scroll_delta <= -guac_mouse.scrollThreshold) { 9752 9753 // Repeatedly click the up button until insufficient delta remains 9754 do { 9755 guac_mouse.click(Guacamole.Mouse.State.Buttons.UP); 9756 scroll_delta += guac_mouse.scrollThreshold; 9757 } while (scroll_delta <= -guac_mouse.scrollThreshold); 9758 9759 // Reset delta 9760 scroll_delta = 0; 9761 9762 } 9763 9764 // Down 9765 if (scroll_delta >= guac_mouse.scrollThreshold) { 9766 9767 // Repeatedly click the down button until insufficient delta remains 9768 do { 9769 guac_mouse.click(Guacamole.Mouse.State.Buttons.DOWN); 9770 scroll_delta -= guac_mouse.scrollThreshold; 9771 } while (scroll_delta >= guac_mouse.scrollThreshold); 9772 9773 // Reset delta 9774 scroll_delta = 0; 9775 9776 } 9777 9778 // All scroll/wheel events must currently be cancelled regardless of 9779 // whether the dispatched event is cancelled, as there is no Guacamole 9780 // scroll event and thus no way to cancel scroll events that are 9781 // smaller than required to produce an up/down click 9782 Guacamole.Event.DOMEvent.cancelEvent(e); 9783 9784 } 9785 9786 element.addEventListener('DOMMouseScroll', mousewheel_handler, false); 9787 element.addEventListener('mousewheel', mousewheel_handler, false); 9788 element.addEventListener('wheel', mousewheel_handler, false); 9789 9790 /** 9791 * Whether the browser supports CSS3 cursor styling, including hotspot 9792 * coordinates. 9793 * 9794 * @private 9795 * @type {!boolean} 9796 */ 9797 var CSS3_CURSOR_SUPPORTED = (function() { 9798 9799 var div = document.createElement("div"); 9800 9801 // If no cursor property at all, then no support 9802 if (!("cursor" in div.style)) 9803 return false; 9804 9805 try { 9806 // Apply simple 1x1 PNG 9807 div.style.cursor = "url(data:image/png;base64," 9808 + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAAB" 9809 + "AQMAAAAl21bKAAAAA1BMVEX///+nxBvI" 9810 + "AAAACklEQVQI12NgAAAAAgAB4iG8MwAA" 9811 + "AABJRU5ErkJggg==) 0 0, auto"; 9812 } 9813 catch (e) { 9814 return false; 9815 } 9816 9817 // Verify cursor property is set to URL with hotspot 9818 return /\burl\([^()]*\)\s+0\s+0\b/.test(div.style.cursor || ""); 9819 9820 })(); 9821 9822 /** 9823 * Changes the local mouse cursor to the given canvas, having the given 9824 * hotspot coordinates. This affects styling of the element backing this 9825 * Guacamole.Mouse only, and may fail depending on browser support for 9826 * setting the mouse cursor. 9827 * 9828 * If setting the local cursor is desired, it is up to the implementation 9829 * to do something else, such as use the software cursor built into 9830 * Guacamole.Display, if the local cursor cannot be set. 9831 * 9832 * @param {!HTMLCanvasElement} canvas 9833 * The cursor image. 9834 * 9835 * @param {!number} x 9836 * The X-coordinate of the cursor hotspot. 9837 * 9838 * @param {!number} y 9839 * The Y-coordinate of the cursor hotspot. 9840 * 9841 * @return {!boolean} 9842 * true if the cursor was successfully set, false if the cursor could 9843 * not be set for any reason. 9844 */ 9845 this.setCursor = function(canvas, x, y) { 9846 9847 // Attempt to set via CSS3 cursor styling 9848 if (CSS3_CURSOR_SUPPORTED) { 9849 var dataURL = canvas.toDataURL('image/png'); 9850 element.style.cursor = "url(" + dataURL + ") " + x + " " + y + ", auto"; 9851 return true; 9852 } 9853 9854 // Otherwise, setting cursor failed 9855 return false; 9856 9857 }; 9858 9859}; 9860 9861/** 9862 * The current state of a mouse, including position and buttons. 9863 * 9864 * @constructor 9865 * @augments Guacamole.Position 9866 * @param {Guacamole.Mouse.State|object} [template={}] 9867 * The object whose properties should be copied within the new 9868 * Guacamole.Mouse.State. 9869 */ 9870Guacamole.Mouse.State = function State(template) { 9871 9872 /** 9873 * Returns the template object that would be provided to the 9874 * Guacamole.Mouse.State constructor to produce a new Guacamole.Mouse.State 9875 * object with the properties specified. The order and type of arguments 9876 * used by this function are identical to those accepted by the 9877 * Guacamole.Mouse.State constructor of Apache Guacamole 1.3.0 and older. 9878 * 9879 * @private 9880 * @param {!number} x 9881 * The X position of the mouse pointer in pixels. 9882 * 9883 * @param {!number} y 9884 * The Y position of the mouse pointer in pixels. 9885 * 9886 * @param {!boolean} left 9887 * Whether the left mouse button is pressed. 9888 * 9889 * @param {!boolean} middle 9890 * Whether the middle mouse button is pressed. 9891 * 9892 * @param {!boolean} right 9893 * Whether the right mouse button is pressed. 9894 * 9895 * @param {!boolean} up 9896 * Whether the up mouse button is pressed (the fourth button, usually 9897 * part of a scroll wheel). 9898 * 9899 * @param {!boolean} down 9900 * Whether the down mouse button is pressed (the fifth button, usually 9901 * part of a scroll wheel). 9902 * 9903 * @return {!object} 9904 * The equivalent template object that would be passed to the new 9905 * Guacamole.Mouse.State constructor. 9906 */ 9907 var legacyConstructor = function legacyConstructor(x, y, left, middle, right, up, down) { 9908 return { 9909 x : x, 9910 y : y, 9911 left : left, 9912 middle : middle, 9913 right : right, 9914 up : up, 9915 down : down 9916 }; 9917 }; 9918 9919 // Accept old-style constructor, as well 9920 if (arguments.length > 1) 9921 template = legacyConstructor.apply(this, arguments); 9922 else 9923 template = template || {}; 9924 9925 Guacamole.Position.call(this, template); 9926 9927 /** 9928 * Whether the left mouse button is currently pressed. 9929 * 9930 * @type {!boolean} 9931 * @default false 9932 */ 9933 this.left = template.left || false; 9934 9935 /** 9936 * Whether the middle mouse button is currently pressed. 9937 * 9938 * @type {!boolean} 9939 * @default false 9940 */ 9941 this.middle = template.middle || false; 9942 9943 /** 9944 * Whether the right mouse button is currently pressed. 9945 * 9946 * @type {!boolean} 9947 * @default false 9948 */ 9949 this.right = template.right || false; 9950 9951 /** 9952 * Whether the up mouse button is currently pressed. This is the fourth 9953 * mouse button, associated with upward scrolling of the mouse scroll 9954 * wheel. 9955 * 9956 * @type {!boolean} 9957 * @default false 9958 */ 9959 this.up = template.up || false; 9960 9961 /** 9962 * Whether the down mouse button is currently pressed. This is the fifth 9963 * mouse button, associated with downward scrolling of the mouse scroll 9964 * wheel. 9965 * 9966 * @type {!boolean} 9967 * @default false 9968 */ 9969 this.down = template.down || false; 9970 9971}; 9972 9973/** 9974 * All mouse buttons that may be represented by a 9975 * {@link Guacamole.Mouse.State}. 9976 * 9977 * @readonly 9978 * @enum 9979 */ 9980Guacamole.Mouse.State.Buttons = { 9981 9982 /** 9983 * The name of the {@link Guacamole.Mouse.State} property representing the 9984 * left mouse button. 9985 * 9986 * @constant 9987 * @type {!string} 9988 */ 9989 LEFT : 'left', 9990 9991 /** 9992 * The name of the {@link Guacamole.Mouse.State} property representing the 9993 * middle mouse button. 9994 * 9995 * @constant 9996 * @type {!string} 9997 */ 9998 MIDDLE : 'middle', 9999 10000 /** 10001 * The name of the {@link Guacamole.Mouse.State} property representing the 10002 * right mouse button. 10003 * 10004 * @constant 10005 * @type {!string} 10006 */ 10007 RIGHT : 'right', 10008 10009 /** 10010 * The name of the {@link Guacamole.Mouse.State} property representing the 10011 * up mouse button (the fourth mouse button, clicked when the mouse scroll 10012 * wheel is scrolled up). 10013 * 10014 * @constant 10015 * @type {!string} 10016 */ 10017 UP : 'up', 10018 10019 /** 10020 * The name of the {@link Guacamole.Mouse.State} property representing the 10021 * down mouse button (the fifth mouse button, clicked when the mouse scroll 10022 * wheel is scrolled up). 10023 * 10024 * @constant 10025 * @type {!string} 10026 */ 10027 DOWN : 'down' 10028 10029}; 10030 10031/** 10032 * Base event type for all mouse events. The mouse producing the event may be 10033 * the user's local mouse (as with {@link Guacamole.Mouse}) or an emulated 10034 * mouse (as with {@link Guacamole.Mouse.Touchpad}). 10035 * 10036 * @constructor 10037 * @augments Guacamole.Event.DOMEvent 10038 * @param {!string} type 10039 * The type name of the event ("mousedown", "mouseup", etc.) 10040 * 10041 * @param {!Guacamole.Mouse.State} state 10042 * The current mouse state. 10043 * 10044 * @param {Event|Event[]} [events=[]] 10045 * The DOM events that are related to this event, if any. 10046 */ 10047Guacamole.Mouse.Event = function MouseEvent(type, state, events) { 10048 10049 Guacamole.Event.DOMEvent.call(this, type, events); 10050 10051 /** 10052 * The name of the event handler used by the Guacamole JavaScript API for 10053 * this event prior to the migration to Guacamole.Event.Target. 10054 * 10055 * @private 10056 * @constant 10057 * @type {!string} 10058 */ 10059 var legacyHandlerName = 'on' + this.type; 10060 10061 /** 10062 * The current mouse state at the time this event was fired. 10063 * 10064 * @type {!Guacamole.Mouse.State} 10065 */ 10066 this.state = state; 10067 10068 /** 10069 * @inheritdoc 10070 */ 10071 this.invokeLegacyHandler = function invokeLegacyHandler(target) { 10072 if (target[legacyHandlerName]) { 10073 10074 this.preventDefault(); 10075 this.stopPropagation(); 10076 10077 target[legacyHandlerName](this.state); 10078 10079 } 10080 }; 10081 10082}; 10083 10084/** 10085 * An object which can dispatch {@link Guacamole.Mouse.Event} objects 10086 * representing mouse events. These mouse events may be produced from an actual 10087 * mouse device (as with {@link Guacamole.Mouse}), from an emulated mouse 10088 * device (as with {@link Guacamole.Mouse.Touchpad}, or may be programmatically 10089 * generated (using functions like [dispatch()]{@link Guacamole.Mouse.Event.Target#dispatch}, 10090 * [press()]{@link Guacamole.Mouse.Event.Target#press}, and 10091 * [release()]{@link Guacamole.Mouse.Event.Target#release}). 10092 * 10093 * @constructor 10094 * @augments Guacamole.Event.Target 10095 */ 10096Guacamole.Mouse.Event.Target = function MouseEventTarget() { 10097 10098 Guacamole.Event.Target.call(this); 10099 10100 /** 10101 * The current mouse state. The properties of this state are updated when 10102 * mouse events fire. This state object is also passed in as a parameter to 10103 * the handler of any mouse events. 10104 * 10105 * @type {!Guacamole.Mouse.State} 10106 */ 10107 this.currentState = new Guacamole.Mouse.State(); 10108 10109 /** 10110 * Fired whenever a mouse button is effectively pressed. Depending on the 10111 * object dispatching the event, this can be due to a true mouse button 10112 * press ({@link Guacamole.Mouse}), an emulated mouse button press from a 10113 * touch gesture ({@link Guacamole.Mouse.Touchpad} and 10114 * {@link Guacamole.Mouse.Touchscreen}), or may be programmatically 10115 * generated through [dispatch()]{@link Guacamole.Mouse.Event.Target#dispatch}, 10116 * [press()]{@link Guacamole.Mouse.Event.Target#press}, or 10117 * [click()]{@link Guacamole.Mouse.Event.Target#click}. 10118 * 10119 * @event Guacamole.Mouse.Event.Target#mousedown 10120 * @param {!Guacamole.Mouse.Event} event 10121 * The mousedown event that was fired. 10122 */ 10123 10124 /** 10125 * Fired whenever a mouse button is effectively released. Depending on the 10126 * object dispatching the event, this can be due to a true mouse button 10127 * release ({@link Guacamole.Mouse}), an emulated mouse button release from 10128 * a touch gesture ({@link Guacamole.Mouse.Touchpad} and 10129 * {@link Guacamole.Mouse.Touchscreen}), or may be programmatically 10130 * generated through [dispatch()]{@link Guacamole.Mouse.Event.Target#dispatch}, 10131 * [release()]{@link Guacamole.Mouse.Event.Target#release}, or 10132 * [click()]{@link Guacamole.Mouse.Event.Target#click}. 10133 * 10134 * @event Guacamole.Mouse.Event.Target#mouseup 10135 * @param {!Guacamole.Mouse.Event} event 10136 * The mouseup event that was fired. 10137 */ 10138 10139 /** 10140 * Fired whenever the mouse pointer is effectively moved. Depending on the 10141 * object dispatching the event, this can be due to true mouse movement 10142 * ({@link Guacamole.Mouse}), emulated mouse movement from 10143 * a touch gesture ({@link Guacamole.Mouse.Touchpad} and 10144 * {@link Guacamole.Mouse.Touchscreen}), or may be programmatically 10145 * generated through [dispatch()]{@link Guacamole.Mouse.Event.Target#dispatch}, 10146 * or [move()]{@link Guacamole.Mouse.Event.Target#move}. 10147 * 10148 * @event Guacamole.Mouse.Event.Target#mousemove 10149 * @param {!Guacamole.Mouse.Event} event 10150 * The mousemove event that was fired. 10151 */ 10152 10153 /** 10154 * Fired whenever the mouse pointer leaves the boundaries of the element 10155 * being monitored for interaction. This will only ever be automatically 10156 * fired due to movement of an actual mouse device via 10157 * {@link Guacamole.Mouse} unless programmatically generated through 10158 * [dispatch()]{@link Guacamole.Mouse.Event.Target#dispatch}, 10159 * or [out()]{@link Guacamole.Mouse.Event.Target#out}. 10160 * 10161 * @event Guacamole.Mouse.Event.Target#mouseout 10162 * @param {!Guacamole.Mouse.Event} event 10163 * The mouseout event that was fired. 10164 */ 10165 10166 /** 10167 * Presses the given mouse button, if it isn't already pressed. Valid 10168 * button names are defined by {@link Guacamole.Mouse.State.Buttons} and 10169 * correspond to the button-related properties of 10170 * {@link Guacamole.Mouse.State}. 10171 * 10172 * @fires Guacamole.Mouse.Event.Target#mousedown 10173 * 10174 * @param {!string} button 10175 * The name of the mouse button to press, as defined by 10176 * {@link Guacamole.Mouse.State.Buttons}. 10177 * 10178 * @param {Event|Event[]} [events=[]] 10179 * The DOM events that are related to the mouse button press, if any. 10180 */ 10181 this.press = function press(button, events) { 10182 if (!this.currentState[button]) { 10183 this.currentState[button] = true; 10184 this.dispatch(new Guacamole.Mouse.Event('mousedown', this.currentState, events)); 10185 } 10186 }; 10187 10188 /** 10189 * Releases the given mouse button, if it isn't already released. Valid 10190 * button names are defined by {@link Guacamole.Mouse.State.Buttons} and 10191 * correspond to the button-related properties of 10192 * {@link Guacamole.Mouse.State}. 10193 * 10194 * @fires Guacamole.Mouse.Event.Target#mouseup 10195 * 10196 * @param {!string} button 10197 * The name of the mouse button to release, as defined by 10198 * {@link Guacamole.Mouse.State.Buttons}. 10199 * 10200 * @param {Event|Event[]} [events=[]] 10201 * The DOM events related to the mouse button release, if any. 10202 */ 10203 this.release = function release(button, events) { 10204 if (this.currentState[button]) { 10205 this.currentState[button] = false; 10206 this.dispatch(new Guacamole.Mouse.Event('mouseup', this.currentState, events)); 10207 } 10208 }; 10209 10210 /** 10211 * Clicks (presses and releases) the given mouse button. Valid button 10212 * names are defined by {@link Guacamole.Mouse.State.Buttons} and 10213 * correspond to the button-related properties of 10214 * {@link Guacamole.Mouse.State}. 10215 * 10216 * @fires Guacamole.Mouse.Event.Target#mousedown 10217 * @fires Guacamole.Mouse.Event.Target#mouseup 10218 * 10219 * @param {!string} button 10220 * The name of the mouse button to click, as defined by 10221 * {@link Guacamole.Mouse.State.Buttons}. 10222 * 10223 * @param {Event|Event[]} [events=[]] 10224 * The DOM events related to the click, if any. 10225 */ 10226 this.click = function click(button, events) { 10227 this.press(button, events); 10228 this.release(button, events); 10229 }; 10230 10231 /** 10232 * Moves the mouse to the given coordinates. 10233 * 10234 * @fires Guacamole.Mouse.Event.Target#mousemove 10235 * 10236 * @param {!(Guacamole.Position|object)} position 10237 * The new coordinates of the mouse pointer. This object may be a 10238 * {@link Guacamole.Position} or any object with "x" and "y" 10239 * properties. 10240 * 10241 * @param {Event|Event[]} [events=[]] 10242 * The DOM events related to the mouse movement, if any. 10243 */ 10244 this.move = function move(position, events) { 10245 10246 if (this.currentState.x !== position.x || this.currentState.y !== position.y) { 10247 this.currentState.x = position.x; 10248 this.currentState.y = position.y; 10249 this.dispatch(new Guacamole.Mouse.Event('mousemove', this.currentState, events)); 10250 } 10251 10252 }; 10253 10254 /** 10255 * Notifies event listeners that the mouse pointer has left the boundaries 10256 * of the area being monitored for mouse events. 10257 * 10258 * @fires Guacamole.Mouse.Event.Target#mouseout 10259 * 10260 * @param {Event|Event[]} [events=[]] 10261 * The DOM events related to the mouse leaving the boundaries of the 10262 * monitored object, if any. 10263 */ 10264 this.out = function out(events) { 10265 this.dispatch(new Guacamole.Mouse.Event('mouseout', this.currentState, events)); 10266 }; 10267 10268 /** 10269 * Releases all mouse buttons that are currently pressed. If all mouse 10270 * buttons have already been released, this function has no effect. 10271 * 10272 * @fires Guacamole.Mouse.Event.Target#mouseup 10273 * 10274 * @param {Event|Event[]} [events=[]] 10275 * The DOM event related to all mouse buttons being released, if any. 10276 */ 10277 this.reset = function reset(events) { 10278 for (var button in Guacamole.Mouse.State.Buttons) { 10279 this.release(Guacamole.Mouse.State.Buttons[button], events); 10280 } 10281 }; 10282 10283}; 10284 10285/** 10286 * Provides cross-browser relative touch event translation for a given element. 10287 * 10288 * Touch events are translated into mouse events as if the touches occurred 10289 * on a touchpad (drag to push the mouse pointer, tap to click). 10290 * 10291 * @example 10292 * var touchpad = new Guacamole.Mouse.Touchpad(client.getDisplay().getElement()); 10293 * 10294 * // Emulate a mouse using touchpad-style gestures, forwarding all mouse 10295 * // interaction over Guacamole connection 10296 * touchpad.onEach(['mousedown', 'mousemove', 'mouseup'], function sendMouseEvent(e) { 10297 * 10298 * // Re-show software mouse cursor if possibly hidden by a prior call to 10299 * // showCursor(), such as a "mouseout" event handler that hides the 10300 * // cursor 10301 * client.getDisplay().showCursor(true); 10302 * 10303 * client.sendMouseState(e.state, true); 10304 * 10305 * }); 10306 * 10307 * @constructor 10308 * @augments Guacamole.Mouse.Event.Target 10309 * @param {!Element} element 10310 * The Element to use to provide touch events. 10311 */ 10312Guacamole.Mouse.Touchpad = function Touchpad(element) { 10313 10314 Guacamole.Mouse.Event.Target.call(this); 10315 10316 /** 10317 * The "mouseout" event will never be fired by Guacamole.Mouse.Touchpad. 10318 * 10319 * @ignore 10320 * @event Guacamole.Mouse.Touchpad#mouseout 10321 */ 10322 10323 /** 10324 * Reference to this Guacamole.Mouse.Touchpad. 10325 * 10326 * @private 10327 * @type {!Guacamole.Mouse.Touchpad} 10328 */ 10329 var guac_touchpad = this; 10330 10331 /** 10332 * The distance a two-finger touch must move per scrollwheel event, in 10333 * pixels. 10334 * 10335 * @type {!number} 10336 */ 10337 this.scrollThreshold = 20 * (window.devicePixelRatio || 1); 10338 10339 /** 10340 * The maximum number of milliseconds to wait for a touch to end for the 10341 * gesture to be considered a click. 10342 * 10343 * @type {!number} 10344 */ 10345 this.clickTimingThreshold = 250; 10346 10347 /** 10348 * The maximum number of pixels to allow a touch to move for the gesture to 10349 * be considered a click. 10350 * 10351 * @type {!number} 10352 */ 10353 this.clickMoveThreshold = 10 * (window.devicePixelRatio || 1); 10354 10355 /** 10356 * The current mouse state. The properties of this state are updated when 10357 * mouse events fire. This state object is also passed in as a parameter to 10358 * the handler of any mouse events. 10359 * 10360 * @type {!Guacamole.Mouse.State} 10361 */ 10362 this.currentState = new Guacamole.Mouse.State(); 10363 10364 var touch_count = 0; 10365 var last_touch_x = 0; 10366 var last_touch_y = 0; 10367 var last_touch_time = 0; 10368 var pixels_moved = 0; 10369 10370 var touch_buttons = { 10371 1: "left", 10372 2: "right", 10373 3: "middle" 10374 }; 10375 10376 var gesture_in_progress = false; 10377 var click_release_timeout = null; 10378 10379 element.addEventListener("touchend", function(e) { 10380 10381 e.preventDefault(); 10382 10383 // If we're handling a gesture AND this is the last touch 10384 if (gesture_in_progress && e.touches.length === 0) { 10385 10386 var time = new Date().getTime(); 10387 10388 // Get corresponding mouse button 10389 var button = touch_buttons[touch_count]; 10390 10391 // If mouse already down, release anad clear timeout 10392 if (guac_touchpad.currentState[button]) { 10393 10394 // Fire button up event 10395 guac_touchpad.release(button, e); 10396 10397 // Clear timeout, if set 10398 if (click_release_timeout) { 10399 window.clearTimeout(click_release_timeout); 10400 click_release_timeout = null; 10401 } 10402 10403 } 10404 10405 // If single tap detected (based on time and distance) 10406 if (time - last_touch_time <= guac_touchpad.clickTimingThreshold 10407 && pixels_moved < guac_touchpad.clickMoveThreshold) { 10408 10409 // Fire button down event 10410 guac_touchpad.press(button, e); 10411 10412 // Delay mouse up - mouse up should be canceled if 10413 // touchstart within timeout. 10414 click_release_timeout = window.setTimeout(function() { 10415 10416 // Fire button up event 10417 guac_touchpad.release(button, e); 10418 10419 // Gesture now over 10420 gesture_in_progress = false; 10421 10422 }, guac_touchpad.clickTimingThreshold); 10423 10424 } 10425 10426 // If we're not waiting to see if this is a click, stop gesture 10427 if (!click_release_timeout) 10428 gesture_in_progress = false; 10429 10430 } 10431 10432 }, false); 10433 10434 element.addEventListener("touchstart", function(e) { 10435 10436 e.preventDefault(); 10437 10438 // Track number of touches, but no more than three 10439 touch_count = Math.min(e.touches.length, 3); 10440 10441 // Clear timeout, if set 10442 if (click_release_timeout) { 10443 window.clearTimeout(click_release_timeout); 10444 click_release_timeout = null; 10445 } 10446 10447 // Record initial touch location and time for touch movement 10448 // and tap gestures 10449 if (!gesture_in_progress) { 10450 10451 // Stop mouse events while touching 10452 gesture_in_progress = true; 10453 10454 // Record touch location and time 10455 var starting_touch = e.touches[0]; 10456 last_touch_x = starting_touch.clientX; 10457 last_touch_y = starting_touch.clientY; 10458 last_touch_time = new Date().getTime(); 10459 pixels_moved = 0; 10460 10461 } 10462 10463 }, false); 10464 10465 element.addEventListener("touchmove", function(e) { 10466 10467 e.preventDefault(); 10468 10469 // Get change in touch location 10470 var touch = e.touches[0]; 10471 var delta_x = touch.clientX - last_touch_x; 10472 var delta_y = touch.clientY - last_touch_y; 10473 10474 // Track pixels moved 10475 pixels_moved += Math.abs(delta_x) + Math.abs(delta_y); 10476 10477 // If only one touch involved, this is mouse move 10478 if (touch_count === 1) { 10479 10480 // Calculate average velocity in Manhatten pixels per millisecond 10481 var velocity = pixels_moved / (new Date().getTime() - last_touch_time); 10482 10483 // Scale mouse movement relative to velocity 10484 var scale = 1 + velocity; 10485 10486 // Update mouse location 10487 var position = new Guacamole.Position(guac_touchpad.currentState); 10488 position.x += delta_x*scale; 10489 position.y += delta_y*scale; 10490 10491 // Prevent mouse from leaving screen 10492 position.x = Math.min(Math.max(0, position.x), element.offsetWidth - 1); 10493 position.y = Math.min(Math.max(0, position.y), element.offsetHeight - 1); 10494 10495 // Fire movement event, if defined 10496 guac_touchpad.move(position, e); 10497 10498 // Update touch location 10499 last_touch_x = touch.clientX; 10500 last_touch_y = touch.clientY; 10501 10502 } 10503 10504 // Interpret two-finger swipe as scrollwheel 10505 else if (touch_count === 2) { 10506 10507 // If change in location passes threshold for scroll 10508 if (Math.abs(delta_y) >= guac_touchpad.scrollThreshold) { 10509 10510 // Decide button based on Y movement direction 10511 var button; 10512 if (delta_y > 0) button = "down"; 10513 else button = "up"; 10514 10515 guac_touchpad.click(button, e); 10516 10517 // Only update touch location after a scroll has been 10518 // detected 10519 last_touch_x = touch.clientX; 10520 last_touch_y = touch.clientY; 10521 10522 } 10523 10524 } 10525 10526 }, false); 10527 10528}; 10529 10530/** 10531 * Provides cross-browser absolute touch event translation for a given element. 10532 * 10533 * Touch events are translated into mouse events as if the touches occurred 10534 * on a touchscreen (tapping anywhere on the screen clicks at that point, 10535 * long-press to right-click). 10536 * 10537 * @example 10538 * var touchscreen = new Guacamole.Mouse.Touchscreen(client.getDisplay().getElement()); 10539 * 10540 * // Emulate a mouse using touchscreen-style gestures, forwarding all mouse 10541 * // interaction over Guacamole connection 10542 * touchscreen.onEach(['mousedown', 'mousemove', 'mouseup'], function sendMouseEvent(e) { 10543 * 10544 * // Re-show software mouse cursor if possibly hidden by a prior call to 10545 * // showCursor(), such as a "mouseout" event handler that hides the 10546 * // cursor 10547 * client.getDisplay().showCursor(true); 10548 * 10549 * client.sendMouseState(e.state, true); 10550 * 10551 * }); 10552 * 10553 * @constructor 10554 * @augments Guacamole.Mouse.Event.Target 10555 * @param {!Element} element 10556 * The Element to use to provide touch events. 10557 */ 10558Guacamole.Mouse.Touchscreen = function Touchscreen(element) { 10559 10560 Guacamole.Mouse.Event.Target.call(this); 10561 10562 /** 10563 * The "mouseout" event will never be fired by Guacamole.Mouse.Touchscreen. 10564 * 10565 * @ignore 10566 * @event Guacamole.Mouse.Touchscreen#mouseout 10567 */ 10568 10569 /** 10570 * Reference to this Guacamole.Mouse.Touchscreen. 10571 * 10572 * @private 10573 * @type {!Guacamole.Mouse.Touchscreen} 10574 */ 10575 var guac_touchscreen = this; 10576 10577 /** 10578 * Whether a gesture is known to be in progress. If false, touch events 10579 * will be ignored. 10580 * 10581 * @private 10582 * @type {!boolean} 10583 */ 10584 var gesture_in_progress = false; 10585 10586 /** 10587 * The start X location of a gesture. 10588 * 10589 * @private 10590 * @type {number} 10591 */ 10592 var gesture_start_x = null; 10593 10594 /** 10595 * The start Y location of a gesture. 10596 * 10597 * @private 10598 * @type {number} 10599 */ 10600 var gesture_start_y = null; 10601 10602 /** 10603 * The timeout associated with the delayed, cancellable click release. 10604 * 10605 * @private 10606 * @type {number} 10607 */ 10608 var click_release_timeout = null; 10609 10610 /** 10611 * The timeout associated with long-press for right click. 10612 * 10613 * @private 10614 * @type {number} 10615 */ 10616 var long_press_timeout = null; 10617 10618 /** 10619 * The distance a two-finger touch must move per scrollwheel event, in 10620 * pixels. 10621 * 10622 * @type {!number} 10623 */ 10624 this.scrollThreshold = 20 * (window.devicePixelRatio || 1); 10625 10626 /** 10627 * The maximum number of milliseconds to wait for a touch to end for the 10628 * gesture to be considered a click. 10629 * 10630 * @type {!number} 10631 */ 10632 this.clickTimingThreshold = 250; 10633 10634 /** 10635 * The maximum number of pixels to allow a touch to move for the gesture to 10636 * be considered a click. 10637 * 10638 * @type {!number} 10639 */ 10640 this.clickMoveThreshold = 16 * (window.devicePixelRatio || 1); 10641 10642 /** 10643 * The amount of time a press must be held for long press to be 10644 * detected. 10645 */ 10646 this.longPressThreshold = 500; 10647 10648 /** 10649 * Returns whether the given touch event exceeds the movement threshold for 10650 * clicking, based on where the touch gesture began. 10651 * 10652 * @private 10653 * @param {!TouchEvent} e 10654 * The touch event to check. 10655 * 10656 * @return {!boolean} 10657 * true if the movement threshold is exceeded, false otherwise. 10658 */ 10659 function finger_moved(e) { 10660 var touch = e.touches[0] || e.changedTouches[0]; 10661 var delta_x = touch.clientX - gesture_start_x; 10662 var delta_y = touch.clientY - gesture_start_y; 10663 return Math.sqrt(delta_x*delta_x + delta_y*delta_y) >= guac_touchscreen.clickMoveThreshold; 10664 } 10665 10666 /** 10667 * Begins a new gesture at the location of the first touch in the given 10668 * touch event. 10669 * 10670 * @private 10671 * @param {!TouchEvent} e 10672 * The touch event beginning this new gesture. 10673 */ 10674 function begin_gesture(e) { 10675 var touch = e.touches[0]; 10676 gesture_in_progress = true; 10677 gesture_start_x = touch.clientX; 10678 gesture_start_y = touch.clientY; 10679 } 10680 10681 /** 10682 * End the current gesture entirely. Wait for all touches to be done before 10683 * resuming gesture detection. 10684 * 10685 * @private 10686 */ 10687 function end_gesture() { 10688 window.clearTimeout(click_release_timeout); 10689 window.clearTimeout(long_press_timeout); 10690 gesture_in_progress = false; 10691 } 10692 10693 element.addEventListener("touchend", function(e) { 10694 10695 // Do not handle if no gesture 10696 if (!gesture_in_progress) 10697 return; 10698 10699 // Ignore if more than one touch 10700 if (e.touches.length !== 0 || e.changedTouches.length !== 1) { 10701 end_gesture(); 10702 return; 10703 } 10704 10705 // Long-press, if any, is over 10706 window.clearTimeout(long_press_timeout); 10707 10708 // Always release mouse button if pressed 10709 guac_touchscreen.release(Guacamole.Mouse.State.Buttons.LEFT, e); 10710 10711 // If finger hasn't moved enough to cancel the click 10712 if (!finger_moved(e)) { 10713 10714 e.preventDefault(); 10715 10716 // If not yet pressed, press and start delay release 10717 if (!guac_touchscreen.currentState.left) { 10718 10719 var touch = e.changedTouches[0]; 10720 guac_touchscreen.move(Guacamole.Position.fromClientPosition(element, touch.clientX, touch.clientY)); 10721 guac_touchscreen.press(Guacamole.Mouse.State.Buttons.LEFT, e); 10722 10723 // Release button after a delay, if not canceled 10724 click_release_timeout = window.setTimeout(function() { 10725 guac_touchscreen.release(Guacamole.Mouse.State.Buttons.LEFT, e); 10726 end_gesture(); 10727 }, guac_touchscreen.clickTimingThreshold); 10728 10729 } 10730 10731 } // end if finger not moved 10732 10733 }, false); 10734 10735 element.addEventListener("touchstart", function(e) { 10736 10737 // Ignore if more than one touch 10738 if (e.touches.length !== 1) { 10739 end_gesture(); 10740 return; 10741 } 10742 10743 e.preventDefault(); 10744 10745 // New touch begins a new gesture 10746 begin_gesture(e); 10747 10748 // Keep button pressed if tap after left click 10749 window.clearTimeout(click_release_timeout); 10750 10751 // Click right button if this turns into a long-press 10752 long_press_timeout = window.setTimeout(function() { 10753 var touch = e.touches[0]; 10754 guac_touchscreen.move(Guacamole.Position.fromClientPosition(element, touch.clientX, touch.clientY)); 10755 guac_touchscreen.click(Guacamole.Mouse.State.Buttons.RIGHT, e); 10756 end_gesture(); 10757 }, guac_touchscreen.longPressThreshold); 10758 10759 }, false); 10760 10761 element.addEventListener("touchmove", function(e) { 10762 10763 // Do not handle if no gesture 10764 if (!gesture_in_progress) 10765 return; 10766 10767 // Cancel long press if finger moved 10768 if (finger_moved(e)) 10769 window.clearTimeout(long_press_timeout); 10770 10771 // Ignore if more than one touch 10772 if (e.touches.length !== 1) { 10773 end_gesture(); 10774 return; 10775 } 10776 10777 // Update mouse position if dragging 10778 if (guac_touchscreen.currentState.left) { 10779 10780 e.preventDefault(); 10781 10782 // Update state 10783 var touch = e.touches[0]; 10784 guac_touchscreen.move(Guacamole.Position.fromClientPosition(element, touch.clientX, touch.clientY), e); 10785 10786 } 10787 10788 }, false); 10789 10790}; 10791/* 10792 * Licensed to the Apache Software Foundation (ASF) under one 10793 * or more contributor license agreements. See the NOTICE file 10794 * distributed with this work for additional information 10795 * regarding copyright ownership. The ASF licenses this file 10796 * to you under the Apache License, Version 2.0 (the 10797 * "License"); you may not use this file except in compliance 10798 * with the License. You may obtain a copy of the License at 10799 * 10800 * http://www.apache.org/licenses/LICENSE-2.0 10801 * 10802 * Unless required by applicable law or agreed to in writing, 10803 * software distributed under the License is distributed on an 10804 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 10805 * KIND, either express or implied. See the License for the 10806 * specific language governing permissions and limitations 10807 * under the License. 10808 */ 10809 10810/** 10811 * The namespace used by the Guacamole JavaScript API. Absolutely all classes 10812 * defined by the Guacamole JavaScript API will be within this namespace. 10813 * 10814 * @namespace 10815 */ 10816var Guacamole = Guacamole || {}; 10817/* 10818 * Licensed to the Apache Software Foundation (ASF) under one 10819 * or more contributor license agreements. See the NOTICE file 10820 * distributed with this work for additional information 10821 * regarding copyright ownership. The ASF licenses this file 10822 * to you under the Apache License, Version 2.0 (the 10823 * "License"); you may not use this file except in compliance 10824 * with the License. You may obtain a copy of the License at 10825 * 10826 * http://www.apache.org/licenses/LICENSE-2.0 10827 * 10828 * Unless required by applicable law or agreed to in writing, 10829 * software distributed under the License is distributed on an 10830 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 10831 * KIND, either express or implied. See the License for the 10832 * specific language governing permissions and limitations 10833 * under the License. 10834 */ 10835 10836var Guacamole = Guacamole || {}; 10837 10838/** 10839 * An object used by the Guacamole client to house arbitrarily-many named 10840 * input and output streams. 10841 * 10842 * @constructor 10843 * @param {!Guacamole.Client} client 10844 * The client owning this object. 10845 * 10846 * @param {!number} index 10847 * The index of this object. 10848 */ 10849Guacamole.Object = function guacamoleObject(client, index) { 10850 10851 /** 10852 * Reference to this Guacamole.Object. 10853 * 10854 * @private 10855 * @type {!Guacamole.Object} 10856 */ 10857 var guacObject = this; 10858 10859 /** 10860 * Map of stream name to corresponding queue of callbacks. The queue of 10861 * callbacks is guaranteed to be in order of request. 10862 * 10863 * @private 10864 * @type {!Object.<string, function[]>} 10865 */ 10866 var bodyCallbacks = {}; 10867 10868 /** 10869 * Removes and returns the callback at the head of the callback queue for 10870 * the stream having the given name. If no such callbacks exist, null is 10871 * returned. 10872 * 10873 * @private 10874 * @param {!string} name 10875 * The name of the stream to retrieve a callback for. 10876 * 10877 * @returns {function} 10878 * The next callback associated with the stream having the given name, 10879 * or null if no such callback exists. 10880 */ 10881 var dequeueBodyCallback = function dequeueBodyCallback(name) { 10882 10883 // If no callbacks defined, simply return null 10884 var callbacks = bodyCallbacks[name]; 10885 if (!callbacks) 10886 return null; 10887 10888 // Otherwise, pull off first callback, deleting the queue if empty 10889 var callback = callbacks.shift(); 10890 if (callbacks.length === 0) 10891 delete bodyCallbacks[name]; 10892 10893 // Return found callback 10894 return callback; 10895 10896 }; 10897 10898 /** 10899 * Adds the given callback to the tail of the callback queue for the stream 10900 * having the given name. 10901 * 10902 * @private 10903 * @param {!string} name 10904 * The name of the stream to associate with the given callback. 10905 * 10906 * @param {!function} callback 10907 * The callback to add to the queue of the stream with the given name. 10908 */ 10909 var enqueueBodyCallback = function enqueueBodyCallback(name, callback) { 10910 10911 // Get callback queue by name, creating first if necessary 10912 var callbacks = bodyCallbacks[name]; 10913 if (!callbacks) { 10914 callbacks = []; 10915 bodyCallbacks[name] = callbacks; 10916 } 10917 10918 // Add callback to end of queue 10919 callbacks.push(callback); 10920 10921 }; 10922 10923 /** 10924 * The index of this object. 10925 * 10926 * @type {!number} 10927 */ 10928 this.index = index; 10929 10930 /** 10931 * Called when this object receives the body of a requested input stream. 10932 * By default, all objects will invoke the callbacks provided to their 10933 * requestInputStream() functions based on the name of the stream 10934 * requested. This behavior can be overridden by specifying a different 10935 * handler here. 10936 * 10937 * @event 10938 * @param {!Guacamole.InputStream} inputStream 10939 * The input stream of the received body. 10940 * 10941 * @param {!string} mimetype 10942 * The mimetype of the data being received. 10943 * 10944 * @param {!string} name 10945 * The name of the stream whose body has been received. 10946 */ 10947 this.onbody = function defaultBodyHandler(inputStream, mimetype, name) { 10948 10949 // Call queued callback for the received body, if any 10950 var callback = dequeueBodyCallback(name); 10951 if (callback) 10952 callback(inputStream, mimetype); 10953 10954 }; 10955 10956 /** 10957 * Called when this object is being undefined. Once undefined, no further 10958 * communication involving this object may occur. 10959 * 10960 * @event 10961 */ 10962 this.onundefine = null; 10963 10964 /** 10965 * Requests read access to the input stream having the given name. If 10966 * successful, a new input stream will be created. 10967 * 10968 * @param {!string} name 10969 * The name of the input stream to request. 10970 * 10971 * @param {function} [bodyCallback] 10972 * The callback to invoke when the body of the requested input stream 10973 * is received. This callback will be provided a Guacamole.InputStream 10974 * and its mimetype as its two only arguments. If the onbody handler of 10975 * this object is overridden, this callback will not be invoked. 10976 */ 10977 this.requestInputStream = function requestInputStream(name, bodyCallback) { 10978 10979 // Queue body callback if provided 10980 if (bodyCallback) 10981 enqueueBodyCallback(name, bodyCallback); 10982 10983 // Send request for input stream 10984 client.requestObjectInputStream(guacObject.index, name); 10985 10986 }; 10987 10988 /** 10989 * Creates a new output stream associated with this object and having the 10990 * given mimetype and name. The legality of a mimetype and name is dictated 10991 * by the object itself. 10992 * 10993 * @param {!string} mimetype 10994 * The mimetype of the data which will be sent to the output stream. 10995 * 10996 * @param {!string} name 10997 * The defined name of an output stream within this object. 10998 * 10999 * @returns {!Guacamole.OutputStream} 11000 * An output stream which will write blobs to the named output stream 11001 * of this object. 11002 */ 11003 this.createOutputStream = function createOutputStream(mimetype, name) { 11004 return client.createObjectOutputStream(guacObject.index, mimetype, name); 11005 }; 11006 11007}; 11008 11009/** 11010 * The reserved name denoting the root stream of any object. The contents of 11011 * the root stream MUST be a JSON map of stream name to mimetype. 11012 * 11013 * @constant 11014 * @type {!string} 11015 */ 11016Guacamole.Object.ROOT_STREAM = '/'; 11017 11018/** 11019 * The mimetype of a stream containing JSON which maps available stream names 11020 * to their corresponding mimetype. The root stream of a Guacamole.Object MUST 11021 * have this mimetype. 11022 * 11023 * @constant 11024 * @type {!string} 11025 */ 11026Guacamole.Object.STREAM_INDEX_MIMETYPE = 'application/vnd.glyptodon.guacamole.stream-index+json'; 11027/* 11028 * Licensed to the Apache Software Foundation (ASF) under one 11029 * or more contributor license agreements. See the NOTICE file 11030 * distributed with this work for additional information 11031 * regarding copyright ownership. The ASF licenses this file 11032 * to you under the Apache License, Version 2.0 (the 11033 * "License"); you may not use this file except in compliance 11034 * with the License. You may obtain a copy of the License at 11035 * 11036 * http://www.apache.org/licenses/LICENSE-2.0 11037 * 11038 * Unless required by applicable law or agreed to in writing, 11039 * software distributed under the License is distributed on an 11040 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 11041 * KIND, either express or implied. See the License for the 11042 * specific language governing permissions and limitations 11043 * under the License. 11044 */ 11045 11046var Guacamole = Guacamole || {}; 11047 11048/** 11049 * Dynamic on-screen keyboard. Given the layout object for an on-screen 11050 * keyboard, this object will construct a clickable on-screen keyboard with its 11051 * own key events. 11052 * 11053 * @constructor 11054 * @param {!Guacamole.OnScreenKeyboard.Layout} layout 11055 * The layout of the on-screen keyboard to display. 11056 */ 11057Guacamole.OnScreenKeyboard = function(layout) { 11058 11059 /** 11060 * Reference to this Guacamole.OnScreenKeyboard. 11061 * 11062 * @private 11063 * @type {!Guacamole.OnScreenKeyboard} 11064 */ 11065 var osk = this; 11066 11067 /** 11068 * Map of currently-set modifiers to the keysym associated with their 11069 * original press. When the modifier is cleared, this keysym must be 11070 * released. 11071 * 11072 * @private 11073 * @type {!Object.<String, Number>} 11074 */ 11075 var modifierKeysyms = {}; 11076 11077 /** 11078 * Map of all key names to their current pressed states. If a key is not 11079 * pressed, it may not be in this map at all, but all pressed keys will 11080 * have a corresponding mapping to true. 11081 * 11082 * @private 11083 * @type {!Object.<String, Boolean>} 11084 */ 11085 var pressed = {}; 11086 11087 /** 11088 * All scalable elements which are part of the on-screen keyboard. Each 11089 * scalable element is carefully controlled to ensure the interface layout 11090 * and sizing remains constant, even on browsers that would otherwise 11091 * experience rounding error due to unit conversions. 11092 * 11093 * @private 11094 * @type {!ScaledElement[]} 11095 */ 11096 var scaledElements = []; 11097 11098 /** 11099 * Adds a CSS class to an element. 11100 * 11101 * @private 11102 * @function 11103 * @param {!Element} element 11104 * The element to add a class to. 11105 * 11106 * @param {!string} classname 11107 * The name of the class to add. 11108 */ 11109 var addClass = function addClass(element, classname) { 11110 11111 // If classList supported, use that 11112 if (element.classList) 11113 element.classList.add(classname); 11114 11115 // Otherwise, simply append the class 11116 else 11117 element.className += " " + classname; 11118 11119 }; 11120 11121 /** 11122 * Removes a CSS class from an element. 11123 * 11124 * @private 11125 * @function 11126 * @param {!Element} element 11127 * The element to remove a class from. 11128 * 11129 * @param {!string} classname 11130 * The name of the class to remove. 11131 */ 11132 var removeClass = function removeClass(element, classname) { 11133 11134 // If classList supported, use that 11135 if (element.classList) 11136 element.classList.remove(classname); 11137 11138 // Otherwise, manually filter out classes with given name 11139 else { 11140 element.className = element.className.replace(/([^ ]+)[ ]*/g, 11141 function removeMatchingClasses(match, testClassname) { 11142 11143 // If same class, remove 11144 if (testClassname === classname) 11145 return ""; 11146 11147 // Otherwise, allow 11148 return match; 11149 11150 } 11151 ); 11152 } 11153 11154 }; 11155 11156 /** 11157 * Counter of mouse events to ignore. This decremented by mousemove, and 11158 * while non-zero, mouse events will have no effect. 11159 * 11160 * @private 11161 * @type {!number} 11162 */ 11163 var ignoreMouse = 0; 11164 11165 /** 11166 * Ignores all pending mouse events when touch events are the apparent 11167 * source. Mouse events are ignored until at least touchMouseThreshold 11168 * mouse events occur without corresponding touch events. 11169 * 11170 * @private 11171 */ 11172 var ignorePendingMouseEvents = function ignorePendingMouseEvents() { 11173 ignoreMouse = osk.touchMouseThreshold; 11174 }; 11175 11176 /** 11177 * An element whose dimensions are maintained according to an arbitrary 11178 * scale. The conversion factor for these arbitrary units to pixels is 11179 * provided later via a call to scale(). 11180 * 11181 * @private 11182 * @constructor 11183 * @param {!Element} element 11184 * The element whose scale should be maintained. 11185 * 11186 * @param {!number} width 11187 * The width of the element, in arbitrary units, relative to other 11188 * ScaledElements. 11189 * 11190 * @param {!number} height 11191 * The height of the element, in arbitrary units, relative to other 11192 * ScaledElements. 11193 * 11194 * @param {boolean} [scaleFont=false] 11195 * Whether the line height and font size should be scaled as well. 11196 */ 11197 var ScaledElement = function ScaledElement(element, width, height, scaleFont) { 11198 11199 /** 11200 * The width of this ScaledElement, in arbitrary units, relative to 11201 * other ScaledElements. 11202 * 11203 * @type {!number} 11204 */ 11205 this.width = width; 11206 11207 /** 11208 * The height of this ScaledElement, in arbitrary units, relative to 11209 * other ScaledElements. 11210 * 11211 * @type {!number} 11212 */ 11213 this.height = height; 11214 11215 /** 11216 * Resizes the associated element, updating its dimensions according to 11217 * the given pixels per unit. 11218 * 11219 * @param {!number} pixels 11220 * The number of pixels to assign per arbitrary unit. 11221 */ 11222 this.scale = function(pixels) { 11223 11224 // Scale element width/height 11225 element.style.width = (width * pixels) + "px"; 11226 element.style.height = (height * pixels) + "px"; 11227 11228 // Scale font, if requested 11229 if (scaleFont) { 11230 element.style.lineHeight = (height * pixels) + "px"; 11231 element.style.fontSize = pixels + "px"; 11232 } 11233 11234 }; 11235 11236 }; 11237 11238 /** 11239 * Returns whether all modifiers having the given names are currently 11240 * active. 11241 * 11242 * @private 11243 * @param {!string[]} names 11244 * The names of all modifiers to test. 11245 * 11246 * @returns {!boolean} 11247 * true if all specified modifiers are pressed, false otherwise. 11248 */ 11249 var modifiersPressed = function modifiersPressed(names) { 11250 11251 // If any required modifiers are not pressed, return false 11252 for (var i=0; i < names.length; i++) { 11253 11254 // Test whether current modifier is pressed 11255 var name = names[i]; 11256 if (!(name in modifierKeysyms)) 11257 return false; 11258 11259 } 11260 11261 // Otherwise, all required modifiers are pressed 11262 return true; 11263 11264 }; 11265 11266 /** 11267 * Returns the single matching Key object associated with the key of the 11268 * given name, where that Key object's requirements (such as pressed 11269 * modifiers) are all currently satisfied. 11270 * 11271 * @private 11272 * @param {!string} keyName 11273 * The name of the key to retrieve. 11274 * 11275 * @returns {Guacamole.OnScreenKeyboard.Key} 11276 * The Key object associated with the given name, where that object's 11277 * requirements are all currently satisfied, or null if no such Key 11278 * can be found. 11279 */ 11280 var getActiveKey = function getActiveKey(keyName) { 11281 11282 // Get key array for given name 11283 var keys = osk.keys[keyName]; 11284 if (!keys) 11285 return null; 11286 11287 // Find last matching key 11288 for (var i = keys.length - 1; i >= 0; i--) { 11289 11290 // Get candidate key 11291 var candidate = keys[i]; 11292 11293 // If all required modifiers are pressed, use that key 11294 if (modifiersPressed(candidate.requires)) 11295 return candidate; 11296 11297 } 11298 11299 // No valid key 11300 return null; 11301 11302 }; 11303 11304 /** 11305 * Presses the key having the given name, updating the associated key 11306 * element with the "guac-keyboard-pressed" CSS class. If the key is 11307 * already pressed, this function has no effect. 11308 * 11309 * @private 11310 * @param {!string} keyName 11311 * The name of the key to press. 11312 * 11313 * @param {!string} keyElement 11314 * The element associated with the given key. 11315 */ 11316 var press = function press(keyName, keyElement) { 11317 11318 // Press key if not yet pressed 11319 if (!pressed[keyName]) { 11320 11321 addClass(keyElement, "guac-keyboard-pressed"); 11322 11323 // Get current key based on modifier state 11324 var key = getActiveKey(keyName); 11325 11326 // Update modifier state 11327 if (key.modifier) { 11328 11329 // Construct classname for modifier 11330 var modifierClass = "guac-keyboard-modifier-" + getCSSName(key.modifier); 11331 11332 // Retrieve originally-pressed keysym, if modifier was already pressed 11333 var originalKeysym = modifierKeysyms[key.modifier]; 11334 11335 // Activate modifier if not pressed 11336 if (originalKeysym === undefined) { 11337 11338 addClass(keyboard, modifierClass); 11339 modifierKeysyms[key.modifier] = key.keysym; 11340 11341 // Send key event only if keysym is meaningful 11342 if (key.keysym && osk.onkeydown) 11343 osk.onkeydown(key.keysym); 11344 11345 } 11346 11347 // Deactivate if not pressed 11348 else { 11349 11350 removeClass(keyboard, modifierClass); 11351 delete modifierKeysyms[key.modifier]; 11352 11353 // Send key event only if original keysym is meaningful 11354 if (originalKeysym && osk.onkeyup) 11355 osk.onkeyup(originalKeysym); 11356 11357 } 11358 11359 } 11360 11361 // If not modifier, send key event now 11362 else if (osk.onkeydown) 11363 osk.onkeydown(key.keysym); 11364 11365 // Mark key as pressed 11366 pressed[keyName] = true; 11367 11368 } 11369 11370 }; 11371 11372 /** 11373 * Releases the key having the given name, removing the 11374 * "guac-keyboard-pressed" CSS class from the associated element. If the 11375 * key is already released, this function has no effect. 11376 * 11377 * @private 11378 * @param {!string} keyName 11379 * The name of the key to release. 11380 * 11381 * @param {!string} keyElement 11382 * The element associated with the given key. 11383 */ 11384 var release = function release(keyName, keyElement) { 11385 11386 // Release key if currently pressed 11387 if (pressed[keyName]) { 11388 11389 removeClass(keyElement, "guac-keyboard-pressed"); 11390 11391 // Get current key based on modifier state 11392 var key = getActiveKey(keyName); 11393 11394 // Send key event if not a modifier key 11395 if (!key.modifier && osk.onkeyup) 11396 osk.onkeyup(key.keysym); 11397 11398 // Mark key as released 11399 pressed[keyName] = false; 11400 11401 } 11402 11403 }; 11404 11405 // Create keyboard 11406 var keyboard = document.createElement("div"); 11407 keyboard.className = "guac-keyboard"; 11408 11409 // Do not allow selection or mouse movement to propagate/register. 11410 keyboard.onselectstart = 11411 keyboard.onmousemove = 11412 keyboard.onmouseup = 11413 keyboard.onmousedown = function handleMouseEvents(e) { 11414 11415 // If ignoring events, decrement counter 11416 if (ignoreMouse) 11417 ignoreMouse--; 11418 11419 e.stopPropagation(); 11420 return false; 11421 11422 }; 11423 11424 /** 11425 * The number of mousemove events to require before re-enabling mouse 11426 * event handling after receiving a touch event. 11427 * 11428 * @type {!number} 11429 */ 11430 this.touchMouseThreshold = 3; 11431 11432 /** 11433 * Fired whenever the user presses a key on this Guacamole.OnScreenKeyboard. 11434 * 11435 * @event 11436 * @param {!number} keysym 11437 * The keysym of the key being pressed. 11438 */ 11439 this.onkeydown = null; 11440 11441 /** 11442 * Fired whenever the user releases a key on this Guacamole.OnScreenKeyboard. 11443 * 11444 * @event 11445 * @param {!number} keysym 11446 * The keysym of the key being released. 11447 */ 11448 this.onkeyup = null; 11449 11450 /** 11451 * The keyboard layout provided at time of construction. 11452 * 11453 * @type {!Guacamole.OnScreenKeyboard.Layout} 11454 */ 11455 this.layout = new Guacamole.OnScreenKeyboard.Layout(layout); 11456 11457 /** 11458 * Returns the element containing the entire on-screen keyboard. 11459 * 11460 * @returns {!Element} 11461 * The element containing the entire on-screen keyboard. 11462 */ 11463 this.getElement = function() { 11464 return keyboard; 11465 }; 11466 11467 /** 11468 * Resizes all elements within this Guacamole.OnScreenKeyboard such that 11469 * the width is close to but does not exceed the specified width. The 11470 * height of the keyboard is determined based on the width. 11471 * 11472 * @param {!number} width 11473 * The width to resize this Guacamole.OnScreenKeyboard to, in pixels. 11474 */ 11475 this.resize = function(width) { 11476 11477 // Get pixel size of a unit 11478 var unit = Math.floor(width * 10 / osk.layout.width) / 10; 11479 11480 // Resize all scaled elements 11481 for (var i=0; i<scaledElements.length; i++) { 11482 var scaledElement = scaledElements[i]; 11483 scaledElement.scale(unit); 11484 } 11485 11486 }; 11487 11488 /** 11489 * Given the name of a key and its corresponding definition, which may be 11490 * an array of keys objects, a number (keysym), a string (key title), or a 11491 * single key object, returns an array of key objects, deriving any missing 11492 * properties as needed, and ensuring the key name is defined. 11493 * 11494 * @private 11495 * @param {!string} name 11496 * The name of the key being coerced into an array of Key objects. 11497 * 11498 * @param {!(number|string|Guacamole.OnScreenKeyboard.Key|Guacamole.OnScreenKeyboard.Key[])} object 11499 * The object defining the behavior of the key having the given name, 11500 * which may be the title of the key (a string), the keysym (a number), 11501 * a single Key object, or an array of Key objects. 11502 * 11503 * @returns {!Guacamole.OnScreenKeyboard.Key[]} 11504 * An array of all keys associated with the given name. 11505 */ 11506 var asKeyArray = function asKeyArray(name, object) { 11507 11508 // If already an array, just coerce into a true Key[] 11509 if (object instanceof Array) { 11510 var keys = []; 11511 for (var i=0; i < object.length; i++) { 11512 keys.push(new Guacamole.OnScreenKeyboard.Key(object[i], name)); 11513 } 11514 return keys; 11515 } 11516 11517 // Derive key object from keysym if that's all we have 11518 if (typeof object === 'number') { 11519 return [new Guacamole.OnScreenKeyboard.Key({ 11520 name : name, 11521 keysym : object 11522 })]; 11523 } 11524 11525 // Derive key object from title if that's all we have 11526 if (typeof object === 'string') { 11527 return [new Guacamole.OnScreenKeyboard.Key({ 11528 name : name, 11529 title : object 11530 })]; 11531 } 11532 11533 // Otherwise, assume it's already a key object, just not an array 11534 return [new Guacamole.OnScreenKeyboard.Key(object, name)]; 11535 11536 }; 11537 11538 /** 11539 * Converts the rather forgiving key mapping allowed by 11540 * Guacamole.OnScreenKeyboard.Layout into a rigorous mapping of key name 11541 * to key definition, where the key definition is always an array of Key 11542 * objects. 11543 * 11544 * @private 11545 * @param {!Object.<string, number|string|Guacamole.OnScreenKeyboard.Key|Guacamole.OnScreenKeyboard.Key[]>} keys 11546 * A mapping of key name to key definition, where the key definition is 11547 * the title of the key (a string), the keysym (a number), a single 11548 * Key object, or an array of Key objects. 11549 * 11550 * @returns {!Object.<string, Guacamole.OnScreenKeyboard.Key[]>} 11551 * A more-predictable mapping of key name to key definition, where the 11552 * key definition is always simply an array of Key objects. 11553 */ 11554 var getKeys = function getKeys(keys) { 11555 11556 var keyArrays = {}; 11557 11558 // Coerce all keys into individual key arrays 11559 for (var name in layout.keys) { 11560 keyArrays[name] = asKeyArray(name, keys[name]); 11561 } 11562 11563 return keyArrays; 11564 11565 }; 11566 11567 /** 11568 * Map of all key names to their corresponding set of keys. Each key name 11569 * may correspond to multiple keys due to the effect of modifiers. 11570 * 11571 * @type {!Object.<string, Guacamole.OnScreenKeyboard.Key[]>} 11572 */ 11573 this.keys = getKeys(layout.keys); 11574 11575 /** 11576 * Given an arbitrary string representing the name of some component of the 11577 * on-screen keyboard, returns a string formatted for use as a CSS class 11578 * name. The result will be lowercase. Word boundaries previously denoted 11579 * by CamelCase will be replaced by individual hyphens, as will all 11580 * contiguous non-alphanumeric characters. 11581 * 11582 * @private 11583 * @param {!string} name 11584 * An arbitrary string representing the name of some component of the 11585 * on-screen keyboard. 11586 * 11587 * @returns {!string} 11588 * A string formatted for use as a CSS class name. 11589 */ 11590 var getCSSName = function getCSSName(name) { 11591 11592 // Convert name from possibly-CamelCase to hyphenated lowercase 11593 var cssName = name 11594 .replace(/([a-z])([A-Z])/g, '$1-$2') 11595 .replace(/[^A-Za-z0-9]+/g, '-') 11596 .toLowerCase(); 11597 11598 return cssName; 11599 11600 }; 11601 11602 /** 11603 * Appends DOM elements to the given element as dictated by the layout 11604 * structure object provided. If a name is provided, an additional CSS 11605 * class, prepended with "guac-keyboard-", will be added to the top-level 11606 * element. 11607 * 11608 * If the layout structure object is an array, all elements within that 11609 * array will be recursively appended as children of a group, and the 11610 * top-level element will be given the CSS class "guac-keyboard-group". 11611 * 11612 * If the layout structure object is an object, all properties within that 11613 * object will be recursively appended as children of a group, and the 11614 * top-level element will be given the CSS class "guac-keyboard-group". The 11615 * name of each property will be applied as the name of each child object 11616 * for the sake of CSS. Each property will be added in sorted order. 11617 * 11618 * If the layout structure object is a string, the key having that name 11619 * will be appended. The key will be given the CSS class 11620 * "guac-keyboard-key" and "guac-keyboard-key-NAME", where NAME is the name 11621 * of the key. If the name of the key is a single character, this will 11622 * first be transformed into the C-style hexadecimal literal for the 11623 * Unicode codepoint of that character. For example, the key "A" would 11624 * become "guac-keyboard-key-0x41". 11625 * 11626 * If the layout structure object is a number, a gap of that size will be 11627 * inserted. The gap will be given the CSS class "guac-keyboard-gap", and 11628 * will be scaled according to the same size units as each key. 11629 * 11630 * @private 11631 * @param {!Element} element 11632 * The element to append elements to. 11633 * 11634 * @param {!(Array|object|string|number)} object 11635 * The layout structure object to use when constructing the elements to 11636 * append. 11637 * 11638 * @param {string} [name] 11639 * The name of the top-level element being appended, if any. 11640 */ 11641 var appendElements = function appendElements(element, object, name) { 11642 11643 var i; 11644 11645 // Create div which will become the group or key 11646 var div = document.createElement('div'); 11647 11648 // Add class based on name, if name given 11649 if (name) 11650 addClass(div, 'guac-keyboard-' + getCSSName(name)); 11651 11652 // If an array, append each element 11653 if (object instanceof Array) { 11654 11655 // Add group class 11656 addClass(div, 'guac-keyboard-group'); 11657 11658 // Append all elements of array 11659 for (i=0; i < object.length; i++) 11660 appendElements(div, object[i]); 11661 11662 } 11663 11664 // If an object, append each property value 11665 else if (object instanceof Object) { 11666 11667 // Add group class 11668 addClass(div, 'guac-keyboard-group'); 11669 11670 // Append all children, sorted by name 11671 var names = Object.keys(object).sort(); 11672 for (i=0; i < names.length; i++) { 11673 var name = names[i]; 11674 appendElements(div, object[name], name); 11675 } 11676 11677 } 11678 11679 // If a number, create as a gap 11680 else if (typeof object === 'number') { 11681 11682 // Add gap class 11683 addClass(div, 'guac-keyboard-gap'); 11684 11685 // Maintain scale 11686 scaledElements.push(new ScaledElement(div, object, object)); 11687 11688 } 11689 11690 // If a string, create as a key 11691 else if (typeof object === 'string') { 11692 11693 // If key name is only one character, use codepoint for name 11694 var keyName = object; 11695 if (keyName.length === 1) 11696 keyName = '0x' + keyName.charCodeAt(0).toString(16); 11697 11698 // Add key container class 11699 addClass(div, 'guac-keyboard-key-container'); 11700 11701 // Create key element which will contain all possible caps 11702 var keyElement = document.createElement('div'); 11703 keyElement.className = 'guac-keyboard-key ' 11704 + 'guac-keyboard-key-' + getCSSName(keyName); 11705 11706 // Add all associated keys as caps within DOM 11707 var keys = osk.keys[object]; 11708 if (keys) { 11709 for (i=0; i < keys.length; i++) { 11710 11711 // Get current key 11712 var key = keys[i]; 11713 11714 // Create cap element for key 11715 var capElement = document.createElement('div'); 11716 capElement.className = 'guac-keyboard-cap'; 11717 capElement.textContent = key.title; 11718 11719 // Add classes for any requirements 11720 for (var j=0; j < key.requires.length; j++) { 11721 var requirement = key.requires[j]; 11722 addClass(capElement, 'guac-keyboard-requires-' + getCSSName(requirement)); 11723 addClass(keyElement, 'guac-keyboard-uses-' + getCSSName(requirement)); 11724 } 11725 11726 // Add cap to key within DOM 11727 keyElement.appendChild(capElement); 11728 11729 } 11730 } 11731 11732 // Add key to DOM, maintain scale 11733 div.appendChild(keyElement); 11734 scaledElements.push(new ScaledElement(div, osk.layout.keyWidths[object] || 1, 1, true)); 11735 11736 /** 11737 * Handles a touch event which results in the pressing of an OSK 11738 * key. Touch events will result in mouse events being ignored for 11739 * touchMouseThreshold events. 11740 * 11741 * @private 11742 * @param {!TouchEvent} e 11743 * The touch event being handled. 11744 */ 11745 var touchPress = function touchPress(e) { 11746 e.preventDefault(); 11747 ignoreMouse = osk.touchMouseThreshold; 11748 press(object, keyElement); 11749 }; 11750 11751 /** 11752 * Handles a touch event which results in the release of an OSK 11753 * key. Touch events will result in mouse events being ignored for 11754 * touchMouseThreshold events. 11755 * 11756 * @private 11757 * @param {!TouchEvent} e 11758 * The touch event being handled. 11759 */ 11760 var touchRelease = function touchRelease(e) { 11761 e.preventDefault(); 11762 ignoreMouse = osk.touchMouseThreshold; 11763 release(object, keyElement); 11764 }; 11765 11766 /** 11767 * Handles a mouse event which results in the pressing of an OSK 11768 * key. If mouse events are currently being ignored, this handler 11769 * does nothing. 11770 * 11771 * @private 11772 * @param {!MouseEvent} e 11773 * The touch event being handled. 11774 */ 11775 var mousePress = function mousePress(e) { 11776 e.preventDefault(); 11777 if (ignoreMouse === 0) 11778 press(object, keyElement); 11779 }; 11780 11781 /** 11782 * Handles a mouse event which results in the release of an OSK 11783 * key. If mouse events are currently being ignored, this handler 11784 * does nothing. 11785 * 11786 * @private 11787 * @param {!MouseEvent} e 11788 * The touch event being handled. 11789 */ 11790 var mouseRelease = function mouseRelease(e) { 11791 e.preventDefault(); 11792 if (ignoreMouse === 0) 11793 release(object, keyElement); 11794 }; 11795 11796 // Handle touch events on key 11797 keyElement.addEventListener("touchstart", touchPress, true); 11798 keyElement.addEventListener("touchend", touchRelease, true); 11799 11800 // Handle mouse events on key 11801 keyElement.addEventListener("mousedown", mousePress, true); 11802 keyElement.addEventListener("mouseup", mouseRelease, true); 11803 keyElement.addEventListener("mouseout", mouseRelease, true); 11804 11805 } // end if object is key name 11806 11807 // Add newly-created group/key 11808 element.appendChild(div); 11809 11810 }; 11811 11812 // Create keyboard layout in DOM 11813 appendElements(keyboard, layout.layout); 11814 11815}; 11816 11817/** 11818 * Represents an entire on-screen keyboard layout, including all available 11819 * keys, their behaviors, and their relative position and sizing. 11820 * 11821 * @constructor 11822 * @param {!(Guacamole.OnScreenKeyboard.Layout|object)} template 11823 * The object whose identically-named properties will be used to initialize 11824 * the properties of this layout. 11825 */ 11826Guacamole.OnScreenKeyboard.Layout = function(template) { 11827 11828 /** 11829 * The language of keyboard layout, such as "en_US". This property is for 11830 * informational purposes only, but it is recommend to conform to the 11831 * [language code]_[country code] format. 11832 * 11833 * @type {!string} 11834 */ 11835 this.language = template.language; 11836 11837 /** 11838 * The type of keyboard layout, such as "qwerty". This property is for 11839 * informational purposes only, and does not conform to any standard. 11840 * 11841 * @type {!string} 11842 */ 11843 this.type = template.type; 11844 11845 /** 11846 * Map of key name to corresponding keysym, title, or key object. If only 11847 * the keysym or title is provided, the key object will be created 11848 * implicitly. In all cases, the name property of the key object will be 11849 * taken from the name given in the mapping. 11850 * 11851 * @type {!Object.<string, number|string|Guacamole.OnScreenKeyboard.Key|Guacamole.OnScreenKeyboard.Key[]>} 11852 */ 11853 this.keys = template.keys; 11854 11855 /** 11856 * Arbitrarily nested, arbitrarily grouped key names. The contents of the 11857 * layout will be traversed to produce an identically-nested grouping of 11858 * keys in the DOM tree. All strings will be transformed into their 11859 * corresponding sets of keys, while all objects and arrays will be 11860 * transformed into named groups and anonymous groups respectively. Any 11861 * numbers present will be transformed into gaps of that size, scaled 11862 * according to the same units as each key. 11863 * 11864 * @type {!object} 11865 */ 11866 this.layout = template.layout; 11867 11868 /** 11869 * The width of the entire keyboard, in arbitrary units. The width of each 11870 * key is relative to this width, as both width values are assumed to be in 11871 * the same units. The conversion factor between these units and pixels is 11872 * derived later via a call to resize() on the Guacamole.OnScreenKeyboard. 11873 * 11874 * @type {!number} 11875 */ 11876 this.width = template.width; 11877 11878 /** 11879 * The width of each key, in arbitrary units, relative to other keys in 11880 * this layout. The true pixel size of each key will be determined by the 11881 * overall size of the keyboard. If not defined here, the width of each 11882 * key will default to 1. 11883 * 11884 * @type {!Object.<string, number>} 11885 */ 11886 this.keyWidths = template.keyWidths || {}; 11887 11888}; 11889 11890/** 11891 * Represents a single key, or a single possible behavior of a key. Each key 11892 * on the on-screen keyboard must have at least one associated 11893 * Guacamole.OnScreenKeyboard.Key, whether that key is explicitly defined or 11894 * implied, and may have multiple Guacamole.OnScreenKeyboard.Key if behavior 11895 * depends on modifier states. 11896 * 11897 * @constructor 11898 * @param {!(Guacamole.OnScreenKeyboard.Key|object)} template 11899 * The object whose identically-named properties will be used to initialize 11900 * the properties of this key. 11901 * 11902 * @param {string} [name] 11903 * The name to use instead of any name provided within the template, if 11904 * any. If omitted, the name within the template will be used, assuming the 11905 * template contains a name. 11906 */ 11907Guacamole.OnScreenKeyboard.Key = function(template, name) { 11908 11909 /** 11910 * The unique name identifying this key within the keyboard layout. 11911 * 11912 * @type {!string} 11913 */ 11914 this.name = name || template.name; 11915 11916 /** 11917 * The human-readable title that will be displayed to the user within the 11918 * key. If not provided, this will be derived from the key name. 11919 * 11920 * @type {!string} 11921 */ 11922 this.title = template.title || this.name; 11923 11924 /** 11925 * The keysym to be pressed/released when this key is pressed/released. If 11926 * not provided, this will be derived from the title if the title is a 11927 * single character. 11928 * 11929 * @type {number} 11930 */ 11931 this.keysym = template.keysym || (function deriveKeysym(title) { 11932 11933 // Do not derive keysym if title is not exactly one character 11934 if (!title || title.length !== 1) 11935 return null; 11936 11937 // For characters between U+0000 and U+00FF, the keysym is the codepoint 11938 var charCode = title.charCodeAt(0); 11939 if (charCode >= 0x0000 && charCode <= 0x00FF) 11940 return charCode; 11941 11942 // For characters between U+0100 and U+10FFFF, the keysym is the codepoint or'd with 0x01000000 11943 if (charCode >= 0x0100 && charCode <= 0x10FFFF) 11944 return 0x01000000 | charCode; 11945 11946 // Unable to derive keysym 11947 return null; 11948 11949 })(this.title); 11950 11951 /** 11952 * The name of the modifier set when the key is pressed and cleared when 11953 * this key is released, if any. The names of modifiers are distinct from 11954 * the names of keys; both the "RightShift" and "LeftShift" keys may set 11955 * the "shift" modifier, for example. By default, the key will affect no 11956 * modifiers. 11957 * 11958 * @type {string} 11959 */ 11960 this.modifier = template.modifier; 11961 11962 /** 11963 * An array containing the names of each modifier required for this key to 11964 * have an effect. For example, a lowercase letter may require nothing, 11965 * while an uppercase letter would require "shift", assuming the Shift key 11966 * is named "shift" within the layout. By default, the key will require 11967 * no modifiers. 11968 * 11969 * @type {!string[]} 11970 */ 11971 this.requires = template.requires || []; 11972 11973}; 11974/* 11975 * Licensed to the Apache Software Foundation (ASF) under one 11976 * or more contributor license agreements. See the NOTICE file 11977 * distributed with this work for additional information 11978 * regarding copyright ownership. The ASF licenses this file 11979 * to you under the Apache License, Version 2.0 (the 11980 * "License"); you may not use this file except in compliance 11981 * with the License. You may obtain a copy of the License at 11982 * 11983 * http://www.apache.org/licenses/LICENSE-2.0 11984 * 11985 * Unless required by applicable law or agreed to in writing, 11986 * software distributed under the License is distributed on an 11987 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 11988 * KIND, either express or implied. See the License for the 11989 * specific language governing permissions and limitations 11990 * under the License. 11991 */ 11992 11993var Guacamole = Guacamole || {}; 11994 11995/** 11996 * Abstract stream which can receive data. 11997 * 11998 * @constructor 11999 * @param {!Guacamole.Client} client 12000 * The client owning this stream. 12001 * 12002 * @param {!number} index 12003 * The index of this stream. 12004 */ 12005Guacamole.OutputStream = function(client, index) { 12006 12007 /** 12008 * Reference to this stream. 12009 * 12010 * @private 12011 * @type {!Guacamole.OutputStream} 12012 */ 12013 var guac_stream = this; 12014 12015 /** 12016 * The index of this stream. 12017 * @type {!number} 12018 */ 12019 this.index = index; 12020 12021 /** 12022 * Fired whenever an acknowledgement is received from the server, indicating 12023 * that a stream operation has completed, or an error has occurred. 12024 * 12025 * @event 12026 * @param {!Guacamole.Status} status 12027 * The status of the operation. 12028 */ 12029 this.onack = null; 12030 12031 /** 12032 * Writes the given base64-encoded data to this stream as a blob. 12033 * 12034 * @param {!string} data 12035 * The base64-encoded data to send. 12036 */ 12037 this.sendBlob = function(data) { 12038 client.sendBlob(guac_stream.index, data); 12039 }; 12040 12041 /** 12042 * Closes this stream. 12043 */ 12044 this.sendEnd = function() { 12045 client.endStream(guac_stream.index); 12046 }; 12047 12048}; 12049/* 12050 * Licensed to the Apache Software Foundation (ASF) under one 12051 * or more contributor license agreements. See the NOTICE file 12052 * distributed with this work for additional information 12053 * regarding copyright ownership. The ASF licenses this file 12054 * to you under the Apache License, Version 2.0 (the 12055 * "License"); you may not use this file except in compliance 12056 * with the License. You may obtain a copy of the License at 12057 * 12058 * http://www.apache.org/licenses/LICENSE-2.0 12059 * 12060 * Unless required by applicable law or agreed to in writing, 12061 * software distributed under the License is distributed on an 12062 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 12063 * KIND, either express or implied. See the License for the 12064 * specific language governing permissions and limitations 12065 * under the License. 12066 */ 12067 12068var Guacamole = Guacamole || {}; 12069 12070/** 12071 * Simple Guacamole protocol parser that invokes an oninstruction event when 12072 * full instructions are available from data received via receive(). 12073 * 12074 * @constructor 12075 */ 12076Guacamole.Parser = function() { 12077 12078 /** 12079 * Reference to this parser. 12080 * @private 12081 */ 12082 var parser = this; 12083 12084 /** 12085 * Current buffer of received data. This buffer grows until a full 12086 * element is available. After a full element is available, that element 12087 * is flushed into the element buffer. 12088 * 12089 * @private 12090 */ 12091 var buffer = ""; 12092 12093 /** 12094 * Buffer of all received, complete elements. After an entire instruction 12095 * is read, this buffer is flushed, and a new instruction begins. 12096 * 12097 * @private 12098 */ 12099 var element_buffer = []; 12100 12101 // The location of the last element's terminator 12102 var element_end = -1; 12103 12104 // Where to start the next length search or the next element 12105 var start_index = 0; 12106 12107 /** 12108 * Appends the given instruction data packet to the internal buffer of 12109 * this Guacamole.Parser, executing all completed instructions at 12110 * the beginning of this buffer, if any. 12111 * 12112 * @param {!string} packet 12113 * The instruction data to receive. 12114 */ 12115 this.receive = function(packet) { 12116 12117 // Truncate buffer as necessary 12118 if (start_index > 4096 && element_end >= start_index) { 12119 12120 buffer = buffer.substring(start_index); 12121 12122 // Reset parse relative to truncation 12123 element_end -= start_index; 12124 start_index = 0; 12125 12126 } 12127 12128 // Append data to buffer 12129 buffer += packet; 12130 12131 // While search is within currently received data 12132 while (element_end < buffer.length) { 12133 12134 // If we are waiting for element data 12135 if (element_end >= start_index) { 12136 12137 // We now have enough data for the element. Parse. 12138 var element = buffer.substring(start_index, element_end); 12139 var terminator = buffer.substring(element_end, element_end+1); 12140 12141 // Add element to array 12142 element_buffer.push(element); 12143 12144 // If last element, handle instruction 12145 if (terminator == ";") { 12146 12147 // Get opcode 12148 var opcode = element_buffer.shift(); 12149 12150 // Call instruction handler. 12151 if (parser.oninstruction != null) 12152 parser.oninstruction(opcode, element_buffer); 12153 12154 // Clear elements 12155 element_buffer.length = 0; 12156 12157 } 12158 else if (terminator != ',') 12159 throw new Error("Illegal terminator."); 12160 12161 // Start searching for length at character after 12162 // element terminator 12163 start_index = element_end + 1; 12164 12165 } 12166 12167 // Search for end of length 12168 var length_end = buffer.indexOf(".", start_index); 12169 if (length_end != -1) { 12170 12171 // Parse length 12172 var length = parseInt(buffer.substring(element_end+1, length_end)); 12173 if (isNaN(length)) 12174 throw new Error("Non-numeric character in element length."); 12175 12176 // Calculate start of element 12177 start_index = length_end + 1; 12178 12179 // Calculate location of element terminator 12180 element_end = start_index + length; 12181 12182 } 12183 12184 // If no period yet, continue search when more data 12185 // is received 12186 else { 12187 start_index = buffer.length; 12188 break; 12189 } 12190 12191 } // end parse loop 12192 12193 }; 12194 12195 /** 12196 * Fired once for every complete Guacamole instruction received, in order. 12197 * 12198 * @event 12199 * @param {!string} opcode 12200 * The Guacamole instruction opcode. 12201 * 12202 * @param {!string[]} parameters 12203 * The parameters provided for the instruction, if any. 12204 */ 12205 this.oninstruction = null; 12206 12207}; 12208/* 12209 * Licensed to the Apache Software Foundation (ASF) under one 12210 * or more contributor license agreements. See the NOTICE file 12211 * distributed with this work for additional information 12212 * regarding copyright ownership. The ASF licenses this file 12213 * to you under the Apache License, Version 2.0 (the 12214 * "License"); you may not use this file except in compliance 12215 * with the License. You may obtain a copy of the License at 12216 * 12217 * http://www.apache.org/licenses/LICENSE-2.0 12218 * 12219 * Unless required by applicable law or agreed to in writing, 12220 * software distributed under the License is distributed on an 12221 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 12222 * KIND, either express or implied. See the License for the 12223 * specific language governing permissions and limitations 12224 * under the License. 12225 */ 12226 12227var Guacamole = Guacamole || {}; 12228 12229/** 12230 * A position in 2-D space. 12231 * 12232 * @constructor 12233 * @param {Guacamole.Position|object} [template={}] 12234 * The object whose properties should be copied within the new 12235 * Guacamole.Position. 12236 */ 12237Guacamole.Position = function Position(template) { 12238 12239 template = template || {}; 12240 12241 /** 12242 * The current X position, in pixels. 12243 * 12244 * @type {!number} 12245 * @default 0 12246 */ 12247 this.x = template.x || 0; 12248 12249 /** 12250 * The current Y position, in pixels. 12251 * 12252 * @type {!number} 12253 * @default 0 12254 */ 12255 this.y = template.y || 0; 12256 12257 /** 12258 * Assigns the position represented by the given element and 12259 * clientX/clientY coordinates. The clientX and clientY coordinates are 12260 * relative to the browser viewport and are commonly available within 12261 * JavaScript event objects. The final position is translated to 12262 * coordinates that are relative the given element. 12263 * 12264 * @param {!Element} element 12265 * The element the coordinates should be relative to. 12266 * 12267 * @param {!number} clientX 12268 * The viewport-relative X coordinate to translate. 12269 * 12270 * @param {!number} clientY 12271 * The viewport-relative Y coordinate to translate. 12272 */ 12273 this.fromClientPosition = function fromClientPosition(element, clientX, clientY) { 12274 12275 this.x = clientX - element.offsetLeft; 12276 this.y = clientY - element.offsetTop; 12277 12278 // This is all JUST so we can get the position within the element 12279 var parent = element.offsetParent; 12280 while (parent && !(parent === document.body)) { 12281 this.x -= parent.offsetLeft - parent.scrollLeft; 12282 this.y -= parent.offsetTop - parent.scrollTop; 12283 12284 parent = parent.offsetParent; 12285 } 12286 12287 // Element ultimately depends on positioning within document body, 12288 // take document scroll into account. 12289 if (parent) { 12290 var documentScrollLeft = document.body.scrollLeft || document.documentElement.scrollLeft; 12291 var documentScrollTop = document.body.scrollTop || document.documentElement.scrollTop; 12292 12293 this.x -= parent.offsetLeft - documentScrollLeft; 12294 this.y -= parent.offsetTop - documentScrollTop; 12295 } 12296 12297 }; 12298 12299}; 12300 12301/** 12302 * Returns a new {@link Guacamole.Position} representing the relative position 12303 * of the given clientX/clientY coordinates within the given element. The 12304 * clientX and clientY coordinates are relative to the browser viewport and are 12305 * commonly available within JavaScript event objects. The final position is 12306 * translated to coordinates that are relative the given element. 12307 * 12308 * @param {!Element} element 12309 * The element the coordinates should be relative to. 12310 * 12311 * @param {!number} clientX 12312 * The viewport-relative X coordinate to translate. 12313 * 12314 * @param {!number} clientY 12315 * The viewport-relative Y coordinate to translate. 12316 * 12317 * @returns {!Guacamole.Position} 12318 * A new Guacamole.Position representing the relative position of the given 12319 * client coordinates. 12320 */ 12321Guacamole.Position.fromClientPosition = function fromClientPosition(element, clientX, clientY) { 12322 var position = new Guacamole.Position(); 12323 position.fromClientPosition(element, clientX, clientY); 12324 return position; 12325}; 12326/* 12327 * Licensed to the Apache Software Foundation (ASF) under one 12328 * or more contributor license agreements. See the NOTICE file 12329 * distributed with this work for additional information 12330 * regarding copyright ownership. The ASF licenses this file 12331 * to you under the Apache License, Version 2.0 (the 12332 * "License"); you may not use this file except in compliance 12333 * with the License. You may obtain a copy of the License at 12334 * 12335 * http://www.apache.org/licenses/LICENSE-2.0 12336 * 12337 * Unless required by applicable law or agreed to in writing, 12338 * software distributed under the License is distributed on an 12339 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 12340 * KIND, either express or implied. See the License for the 12341 * specific language governing permissions and limitations 12342 * under the License. 12343 */ 12344 12345var Guacamole = Guacamole || {}; 12346 12347/** 12348 * A description of the format of raw PCM audio, such as that used by 12349 * Guacamole.RawAudioPlayer and Guacamole.RawAudioRecorder. This object 12350 * describes the number of bytes per sample, the number of channels, and the 12351 * overall sample rate. 12352 * 12353 * @constructor 12354 * @param {!(Guacamole.RawAudioFormat|object)} template 12355 * The object whose properties should be copied into the corresponding 12356 * properties of the new Guacamole.RawAudioFormat. 12357 */ 12358Guacamole.RawAudioFormat = function RawAudioFormat(template) { 12359 12360 /** 12361 * The number of bytes in each sample of audio data. This value is 12362 * independent of the number of channels. 12363 * 12364 * @type {!number} 12365 */ 12366 this.bytesPerSample = template.bytesPerSample; 12367 12368 /** 12369 * The number of audio channels (ie: 1 for mono, 2 for stereo). 12370 * 12371 * @type {!number} 12372 */ 12373 this.channels = template.channels; 12374 12375 /** 12376 * The number of samples per second, per channel. 12377 * 12378 * @type {!number} 12379 */ 12380 this.rate = template.rate; 12381 12382}; 12383 12384/** 12385 * Parses the given mimetype, returning a new Guacamole.RawAudioFormat 12386 * which describes the type of raw audio data represented by that mimetype. If 12387 * the mimetype is not a supported raw audio data mimetype, null is returned. 12388 * 12389 * @param {!string} mimetype 12390 * The audio mimetype to parse. 12391 * 12392 * @returns {Guacamole.RawAudioFormat} 12393 * A new Guacamole.RawAudioFormat which describes the type of raw 12394 * audio data represented by the given mimetype, or null if the given 12395 * mimetype is not supported. 12396 */ 12397Guacamole.RawAudioFormat.parse = function parseFormat(mimetype) { 12398 12399 var bytesPerSample; 12400 12401 // Rate is absolutely required - if null is still present later, the 12402 // mimetype must not be supported 12403 var rate = null; 12404 12405 // Default for both "audio/L8" and "audio/L16" is one channel 12406 var channels = 1; 12407 12408 // "audio/L8" has one byte per sample 12409 if (mimetype.substring(0, 9) === 'audio/L8;') { 12410 mimetype = mimetype.substring(9); 12411 bytesPerSample = 1; 12412 } 12413 12414 // "audio/L16" has two bytes per sample 12415 else if (mimetype.substring(0, 10) === 'audio/L16;') { 12416 mimetype = mimetype.substring(10); 12417 bytesPerSample = 2; 12418 } 12419 12420 // All other types are unsupported 12421 else 12422 return null; 12423 12424 // Parse all parameters 12425 var parameters = mimetype.split(','); 12426 for (var i = 0; i < parameters.length; i++) { 12427 12428 var parameter = parameters[i]; 12429 12430 // All parameters must have an equals sign separating name from value 12431 var equals = parameter.indexOf('='); 12432 if (equals === -1) 12433 return null; 12434 12435 // Parse name and value from parameter string 12436 var name = parameter.substring(0, equals); 12437 var value = parameter.substring(equals+1); 12438 12439 // Handle each supported parameter 12440 switch (name) { 12441 12442 // Number of audio channels 12443 case 'channels': 12444 channels = parseInt(value); 12445 break; 12446 12447 // Sample rate 12448 case 'rate': 12449 rate = parseInt(value); 12450 break; 12451 12452 // All other parameters are unsupported 12453 default: 12454 return null; 12455 12456 } 12457 12458 }; 12459 12460 // The rate parameter is required 12461 if (rate === null) 12462 return null; 12463 12464 // Return parsed format details 12465 return new Guacamole.RawAudioFormat({ 12466 bytesPerSample : bytesPerSample, 12467 channels : channels, 12468 rate : rate 12469 }); 12470 12471}; 12472/* 12473 * Licensed to the Apache Software Foundation (ASF) under one 12474 * or more contributor license agreements. See the NOTICE file 12475 * distributed with this work for additional information 12476 * regarding copyright ownership. The ASF licenses this file 12477 * to you under the Apache License, Version 2.0 (the 12478 * "License"); you may not use this file except in compliance 12479 * with the License. You may obtain a copy of the License at 12480 * 12481 * http://www.apache.org/licenses/LICENSE-2.0 12482 * 12483 * Unless required by applicable law or agreed to in writing, 12484 * software distributed under the License is distributed on an 12485 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 12486 * KIND, either express or implied. See the License for the 12487 * specific language governing permissions and limitations 12488 * under the License. 12489 */ 12490 12491var Guacamole = Guacamole || {}; 12492 12493/** 12494 * A recording of a Guacamole session. Given a {@link Guacamole.Tunnel} or Blob, 12495 * the Guacamole.SessionRecording automatically parses Guacamole instructions 12496 * within the recording source as it plays back the recording. Playback of the 12497 * recording may be controlled through function calls to the 12498 * Guacamole.SessionRecording, even while the recording has not yet finished 12499 * being created or downloaded. Parsing of the contents of the recording will 12500 * begin immediately and automatically after this constructor is invoked. 12501 * 12502 * @constructor 12503 * @param {!Blob|Guacamole.Tunnel} source 12504 * The Blob from which the instructions of the recording should 12505 * be read. 12506 */ 12507Guacamole.SessionRecording = function SessionRecording(source) { 12508 12509 /** 12510 * Reference to this Guacamole.SessionRecording. 12511 * 12512 * @private 12513 * @type {!Guacamole.SessionRecording} 12514 */ 12515 var recording = this; 12516 12517 /** 12518 * The Blob from which the instructions of the recording should be read. 12519 * Note that this value is initialized far below. 12520 * 12521 * @private 12522 * @type {!Blob} 12523 */ 12524 var recordingBlob; 12525 12526 /** 12527 * The tunnel from which the recording should be read, if the recording is 12528 * being read from a tunnel. If the recording was supplied as a Blob, this 12529 * will be null. 12530 * 12531 * @private 12532 * @type {Guacamole.Tunnel} 12533 */ 12534 var tunnel = null; 12535 12536 /** 12537 * The number of bytes that this Guacamole.SessionRecording should attempt 12538 * to read from the given blob in each read operation. Larger blocks will 12539 * generally read the blob more quickly, but may result in excessive 12540 * time being spent within the parser, making the page unresponsive 12541 * while the recording is loading. 12542 * 12543 * @private 12544 * @constant 12545 * @type {Number} 12546 */ 12547 var BLOCK_SIZE = 262144; 12548 12549 /** 12550 * The minimum number of characters which must have been read between 12551 * keyframes. 12552 * 12553 * @private 12554 * @constant 12555 * @type {Number} 12556 */ 12557 var KEYFRAME_CHAR_INTERVAL = 16384; 12558 12559 /** 12560 * The minimum number of milliseconds which must elapse between keyframes. 12561 * 12562 * @private 12563 * @constant 12564 * @type {Number} 12565 */ 12566 var KEYFRAME_TIME_INTERVAL = 5000; 12567 12568 /** 12569 * All frames parsed from the provided blob. 12570 * 12571 * @private 12572 * @type {!Guacamole.SessionRecording._Frame[]} 12573 */ 12574 var frames = []; 12575 12576 /** 12577 * The timestamp of the last frame which was flagged for use as a keyframe. 12578 * If no timestamp has yet been flagged, this will be 0. 12579 * 12580 * @private 12581 * @type {!number} 12582 */ 12583 var lastKeyframe = 0; 12584 12585 /** 12586 * Tunnel which feeds arbitrary instructions to the client used by this 12587 * Guacamole.SessionRecording for playback of the session recording. 12588 * 12589 * @private 12590 * @type {!Guacamole.SessionRecording._PlaybackTunnel} 12591 */ 12592 var playbackTunnel = new Guacamole.SessionRecording._PlaybackTunnel(); 12593 12594 /** 12595 * Guacamole.Client instance used for visible playback of the session 12596 * recording. 12597 * 12598 * @private 12599 * @type {!Guacamole.Client} 12600 */ 12601 var playbackClient = new Guacamole.Client(playbackTunnel); 12602 12603 /** 12604 * The current frame rendered within the playback client. If no frame is 12605 * yet rendered, this will be -1. 12606 * 12607 * @private 12608 * @type {!number} 12609 */ 12610 var currentFrame = -1; 12611 12612 /** 12613 * The timestamp of the frame when playback began, in milliseconds. If 12614 * playback is not in progress, this will be null. 12615 * 12616 * @private 12617 * @type {number} 12618 */ 12619 var startVideoTimestamp = null; 12620 12621 /** 12622 * The real-world timestamp when playback began, in milliseconds. If 12623 * playback is not in progress, this will be null. 12624 * 12625 * @private 12626 * @type {number} 12627 */ 12628 var startRealTimestamp = null; 12629 12630 /** 12631 * An object containing a single "aborted" property which is set to 12632 * true if the in-progress seek operation should be aborted. If no seek 12633 * operation is in progress, this will be null. 12634 * 12635 * @private 12636 * @type {object} 12637 */ 12638 var activeSeek = null; 12639 12640 /** 12641 * The byte offset within the recording blob of the first character of 12642 * the first instruction of the current frame. Here, "current frame" 12643 * refers to the frame currently being parsed when the provided 12644 * recording is initially loading. If the recording is not being 12645 * loaded, this value has no meaning. 12646 * 12647 * @private 12648 * @type {!number} 12649 */ 12650 var frameStart = 0; 12651 12652 /** 12653 * The byte offset within the recording blob of the character which 12654 * follows the last character of the most recently parsed instruction 12655 * of the current frame. Here, "current frame" refers to the frame 12656 * currently being parsed when the provided recording is initially 12657 * loading. If the recording is not being loaded, this value has no 12658 * meaning. 12659 * 12660 * @private 12661 * @type {!number} 12662 */ 12663 var frameEnd = 0; 12664 12665 /** 12666 * Whether the initial loading process has been aborted. If the loading 12667 * process has been aborted, no further blocks of data should be read 12668 * from the recording. 12669 * 12670 * @private 12671 * @type {!boolean} 12672 */ 12673 var aborted = false; 12674 12675 /** 12676 * The function to invoke when the seek operation initiated by a call 12677 * to seek() is cancelled or successfully completed. If no seek 12678 * operation is in progress, this will be null. 12679 * 12680 * @private 12681 * @type {function} 12682 */ 12683 var seekCallback = null; 12684 12685 /** 12686 * Parses all Guacamole instructions within the given blob, invoking 12687 * the provided instruction callback for each such instruction. Once 12688 * the end of the blob has been reached (no instructions remain to be 12689 * parsed), the provided completion callback is invoked. If a parse 12690 * error prevents reading instructions from the blob, the onerror 12691 * callback of the Guacamole.SessionRecording is invoked, and no further 12692 * data is handled within the blob. 12693 * 12694 * @private 12695 * @param {!Blob} blob 12696 * The blob to parse Guacamole instructions from. 12697 * 12698 * @param {function} [instructionCallback] 12699 * The callback to invoke for each Guacamole instruction read from 12700 * the given blob. This function must accept the same arguments 12701 * as the oninstruction handler of Guacamole.Parser. 12702 * 12703 * @param {function} [completionCallback] 12704 * The callback to invoke once all instructions have been read from 12705 * the given blob. 12706 */ 12707 var parseBlob = function parseBlob(blob, instructionCallback, completionCallback) { 12708 12709 // Do not read any further blocks if loading has been aborted 12710 if (aborted && blob === recordingBlob) 12711 return; 12712 12713 // Prepare a parser to handle all instruction data within the blob, 12714 // automatically invoking the provided instruction callback for all 12715 // parsed instructions 12716 var parser = new Guacamole.Parser(); 12717 parser.oninstruction = instructionCallback; 12718 12719 var offset = 0; 12720 var reader = new FileReader(); 12721 12722 /** 12723 * Reads the block of data at offset bytes within the blob. If no 12724 * such block exists, then the completion callback provided to 12725 * parseBlob() is invoked as all data has been read. 12726 * 12727 * @private 12728 */ 12729 var readNextBlock = function readNextBlock() { 12730 12731 // Do not read any further blocks if loading has been aborted 12732 if (aborted && blob === recordingBlob) 12733 return; 12734 12735 // Parse all instructions within the block, invoking the 12736 // onerror handler if a parse error occurs 12737 if (reader.readyState === 2 /* DONE */) { 12738 try { 12739 parser.receive(reader.result); 12740 } 12741 catch (parseError) { 12742 if (recording.onerror) { 12743 recording.onerror(parseError.message); 12744 } 12745 return; 12746 } 12747 } 12748 12749 // If no data remains, the read operation is complete and no 12750 // further blocks need to be read 12751 if (offset >= blob.size) { 12752 if (completionCallback) 12753 completionCallback(); 12754 } 12755 12756 // Otherwise, read the next block 12757 else { 12758 var block = blob.slice(offset, offset + BLOCK_SIZE); 12759 offset += block.size; 12760 reader.readAsText(block); 12761 } 12762 12763 }; 12764 12765 // Read blocks until the end of the given blob is reached 12766 reader.onload = readNextBlock; 12767 readNextBlock(); 12768 12769 }; 12770 12771 /** 12772 * Calculates the size of the given Guacamole instruction element, in 12773 * Unicode characters. The size returned includes the characters which 12774 * make up the length, the "." separator between the length and the 12775 * element itself, and the "," or ";" terminator which follows the 12776 * element. 12777 * 12778 * @private 12779 * @param {!string} value 12780 * The value of the element which has already been parsed (lacks 12781 * the initial length, "." separator, and "," or ";" terminator). 12782 * 12783 * @returns {!number} 12784 * The number of Unicode characters which would make up the given 12785 * element within a Guacamole instruction. 12786 */ 12787 var getElementSize = function getElementSize(value) { 12788 12789 var valueLength = value.length; 12790 12791 // Calculate base size, assuming at least one digit, the "." 12792 // separator, and the "," or ";" terminator 12793 var protocolSize = valueLength + 3; 12794 12795 // Add one character for each additional digit that would occur 12796 // in the element length prefix 12797 while (valueLength >= 10) { 12798 protocolSize++; 12799 valueLength = Math.floor(valueLength / 10); 12800 } 12801 12802 return protocolSize; 12803 12804 }; 12805 12806 // Start playback client connected 12807 playbackClient.connect(); 12808 12809 // Hide cursor unless mouse position is received 12810 playbackClient.getDisplay().showCursor(false); 12811 12812 /** 12813 * Handles a newly-received instruction, whether from the main Blob or a 12814 * tunnel, adding new frames and keyframes as necessary. Load progress is 12815 * reported via onprogress automatically. 12816 * 12817 * @private 12818 * @param {!string} opcode 12819 * The opcode of the instruction to handle. 12820 * 12821 * @param {!string[]} args 12822 * The arguments of the received instruction, if any. 12823 */ 12824 var loadInstruction = function loadInstruction(opcode, args) { 12825 12826 // Advance end of frame by overall length of parsed instruction 12827 frameEnd += getElementSize(opcode); 12828 for (var i = 0; i < args.length; i++) 12829 frameEnd += getElementSize(args[i]); 12830 12831 // Once a sync is received, store all instructions since the last 12832 // frame as a new frame 12833 if (opcode === 'sync') { 12834 12835 // Parse frame timestamp from sync instruction 12836 var timestamp = parseInt(args[0]); 12837 12838 // Add a new frame containing the instructions read since last frame 12839 var frame = new Guacamole.SessionRecording._Frame(timestamp, frameStart, frameEnd); 12840 frames.push(frame); 12841 frameStart = frameEnd; 12842 12843 // This frame should eventually become a keyframe if enough data 12844 // has been processed and enough recording time has elapsed, or if 12845 // this is the absolute first frame 12846 if (frames.length === 1 || (frameEnd - frames[lastKeyframe].start >= KEYFRAME_CHAR_INTERVAL 12847 && timestamp - frames[lastKeyframe].timestamp >= KEYFRAME_TIME_INTERVAL)) { 12848 frame.keyframe = true; 12849 lastKeyframe = frames.length - 1; 12850 } 12851 12852 // Notify that additional content is available 12853 if (recording.onprogress) 12854 recording.onprogress(recording.getDuration(), frameEnd); 12855 12856 } 12857 12858 }; 12859 12860 /** 12861 * Notifies that the session recording has been fully loaded. If the onload 12862 * handler has not been defined, this function has no effect. 12863 * 12864 * @private 12865 */ 12866 var notifyLoaded = function notifyLoaded() { 12867 if (recording.onload) 12868 recording.onload(); 12869 }; 12870 12871 // Read instructions from provided blob, extracting each frame 12872 if (source instanceof Blob) 12873 parseBlob(recordingBlob, loadInstruction, notifyLoaded); 12874 12875 // If tunnel provided instead of Blob, extract frames, etc. as instructions 12876 // are received, buffering things into a Blob for future seeks 12877 else { 12878 12879 tunnel = source; 12880 recordingBlob = new Blob(); 12881 12882 var errorEncountered = false; 12883 var instructionBuffer = ''; 12884 12885 // Read instructions from provided tunnel, extracting each frame 12886 tunnel.oninstruction = function handleInstruction(opcode, args) { 12887 12888 // Reconstitute received instruction 12889 instructionBuffer += opcode.length + '.' + opcode; 12890 args.forEach(function appendArg(arg) { 12891 instructionBuffer += ',' + arg.length + '.' + arg; 12892 }); 12893 instructionBuffer += ';'; 12894 12895 // Append to Blob (creating a new Blob in the process) 12896 if (instructionBuffer.length >= BLOCK_SIZE) { 12897 recordingBlob = new Blob([recordingBlob, instructionBuffer]); 12898 instructionBuffer = ''; 12899 } 12900 12901 // Load parsed instruction into recording 12902 loadInstruction(opcode, args); 12903 12904 }; 12905 12906 // Report any errors encountered 12907 tunnel.onerror = function tunnelError(status) { 12908 errorEncountered = true; 12909 if (recording.onerror) 12910 recording.onerror(status.message); 12911 }; 12912 12913 tunnel.onstatechange = function tunnelStateChanged(state) { 12914 if (state === Guacamole.Tunnel.State.CLOSED) { 12915 12916 // Append any remaining instructions 12917 if (instructionBuffer.length) { 12918 recordingBlob = new Blob([recordingBlob, instructionBuffer]); 12919 instructionBuffer = ''; 12920 } 12921 12922 // Consider recording loaded if tunnel has closed without errors 12923 if (!errorEncountered) 12924 notifyLoaded(); 12925 } 12926 }; 12927 12928 } 12929 12930 /** 12931 * Converts the given absolute timestamp to a timestamp which is relative 12932 * to the first frame in the recording. 12933 * 12934 * @private 12935 * @param {!number} timestamp 12936 * The timestamp to convert to a relative timestamp. 12937 * 12938 * @returns {!number} 12939 * The difference in milliseconds between the given timestamp and the 12940 * first frame of the recording, or zero if no frames yet exist. 12941 */ 12942 var toRelativeTimestamp = function toRelativeTimestamp(timestamp) { 12943 12944 // If no frames yet exist, all timestamps are zero 12945 if (frames.length === 0) 12946 return 0; 12947 12948 // Calculate timestamp relative to first frame 12949 return timestamp - frames[0].timestamp; 12950 12951 }; 12952 12953 /** 12954 * Searches through the given region of frames for the frame having a 12955 * relative timestamp closest to the timestamp given. 12956 * 12957 * @private 12958 * @param {!number} minIndex 12959 * The index of the first frame in the region (the frame having the 12960 * smallest timestamp). 12961 * 12962 * @param {!number} maxIndex 12963 * The index of the last frame in the region (the frame having the 12964 * largest timestamp). 12965 * 12966 * @param {!number} timestamp 12967 * The relative timestamp to search for, where zero denotes the first 12968 * frame in the recording. 12969 * 12970 * @returns {!number} 12971 * The index of the frame having a relative timestamp closest to the 12972 * given value. 12973 */ 12974 var findFrame = function findFrame(minIndex, maxIndex, timestamp) { 12975 12976 // Do not search if the region contains only one element 12977 if (minIndex === maxIndex) 12978 return minIndex; 12979 12980 // Split search region into two halves 12981 var midIndex = Math.floor((minIndex + maxIndex) / 2); 12982 var midTimestamp = toRelativeTimestamp(frames[midIndex].timestamp); 12983 12984 // If timestamp is within lesser half, search again within that half 12985 if (timestamp < midTimestamp && midIndex > minIndex) 12986 return findFrame(minIndex, midIndex - 1, timestamp); 12987 12988 // If timestamp is within greater half, search again within that half 12989 if (timestamp > midTimestamp && midIndex < maxIndex) 12990 return findFrame(midIndex + 1, maxIndex, timestamp); 12991 12992 // Otherwise, we lucked out and found a frame with exactly the 12993 // desired timestamp 12994 return midIndex; 12995 12996 }; 12997 12998 /** 12999 * Replays the instructions associated with the given frame, sending those 13000 * instructions to the playback client. 13001 * 13002 * @private 13003 * @param {!number} index 13004 * The index of the frame within the frames array which should be 13005 * replayed. 13006 * 13007 * @param {function} callback 13008 * The callback to invoke once replay of the frame has completed. 13009 */ 13010 var replayFrame = function replayFrame(index, callback) { 13011 13012 var frame = frames[index]; 13013 13014 // Replay all instructions within the retrieved frame 13015 parseBlob(recordingBlob.slice(frame.start, frame.end), function handleInstruction(opcode, args) { 13016 playbackTunnel.receiveInstruction(opcode, args); 13017 }, function replayCompleted() { 13018 13019 // Store client state if frame is flagged as a keyframe 13020 if (frame.keyframe && !frame.clientState) { 13021 playbackClient.exportState(function storeClientState(state) { 13022 frame.clientState = new Blob([JSON.stringify(state)]); 13023 }); 13024 } 13025 13026 // Update state to correctly represent the current frame 13027 currentFrame = index; 13028 13029 if (callback) 13030 callback(); 13031 13032 }); 13033 13034 }; 13035 13036 /** 13037 * Moves the playback position to the given frame, resetting the state of 13038 * the playback client and replaying frames as necessary. The seek 13039 * operation will proceed asynchronously. If a seek operation is already in 13040 * progress, that seek is first aborted. The progress of the seek operation 13041 * can be observed through the onseek handler and the provided callback. 13042 * 13043 * @private 13044 * @param {!number} index 13045 * The index of the frame which should become the new playback 13046 * position. 13047 * 13048 * @param {function} callback 13049 * The callback to invoke once the seek operation has completed. 13050 * 13051 * @param {number} [nextRealTimestamp] 13052 * The timestamp of the point in time that the given frame should be 13053 * displayed, as would be returned by new Date().getTime(). If omitted, 13054 * the frame will be displayed as soon as possible. 13055 */ 13056 var seekToFrame = function seekToFrame(index, callback, nextRealTimestamp) { 13057 13058 // Abort any in-progress seek 13059 abortSeek(); 13060 13061 // Note that a new seek operation is in progress 13062 var thisSeek = activeSeek = { 13063 aborted : false 13064 }; 13065 13066 var startIndex = index; 13067 13068 // Replay any applicable incremental frames 13069 var continueReplay = function continueReplay() { 13070 13071 // Notify of changes in position 13072 if (recording.onseek && currentFrame > startIndex) { 13073 recording.onseek(toRelativeTimestamp(frames[currentFrame].timestamp), 13074 currentFrame - startIndex, index - startIndex); 13075 } 13076 13077 // Cancel seek if aborted 13078 if (thisSeek.aborted) 13079 return; 13080 13081 // If frames remain, replay the next frame 13082 if (currentFrame < index) 13083 replayFrame(currentFrame + 1, continueReplay); 13084 13085 // Otherwise, the seek operation is completed 13086 else 13087 callback(); 13088 13089 }; 13090 13091 // Continue replay after requested delay has elapsed, or 13092 // immediately if no delay was requested 13093 var continueAfterRequiredDelay = function continueAfterRequiredDelay() { 13094 var delay = nextRealTimestamp ? Math.max(nextRealTimestamp - new Date().getTime(), 0) : 0; 13095 if (delay) 13096 window.setTimeout(continueReplay, delay); 13097 else 13098 continueReplay(); 13099 }; 13100 13101 // Back up until startIndex represents current state 13102 for (; startIndex >= 0; startIndex--) { 13103 13104 var frame = frames[startIndex]; 13105 13106 // If we've reached the current frame, startIndex represents 13107 // current state by definition 13108 if (startIndex === currentFrame) 13109 break; 13110 13111 // If frame has associated absolute state, make that frame the 13112 // current state 13113 if (frame.clientState) { 13114 frame.clientState.text().then(function textReady(text) { 13115 playbackClient.importState(JSON.parse(text)); 13116 currentFrame = startIndex; 13117 continueAfterRequiredDelay(); 13118 }); 13119 return; 13120 } 13121 13122 } 13123 13124 continueAfterRequiredDelay(); 13125 13126 }; 13127 13128 /** 13129 * Aborts the seek operation currently in progress, if any. If no seek 13130 * operation is in progress, this function has no effect. 13131 * 13132 * @private 13133 */ 13134 var abortSeek = function abortSeek() { 13135 if (activeSeek) { 13136 activeSeek.aborted = true; 13137 activeSeek = null; 13138 } 13139 }; 13140 13141 /** 13142 * Advances playback to the next frame in the frames array and schedules 13143 * playback of the frame following that frame based on their associated 13144 * timestamps. If no frames exist after the next frame, playback is paused. 13145 * 13146 * @private 13147 */ 13148 var continuePlayback = function continuePlayback() { 13149 13150 // If frames remain after advancing, schedule next frame 13151 if (currentFrame + 1 < frames.length) { 13152 13153 // Pull the upcoming frame 13154 var next = frames[currentFrame + 1]; 13155 13156 // Calculate the real timestamp corresponding to when the next 13157 // frame begins 13158 var nextRealTimestamp = next.timestamp - startVideoTimestamp + startRealTimestamp; 13159 13160 // Advance to next frame after enough time has elapsed 13161 seekToFrame(currentFrame + 1, function frameDelayElapsed() { 13162 continuePlayback(); 13163 }, nextRealTimestamp); 13164 13165 } 13166 13167 // Otherwise stop playback 13168 else 13169 recording.pause(); 13170 13171 }; 13172 13173 /** 13174 * Fired when loading of this recording has completed and all frames 13175 * are available. 13176 * 13177 * @event 13178 */ 13179 this.onload = null; 13180 13181 /** 13182 * Fired when an error occurs which prevents the recording from being 13183 * played back. 13184 * 13185 * @event 13186 * @param {!string} message 13187 * A human-readable message describing the error that occurred. 13188 */ 13189 this.onerror = null; 13190 13191 /** 13192 * Fired when further loading of this recording has been explicitly 13193 * aborted through a call to abort(). 13194 * 13195 * @event 13196 */ 13197 this.onabort = null; 13198 13199 /** 13200 * Fired when new frames have become available while the recording is 13201 * being downloaded. 13202 * 13203 * @event 13204 * @param {!number} duration 13205 * The new duration of the recording, in milliseconds. 13206 * 13207 * @param {!number} parsedSize 13208 * The number of bytes that have been loaded/parsed. 13209 */ 13210 this.onprogress = null; 13211 13212 /** 13213 * Fired whenever playback of the recording has started. 13214 * 13215 * @event 13216 */ 13217 this.onplay = null; 13218 13219 /** 13220 * Fired whenever playback of the recording has been paused. This may 13221 * happen when playback is explicitly paused with a call to pause(), or 13222 * when playback is implicitly paused due to reaching the end of the 13223 * recording. 13224 * 13225 * @event 13226 */ 13227 this.onpause = null; 13228 13229 /** 13230 * Fired whenever the playback position within the recording changes. 13231 * 13232 * @event 13233 * @param {!number} position 13234 * The new position within the recording, in milliseconds. 13235 * 13236 * @param {!number} current 13237 * The number of frames that have been seeked through. If not 13238 * seeking through multiple frames due to a call to seek(), this 13239 * will be 1. 13240 * 13241 * @param {!number} total 13242 * The number of frames that are being seeked through in the 13243 * current seek operation. If not seeking through multiple frames 13244 * due to a call to seek(), this will be 1. 13245 */ 13246 this.onseek = null; 13247 13248 /** 13249 * Connects the underlying tunnel, beginning download of the Guacamole 13250 * session. Playback of the Guacamole session cannot occur until at least 13251 * one frame worth of instructions has been downloaded. If the underlying 13252 * recording source is a Blob, this function has no effect. 13253 * 13254 * @param {string} [data] 13255 * The data to send to the tunnel when connecting. 13256 */ 13257 this.connect = function connect(data) { 13258 if (tunnel) 13259 tunnel.connect(data); 13260 }; 13261 13262 /** 13263 * Disconnects the underlying tunnel, stopping further download of the 13264 * Guacamole session. If the underlying recording source is a Blob, this 13265 * function has no effect. 13266 */ 13267 this.disconnect = function disconnect() { 13268 if (tunnel) 13269 tunnel.disconnect(); 13270 }; 13271 13272 /** 13273 * Aborts the loading process, stopping further processing of the 13274 * provided data. If the underlying recording source is a Guacamole tunnel, 13275 * it will be disconnected. 13276 */ 13277 this.abort = function abort() { 13278 if (!aborted) { 13279 13280 aborted = true; 13281 if (recording.onabort) 13282 recording.onabort(); 13283 13284 if (tunnel) 13285 tunnel.disconnect(); 13286 13287 } 13288 }; 13289 13290 /** 13291 * Returns the underlying display of the Guacamole.Client used by this 13292 * Guacamole.SessionRecording for playback. The display contains an Element 13293 * which can be added to the DOM, causing the display (and thus playback of 13294 * the recording) to become visible. 13295 * 13296 * @return {!Guacamole.Display} 13297 * The underlying display of the Guacamole.Client used by this 13298 * Guacamole.SessionRecording for playback. 13299 */ 13300 this.getDisplay = function getDisplay() { 13301 return playbackClient.getDisplay(); 13302 }; 13303 13304 /** 13305 * Returns whether playback is currently in progress. 13306 * 13307 * @returns {!boolean} 13308 * true if playback is currently in progress, false otherwise. 13309 */ 13310 this.isPlaying = function isPlaying() { 13311 return !!startVideoTimestamp; 13312 }; 13313 13314 /** 13315 * Returns the current playback position within the recording, in 13316 * milliseconds, where zero is the start of the recording. 13317 * 13318 * @returns {!number} 13319 * The current playback position within the recording, in milliseconds. 13320 */ 13321 this.getPosition = function getPosition() { 13322 13323 // Position is simply zero if playback has not started at all 13324 if (currentFrame === -1) 13325 return 0; 13326 13327 // Return current position as a millisecond timestamp relative to the 13328 // start of the recording 13329 return toRelativeTimestamp(frames[currentFrame].timestamp); 13330 13331 }; 13332 13333 /** 13334 * Returns the duration of this recording, in milliseconds. If the 13335 * recording is still being downloaded, this value will gradually increase. 13336 * 13337 * @returns {!number} 13338 * The duration of this recording, in milliseconds. 13339 */ 13340 this.getDuration = function getDuration() { 13341 13342 // If no frames yet exist, duration is zero 13343 if (frames.length === 0) 13344 return 0; 13345 13346 // Recording duration is simply the timestamp of the last frame 13347 return toRelativeTimestamp(frames[frames.length - 1].timestamp); 13348 13349 }; 13350 13351 /** 13352 * Begins continuous playback of the recording downloaded thus far. 13353 * Playback of the recording will continue until pause() is invoked or 13354 * until no further frames exist. Playback is initially paused when a 13355 * Guacamole.SessionRecording is created, and must be explicitly started 13356 * through a call to this function. If playback is already in progress, 13357 * this function has no effect. If a seek operation is in progress, 13358 * playback resumes at the current position, and the seek is aborted as if 13359 * completed. 13360 */ 13361 this.play = function play() { 13362 13363 // If playback is not already in progress and frames remain, 13364 // begin playback 13365 if (!recording.isPlaying() && currentFrame + 1 < frames.length) { 13366 13367 // Notify that playback is starting 13368 if (recording.onplay) 13369 recording.onplay(); 13370 13371 // Store timestamp of playback start for relative scheduling of 13372 // future frames 13373 var next = frames[currentFrame + 1]; 13374 startVideoTimestamp = next.timestamp; 13375 startRealTimestamp = new Date().getTime(); 13376 13377 // Begin playback of video 13378 continuePlayback(); 13379 13380 } 13381 13382 }; 13383 13384 /** 13385 * Seeks to the given position within the recording. If the recording is 13386 * currently being played back, playback will continue after the seek is 13387 * performed. If the recording is currently paused, playback will be 13388 * paused after the seek is performed. If a seek operation is already in 13389 * progress, that seek is first aborted. The seek operation will proceed 13390 * asynchronously. 13391 * 13392 * @param {!number} position 13393 * The position within the recording to seek to, in milliseconds. 13394 * 13395 * @param {function} [callback] 13396 * The callback to invoke once the seek operation has completed. 13397 */ 13398 this.seek = function seek(position, callback) { 13399 13400 // Do not seek if no frames exist 13401 if (frames.length === 0) 13402 return; 13403 13404 // Abort active seek operation, if any 13405 recording.cancel(); 13406 13407 // Pause playback, preserving playback state 13408 var originallyPlaying = recording.isPlaying(); 13409 recording.pause(); 13410 13411 // Restore playback when seek is completed or cancelled 13412 seekCallback = function restorePlaybackState() { 13413 13414 // Seek is no longer in progress 13415 seekCallback = null; 13416 13417 // Restore playback state 13418 if (originallyPlaying) { 13419 recording.play(); 13420 originallyPlaying = null; 13421 } 13422 13423 // Notify that seek has completed 13424 if (callback) 13425 callback(); 13426 13427 }; 13428 13429 // Perform seek 13430 seekToFrame(findFrame(0, frames.length - 1, position), seekCallback); 13431 13432 }; 13433 13434 /** 13435 * Cancels the current seek operation, setting the current frame of the 13436 * recording to wherever the seek operation was able to reach prior to 13437 * being cancelled. If a callback was provided to seek(), that callback 13438 * is invoked. If a seek operation is not currently underway, this 13439 * function has no effect. 13440 */ 13441 this.cancel = function cancel() { 13442 if (seekCallback) { 13443 abortSeek(); 13444 seekCallback(); 13445 } 13446 }; 13447 13448 /** 13449 * Pauses playback of the recording, if playback is currently in progress. 13450 * If playback is not in progress, this function has no effect. If a seek 13451 * operation is in progress, the seek is aborted. Playback is initially 13452 * paused when a Guacamole.SessionRecording is created, and must be 13453 * explicitly started through a call to play(). 13454 */ 13455 this.pause = function pause() { 13456 13457 // Abort any in-progress seek / playback 13458 abortSeek(); 13459 13460 // Stop playback only if playback is in progress 13461 if (recording.isPlaying()) { 13462 13463 // Notify that playback is stopping 13464 if (recording.onpause) 13465 recording.onpause(); 13466 13467 // Playback is stopped 13468 startVideoTimestamp = null; 13469 startRealTimestamp = null; 13470 13471 } 13472 13473 }; 13474 13475}; 13476 13477/** 13478 * A single frame of Guacamole session data. Each frame is made up of the set 13479 * of instructions used to generate that frame, and the timestamp as dictated 13480 * by the "sync" instruction terminating the frame. Optionally, a frame may 13481 * also be associated with a snapshot of Guacamole client state, such that the 13482 * frame can be rendered without replaying all previous frames. 13483 * 13484 * @private 13485 * @constructor 13486 * @param {!number} timestamp 13487 * The timestamp of this frame, as dictated by the "sync" instruction which 13488 * terminates the frame. 13489 * 13490 * @param {!number} start 13491 * The byte offset within the blob of the first character of the first 13492 * instruction of this frame. 13493 * 13494 * @param {!number} end 13495 * The byte offset within the blob of character which follows the last 13496 * character of the last instruction of this frame. 13497 */ 13498Guacamole.SessionRecording._Frame = function _Frame(timestamp, start, end) { 13499 13500 /** 13501 * Whether this frame should be used as a keyframe if possible. This value 13502 * is purely advisory. The stored clientState must eventually be manually 13503 * set for the frame to be used as a keyframe. By default, frames are not 13504 * keyframes. 13505 * 13506 * @type {!boolean} 13507 * @default false 13508 */ 13509 this.keyframe = false; 13510 13511 /** 13512 * The timestamp of this frame, as dictated by the "sync" instruction which 13513 * terminates the frame. 13514 * 13515 * @type {!number} 13516 */ 13517 this.timestamp = timestamp; 13518 13519 /** 13520 * The byte offset within the blob of the first character of the first 13521 * instruction of this frame. 13522 * 13523 * @type {!number} 13524 */ 13525 this.start = start; 13526 13527 /** 13528 * The byte offset within the blob of character which follows the last 13529 * character of the last instruction of this frame. 13530 * 13531 * @type {!number} 13532 */ 13533 this.end = end; 13534 13535 /** 13536 * A snapshot of client state after this frame was rendered, as returned by 13537 * a call to exportState(), serialized as JSON, and stored within a Blob. 13538 * Use of Blobs here is required to ensure the browser can make use of 13539 * larger disk-backed storage if the size of the recording is large. If no 13540 * such snapshot has been taken, this will be null. 13541 * 13542 * @type {Blob} 13543 * @default null 13544 */ 13545 this.clientState = null; 13546 13547}; 13548 13549/** 13550 * A read-only Guacamole.Tunnel implementation which streams instructions 13551 * received through explicit calls to its receiveInstruction() function. 13552 * 13553 * @private 13554 * @constructor 13555 * @augments {Guacamole.Tunnel} 13556 */ 13557Guacamole.SessionRecording._PlaybackTunnel = function _PlaybackTunnel() { 13558 13559 /** 13560 * Reference to this Guacamole.SessionRecording._PlaybackTunnel. 13561 * 13562 * @private 13563 * @type {!Guacamole.SessionRecording._PlaybackTunnel} 13564 */ 13565 var tunnel = this; 13566 13567 this.connect = function connect(data) { 13568 // Do nothing 13569 }; 13570 13571 this.sendMessage = function sendMessage(elements) { 13572 // Do nothing 13573 }; 13574 13575 this.disconnect = function disconnect() { 13576 // Do nothing 13577 }; 13578 13579 /** 13580 * Invokes this tunnel's oninstruction handler, notifying users of this 13581 * tunnel (such as a Guacamole.Client instance) that an instruction has 13582 * been received. If the oninstruction handler has not been set, this 13583 * function has no effect. 13584 * 13585 * @param {!string} opcode 13586 * The opcode of the Guacamole instruction. 13587 * 13588 * @param {!string[]} args 13589 * All arguments associated with this Guacamole instruction. 13590 */ 13591 this.receiveInstruction = function receiveInstruction(opcode, args) { 13592 if (tunnel.oninstruction) 13593 tunnel.oninstruction(opcode, args); 13594 }; 13595 13596};/* 13597 * Licensed to the Apache Software Foundation (ASF) under one 13598 * or more contributor license agreements. See the NOTICE file 13599 * distributed with this work for additional information 13600 * regarding copyright ownership. The ASF licenses this file 13601 * to you under the Apache License, Version 2.0 (the 13602 * "License"); you may not use this file except in compliance 13603 * with the License. You may obtain a copy of the License at 13604 * 13605 * http://www.apache.org/licenses/LICENSE-2.0 13606 * 13607 * Unless required by applicable law or agreed to in writing, 13608 * software distributed under the License is distributed on an 13609 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 13610 * KIND, either express or implied. See the License for the 13611 * specific language governing permissions and limitations 13612 * under the License. 13613 */ 13614 13615var Guacamole = Guacamole || {}; 13616 13617/** 13618 * A Guacamole status. Each Guacamole status consists of a status code, defined 13619 * by the protocol, and an optional human-readable message, usually only 13620 * included for debugging convenience. 13621 * 13622 * @constructor 13623 * @param {!number} code 13624 * The Guacamole status code, as defined by Guacamole.Status.Code. 13625 * 13626 * @param {string} [message] 13627 * An optional human-readable message. 13628 */ 13629Guacamole.Status = function(code, message) { 13630 13631 /** 13632 * Reference to this Guacamole.Status. 13633 * 13634 * @private 13635 * @type {!Guacamole.Status} 13636 */ 13637 var guac_status = this; 13638 13639 /** 13640 * The Guacamole status code. 13641 * 13642 * @see Guacamole.Status.Code 13643 * @type {!number} 13644 */ 13645 this.code = code; 13646 13647 /** 13648 * An arbitrary human-readable message associated with this status, if any. 13649 * The human-readable message is not required, and is generally provided 13650 * for debugging purposes only. For user feedback, it is better to translate 13651 * the Guacamole status code into a message. 13652 * 13653 * @type {string} 13654 */ 13655 this.message = message; 13656 13657 /** 13658 * Returns whether this status represents an error. 13659 * 13660 * @returns {!boolean} 13661 * true if this status represents an error, false otherwise. 13662 */ 13663 this.isError = function() { 13664 return guac_status.code < 0 || guac_status.code > 0x00FF; 13665 }; 13666 13667}; 13668 13669/** 13670 * Enumeration of all Guacamole status codes. 13671 */ 13672Guacamole.Status.Code = { 13673 13674 /** 13675 * The operation succeeded. 13676 * 13677 * @type {!number} 13678 */ 13679 "SUCCESS": 0x0000, 13680 13681 /** 13682 * The requested operation is unsupported. 13683 * 13684 * @type {!number} 13685 */ 13686 "UNSUPPORTED": 0x0100, 13687 13688 /** 13689 * The operation could not be performed due to an internal failure. 13690 * 13691 * @type {!number} 13692 */ 13693 "SERVER_ERROR": 0x0200, 13694 13695 /** 13696 * The operation could not be performed as the server is busy. 13697 * 13698 * @type {!number} 13699 */ 13700 "SERVER_BUSY": 0x0201, 13701 13702 /** 13703 * The operation could not be performed because the upstream server is not 13704 * responding. 13705 * 13706 * @type {!number} 13707 */ 13708 "UPSTREAM_TIMEOUT": 0x0202, 13709 13710 /** 13711 * The operation was unsuccessful due to an error or otherwise unexpected 13712 * condition of the upstream server. 13713 * 13714 * @type {!number} 13715 */ 13716 "UPSTREAM_ERROR": 0x0203, 13717 13718 /** 13719 * The operation could not be performed as the requested resource does not 13720 * exist. 13721 * 13722 * @type {!number} 13723 */ 13724 "RESOURCE_NOT_FOUND": 0x0204, 13725 13726 /** 13727 * The operation could not be performed as the requested resource is 13728 * already in use. 13729 * 13730 * @type {!number} 13731 */ 13732 "RESOURCE_CONFLICT": 0x0205, 13733 13734 /** 13735 * The operation could not be performed as the requested resource is now 13736 * closed. 13737 * 13738 * @type {!number} 13739 */ 13740 "RESOURCE_CLOSED": 0x0206, 13741 13742 /** 13743 * The operation could not be performed because the upstream server does 13744 * not appear to exist. 13745 * 13746 * @type {!number} 13747 */ 13748 "UPSTREAM_NOT_FOUND": 0x0207, 13749 13750 /** 13751 * The operation could not be performed because the upstream server is not 13752 * available to service the request. 13753 * 13754 * @type {!number} 13755 */ 13756 "UPSTREAM_UNAVAILABLE": 0x0208, 13757 13758 /** 13759 * The session within the upstream server has ended because it conflicted 13760 * with another session. 13761 * 13762 * @type {!number} 13763 */ 13764 "SESSION_CONFLICT": 0x0209, 13765 13766 /** 13767 * The session within the upstream server has ended because it appeared to 13768 * be inactive. 13769 * 13770 * @type {!number} 13771 */ 13772 "SESSION_TIMEOUT": 0x020A, 13773 13774 /** 13775 * The session within the upstream server has been forcibly terminated. 13776 * 13777 * @type {!number} 13778 */ 13779 "SESSION_CLOSED": 0x020B, 13780 13781 /** 13782 * The operation could not be performed because bad parameters were given. 13783 * 13784 * @type {!number} 13785 */ 13786 "CLIENT_BAD_REQUEST": 0x0300, 13787 13788 /** 13789 * Permission was denied to perform the operation, as the user is not yet 13790 * authorized (not yet logged in, for example). 13791 * 13792 * @type {!number} 13793 */ 13794 "CLIENT_UNAUTHORIZED": 0x0301, 13795 13796 /** 13797 * Permission was denied to perform the operation, and this permission will 13798 * not be granted even if the user is authorized. 13799 * 13800 * @type {!number} 13801 */ 13802 "CLIENT_FORBIDDEN": 0x0303, 13803 13804 /** 13805 * The client took too long to respond. 13806 * 13807 * @type {!number} 13808 */ 13809 "CLIENT_TIMEOUT": 0x0308, 13810 13811 /** 13812 * The client sent too much data. 13813 * 13814 * @type {!number} 13815 */ 13816 "CLIENT_OVERRUN": 0x030D, 13817 13818 /** 13819 * The client sent data of an unsupported or unexpected type. 13820 * 13821 * @type {!number} 13822 */ 13823 "CLIENT_BAD_TYPE": 0x030F, 13824 13825 /** 13826 * The operation failed because the current client is already using too 13827 * many resources. 13828 * 13829 * @type {!number} 13830 */ 13831 "CLIENT_TOO_MANY": 0x031D 13832 13833}; 13834 13835/** 13836 * Returns the Guacamole protocol status code which most closely 13837 * represents the given HTTP status code. 13838 * 13839 * @param {!number} status 13840 * The HTTP status code to translate into a Guacamole protocol status 13841 * code. 13842 * 13843 * @returns {!number} 13844 * The Guacamole protocol status code which most closely represents the 13845 * given HTTP status code. 13846 */ 13847Guacamole.Status.Code.fromHTTPCode = function fromHTTPCode(status) { 13848 13849 // Translate status codes with known equivalents 13850 switch (status) { 13851 13852 // HTTP 400 - Bad request 13853 case 400: 13854 return Guacamole.Status.Code.CLIENT_BAD_REQUEST; 13855 13856 // HTTP 403 - Forbidden 13857 case 403: 13858 return Guacamole.Status.Code.CLIENT_FORBIDDEN; 13859 13860 // HTTP 404 - Resource not found 13861 case 404: 13862 return Guacamole.Status.Code.RESOURCE_NOT_FOUND; 13863 13864 // HTTP 429 - Too many requests 13865 case 429: 13866 return Guacamole.Status.Code.CLIENT_TOO_MANY; 13867 13868 // HTTP 503 - Server unavailable 13869 case 503: 13870 return Guacamole.Status.Code.SERVER_BUSY; 13871 13872 } 13873 13874 // Default all other codes to generic internal error 13875 return Guacamole.Status.Code.SERVER_ERROR; 13876 13877}; 13878 13879/** 13880 * Returns the Guacamole protocol status code which most closely 13881 * represents the given WebSocket status code. 13882 * 13883 * @param {!number} code 13884 * The WebSocket status code to translate into a Guacamole protocol 13885 * status code. 13886 * 13887 * @returns {!number} 13888 * The Guacamole protocol status code which most closely represents the 13889 * given WebSocket status code. 13890 */ 13891Guacamole.Status.Code.fromWebSocketCode = function fromWebSocketCode(code) { 13892 13893 // Translate status codes with known equivalents 13894 switch (code) { 13895 13896 // Successful disconnect (no error) 13897 case 1000: // Normal Closure 13898 return Guacamole.Status.Code.SUCCESS; 13899 13900 // Codes which indicate the server is not reachable 13901 case 1006: // Abnormal Closure (also signalled by JavaScript when the connection cannot be opened in the first place) 13902 case 1015: // TLS Handshake 13903 return Guacamole.Status.Code.UPSTREAM_NOT_FOUND; 13904 13905 // Codes which indicate the server is reachable but busy/unavailable 13906 case 1001: // Going Away 13907 case 1012: // Service Restart 13908 case 1013: // Try Again Later 13909 case 1014: // Bad Gateway 13910 return Guacamole.Status.Code.UPSTREAM_UNAVAILABLE; 13911 13912 } 13913 13914 // Default all other codes to generic internal error 13915 return Guacamole.Status.Code.SERVER_ERROR; 13916 13917}; 13918/* 13919 * Licensed to the Apache Software Foundation (ASF) under one 13920 * or more contributor license agreements. See the NOTICE file 13921 * distributed with this work for additional information 13922 * regarding copyright ownership. The ASF licenses this file 13923 * to you under the Apache License, Version 2.0 (the 13924 * "License"); you may not use this file except in compliance 13925 * with the License. You may obtain a copy of the License at 13926 * 13927 * http://www.apache.org/licenses/LICENSE-2.0 13928 * 13929 * Unless required by applicable law or agreed to in writing, 13930 * software distributed under the License is distributed on an 13931 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 13932 * KIND, either express or implied. See the License for the 13933 * specific language governing permissions and limitations 13934 * under the License. 13935 */ 13936 13937var Guacamole = Guacamole || {}; 13938 13939/** 13940 * A reader which automatically handles the given input stream, returning 13941 * strictly text data. Note that this object will overwrite any installed event 13942 * handlers on the given Guacamole.InputStream. 13943 * 13944 * @constructor 13945 * @param {!Guacamole.InputStream} stream 13946 * The stream that data will be read from. 13947 */ 13948Guacamole.StringReader = function(stream) { 13949 13950 /** 13951 * Reference to this Guacamole.InputStream. 13952 * 13953 * @private 13954 * @type {!Guacamole.StringReader} 13955 */ 13956 var guac_reader = this; 13957 13958 /** 13959 * Parser for received UTF-8 data. 13960 * 13961 * @type {!Guacamole.UTF8Parser} 13962 */ 13963 var utf8Parser = new Guacamole.UTF8Parser(); 13964 13965 /** 13966 * Wrapped Guacamole.ArrayBufferReader. 13967 * 13968 * @private 13969 * @type {!Guacamole.ArrayBufferReader} 13970 */ 13971 var array_reader = new Guacamole.ArrayBufferReader(stream); 13972 13973 // Receive blobs as strings 13974 array_reader.ondata = function(buffer) { 13975 13976 // Decode UTF-8 13977 var text = utf8Parser.decode(buffer); 13978 13979 // Call handler, if present 13980 if (guac_reader.ontext) 13981 guac_reader.ontext(text); 13982 13983 }; 13984 13985 // Simply call onend when end received 13986 array_reader.onend = function() { 13987 if (guac_reader.onend) 13988 guac_reader.onend(); 13989 }; 13990 13991 /** 13992 * Fired once for every blob of text data received. 13993 * 13994 * @event 13995 * @param {!string} text 13996 * The data packet received. 13997 */ 13998 this.ontext = null; 13999 14000 /** 14001 * Fired once this stream is finished and no further data will be written. 14002 * @event 14003 */ 14004 this.onend = null; 14005 14006};/* 14007 * Licensed to the Apache Software Foundation (ASF) under one 14008 * or more contributor license agreements. See the NOTICE file 14009 * distributed with this work for additional information 14010 * regarding copyright ownership. The ASF licenses this file 14011 * to you under the Apache License, Version 2.0 (the 14012 * "License"); you may not use this file except in compliance 14013 * with the License. You may obtain a copy of the License at 14014 * 14015 * http://www.apache.org/licenses/LICENSE-2.0 14016 * 14017 * Unless required by applicable law or agreed to in writing, 14018 * software distributed under the License is distributed on an 14019 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14020 * KIND, either express or implied. See the License for the 14021 * specific language governing permissions and limitations 14022 * under the License. 14023 */ 14024 14025var Guacamole = Guacamole || {}; 14026 14027/** 14028 * A writer which automatically writes to the given output stream with text 14029 * data. 14030 * 14031 * @constructor 14032 * @param {!Guacamole.OutputStream} stream 14033 * The stream that data will be written to. 14034 */ 14035Guacamole.StringWriter = function(stream) { 14036 14037 /** 14038 * Reference to this Guacamole.StringWriter. 14039 * 14040 * @private 14041 * @type {!Guacamole.StringWriter} 14042 */ 14043 var guac_writer = this; 14044 14045 /** 14046 * Wrapped Guacamole.ArrayBufferWriter. 14047 * 14048 * @private 14049 * @type {!Guacamole.ArrayBufferWriter} 14050 */ 14051 var array_writer = new Guacamole.ArrayBufferWriter(stream); 14052 14053 /** 14054 * Internal buffer for UTF-8 output. 14055 * 14056 * @private 14057 * @type {!Uint8Array} 14058 */ 14059 var buffer = new Uint8Array(8192); 14060 14061 /** 14062 * The number of bytes currently in the buffer. 14063 * 14064 * @private 14065 * @type {!number} 14066 */ 14067 var length = 0; 14068 14069 // Simply call onack for acknowledgements 14070 array_writer.onack = function(status) { 14071 if (guac_writer.onack) 14072 guac_writer.onack(status); 14073 }; 14074 14075 /** 14076 * Expands the size of the underlying buffer by the given number of bytes, 14077 * updating the length appropriately. 14078 * 14079 * @private 14080 * @param {!number} bytes 14081 * The number of bytes to add to the underlying buffer. 14082 */ 14083 function __expand(bytes) { 14084 14085 // Resize buffer if more space needed 14086 if (length+bytes >= buffer.length) { 14087 var new_buffer = new Uint8Array((length+bytes)*2); 14088 new_buffer.set(buffer); 14089 buffer = new_buffer; 14090 } 14091 14092 length += bytes; 14093 14094 } 14095 14096 /** 14097 * Appends a single Unicode character to the current buffer, resizing the 14098 * buffer if necessary. The character will be encoded as UTF-8. 14099 * 14100 * @private 14101 * @param {!number} codepoint 14102 * The codepoint of the Unicode character to append. 14103 */ 14104 function __append_utf8(codepoint) { 14105 14106 var mask; 14107 var bytes; 14108 14109 // 1 byte 14110 if (codepoint <= 0x7F) { 14111 mask = 0x00; 14112 bytes = 1; 14113 } 14114 14115 // 2 byte 14116 else if (codepoint <= 0x7FF) { 14117 mask = 0xC0; 14118 bytes = 2; 14119 } 14120 14121 // 3 byte 14122 else if (codepoint <= 0xFFFF) { 14123 mask = 0xE0; 14124 bytes = 3; 14125 } 14126 14127 // 4 byte 14128 else if (codepoint <= 0x1FFFFF) { 14129 mask = 0xF0; 14130 bytes = 4; 14131 } 14132 14133 // If invalid codepoint, append replacement character 14134 else { 14135 __append_utf8(0xFFFD); 14136 return; 14137 } 14138 14139 // Offset buffer by size 14140 __expand(bytes); 14141 var offset = length - 1; 14142 14143 // Add trailing bytes, if any 14144 for (var i=1; i<bytes; i++) { 14145 buffer[offset--] = 0x80 | (codepoint & 0x3F); 14146 codepoint >>= 6; 14147 } 14148 14149 // Set initial byte 14150 buffer[offset] = mask | codepoint; 14151 14152 } 14153 14154 /** 14155 * Encodes the given string as UTF-8, returning an ArrayBuffer containing 14156 * the resulting bytes. 14157 * 14158 * @private 14159 * @param {!string} text 14160 * The string to encode as UTF-8. 14161 * 14162 * @return {!Uint8Array} 14163 * The encoded UTF-8 data. 14164 */ 14165 function __encode_utf8(text) { 14166 14167 // Fill buffer with UTF-8 14168 for (var i=0; i<text.length; i++) { 14169 var codepoint = text.charCodeAt(i); 14170 __append_utf8(codepoint); 14171 } 14172 14173 // Flush buffer 14174 if (length > 0) { 14175 var out_buffer = buffer.subarray(0, length); 14176 length = 0; 14177 return out_buffer; 14178 } 14179 14180 } 14181 14182 /** 14183 * Sends the given text. 14184 * 14185 * @param {!string} text 14186 * The text to send. 14187 */ 14188 this.sendText = function(text) { 14189 if (text.length) 14190 array_writer.sendData(__encode_utf8(text)); 14191 }; 14192 14193 /** 14194 * Signals that no further text will be sent, effectively closing the 14195 * stream. 14196 */ 14197 this.sendEnd = function() { 14198 array_writer.sendEnd(); 14199 }; 14200 14201 /** 14202 * Fired for received data, if acknowledged by the server. 14203 * 14204 * @event 14205 * @param {!Guacamole.Status} status 14206 * The status of the operation. 14207 */ 14208 this.onack = null; 14209 14210};/* 14211 * Licensed to the Apache Software Foundation (ASF) under one 14212 * or more contributor license agreements. See the NOTICE file 14213 * distributed with this work for additional information 14214 * regarding copyright ownership. The ASF licenses this file 14215 * to you under the Apache License, Version 2.0 (the 14216 * "License"); you may not use this file except in compliance 14217 * with the License. You may obtain a copy of the License at 14218 * 14219 * http://www.apache.org/licenses/LICENSE-2.0 14220 * 14221 * Unless required by applicable law or agreed to in writing, 14222 * software distributed under the License is distributed on an 14223 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14224 * KIND, either express or implied. See the License for the 14225 * specific language governing permissions and limitations 14226 * under the License. 14227 */ 14228 14229var Guacamole = Guacamole || {}; 14230 14231/** 14232 * Provides cross-browser multi-touch events for a given element. The events of 14233 * the given element are automatically populated with handlers that translate 14234 * touch events into a non-browser-specific event provided by the 14235 * Guacamole.Touch instance. 14236 * 14237 * @constructor 14238 * @augments Guacamole.Event.Target 14239 * @param {!Element} element 14240 * The Element to use to provide touch events. 14241 */ 14242Guacamole.Touch = function Touch(element) { 14243 14244 Guacamole.Event.Target.call(this); 14245 14246 /** 14247 * Reference to this Guacamole.Touch. 14248 * 14249 * @private 14250 * @type {!Guacamole.Touch} 14251 */ 14252 var guacTouch = this; 14253 14254 /** 14255 * The default X/Y radius of each touch if the device or browser does not 14256 * expose the size of the contact area. 14257 * 14258 * @private 14259 * @constant 14260 * @type {!number} 14261 */ 14262 var DEFAULT_CONTACT_RADIUS = Math.floor(16 * window.devicePixelRatio); 14263 14264 /** 14265 * The set of all active touches, stored by their unique identifiers. 14266 * 14267 * @type {!Object.<Number, Guacamole.Touch.State>} 14268 */ 14269 this.touches = {}; 14270 14271 /** 14272 * The number of active touches currently stored within 14273 * {@link Guacamole.Touch#touches touches}. 14274 */ 14275 this.activeTouches = 0; 14276 14277 /** 14278 * Fired whenever a new touch contact is initiated on the element 14279 * associated with this Guacamole.Touch. 14280 * 14281 * @event Guacamole.Touch#touchstart 14282 * @param {!Guacamole.Touch.Event} event 14283 * A {@link Guacamole.Touch.Event} object representing the "touchstart" 14284 * event. 14285 */ 14286 14287 /** 14288 * Fired whenever an established touch contact moves within the element 14289 * associated with this Guacamole.Touch. 14290 * 14291 * @event Guacamole.Touch#touchmove 14292 * @param {!Guacamole.Touch.Event} event 14293 * A {@link Guacamole.Touch.Event} object representing the "touchmove" 14294 * event. 14295 */ 14296 14297 /** 14298 * Fired whenever an established touch contact is lifted from the element 14299 * associated with this Guacamole.Touch. 14300 * 14301 * @event Guacamole.Touch#touchend 14302 * @param {!Guacamole.Touch.Event} event 14303 * A {@link Guacamole.Touch.Event} object representing the "touchend" 14304 * event. 14305 */ 14306 14307 element.addEventListener('touchstart', function touchstart(e) { 14308 14309 // Fire "ontouchstart" events for all new touches 14310 for (var i = 0; i < e.changedTouches.length; i++) { 14311 14312 var changedTouch = e.changedTouches[i]; 14313 var identifier = changedTouch.identifier; 14314 14315 // Ignore duplicated touches 14316 if (guacTouch.touches[identifier]) 14317 continue; 14318 14319 var touch = guacTouch.touches[identifier] = new Guacamole.Touch.State({ 14320 id : identifier, 14321 radiusX : changedTouch.radiusX || DEFAULT_CONTACT_RADIUS, 14322 radiusY : changedTouch.radiusY || DEFAULT_CONTACT_RADIUS, 14323 angle : changedTouch.angle || 0.0, 14324 force : changedTouch.force || 1.0 /* Within JavaScript changedTouch events, a force of 0.0 indicates the device does not support reporting changedTouch force */ 14325 }); 14326 14327 guacTouch.activeTouches++; 14328 14329 touch.fromClientPosition(element, changedTouch.clientX, changedTouch.clientY); 14330 guacTouch.dispatch(new Guacamole.Touch.Event('touchmove', e, touch)); 14331 14332 } 14333 14334 }, false); 14335 14336 element.addEventListener('touchmove', function touchstart(e) { 14337 14338 // Fire "ontouchmove" events for all updated touches 14339 for (var i = 0; i < e.changedTouches.length; i++) { 14340 14341 var changedTouch = e.changedTouches[i]; 14342 var identifier = changedTouch.identifier; 14343 14344 // Ignore any unrecognized touches 14345 var touch = guacTouch.touches[identifier]; 14346 if (!touch) 14347 continue; 14348 14349 // Update force only if supported by browser (otherwise, assume 14350 // force is unchanged) 14351 if (changedTouch.force) 14352 touch.force = changedTouch.force; 14353 14354 // Update touch area, if supported by browser and device 14355 touch.angle = changedTouch.angle || 0.0; 14356 touch.radiusX = changedTouch.radiusX || DEFAULT_CONTACT_RADIUS; 14357 touch.radiusY = changedTouch.radiusY || DEFAULT_CONTACT_RADIUS; 14358 14359 // Update with any change in position 14360 touch.fromClientPosition(element, changedTouch.clientX, changedTouch.clientY); 14361 guacTouch.dispatch(new Guacamole.Touch.Event('touchmove', e, touch)); 14362 14363 } 14364 14365 }, false); 14366 14367 element.addEventListener('touchend', function touchstart(e) { 14368 14369 // Fire "ontouchend" events for all updated touches 14370 for (var i = 0; i < e.changedTouches.length; i++) { 14371 14372 var changedTouch = e.changedTouches[i]; 14373 var identifier = changedTouch.identifier; 14374 14375 // Ignore any unrecognized touches 14376 var touch = guacTouch.touches[identifier]; 14377 if (!touch) 14378 continue; 14379 14380 // Stop tracking this particular touch 14381 delete guacTouch.touches[identifier]; 14382 guacTouch.activeTouches--; 14383 14384 // Touch has ended 14385 touch.force = 0.0; 14386 14387 // Update with final position 14388 touch.fromClientPosition(element, changedTouch.clientX, changedTouch.clientY); 14389 guacTouch.dispatch(new Guacamole.Touch.Event('touchend', e, touch)); 14390 14391 } 14392 14393 }, false); 14394 14395}; 14396 14397/** 14398 * The current state of a touch contact. 14399 * 14400 * @constructor 14401 * @augments Guacamole.Position 14402 * @param {Guacamole.Touch.State|object} [template={}] 14403 * The object whose properties should be copied within the new 14404 * Guacamole.Touch.State. 14405 */ 14406Guacamole.Touch.State = function State(template) { 14407 14408 template = template || {}; 14409 14410 Guacamole.Position.call(this, template); 14411 14412 /** 14413 * An arbitrary integer ID which uniquely identifies this contact relative 14414 * to other active contacts. 14415 * 14416 * @type {!number} 14417 * @default 0 14418 */ 14419 this.id = template.id || 0; 14420 14421 /** 14422 * The Y radius of the ellipse covering the general area of the touch 14423 * contact, in pixels. 14424 * 14425 * @type {!number} 14426 * @default 0 14427 */ 14428 this.radiusX = template.radiusX || 0; 14429 14430 /** 14431 * The X radius of the ellipse covering the general area of the touch 14432 * contact, in pixels. 14433 * 14434 * @type {!number} 14435 * @default 0 14436 */ 14437 this.radiusY = template.radiusY || 0; 14438 14439 /** 14440 * The rough angle of clockwise rotation of the general area of the touch 14441 * contact, in degrees. 14442 * 14443 * @type {!number} 14444 * @default 0.0 14445 */ 14446 this.angle = template.angle || 0.0; 14447 14448 /** 14449 * The relative force exerted by the touch contact, where 0 is no force 14450 * (the touch has been lifted) and 1 is maximum force (the maximum amount 14451 * of force representable by the device). 14452 * 14453 * @type {!number} 14454 * @default 1.0 14455 */ 14456 this.force = template.force || 1.0; 14457 14458}; 14459 14460/** 14461 * An event which represents a change in state of a single touch contact, 14462 * including the creation or removal of that contact. If multiple contacts are 14463 * involved in a touch interaction, each contact will be associated with its 14464 * own event. 14465 * 14466 * @constructor 14467 * @augments Guacamole.Event.DOMEvent 14468 * @param {!string} type 14469 * The name of the touch event type. Possible values are "touchstart", 14470 * "touchmove", and "touchend". 14471 * 14472 * @param {!TouchEvent} event 14473 * The DOM touch event that produced this Guacamole.Touch.Event. 14474 * 14475 * @param {!Guacamole.Touch.State} state 14476 * The state of the touch contact associated with this event. 14477 */ 14478Guacamole.Touch.Event = function TouchEvent(type, event, state) { 14479 14480 Guacamole.Event.DOMEvent.call(this, type, [ event ]); 14481 14482 /** 14483 * The state of the touch contact associated with this event. 14484 * 14485 * @type {!Guacamole.Touch.State} 14486 */ 14487 this.state = state; 14488 14489}; 14490/* 14491 * Licensed to the Apache Software Foundation (ASF) under one 14492 * or more contributor license agreements. See the NOTICE file 14493 * distributed with this work for additional information 14494 * regarding copyright ownership. The ASF licenses this file 14495 * to you under the Apache License, Version 2.0 (the 14496 * "License"); you may not use this file except in compliance 14497 * with the License. You may obtain a copy of the License at 14498 * 14499 * http://www.apache.org/licenses/LICENSE-2.0 14500 * 14501 * Unless required by applicable law or agreed to in writing, 14502 * software distributed under the License is distributed on an 14503 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14504 * KIND, either express or implied. See the License for the 14505 * specific language governing permissions and limitations 14506 * under the License. 14507 */ 14508 14509var Guacamole = Guacamole || {}; 14510 14511/** 14512 * Core object providing abstract communication for Guacamole. This object 14513 * is a null implementation whose functions do nothing. Guacamole applications 14514 * should use {@link Guacamole.HTTPTunnel} instead, or implement their own tunnel based 14515 * on this one. 14516 * 14517 * @constructor 14518 * @see Guacamole.HTTPTunnel 14519 */ 14520Guacamole.Tunnel = function() { 14521 14522 /** 14523 * Connect to the tunnel with the given optional data. This data is 14524 * typically used for authentication. The format of data accepted is 14525 * up to the tunnel implementation. 14526 * 14527 * @param {string} [data] 14528 * The data to send to the tunnel when connecting. 14529 */ 14530 this.connect = function(data) {}; 14531 14532 /** 14533 * Disconnect from the tunnel. 14534 */ 14535 this.disconnect = function() {}; 14536 14537 /** 14538 * Send the given message through the tunnel to the service on the other 14539 * side. All messages are guaranteed to be received in the order sent. 14540 * 14541 * @param {...*} elements 14542 * The elements of the message to send to the service on the other side 14543 * of the tunnel. 14544 */ 14545 this.sendMessage = function(elements) {}; 14546 14547 /** 14548 * Changes the stored numeric state of this tunnel, firing the onstatechange 14549 * event if the new state is different and a handler has been defined. 14550 * 14551 * @private 14552 * @param {!number} state 14553 * The new state of this tunnel. 14554 */ 14555 this.setState = function(state) { 14556 14557 // Notify only if state changes 14558 if (state !== this.state) { 14559 this.state = state; 14560 if (this.onstatechange) 14561 this.onstatechange(state); 14562 } 14563 14564 }; 14565 14566 /** 14567 * Changes the stored UUID that uniquely identifies this tunnel, firing the 14568 * onuuid event if a handler has been defined. 14569 * 14570 * @private 14571 * @param {string} uuid 14572 * The new state of this tunnel. 14573 */ 14574 this.setUUID = function setUUID(uuid) { 14575 this.uuid = uuid; 14576 if (this.onuuid) 14577 this.onuuid(uuid); 14578 }; 14579 14580 /** 14581 * Returns whether this tunnel is currently connected. 14582 * 14583 * @returns {!boolean} 14584 * true if this tunnel is currently connected, false otherwise. 14585 */ 14586 this.isConnected = function isConnected() { 14587 return this.state === Guacamole.Tunnel.State.OPEN 14588 || this.state === Guacamole.Tunnel.State.UNSTABLE; 14589 }; 14590 14591 /** 14592 * The current state of this tunnel. 14593 * 14594 * @type {!number} 14595 */ 14596 this.state = Guacamole.Tunnel.State.CLOSED; 14597 14598 /** 14599 * The maximum amount of time to wait for data to be received, in 14600 * milliseconds. If data is not received within this amount of time, 14601 * the tunnel is closed with an error. The default value is 15000. 14602 * 14603 * @type {!number} 14604 */ 14605 this.receiveTimeout = 15000; 14606 14607 /** 14608 * The amount of time to wait for data to be received before considering 14609 * the connection to be unstable, in milliseconds. If data is not received 14610 * within this amount of time, the tunnel status is updated to warn that 14611 * the connection appears unresponsive and may close. The default value is 14612 * 1500. 14613 * 14614 * @type {!number} 14615 */ 14616 this.unstableThreshold = 1500; 14617 14618 /** 14619 * The UUID uniquely identifying this tunnel. If not yet known, this will 14620 * be null. 14621 * 14622 * @type {string} 14623 */ 14624 this.uuid = null; 14625 14626 /** 14627 * Fired when the UUID that uniquely identifies this tunnel is known. 14628 * 14629 * @event 14630 * @param {!string} 14631 * The UUID uniquely identifying this tunnel. 14632 */ 14633 this.onuuid = null; 14634 14635 /** 14636 * Fired whenever an error is encountered by the tunnel. 14637 * 14638 * @event 14639 * @param {!Guacamole.Status} status 14640 * A status object which describes the error. 14641 */ 14642 this.onerror = null; 14643 14644 /** 14645 * Fired whenever the state of the tunnel changes. 14646 * 14647 * @event 14648 * @param {!number} state 14649 * The new state of the client. 14650 */ 14651 this.onstatechange = null; 14652 14653 /** 14654 * Fired once for every complete Guacamole instruction received, in order. 14655 * 14656 * @event 14657 * @param {!string} opcode 14658 * The Guacamole instruction opcode. 14659 * 14660 * @param {!string[]} parameters 14661 * The parameters provided for the instruction, if any. 14662 */ 14663 this.oninstruction = null; 14664 14665}; 14666 14667/** 14668 * The Guacamole protocol instruction opcode reserved for arbitrary internal 14669 * use by tunnel implementations. The value of this opcode is guaranteed to be 14670 * the empty string (""). Tunnel implementations may use this opcode for any 14671 * purpose. It is currently used by the HTTP tunnel to mark the end of the HTTP 14672 * response, and by the WebSocket tunnel to transmit the tunnel UUID and send 14673 * connection stability test pings/responses. 14674 * 14675 * @constant 14676 * @type {!string} 14677 */ 14678Guacamole.Tunnel.INTERNAL_DATA_OPCODE = ''; 14679 14680/** 14681 * All possible tunnel states. 14682 * 14683 * @type {!Object.<string, number>} 14684 */ 14685Guacamole.Tunnel.State = { 14686 14687 /** 14688 * A connection is in pending. It is not yet known whether connection was 14689 * successful. 14690 * 14691 * @type {!number} 14692 */ 14693 "CONNECTING": 0, 14694 14695 /** 14696 * Connection was successful, and data is being received. 14697 * 14698 * @type {!number} 14699 */ 14700 "OPEN": 1, 14701 14702 /** 14703 * The connection is closed. Connection may not have been successful, the 14704 * tunnel may have been explicitly closed by either side, or an error may 14705 * have occurred. 14706 * 14707 * @type {!number} 14708 */ 14709 "CLOSED": 2, 14710 14711 /** 14712 * The connection is open, but communication through the tunnel appears to 14713 * be disrupted, and the connection may close as a result. 14714 * 14715 * @type {!number} 14716 */ 14717 "UNSTABLE" : 3 14718 14719}; 14720 14721/** 14722 * Guacamole Tunnel implemented over HTTP via XMLHttpRequest. 14723 * 14724 * @constructor 14725 * @augments Guacamole.Tunnel 14726 * 14727 * @param {!string} tunnelURL 14728 * The URL of the HTTP tunneling service. 14729 * 14730 * @param {boolean} [crossDomain=false] 14731 * Whether tunnel requests will be cross-domain, and thus must use CORS 14732 * mechanisms and headers. By default, it is assumed that tunnel requests 14733 * will be made to the same domain. 14734 * 14735 * @param {object} [extraTunnelHeaders={}] 14736 * Key value pairs containing the header names and values of any additional 14737 * headers to be sent in tunnel requests. By default, no extra headers will 14738 * be added. 14739 */ 14740Guacamole.HTTPTunnel = function(tunnelURL, crossDomain, extraTunnelHeaders) { 14741 14742 /** 14743 * Reference to this HTTP tunnel. 14744 * 14745 * @private 14746 * @type {!Guacamole.HTTPTunnel} 14747 */ 14748 var tunnel = this; 14749 14750 var TUNNEL_CONNECT = tunnelURL + "?connect"; 14751 var TUNNEL_READ = tunnelURL + "?read:"; 14752 var TUNNEL_WRITE = tunnelURL + "?write:"; 14753 14754 var POLLING_ENABLED = 1; 14755 var POLLING_DISABLED = 0; 14756 14757 // Default to polling - will be turned off automatically if not needed 14758 var pollingMode = POLLING_ENABLED; 14759 14760 var sendingMessages = false; 14761 var outputMessageBuffer = ""; 14762 14763 // If requests are expected to be cross-domain, the cookie that the HTTP 14764 // tunnel depends on will only be sent if withCredentials is true 14765 var withCredentials = !!crossDomain; 14766 14767 /** 14768 * The current receive timeout ID, if any. 14769 * 14770 * @private 14771 * @type {number} 14772 */ 14773 var receive_timeout = null; 14774 14775 /** 14776 * The current connection stability timeout ID, if any. 14777 * 14778 * @private 14779 * @type {number} 14780 */ 14781 var unstableTimeout = null; 14782 14783 /** 14784 * The current connection stability test ping interval ID, if any. This 14785 * will only be set upon successful connection. 14786 * 14787 * @private 14788 * @type {number} 14789 */ 14790 var pingInterval = null; 14791 14792 /** 14793 * The number of milliseconds to wait between connection stability test 14794 * pings. 14795 * 14796 * @private 14797 * @constant 14798 * @type {!number} 14799 */ 14800 var PING_FREQUENCY = 500; 14801 14802 /** 14803 * Additional headers to be sent in tunnel requests. This dictionary can be 14804 * populated with key/value header pairs to pass information such as authentication 14805 * tokens, etc. 14806 * 14807 * @private 14808 * @type {!object} 14809 */ 14810 var extraHeaders = extraTunnelHeaders || {}; 14811 14812 /** 14813 * The name of the HTTP header containing the session token specific to the 14814 * HTTP tunnel implementation. 14815 * 14816 * @private 14817 * @constant 14818 * @type {!string} 14819 */ 14820 var TUNNEL_TOKEN_HEADER = 'Guacamole-Tunnel-Token'; 14821 14822 /** 14823 * The session token currently assigned to this HTTP tunnel. All distinct 14824 * HTTP tunnel connections will have their own dedicated session token. 14825 * 14826 * @private 14827 * @type {string} 14828 */ 14829 var tunnelSessionToken = null; 14830 14831 /** 14832 * Adds the configured additional headers to the given request. 14833 * 14834 * @private 14835 * @param {!XMLHttpRequest} request 14836 * The request where the configured extra headers will be added. 14837 * 14838 * @param {!object} headers 14839 * The headers to be added to the request. 14840 */ 14841 function addExtraHeaders(request, headers) { 14842 for (var name in headers) { 14843 request.setRequestHeader(name, headers[name]); 14844 } 14845 } 14846 14847 /** 14848 * Resets the state of timers tracking network activity and stability. If 14849 * those timers are not yet started, invoking this function starts them. 14850 * This function should be invoked when the tunnel is established and every 14851 * time there is network activity on the tunnel, such that the timers can 14852 * safely assume the network and/or server are not responding if this 14853 * function has not been invoked for a significant period of time. 14854 * 14855 * @private 14856 */ 14857 var resetTimers = function resetTimers() { 14858 14859 // Get rid of old timeouts (if any) 14860 window.clearTimeout(receive_timeout); 14861 window.clearTimeout(unstableTimeout); 14862 14863 // Clear unstable status 14864 if (tunnel.state === Guacamole.Tunnel.State.UNSTABLE) 14865 tunnel.setState(Guacamole.Tunnel.State.OPEN); 14866 14867 // Set new timeout for tracking overall connection timeout 14868 receive_timeout = window.setTimeout(function () { 14869 close_tunnel(new Guacamole.Status(Guacamole.Status.Code.UPSTREAM_TIMEOUT, "Server timeout.")); 14870 }, tunnel.receiveTimeout); 14871 14872 // Set new timeout for tracking suspected connection instability 14873 unstableTimeout = window.setTimeout(function() { 14874 tunnel.setState(Guacamole.Tunnel.State.UNSTABLE); 14875 }, tunnel.unstableThreshold); 14876 14877 }; 14878 14879 /** 14880 * Closes this tunnel, signaling the given status and corresponding 14881 * message, which will be sent to the onerror handler if the status is 14882 * an error status. 14883 * 14884 * @private 14885 * @param {!Guacamole.Status} status 14886 * The status causing the connection to close; 14887 */ 14888 function close_tunnel(status) { 14889 14890 // Get rid of old timeouts (if any) 14891 window.clearTimeout(receive_timeout); 14892 window.clearTimeout(unstableTimeout); 14893 14894 // Cease connection test pings 14895 window.clearInterval(pingInterval); 14896 14897 // Ignore if already closed 14898 if (tunnel.state === Guacamole.Tunnel.State.CLOSED) 14899 return; 14900 14901 // If connection closed abnormally, signal error. 14902 if (status.code !== Guacamole.Status.Code.SUCCESS && tunnel.onerror) { 14903 14904 // Ignore RESOURCE_NOT_FOUND if we've already connected, as that 14905 // only signals end-of-stream for the HTTP tunnel. 14906 if (tunnel.state === Guacamole.Tunnel.State.CONNECTING 14907 || status.code !== Guacamole.Status.Code.RESOURCE_NOT_FOUND) 14908 tunnel.onerror(status); 14909 14910 } 14911 14912 // Reset output message buffer 14913 sendingMessages = false; 14914 14915 // Mark as closed 14916 tunnel.setState(Guacamole.Tunnel.State.CLOSED); 14917 14918 } 14919 14920 14921 this.sendMessage = function() { 14922 14923 // Do not attempt to send messages if not connected 14924 if (!tunnel.isConnected()) 14925 return; 14926 14927 // Do not attempt to send empty messages 14928 if (arguments.length === 0) 14929 return; 14930 14931 /** 14932 * Converts the given value to a length/string pair for use as an 14933 * element in a Guacamole instruction. 14934 * 14935 * @private 14936 * @param value 14937 * The value to convert. 14938 * 14939 * @return {!string} 14940 * The converted value. 14941 */ 14942 function getElement(value) { 14943 var string = new String(value); 14944 return string.length + "." + string; 14945 } 14946 14947 // Initialized message with first element 14948 var message = getElement(arguments[0]); 14949 14950 // Append remaining elements 14951 for (var i=1; i<arguments.length; i++) 14952 message += "," + getElement(arguments[i]); 14953 14954 // Final terminator 14955 message += ";"; 14956 14957 // Add message to buffer 14958 outputMessageBuffer += message; 14959 14960 // Send if not currently sending 14961 if (!sendingMessages) 14962 sendPendingMessages(); 14963 14964 }; 14965 14966 function sendPendingMessages() { 14967 14968 // Do not attempt to send messages if not connected 14969 if (!tunnel.isConnected()) 14970 return; 14971 14972 if (outputMessageBuffer.length > 0) { 14973 14974 sendingMessages = true; 14975 14976 var message_xmlhttprequest = new XMLHttpRequest(); 14977 message_xmlhttprequest.open("POST", TUNNEL_WRITE + tunnel.uuid); 14978 message_xmlhttprequest.withCredentials = withCredentials; 14979 addExtraHeaders(message_xmlhttprequest, extraHeaders); 14980 message_xmlhttprequest.setRequestHeader("Content-type", "application/octet-stream"); 14981 message_xmlhttprequest.setRequestHeader(TUNNEL_TOKEN_HEADER, tunnelSessionToken); 14982 14983 // Once response received, send next queued event. 14984 message_xmlhttprequest.onreadystatechange = function() { 14985 if (message_xmlhttprequest.readyState === 4) { 14986 14987 resetTimers(); 14988 14989 // If an error occurs during send, handle it 14990 if (message_xmlhttprequest.status !== 200) 14991 handleHTTPTunnelError(message_xmlhttprequest); 14992 14993 // Otherwise, continue the send loop 14994 else 14995 sendPendingMessages(); 14996 14997 } 14998 }; 14999 15000 message_xmlhttprequest.send(outputMessageBuffer); 15001 outputMessageBuffer = ""; // Clear buffer 15002 15003 } 15004 else 15005 sendingMessages = false; 15006 15007 } 15008 15009 function handleHTTPTunnelError(xmlhttprequest) { 15010 15011 // Pull status code directly from headers provided by Guacamole 15012 var code = parseInt(xmlhttprequest.getResponseHeader("Guacamole-Status-Code")); 15013 if (code) { 15014 var message = xmlhttprequest.getResponseHeader("Guacamole-Error-Message"); 15015 close_tunnel(new Guacamole.Status(code, message)); 15016 } 15017 15018 // Failing that, derive a Guacamole status code from the HTTP status 15019 // code provided by the browser 15020 else if (xmlhttprequest.status) 15021 close_tunnel(new Guacamole.Status( 15022 Guacamole.Status.Code.fromHTTPCode(xmlhttprequest.status), 15023 xmlhttprequest.statusText)); 15024 15025 // Otherwise, assume server is unreachable 15026 else 15027 close_tunnel(new Guacamole.Status(Guacamole.Status.Code.UPSTREAM_NOT_FOUND)); 15028 15029 } 15030 15031 function handleResponse(xmlhttprequest) { 15032 15033 var interval = null; 15034 var nextRequest = null; 15035 15036 var dataUpdateEvents = 0; 15037 15038 // The location of the last element's terminator 15039 var elementEnd = -1; 15040 15041 // Where to start the next length search or the next element 15042 var startIndex = 0; 15043 15044 // Parsed elements 15045 var elements = new Array(); 15046 15047 function parseResponse() { 15048 15049 // Do not handle responses if not connected 15050 if (!tunnel.isConnected()) { 15051 15052 // Clean up interval if polling 15053 if (interval !== null) 15054 clearInterval(interval); 15055 15056 return; 15057 } 15058 15059 // Do not parse response yet if not ready 15060 if (xmlhttprequest.readyState < 2) return; 15061 15062 // Attempt to read status 15063 var status; 15064 try { status = xmlhttprequest.status; } 15065 15066 // If status could not be read, assume successful. 15067 catch (e) { status = 200; } 15068 15069 // Start next request as soon as possible IF request was successful 15070 if (!nextRequest && status === 200) 15071 nextRequest = makeRequest(); 15072 15073 // Parse stream when data is received and when complete. 15074 if (xmlhttprequest.readyState === 3 || 15075 xmlhttprequest.readyState === 4) { 15076 15077 resetTimers(); 15078 15079 // Also poll every 30ms (some browsers don't repeatedly call onreadystatechange for new data) 15080 if (pollingMode === POLLING_ENABLED) { 15081 if (xmlhttprequest.readyState === 3 && !interval) 15082 interval = setInterval(parseResponse, 30); 15083 else if (xmlhttprequest.readyState === 4 && interval) 15084 clearInterval(interval); 15085 } 15086 15087 // If canceled, stop transfer 15088 if (xmlhttprequest.status === 0) { 15089 tunnel.disconnect(); 15090 return; 15091 } 15092 15093 // Halt on error during request 15094 else if (xmlhttprequest.status !== 200) { 15095 handleHTTPTunnelError(xmlhttprequest); 15096 return; 15097 } 15098 15099 // Attempt to read in-progress data 15100 var current; 15101 try { current = xmlhttprequest.responseText; } 15102 15103 // Do not attempt to parse if data could not be read 15104 catch (e) { return; } 15105 15106 // While search is within currently received data 15107 while (elementEnd < current.length) { 15108 15109 // If we are waiting for element data 15110 if (elementEnd >= startIndex) { 15111 15112 // We now have enough data for the element. Parse. 15113 var element = current.substring(startIndex, elementEnd); 15114 var terminator = current.substring(elementEnd, elementEnd+1); 15115 15116 // Add element to array 15117 elements.push(element); 15118 15119 // If last element, handle instruction 15120 if (terminator === ";") { 15121 15122 // Get opcode 15123 var opcode = elements.shift(); 15124 15125 // Call instruction handler. 15126 if (tunnel.oninstruction) 15127 tunnel.oninstruction(opcode, elements); 15128 15129 // Clear elements 15130 elements.length = 0; 15131 15132 } 15133 15134 // Start searching for length at character after 15135 // element terminator 15136 startIndex = elementEnd + 1; 15137 15138 } 15139 15140 // Search for end of length 15141 var lengthEnd = current.indexOf(".", startIndex); 15142 if (lengthEnd !== -1) { 15143 15144 // Parse length 15145 var length = parseInt(current.substring(elementEnd+1, lengthEnd)); 15146 15147 // If we're done parsing, handle the next response. 15148 if (length === 0) { 15149 15150 // Clean up interval if polling 15151 if (interval) 15152 clearInterval(interval); 15153 15154 // Clean up object 15155 xmlhttprequest.onreadystatechange = null; 15156 xmlhttprequest.abort(); 15157 15158 // Start handling next request 15159 if (nextRequest) 15160 handleResponse(nextRequest); 15161 15162 // Done parsing 15163 break; 15164 15165 } 15166 15167 // Calculate start of element 15168 startIndex = lengthEnd + 1; 15169 15170 // Calculate location of element terminator 15171 elementEnd = startIndex + length; 15172 15173 } 15174 15175 // If no period yet, continue search when more data 15176 // is received 15177 else { 15178 startIndex = current.length; 15179 break; 15180 } 15181 15182 } // end parse loop 15183 15184 } 15185 15186 } 15187 15188 // If response polling enabled, attempt to detect if still 15189 // necessary (via wrapping parseResponse()) 15190 if (pollingMode === POLLING_ENABLED) { 15191 xmlhttprequest.onreadystatechange = function() { 15192 15193 // If we receive two or more readyState==3 events, 15194 // there is no need to poll. 15195 if (xmlhttprequest.readyState === 3) { 15196 dataUpdateEvents++; 15197 if (dataUpdateEvents >= 2) { 15198 pollingMode = POLLING_DISABLED; 15199 xmlhttprequest.onreadystatechange = parseResponse; 15200 } 15201 } 15202 15203 parseResponse(); 15204 }; 15205 } 15206 15207 // Otherwise, just parse 15208 else 15209 xmlhttprequest.onreadystatechange = parseResponse; 15210 15211 parseResponse(); 15212 15213 } 15214 15215 /** 15216 * Arbitrary integer, unique for each tunnel read request. 15217 * @private 15218 */ 15219 var request_id = 0; 15220 15221 function makeRequest() { 15222 15223 // Make request, increment request ID 15224 var xmlhttprequest = new XMLHttpRequest(); 15225 xmlhttprequest.open("GET", TUNNEL_READ + tunnel.uuid + ":" + (request_id++)); 15226 xmlhttprequest.setRequestHeader(TUNNEL_TOKEN_HEADER, tunnelSessionToken); 15227 xmlhttprequest.withCredentials = withCredentials; 15228 addExtraHeaders(xmlhttprequest, extraHeaders); 15229 xmlhttprequest.send(null); 15230 15231 return xmlhttprequest; 15232 15233 } 15234 15235 this.connect = function(data) { 15236 15237 // Start waiting for connect 15238 resetTimers(); 15239 15240 // Mark the tunnel as connecting 15241 tunnel.setState(Guacamole.Tunnel.State.CONNECTING); 15242 15243 // Start tunnel and connect 15244 var connect_xmlhttprequest = new XMLHttpRequest(); 15245 connect_xmlhttprequest.onreadystatechange = function() { 15246 15247 if (connect_xmlhttprequest.readyState !== 4) 15248 return; 15249 15250 // If failure, throw error 15251 if (connect_xmlhttprequest.status !== 200) { 15252 handleHTTPTunnelError(connect_xmlhttprequest); 15253 return; 15254 } 15255 15256 resetTimers(); 15257 15258 // Get UUID and HTTP-specific tunnel session token from response 15259 tunnel.setUUID(connect_xmlhttprequest.responseText); 15260 tunnelSessionToken = connect_xmlhttprequest.getResponseHeader(TUNNEL_TOKEN_HEADER); 15261 15262 // Fail connect attempt if token is not successfully assigned 15263 if (!tunnelSessionToken) { 15264 close_tunnel(new Guacamole.Status(Guacamole.Status.Code.UPSTREAM_NOT_FOUND)); 15265 return; 15266 } 15267 15268 // Mark as open 15269 tunnel.setState(Guacamole.Tunnel.State.OPEN); 15270 15271 // Ping tunnel endpoint regularly to test connection stability 15272 pingInterval = setInterval(function sendPing() { 15273 tunnel.sendMessage("nop"); 15274 }, PING_FREQUENCY); 15275 15276 // Start reading data 15277 handleResponse(makeRequest()); 15278 15279 }; 15280 15281 connect_xmlhttprequest.open("POST", TUNNEL_CONNECT, true); 15282 connect_xmlhttprequest.withCredentials = withCredentials; 15283 addExtraHeaders(connect_xmlhttprequest, extraHeaders); 15284 connect_xmlhttprequest.setRequestHeader("Content-type", "application/x-www-form-urlencoded; charset=UTF-8"); 15285 connect_xmlhttprequest.send(data); 15286 15287 }; 15288 15289 this.disconnect = function() { 15290 close_tunnel(new Guacamole.Status(Guacamole.Status.Code.SUCCESS, "Manually closed.")); 15291 }; 15292 15293}; 15294 15295Guacamole.HTTPTunnel.prototype = new Guacamole.Tunnel(); 15296 15297/** 15298 * Guacamole Tunnel implemented over WebSocket via XMLHttpRequest. 15299 * 15300 * @constructor 15301 * @augments Guacamole.Tunnel 15302 * @param {!string} tunnelURL 15303 * The URL of the WebSocket tunneling service. 15304 */ 15305Guacamole.WebSocketTunnel = function(tunnelURL) { 15306 15307 /** 15308 * Reference to this WebSocket tunnel. 15309 * 15310 * @private 15311 * @type {Guacamole.WebSocketTunnel} 15312 */ 15313 var tunnel = this; 15314 15315 /** 15316 * The WebSocket used by this tunnel. 15317 * 15318 * @private 15319 * @type {WebSocket} 15320 */ 15321 var socket = null; 15322 15323 /** 15324 * The current receive timeout ID, if any. 15325 * 15326 * @private 15327 * @type {number} 15328 */ 15329 var receive_timeout = null; 15330 15331 /** 15332 * The current connection stability timeout ID, if any. 15333 * 15334 * @private 15335 * @type {number} 15336 */ 15337 var unstableTimeout = null; 15338 15339 /** 15340 * The current connection stability test ping timeout ID, if any. This 15341 * will only be set upon successful connection. 15342 * 15343 * @private 15344 * @type {number} 15345 */ 15346 var pingTimeout = null; 15347 15348 /** 15349 * The WebSocket protocol corresponding to the protocol used for the current 15350 * location. 15351 * 15352 * @private 15353 * @type {!Object.<string, string>} 15354 */ 15355 var ws_protocol = { 15356 "http:": "ws:", 15357 "https:": "wss:" 15358 }; 15359 15360 /** 15361 * The number of milliseconds to wait between connection stability test 15362 * pings. 15363 * 15364 * @private 15365 * @constant 15366 * @type {!number} 15367 */ 15368 var PING_FREQUENCY = 500; 15369 15370 /** 15371 * The timestamp of the point in time that the last connection stability 15372 * test ping was sent, in milliseconds elapsed since midnight of January 1, 15373 * 1970 UTC. 15374 * 15375 * @private 15376 * @type {!number} 15377 */ 15378 var lastSentPing = 0; 15379 15380 // Transform current URL to WebSocket URL 15381 15382 // If not already a websocket URL 15383 if ( tunnelURL.substring(0, 3) !== "ws:" 15384 && tunnelURL.substring(0, 4) !== "wss:") { 15385 15386 var protocol = ws_protocol[window.location.protocol]; 15387 15388 // If absolute URL, convert to absolute WS URL 15389 if (tunnelURL.substring(0, 1) === "/") 15390 tunnelURL = 15391 protocol 15392 + "//" + window.location.host 15393 + tunnelURL; 15394 15395 // Otherwise, construct absolute from relative URL 15396 else { 15397 15398 // Get path from pathname 15399 var slash = window.location.pathname.lastIndexOf("/"); 15400 var path = window.location.pathname.substring(0, slash + 1); 15401 15402 // Construct absolute URL 15403 tunnelURL = 15404 protocol 15405 + "//" + window.location.host 15406 + path 15407 + tunnelURL; 15408 15409 } 15410 15411 } 15412 15413 /** 15414 * Sends an internal "ping" instruction to the Guacamole WebSocket 15415 * endpoint, verifying network connection stability. If the network is 15416 * stable, the Guacamole server will receive this instruction and respond 15417 * with an identical ping. 15418 * 15419 * @private 15420 */ 15421 var sendPing = function sendPing() { 15422 var currentTime = new Date().getTime(); 15423 tunnel.sendMessage(Guacamole.Tunnel.INTERNAL_DATA_OPCODE, 'ping', currentTime); 15424 lastSentPing = currentTime; 15425 }; 15426 15427 /** 15428 * Resets the state of timers tracking network activity and stability. If 15429 * those timers are not yet started, invoking this function starts them. 15430 * This function should be invoked when the tunnel is established and every 15431 * time there is network activity on the tunnel, such that the timers can 15432 * safely assume the network and/or server are not responding if this 15433 * function has not been invoked for a significant period of time. 15434 * 15435 * @private 15436 */ 15437 var resetTimers = function resetTimers() { 15438 15439 // Get rid of old timeouts (if any) 15440 window.clearTimeout(receive_timeout); 15441 window.clearTimeout(unstableTimeout); 15442 window.clearTimeout(pingTimeout); 15443 15444 // Clear unstable status 15445 if (tunnel.state === Guacamole.Tunnel.State.UNSTABLE) 15446 tunnel.setState(Guacamole.Tunnel.State.OPEN); 15447 15448 // Set new timeout for tracking overall connection timeout 15449 receive_timeout = window.setTimeout(function () { 15450 close_tunnel(new Guacamole.Status(Guacamole.Status.Code.UPSTREAM_TIMEOUT, "Server timeout.")); 15451 }, tunnel.receiveTimeout); 15452 15453 // Set new timeout for tracking suspected connection instability 15454 unstableTimeout = window.setTimeout(function() { 15455 tunnel.setState(Guacamole.Tunnel.State.UNSTABLE); 15456 }, tunnel.unstableThreshold); 15457 15458 var currentTime = new Date().getTime(); 15459 var pingDelay = Math.max(lastSentPing + PING_FREQUENCY - currentTime, 0); 15460 15461 // Ping tunnel endpoint regularly to test connection stability, sending 15462 // the ping immediately if enough time has already elapsed 15463 if (pingDelay > 0) 15464 pingTimeout = window.setTimeout(sendPing, pingDelay); 15465 else 15466 sendPing(); 15467 15468 }; 15469 15470 /** 15471 * Closes this tunnel, signaling the given status and corresponding 15472 * message, which will be sent to the onerror handler if the status is 15473 * an error status. 15474 * 15475 * @private 15476 * @param {!Guacamole.Status} status 15477 * The status causing the connection to close; 15478 */ 15479 function close_tunnel(status) { 15480 15481 // Get rid of old timeouts (if any) 15482 window.clearTimeout(receive_timeout); 15483 window.clearTimeout(unstableTimeout); 15484 window.clearTimeout(pingTimeout); 15485 15486 // Ignore if already closed 15487 if (tunnel.state === Guacamole.Tunnel.State.CLOSED) 15488 return; 15489 15490 // If connection closed abnormally, signal error. 15491 if (status.code !== Guacamole.Status.Code.SUCCESS && tunnel.onerror) 15492 tunnel.onerror(status); 15493 15494 // Mark as closed 15495 tunnel.setState(Guacamole.Tunnel.State.CLOSED); 15496 15497 socket.close(); 15498 15499 } 15500 15501 this.sendMessage = function(elements) { 15502 15503 // Do not attempt to send messages if not connected 15504 if (!tunnel.isConnected()) 15505 return; 15506 15507 // Do not attempt to send empty messages 15508 if (arguments.length === 0) 15509 return; 15510 15511 /** 15512 * Converts the given value to a length/string pair for use as an 15513 * element in a Guacamole instruction. 15514 * 15515 * @private 15516 * @param {*} value 15517 * The value to convert. 15518 * 15519 * @return {!string} 15520 * The converted value. 15521 */ 15522 function getElement(value) { 15523 var string = new String(value); 15524 return string.length + "." + string; 15525 } 15526 15527 // Initialized message with first element 15528 var message = getElement(arguments[0]); 15529 15530 // Append remaining elements 15531 for (var i=1; i<arguments.length; i++) 15532 message += "," + getElement(arguments[i]); 15533 15534 // Final terminator 15535 message += ";"; 15536 15537 socket.send(message); 15538 15539 }; 15540 15541 this.connect = function(data) { 15542 15543 resetTimers(); 15544 15545 // Mark the tunnel as connecting 15546 tunnel.setState(Guacamole.Tunnel.State.CONNECTING); 15547 15548 // Connect socket 15549 socket = new WebSocket(tunnelURL + "?" + data, "guacamole"); 15550 15551 socket.onopen = function(event) { 15552 resetTimers(); 15553 }; 15554 15555 socket.onclose = function(event) { 15556 15557 // Pull status code directly from closure reason provided by Guacamole 15558 if (event.reason) 15559 close_tunnel(new Guacamole.Status(parseInt(event.reason), event.reason)); 15560 15561 // Failing that, derive a Guacamole status code from the WebSocket 15562 // status code provided by the browser 15563 else if (event.code) 15564 close_tunnel(new Guacamole.Status(Guacamole.Status.Code.fromWebSocketCode(event.code))); 15565 15566 // Otherwise, assume server is unreachable 15567 else 15568 close_tunnel(new Guacamole.Status(Guacamole.Status.Code.UPSTREAM_NOT_FOUND)); 15569 15570 }; 15571 15572 socket.onmessage = function(event) { 15573 15574 resetTimers(); 15575 15576 var message = event.data; 15577 var startIndex = 0; 15578 var elementEnd; 15579 15580 var elements = []; 15581 15582 do { 15583 15584 // Search for end of length 15585 var lengthEnd = message.indexOf(".", startIndex); 15586 if (lengthEnd !== -1) { 15587 15588 // Parse length 15589 var length = parseInt(message.substring(elementEnd+1, lengthEnd)); 15590 15591 // Calculate start of element 15592 startIndex = lengthEnd + 1; 15593 15594 // Calculate location of element terminator 15595 elementEnd = startIndex + length; 15596 15597 } 15598 15599 // If no period, incomplete instruction. 15600 else 15601 close_tunnel(new Guacamole.Status(Guacamole.Status.Code.SERVER_ERROR, "Incomplete instruction.")); 15602 15603 // We now have enough data for the element. Parse. 15604 var element = message.substring(startIndex, elementEnd); 15605 var terminator = message.substring(elementEnd, elementEnd+1); 15606 15607 // Add element to array 15608 elements.push(element); 15609 15610 // If last element, handle instruction 15611 if (terminator === ";") { 15612 15613 // Get opcode 15614 var opcode = elements.shift(); 15615 15616 // Update state and UUID when first instruction received 15617 if (tunnel.uuid === null) { 15618 15619 // Associate tunnel UUID if received 15620 if (opcode === Guacamole.Tunnel.INTERNAL_DATA_OPCODE && elements.length === 1) 15621 tunnel.setUUID(elements[0]); 15622 15623 // Tunnel is now open and UUID is available 15624 tunnel.setState(Guacamole.Tunnel.State.OPEN); 15625 15626 } 15627 15628 // Call instruction handler. 15629 if (opcode !== Guacamole.Tunnel.INTERNAL_DATA_OPCODE && tunnel.oninstruction) 15630 tunnel.oninstruction(opcode, elements); 15631 15632 // Clear elements 15633 elements.length = 0; 15634 15635 } 15636 15637 // Start searching for length at character after 15638 // element terminator 15639 startIndex = elementEnd + 1; 15640 15641 } while (startIndex < message.length); 15642 15643 }; 15644 15645 }; 15646 15647 this.disconnect = function() { 15648 close_tunnel(new Guacamole.Status(Guacamole.Status.Code.SUCCESS, "Manually closed.")); 15649 }; 15650 15651}; 15652 15653Guacamole.WebSocketTunnel.prototype = new Guacamole.Tunnel(); 15654 15655/** 15656 * Guacamole Tunnel which cycles between all specified tunnels until 15657 * no tunnels are left. Another tunnel is used if an error occurs but 15658 * no instructions have been received. If an instruction has been 15659 * received, or no tunnels remain, the error is passed directly out 15660 * through the onerror handler (if defined). 15661 * 15662 * @constructor 15663 * @augments Guacamole.Tunnel 15664 * @param {...Guacamole.Tunnel} tunnelChain 15665 * The tunnels to use, in order of priority. 15666 */ 15667Guacamole.ChainedTunnel = function(tunnelChain) { 15668 15669 /** 15670 * Reference to this chained tunnel. 15671 * @private 15672 */ 15673 var chained_tunnel = this; 15674 15675 /** 15676 * Data passed in via connect(), to be used for 15677 * wrapped calls to other tunnels' connect() functions. 15678 * @private 15679 */ 15680 var connect_data; 15681 15682 /** 15683 * Array of all tunnels passed to this ChainedTunnel through the 15684 * constructor arguments. 15685 * @private 15686 */ 15687 var tunnels = []; 15688 15689 /** 15690 * The tunnel committed via commit_tunnel(), if any, or null if no tunnel 15691 * has yet been committed. 15692 * 15693 * @private 15694 * @type {Guacamole.Tunnel} 15695 */ 15696 var committedTunnel = null; 15697 15698 // Load all tunnels into array 15699 for (var i=0; i<arguments.length; i++) 15700 tunnels.push(arguments[i]); 15701 15702 /** 15703 * Sets the current tunnel. 15704 * 15705 * @private 15706 * @param {!Guacamole.Tunnel} tunnel 15707 * The tunnel to set as the current tunnel. 15708 */ 15709 function attach(tunnel) { 15710 15711 // Set own functions to tunnel's functions 15712 chained_tunnel.disconnect = tunnel.disconnect; 15713 chained_tunnel.sendMessage = tunnel.sendMessage; 15714 15715 /** 15716 * Fails the currently-attached tunnel, attaching a new tunnel if 15717 * possible. 15718 * 15719 * @private 15720 * @param {Guacamole.Status} [status] 15721 * An object representing the failure that occured in the 15722 * currently-attached tunnel, if known. 15723 * 15724 * @return {Guacamole.Tunnel} 15725 * The next tunnel, or null if there are no more tunnels to try or 15726 * if no more tunnels should be tried. 15727 */ 15728 var failTunnel = function failTunnel(status) { 15729 15730 // Do not attempt to continue using next tunnel on server timeout 15731 if (status && status.code === Guacamole.Status.Code.UPSTREAM_TIMEOUT) { 15732 tunnels = []; 15733 return null; 15734 } 15735 15736 // Get next tunnel 15737 var next_tunnel = tunnels.shift(); 15738 15739 // If there IS a next tunnel, try using it. 15740 if (next_tunnel) { 15741 tunnel.onerror = null; 15742 tunnel.oninstruction = null; 15743 tunnel.onstatechange = null; 15744 attach(next_tunnel); 15745 } 15746 15747 return next_tunnel; 15748 15749 }; 15750 15751 /** 15752 * Use the current tunnel from this point forward. Do not try any more 15753 * tunnels, even if the current tunnel fails. 15754 * 15755 * @private 15756 */ 15757 function commit_tunnel() { 15758 15759 tunnel.onstatechange = chained_tunnel.onstatechange; 15760 tunnel.oninstruction = chained_tunnel.oninstruction; 15761 tunnel.onerror = chained_tunnel.onerror; 15762 15763 // Assign UUID if already known 15764 if (tunnel.uuid) 15765 chained_tunnel.setUUID(tunnel.uuid); 15766 15767 // Assign any future received UUIDs such that they are 15768 // accessible from the main uuid property of the chained tunnel 15769 tunnel.onuuid = function uuidReceived(uuid) { 15770 chained_tunnel.setUUID(uuid); 15771 }; 15772 15773 committedTunnel = tunnel; 15774 15775 } 15776 15777 // Wrap own onstatechange within current tunnel 15778 tunnel.onstatechange = function(state) { 15779 15780 switch (state) { 15781 15782 // If open, use this tunnel from this point forward. 15783 case Guacamole.Tunnel.State.OPEN: 15784 commit_tunnel(); 15785 if (chained_tunnel.onstatechange) 15786 chained_tunnel.onstatechange(state); 15787 break; 15788 15789 // If closed, mark failure, attempt next tunnel 15790 case Guacamole.Tunnel.State.CLOSED: 15791 if (!failTunnel() && chained_tunnel.onstatechange) 15792 chained_tunnel.onstatechange(state); 15793 break; 15794 15795 } 15796 15797 }; 15798 15799 // Wrap own oninstruction within current tunnel 15800 tunnel.oninstruction = function(opcode, elements) { 15801 15802 // Accept current tunnel 15803 commit_tunnel(); 15804 15805 // Invoke handler 15806 if (chained_tunnel.oninstruction) 15807 chained_tunnel.oninstruction(opcode, elements); 15808 15809 }; 15810 15811 // Attach next tunnel on error 15812 tunnel.onerror = function(status) { 15813 15814 // Mark failure, attempt next tunnel 15815 if (!failTunnel(status) && chained_tunnel.onerror) 15816 chained_tunnel.onerror(status); 15817 15818 }; 15819 15820 // Attempt connection 15821 tunnel.connect(connect_data); 15822 15823 } 15824 15825 this.connect = function(data) { 15826 15827 // Remember connect data 15828 connect_data = data; 15829 15830 // Get committed tunnel if exists or the first tunnel on the list 15831 var next_tunnel = committedTunnel ? committedTunnel : tunnels.shift(); 15832 15833 // Attach first tunnel 15834 if (next_tunnel) 15835 attach(next_tunnel); 15836 15837 // If there IS no first tunnel, error 15838 else if (chained_tunnel.onerror) 15839 chained_tunnel.onerror(Guacamole.Status.Code.SERVER_ERROR, "No tunnels to try."); 15840 15841 }; 15842 15843}; 15844 15845Guacamole.ChainedTunnel.prototype = new Guacamole.Tunnel(); 15846 15847/** 15848 * Guacamole Tunnel which replays a Guacamole protocol dump from a static file 15849 * received via HTTP. Instructions within the file are parsed and handled as 15850 * quickly as possible, while the file is being downloaded. 15851 * 15852 * @constructor 15853 * @augments Guacamole.Tunnel 15854 * @param {!string} url 15855 * The URL of a Guacamole protocol dump. 15856 * 15857 * @param {boolean} [crossDomain=false] 15858 * Whether tunnel requests will be cross-domain, and thus must use CORS 15859 * mechanisms and headers. By default, it is assumed that tunnel requests 15860 * will be made to the same domain. 15861 * 15862 * @param {object} [extraTunnelHeaders={}] 15863 * Key value pairs containing the header names and values of any additional 15864 * headers to be sent in tunnel requests. By default, no extra headers will 15865 * be added. 15866 */ 15867Guacamole.StaticHTTPTunnel = function StaticHTTPTunnel(url, crossDomain, extraTunnelHeaders) { 15868 15869 /** 15870 * Reference to this Guacamole.StaticHTTPTunnel. 15871 * 15872 * @private 15873 */ 15874 var tunnel = this; 15875 15876 /** 15877 * AbortController instance which allows the current, in-progress HTTP 15878 * request to be aborted. If no request is currently in progress, this will 15879 * be null. 15880 * 15881 * @private 15882 * @type {AbortController} 15883 */ 15884 var abortController = null; 15885 15886 /** 15887 * Additional headers to be sent in tunnel requests. This dictionary can be 15888 * populated with key/value header pairs to pass information such as authentication 15889 * tokens, etc. 15890 * 15891 * @private 15892 * @type {!object} 15893 */ 15894 var extraHeaders = extraTunnelHeaders || {}; 15895 15896 /** 15897 * The number of bytes in the file being downloaded, or null if this is not 15898 * known. 15899 * 15900 * @type {number} 15901 */ 15902 this.size = null; 15903 15904 this.sendMessage = function sendMessage(elements) { 15905 // Do nothing 15906 }; 15907 15908 this.connect = function connect(data) { 15909 15910 // Ensure any existing connection is killed 15911 tunnel.disconnect(); 15912 15913 // Connection is now starting 15914 tunnel.setState(Guacamole.Tunnel.State.CONNECTING); 15915 15916 // Create Guacamole protocol and UTF-8 parsers specifically for this 15917 // connection 15918 var parser = new Guacamole.Parser(); 15919 var utf8Parser = new Guacamole.UTF8Parser(); 15920 15921 // Invoke tunnel's oninstruction handler for each parsed instruction 15922 parser.oninstruction = function instructionReceived(opcode, args) { 15923 if (tunnel.oninstruction) 15924 tunnel.oninstruction(opcode, args); 15925 }; 15926 15927 // Allow new request to be aborted 15928 abortController = new AbortController(); 15929 15930 // Stream using the Fetch API 15931 fetch(url, { 15932 headers : extraHeaders, 15933 credentials : crossDomain ? 'include' : 'same-origin', 15934 signal : abortController.signal 15935 }) 15936 .then(function gotResponse(response) { 15937 15938 // Reset state and close upon error 15939 if (!response.ok) { 15940 15941 if (tunnel.onerror) 15942 tunnel.onerror(new Guacamole.Status( 15943 Guacamole.Status.Code.fromHTTPCode(response.status), response.statusText)); 15944 15945 tunnel.disconnect(); 15946 return; 15947 15948 } 15949 15950 // Report overall size of stream in bytes, if known 15951 tunnel.size = response.headers.get('Content-Length'); 15952 15953 // Connection is open 15954 tunnel.setState(Guacamole.Tunnel.State.OPEN); 15955 15956 var reader = response.body.getReader(); 15957 var processReceivedText = function processReceivedText(result) { 15958 15959 // Clean up and close when done 15960 if (result.done) { 15961 tunnel.disconnect(); 15962 return; 15963 } 15964 15965 // Parse only the portion of data which is newly received 15966 parser.receive(utf8Parser.decode(result.value)); 15967 15968 // Continue parsing when next chunk is received 15969 reader.read().then(processReceivedText); 15970 15971 }; 15972 15973 // Schedule parse of first chunk 15974 reader.read().then(processReceivedText); 15975 15976 }); 15977 15978 }; 15979 15980 this.disconnect = function disconnect() { 15981 15982 // Abort any in-progress request 15983 if (abortController) { 15984 abortController.abort(); 15985 abortController = null; 15986 } 15987 15988 // Connection is now closed 15989 tunnel.setState(Guacamole.Tunnel.State.CLOSED); 15990 15991 }; 15992 15993}; 15994 15995Guacamole.StaticHTTPTunnel.prototype = new Guacamole.Tunnel(); 15996/* 15997 * Licensed to the Apache Software Foundation (ASF) under one 15998 * or more contributor license agreements. See the NOTICE file 15999 * distributed with this work for additional information 16000 * regarding copyright ownership. The ASF licenses this file 16001 * to you under the Apache License, Version 2.0 (the 16002 * "License"); you may not use this file except in compliance 16003 * with the License. You may obtain a copy of the License at 16004 * 16005 * http://www.apache.org/licenses/LICENSE-2.0 16006 * 16007 * Unless required by applicable law or agreed to in writing, 16008 * software distributed under the License is distributed on an 16009 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16010 * KIND, either express or implied. See the License for the 16011 * specific language governing permissions and limitations 16012 * under the License. 16013 */ 16014 16015var Guacamole = Guacamole || {}; 16016 16017/** 16018 * Parser that decodes UTF-8 text from a series of provided ArrayBuffers. 16019 * Multi-byte characters that continue from one buffer to the next are handled 16020 * correctly. 16021 * 16022 * @constructor 16023 */ 16024Guacamole.UTF8Parser = function UTF8Parser() { 16025 16026 /** 16027 * The number of bytes remaining for the current codepoint. 16028 * 16029 * @private 16030 * @type {!number} 16031 */ 16032 var bytesRemaining = 0; 16033 16034 /** 16035 * The current codepoint value, as calculated from bytes read so far. 16036 * 16037 * @private 16038 * @type {!number} 16039 */ 16040 var codepoint = 0; 16041 16042 /** 16043 * Decodes the given UTF-8 data into a Unicode string, returning a string 16044 * containing all complete UTF-8 characters within the provided data. The 16045 * data may end in the middle of a multi-byte character, in which case the 16046 * complete character will be returned from a later call to decode() after 16047 * enough bytes have been provided. 16048 * 16049 * @private 16050 * @param {!ArrayBuffer} buffer 16051 * Arbitrary UTF-8 data. 16052 * 16053 * @return {!string} 16054 * The decoded Unicode string. 16055 */ 16056 this.decode = function decode(buffer) { 16057 16058 var text = ''; 16059 16060 var bytes = new Uint8Array(buffer); 16061 for (var i=0; i<bytes.length; i++) { 16062 16063 // Get current byte 16064 var value = bytes[i]; 16065 16066 // Start new codepoint if nothing yet read 16067 if (bytesRemaining === 0) { 16068 16069 // 1 byte (0xxxxxxx) 16070 if ((value | 0x7F) === 0x7F) 16071 text += String.fromCharCode(value); 16072 16073 // 2 byte (110xxxxx) 16074 else if ((value | 0x1F) === 0xDF) { 16075 codepoint = value & 0x1F; 16076 bytesRemaining = 1; 16077 } 16078 16079 // 3 byte (1110xxxx) 16080 else if ((value | 0x0F )=== 0xEF) { 16081 codepoint = value & 0x0F; 16082 bytesRemaining = 2; 16083 } 16084 16085 // 4 byte (11110xxx) 16086 else if ((value | 0x07) === 0xF7) { 16087 codepoint = value & 0x07; 16088 bytesRemaining = 3; 16089 } 16090 16091 // Invalid byte 16092 else 16093 text += '\uFFFD'; 16094 16095 } 16096 16097 // Continue existing codepoint (10xxxxxx) 16098 else if ((value | 0x3F) === 0xBF) { 16099 16100 codepoint = (codepoint << 6) | (value & 0x3F); 16101 bytesRemaining--; 16102 16103 // Write codepoint if finished 16104 if (bytesRemaining === 0) 16105 text += String.fromCharCode(codepoint); 16106 16107 } 16108 16109 // Invalid byte 16110 else { 16111 bytesRemaining = 0; 16112 text += '\uFFFD'; 16113 } 16114 16115 } 16116 16117 return text; 16118 16119 }; 16120 16121};/* 16122 * Licensed to the Apache Software Foundation (ASF) under one 16123 * or more contributor license agreements. See the NOTICE file 16124 * distributed with this work for additional information 16125 * regarding copyright ownership. The ASF licenses this file 16126 * to you under the Apache License, Version 2.0 (the 16127 * "License"); you may not use this file except in compliance 16128 * with the License. You may obtain a copy of the License at 16129 * 16130 * http://www.apache.org/licenses/LICENSE-2.0 16131 * 16132 * Unless required by applicable law or agreed to in writing, 16133 * software distributed under the License is distributed on an 16134 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16135 * KIND, either express or implied. See the License for the 16136 * specific language governing permissions and limitations 16137 * under the License. 16138 */ 16139 16140var Guacamole = Guacamole || {}; 16141 16142/** 16143 * The unique ID of this version of the Guacamole JavaScript API. This ID will 16144 * be the version string of the guacamole-common-js Maven project, and can be 16145 * used in downstream applications as a sanity check that the proper version 16146 * of the APIs is being used (in case an older version is cached, for example). 16147 * 16148 * @type {!string} 16149 */ 16150Guacamole.API_VERSION = "1.5.0"; 16151/* 16152 * Licensed to the Apache Software Foundation (ASF) under one 16153 * or more contributor license agreements. See the NOTICE file 16154 * distributed with this work for additional information 16155 * regarding copyright ownership. The ASF licenses this file 16156 * to you under the Apache License, Version 2.0 (the 16157 * "License"); you may not use this file except in compliance 16158 * with the License. You may obtain a copy of the License at 16159 * 16160 * http://www.apache.org/licenses/LICENSE-2.0 16161 * 16162 * Unless required by applicable law or agreed to in writing, 16163 * software distributed under the License is distributed on an 16164 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16165 * KIND, either express or implied. See the License for the 16166 * specific language governing permissions and limitations 16167 * under the License. 16168 */ 16169 16170var Guacamole = Guacamole || {}; 16171 16172/** 16173 * Abstract video player which accepts, queues and plays back arbitrary video 16174 * data. It is up to implementations of this class to provide some means of 16175 * handling a provided Guacamole.InputStream and rendering the received data to 16176 * the provided Guacamole.Display.VisibleLayer. Data received along the 16177 * provided stream is to be played back immediately. 16178 * 16179 * @constructor 16180 */ 16181Guacamole.VideoPlayer = function VideoPlayer() { 16182 16183 /** 16184 * Notifies this Guacamole.VideoPlayer that all video up to the current 16185 * point in time has been given via the underlying stream, and that any 16186 * difference in time between queued video data and the current time can be 16187 * considered latency. 16188 */ 16189 this.sync = function sync() { 16190 // Default implementation - do nothing 16191 }; 16192 16193}; 16194 16195/** 16196 * Determines whether the given mimetype is supported by any built-in 16197 * implementation of Guacamole.VideoPlayer, and thus will be properly handled 16198 * by Guacamole.VideoPlayer.getInstance(). 16199 * 16200 * @param {!string} mimetype 16201 * The mimetype to check. 16202 * 16203 * @returns {!boolean} 16204 * true if the given mimetype is supported by any built-in 16205 * Guacamole.VideoPlayer, false otherwise. 16206 */ 16207Guacamole.VideoPlayer.isSupportedType = function isSupportedType(mimetype) { 16208 16209 // There are currently no built-in video players (and therefore no 16210 // supported types) 16211 return false; 16212 16213}; 16214 16215/** 16216 * Returns a list of all mimetypes supported by any built-in 16217 * Guacamole.VideoPlayer, in rough order of priority. Beware that only the core 16218 * mimetypes themselves will be listed. Any mimetype parameters, even required 16219 * ones, will not be included in the list. 16220 * 16221 * @returns {!string[]} 16222 * A list of all mimetypes supported by any built-in Guacamole.VideoPlayer, 16223 * excluding any parameters. 16224 */ 16225Guacamole.VideoPlayer.getSupportedTypes = function getSupportedTypes() { 16226 16227 // There are currently no built-in video players (and therefore no 16228 // supported types) 16229 return []; 16230 16231}; 16232 16233/** 16234 * Returns an instance of Guacamole.VideoPlayer providing support for the given 16235 * video format. If support for the given video format is not available, null 16236 * is returned. 16237 * 16238 * @param {!Guacamole.InputStream} stream 16239 * The Guacamole.InputStream to read video data from. 16240 * 16241 * @param {!Guacamole.Display.VisibleLayer} layer 16242 * The destination layer in which this Guacamole.VideoPlayer should play 16243 * the received video data. 16244 * 16245 * @param {!string} mimetype 16246 * The mimetype of the video data in the provided stream. 16247 * 16248 * @return {Guacamole.VideoPlayer} 16249 * A Guacamole.VideoPlayer instance supporting the given mimetype and 16250 * reading from the given stream, or null if support for the given mimetype 16251 * is absent. 16252 */ 16253Guacamole.VideoPlayer.getInstance = function getInstance(stream, layer, mimetype) { 16254 16255 // There are currently no built-in video players 16256 return null; 16257 16258}; 16259export default Guacamole;