diff --git a/app/styles/base.css b/app/styles/base.css index 6f6afb75..0ed0db7c 100644 --- a/app/styles/base.css +++ b/app/styles/base.css @@ -147,6 +147,11 @@ input[type=button]:active, select:active { background: linear-gradient(to top, rgb(255, 255, 255), rgb(250, 250, 250)); } +/* Settings slider: video quality (quantization table selector) in AST2100 mode. */ +#noVNC_settings #noVNC_setting_ast2100_quality { + margin: 0 10px; +} + /* ---------------------------------------- * WebKit centering hacks * ---------------------------------------- diff --git a/app/ui.js b/app/ui.js index a0feae6a..537bac90 100644 --- a/app/ui.js +++ b/app/ui.js @@ -100,6 +100,12 @@ var UI; lastKeyboardinput: null, defaultKeyboardinputLen: 100, + // True if we are connected to an ATEN iKVM server speaking the AST2100 video encoding. + // This variable tracks whether the extra UI elements used to configure video settings + // for the AST2100 encoding have been shown yet; it's set on the first FramebufferUpdate + // message that we receive. + _ast2100_videoSettingsInitialized: false, + // Setup rfb object, load settings from browser storage, then call // UI.init to setup the UI/menus load: function(callback) { @@ -392,7 +398,9 @@ var UI; 'onBell': UI.bell, 'onFBUComplete': UI.initialResize, 'onFBResize': UI.updateSessionSize, - 'onDesktopName': UI.updateDesktopName}); + 'onDesktopName': UI.updateDesktopName, + 'ast2100_onVideoSettingsChanged': UI.ast2100_handleVideoSettingsChanged + }); return true; } catch (exc) { var msg = "Unable to create RFB client -- " + exc; @@ -436,7 +444,9 @@ var UI; document.documentElement.classList.add("noVNC_disconnecting"); break; case 'disconnected': + UI.connected = false; UI.showStatus(_("Disconnected")); + UI.ast2100_reset(); break; default: msg = "Invalid UI state"; @@ -843,6 +853,15 @@ var UI; UI.saveSetting('path'); UI.saveSetting('repeaterID'); UI.saveSetting('logging'); + + if (UI._ast2100_videoSettingsInitialized) { + var videoSettings = UI.ast2100_getConfiguredSettings(); + if (videoSettings != UI._ast2100_serverVideoSettings) + UI.rfb.atenChangeVideoSettings( + videoSettings.quantTableSelectorLuma, + videoSettings.quantTableSelectorChroma, + videoSettings.subsamplingMode); + } // Settings with immediate (non-connected related) effect WebUtil.init_logging(UI.getSetting('logging')); @@ -963,9 +982,93 @@ var UI; /* ------^------- * /XVP * ============== - * CLIPBOARD + * AST2100 * ------v------*/ + ast2100_handleVideoSettingsChanged: function (settings) { + if (!UI._ast2100_videoSettingsInitialized) + UI.ast2100_setDefaultSettings(settings); + UI.ast2100_updateVideoSettings(settings); + }, + + // Called the first time we receive a FramebufferUpdate object. Responsible + // for telling the server about any configured default settings. + ast2100_setDefaultSettings: function (settings) { + // Convert the settings that noVNC has been configured to set for all + // AST2100 servers to the familiar videoSettings format. + var defaultQuality = parseInt(UI.ast2100_quality); + var defaultSettings = { + quantTableSelectorLuma: defaultQuality, + quantTableSelectorChroma: defaultQuality, + subsamplingMode: parseInt(UI.ast2100_subsamplingMode) + }; + + // If defaults were not given or were invalid, stick with what the + // server is already using. + if (!(defaultSettings.subsamplingMode == 422 || defaultSettings.subsamplingMode == 444)) + defaultSettings.subsamplingMode = settings.subsamplingMode; + if (!inRangeIncl(defaultQuality, 0x0, 0xB)) { + defaultSettings.quantTableSelectorLuma = settings.quantTableSelectorLuma; + defaultSettings.quantTableSelectorChroma = settings.quantTableSelectorChroma; + } + + if (defaultSettings != settings) + UI.rfb.atenChangeVideoSettings( + defaultSettings.quantTableSelectorLuma, + defaultSettings.quantTableSelectorChroma, + defaultSettings.subsamplingMode); + }, + + // Should be called at init time and after disconnects. This is sort + // of the opposite of _init(). + ast2100_reset: function() { + document.getElementById("noVNC_ast2100_settings").classList.add('noVNC_hidden'); + UI._ast2100_videoSettingsInitialized = false; + UI._ast2100_serverVideoSettings = undefined; + }, + + // Updates the UI to reflect values received from the server. + ast2100_updateVideoSettings: function (videoSettings) { + Util.Info("AST2100 video settings changed:"); + Util.Info(videoSettings); + + // We use this to tell if the user changed anything when they apply + // settings. + UI._ast2100_serverVideoSettings = videoSettings; + + // First run: tell UI to show video quality controls, now that we + // know we are on a machine that supports them, and we know their + // current values. + if (!UI._ast2100_videoSettingsInitialized) { + document.getElementById("noVNC_ast2100_settings").classList.remove('noVNC_hidden'); + UI._ast2100_videoSettingsInitialized = true; + } + + // Average the two quant table selectors as a poor way of dealing + // with the fact that they can, technically, be different. + var quality = ~~((videoSettings.quantTableSelectorLuma + videoSettings.quantTableSelectorChroma) / 2); + document.getElementById("noVNC_setting_ast2100_quality").value = quality; + + // Either 444 or 422 (which is really 4:2:0). + document.getElementById("noVNC_setting_ast2100_subsampling").value = videoSettings.subsamplingMode; + }, + + // Returns the current state of the UI. + ast2100_getConfiguredSettings: function () { + var quality = +document.getElementById("noVNC_setting_ast2100_quality").value; + return { + quantTableSelectorLuma: quality, + quantTableSelectorChroma: quality, + subsamplingMode: +document.getElementById("noVNC_setting_ast2100_subsampling").value + }; + }, + +/* ------^------- +* /AST2100 +* ============== +* CLIPBOARD +* ------v------*/ + openClipboardPanel: function() { UI.closeAllPanels(); UI.openControlbar(); diff --git a/core/ast2100/ast2100.js b/core/ast2100/ast2100.js index ce40d647..4bae3726 100644 --- a/core/ast2100/ast2100.js +++ b/core/ast2100/ast2100.js @@ -30,6 +30,7 @@ var Ast2100Decoder; Ast2100Decoder = function (defaults) { this._blitCallback = defaults.blitCallback; + this._videoSettingsChangedCallback = defaults.videoSettingsChangedCallback; this._frame_width = defaults.width; this._frame_height = defaults.height; @@ -157,6 +158,18 @@ var Ast2100Decoder; this._frame_width = width; this._frame_height = height; }, + + // Each quant table selector is between 0x0 (lowest quality) and 0xB (highest quality). The ATEN client shows a + // single quality slider, which changes both values in tandem. The server sends all three values with each + // FramebufferUpdate message, so these values are updated with every call to decode(). They will be -1 before + // the first frame is decoded. + getVideoSettings: function () { + return { + quantTableSelectorLuma: this._loadedQuantTables[0], + quantTableSelectorChroma: this._loadedQuantTables[1], + subsamplingMode: this.subsamplingMode + }; + }, decode: function (data) { @@ -174,10 +187,13 @@ var Ast2100Decoder; var quantTableSelectorLuma = data[0]; // 0 <= x <= 0xB var quantTableSelectorChroma = data[1]; // 0 <= x <= 0xB var subsamplingMode = (data[2] << 8) | data[3]; // 422u or 444u + + var changedSettings = false; if (this.subsamplingMode != subsamplingMode) { if (verboseVideoSettings) console.log('decode(): new subsampling mode: '+subsamplingMode); this.subsamplingMode = subsamplingMode; + changedSettings = true; } // The remainder of the stream is byte-swapped in four-byte chunks. @@ -193,6 +209,7 @@ var Ast2100Decoder; console.log('decode(): loading new luma quant table: '+fmt_u8(quantTableSelectorLuma)); this._loadQuantTable(0, ATEN_QT_LUMA[quantTableSelectorLuma]); this._loadedQuantTables[0] = quantTableSelectorLuma; + changedSettings = true; } if (quantTableSelectorChroma != this._loadedQuantTables[1]) { if (!inRangeIncl(quantTableSelectorChroma, 0, 0xB)) @@ -201,9 +218,14 @@ var Ast2100Decoder; console.log('decode(): loading new chroma quant table: '+fmt_u8(quantTableSelectorChroma)); this._loadQuantTable(1, ATEN_QT_CHROMA[quantTableSelectorChroma]); this._loadedQuantTables[1] = quantTableSelectorChroma; + changedSettings = true; } + if (this.subsamplingMode != 422 && this.subsamplingMode != 444) throw 'Unexpected value for subsamplingMode: 0x' + fmt_u16(this.subsamplingMode); + + if (changedSettings && this._videoSettingsChangedCallback) + this._videoSettingsChangedCallback(this.getVideoSettings()); // The remainder of the stream is byte-swapped in four-byte chunks. BitStream takes care of this. this._stream = new BitStream({data: data}); diff --git a/core/rfb.js b/core/rfb.js index 6218127e..0386886a 100644 --- a/core/rfb.js +++ b/core/rfb.js @@ -107,6 +107,8 @@ subrects: 0, // RRE lines: 0, // RAW tiles: 0, // HEXTILE + aten_len: -1, // ATEN + aten_type: -1, // ATEN bytes: 0, x: 0, y: 0, @@ -170,7 +172,13 @@ 'wsProtocols': ['binary'], // Protocols to use in the WebSocket connection 'repeaterID': '', // [UltraVNC] RepeaterID to connect to 'viewportDrag': false, // Move the viewport on mouse drags - + 'ast2100_quality': -1, // If set, use this quality upon connection to a server + // using the AST2100 video encoding. Ranges from 0 (lowest) + // to 0xB (highest) quality. + 'ast2100_subsamplingMode': -1, // If set, use this subsampling mode upon connection to a + // server using the AST2100 video encoding. The value may + // either be 444 or 422 (which is really 4:2:0 subsampling). + // Callback functions 'onUpdateState': function () { }, // onUpdateState(rfb, state, oldstate): connection state change 'onNotification': function () { }, // onNotification(rfb, msg, level, options): notification for UI @@ -182,7 +190,8 @@ 'onFBUComplete': function () { }, // onFBUComplete(rfb, fbu): RFB FBU received and processed 'onFBResize': function () { }, // onFBResize(rfb, width, height): frame buffer resized 'onDesktopName': function () { }, // onDesktopName(rfb, name): desktop name received - 'onXvpInit': function () { } // onXvpInit(version): XVP extensions active for this connection + 'onXvpInit': function () { }, // onXvpInit(version): XVP extensions active for this connection + 'ast2100_onVideoSettingsChanged': function () { } }); // main setup @@ -312,12 +321,19 @@ if (this._rfb_connection_state !== 'connected' || this._view_only) { return false; } Util.Info("Sending Ctrl-Alt-Del"); - RFB.messages.keyEvent(this._sock, KeyTable.XK_Control_L, 1); - RFB.messages.keyEvent(this._sock, KeyTable.XK_Alt_L, 1); - RFB.messages.keyEvent(this._sock, KeyTable.XK_Delete, 1); - RFB.messages.keyEvent(this._sock, KeyTable.XK_Delete, 0); - RFB.messages.keyEvent(this._sock, KeyTable.XK_Alt_L, 0); - RFB.messages.keyEvent(this._sock, KeyTable.XK_Control_L, 0); + var keyEvent; + if (this._rfb_atenikvm) { + keyEvent = RFB.messages.atenKeyEvent; + } else { + keyEvent = RFB.messages.keyEvent; + } + + keyEvent(this._sock, KeyTable.XK_Control_L, 1); + keyEvent(this._sock, KeyTable.XK_Alt_L, 1); + keyEvent(this._sock, KeyTable.XK_Delete, 1); + keyEvent(this._sock, KeyTable.XK_Delete, 0); + keyEvent(this._sock, KeyTable.XK_Alt_L, 0); + keyEvent(this._sock, KeyTable.XK_Control_L, 0); return true; }, @@ -344,13 +360,21 @@ // followed by an up key. sendKey: function (keysym, down) { if (this._rfb_connection_state !== 'connected' || this._view_only) { return false; } + + var keyEvent; + if (this._rfb_atenikvm) { + keyEvent = RFB.messages.atenKeyEvent; + } else { + keyEvent = RFB.messages.keyEvent; + } + if (typeof down !== 'undefined') { Util.Info("Sending keysym (" + (down ? "down" : "up") + "): " + keysym); - RFB.messages.keyEvent(this._sock, keysym, down ? 1 : 0); + keyEvent(this._sock, keysym, down ? 1 : 0); } else { Util.Info("Sending keysym (down + up): " + keysym); - RFB.messages.keyEvent(this._sock, keysym, 1); - RFB.messages.keyEvent(this._sock, keysym, 0); + keyEvent(this._sock, keysym, 1); + keyEvent(this._sock, keysym, 0); } return true; }, @@ -685,7 +709,11 @@ } } else { keysym = keyevent.keysym.keysym; - RFB.messages.keyEvent(this._sock, keysym, down); + if (this._rfb_atenikvm) { + RFB.messages.atenKeyEvent(this._sock, keysym, down); + } else { + RFB.messages.keyEvent(this._sock, keysym, down); + } } }, @@ -709,7 +737,11 @@ // If the viewport didn't actually move, then treat as a mouse click event // Send the button down event here, as the button up event is sent at the end of this function if (!this._viewportHasMoved && !this._view_only) { - RFB.messages.pointerEvent(this._sock, this._display.absX(x), this._display.absY(y), bmask); + if (this._rfb_atenikvm) { + RFB.messages.atenPointerEvent(this._sock, this._display.absX(x), this._display.absY(y), bmask); + } else { + RFB.messages.pointerEvent(this._sock, this._display.absX(x), this._display.absY(y), bmask); + } } this._viewportHasMoved = false; } @@ -718,7 +750,11 @@ if (this._view_only) { return; } // View only, skip mouse events if (this._rfb_connection_state !== 'connected') { return; } - RFB.messages.pointerEvent(this._sock, this._display.absX(x), this._display.absY(y), this._mouse_buttonMask); + if (this._rfb_atenikvm) { + RFB.messages.atenPointerEvent(this._sock, this._display.absX(x), this._display.absY(y), this._mouse_buttonMask); + } else { + RFB.messages.pointerEvent(this._sock, this._display.absX(x), this._display.absY(y), this._mouse_buttonMask); + } }, _handleMouseMove: function (x, y) { @@ -745,7 +781,11 @@ if (this._view_only) { return; } // View only, skip mouse events if (this._rfb_connection_state !== 'connected') { return; } - RFB.messages.pointerEvent(this._sock, this._display.absX(x), this._display.absY(y), this._mouse_buttonMask); + if (this._rfb_atenikvm) { + RFB.messages.atenPointerEvent(this._sock, this._display.absX(x), this._display.absY(y), this._mouse_buttonMask); + } else { + RFB.messages.pointerEvent(this._sock, this._display.absX(x), this._display.absY(y), this._mouse_buttonMask); + } }, // Message Handlers @@ -1233,13 +1273,6 @@ this._pixelFormat.red_shift = 10; this._pixelFormat.green_shift = 5; this._pixelFormat.blue_shift = 0; - - // XXX(kelleyk): Doing this will break interaction with non-ATEN - // servers until the page is reloaded; it also breaks the mouse/ - // keyboard portions of the test suite! - // - RFB.messages.keyEvent = RFB.messages.atenKeyEvent; - RFB.messages.pointerEvent = RFB.messages.atenPointerEvent; } if (this._convert_color) @@ -1771,6 +1804,9 @@ ['wsProtocols', 'rw', 'arr'], // Protocols to use in the WebSocket connection ['repeaterID', 'rw', 'str'], // [UltraVNC] RepeaterID to connect to ['viewportDrag', 'rw', 'bool'], // Move the viewport on mouse drags + ['ast2100_quality', 'rw','int'], // Ranges from 0 (lowest) to 0xB (highest) quality. + ['ast2100_subsamplingMode', 'rw', 'int'], // Chroma subsampling; either 444 or 422 (which is + // really 4:2:0 subsampling). // Callback functions ['onUpdateState', 'rw', 'func'], // onUpdateState(rfb, state, oldstate): connection state change @@ -1783,7 +1819,9 @@ ['onFBUComplete', 'rw', 'func'], // onFBUComplete(rfb, fbu): RFB FBU received and processed ['onFBResize', 'rw', 'func'], // onFBResize(rfb, width, height): frame buffer resized ['onDesktopName', 'rw', 'func'], // onDesktopName(rfb, name): desktop name received - ['onXvpInit', 'rw', 'func'] // onXvpInit(version): XVP extensions active for this connection + ['onXvpInit', 'rw', 'func'], // onXvpInit(version): XVP extensions active for this connection + ['ast2100_onVideoSettingsChanged', 'rw', 'func'], // onVideoSettingsChanged(videoSettings): AST2100 video + // quality settings changed in latest FBU. ]); RFB.prototype.set_local_cursor = function (cursor) { @@ -2974,6 +3012,7 @@ } if (!this._aten_ast2100_dec) { + var _rfb = this; var display = this._display; this._aten_ast2100_dec = new Ast2100Decoder({ width: this._FBU.width, @@ -2985,6 +3024,9 @@ // the block to be enqueued instead of being blitted right // away via a call to _rgbxImageData(). display.blitRgbxImage(x, y, width, height, buf, 0, true); + }, + videoSettingsChangedCallback: function (settings) { + _rfb._ast2100_onVideoSettingsChanged(settings); } }); } diff --git a/vnc.html b/vnc.html index 733a3299..946f37cc 100644 --- a/vnc.html +++ b/vnc.html @@ -237,6 +237,38 @@ + + +