Implement UI for controlling ATEN-specific settings

Add video settings change callback to Ast2100Decoder for UI integration.

Implement slider widget (used to select AST2100 quantization tables).
This commit is contained in:
Kevin Kelley 2017-01-12 00:04:27 -08:00
parent 0d048acaca
commit eb386b7672
6 changed files with 231 additions and 25 deletions

View File

@ -147,6 +147,11 @@ input[type=button]:active, select:active {
background: linear-gradient(to top, rgb(255, 255, 255), rgb(250, 250, 250)); 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 * WebKit centering hacks
* ---------------------------------------- * ----------------------------------------

105
app/ui.js
View File

@ -100,6 +100,12 @@ var UI;
lastKeyboardinput: null, lastKeyboardinput: null,
defaultKeyboardinputLen: 100, 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 // Setup rfb object, load settings from browser storage, then call
// UI.init to setup the UI/menus // UI.init to setup the UI/menus
load: function(callback) { load: function(callback) {
@ -392,7 +398,9 @@ var UI;
'onBell': UI.bell, 'onBell': UI.bell,
'onFBUComplete': UI.initialResize, 'onFBUComplete': UI.initialResize,
'onFBResize': UI.updateSessionSize, 'onFBResize': UI.updateSessionSize,
'onDesktopName': UI.updateDesktopName}); 'onDesktopName': UI.updateDesktopName,
'ast2100_onVideoSettingsChanged': UI.ast2100_handleVideoSettingsChanged
});
return true; return true;
} catch (exc) { } catch (exc) {
var msg = "Unable to create RFB client -- " + exc; var msg = "Unable to create RFB client -- " + exc;
@ -436,7 +444,9 @@ var UI;
document.documentElement.classList.add("noVNC_disconnecting"); document.documentElement.classList.add("noVNC_disconnecting");
break; break;
case 'disconnected': case 'disconnected':
UI.connected = false;
UI.showStatus(_("Disconnected")); UI.showStatus(_("Disconnected"));
UI.ast2100_reset();
break; break;
default: default:
msg = "Invalid UI state"; msg = "Invalid UI state";
@ -844,6 +854,15 @@ var UI;
UI.saveSetting('repeaterID'); UI.saveSetting('repeaterID');
UI.saveSetting('logging'); 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 // Settings with immediate (non-connected related) effect
WebUtil.init_logging(UI.getSetting('logging')); WebUtil.init_logging(UI.getSetting('logging'));
UI.updateViewClip(); UI.updateViewClip();
@ -963,6 +982,90 @@ var UI;
/* ------^------- /* ------^-------
* /XVP * /XVP
* ============== * ==============
* 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 * CLIPBOARD
* ------v------*/ * ------v------*/

View File

@ -30,6 +30,7 @@ var Ast2100Decoder;
Ast2100Decoder = function (defaults) { Ast2100Decoder = function (defaults) {
this._blitCallback = defaults.blitCallback; this._blitCallback = defaults.blitCallback;
this._videoSettingsChangedCallback = defaults.videoSettingsChangedCallback;
this._frame_width = defaults.width; this._frame_width = defaults.width;
this._frame_height = defaults.height; this._frame_height = defaults.height;
@ -158,6 +159,18 @@ var Ast2100Decoder;
this._frame_height = height; 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) { decode: function (data) {
var mcuIdx = 0; var mcuIdx = 0;
@ -174,10 +187,13 @@ var Ast2100Decoder;
var quantTableSelectorLuma = data[0]; // 0 <= x <= 0xB var quantTableSelectorLuma = data[0]; // 0 <= x <= 0xB
var quantTableSelectorChroma = data[1]; // 0 <= x <= 0xB var quantTableSelectorChroma = data[1]; // 0 <= x <= 0xB
var subsamplingMode = (data[2] << 8) | data[3]; // 422u or 444u var subsamplingMode = (data[2] << 8) | data[3]; // 422u or 444u
var changedSettings = false;
if (this.subsamplingMode != subsamplingMode) { if (this.subsamplingMode != subsamplingMode) {
if (verboseVideoSettings) if (verboseVideoSettings)
console.log('decode(): new subsampling mode: '+subsamplingMode); console.log('decode(): new subsampling mode: '+subsamplingMode);
this.subsamplingMode = subsamplingMode; this.subsamplingMode = subsamplingMode;
changedSettings = true;
} }
// The remainder of the stream is byte-swapped in four-byte chunks. // 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)); console.log('decode(): loading new luma quant table: '+fmt_u8(quantTableSelectorLuma));
this._loadQuantTable(0, ATEN_QT_LUMA[quantTableSelectorLuma]); this._loadQuantTable(0, ATEN_QT_LUMA[quantTableSelectorLuma]);
this._loadedQuantTables[0] = quantTableSelectorLuma; this._loadedQuantTables[0] = quantTableSelectorLuma;
changedSettings = true;
} }
if (quantTableSelectorChroma != this._loadedQuantTables[1]) { if (quantTableSelectorChroma != this._loadedQuantTables[1]) {
if (!inRangeIncl(quantTableSelectorChroma, 0, 0xB)) if (!inRangeIncl(quantTableSelectorChroma, 0, 0xB))
@ -201,10 +218,15 @@ var Ast2100Decoder;
console.log('decode(): loading new chroma quant table: '+fmt_u8(quantTableSelectorChroma)); console.log('decode(): loading new chroma quant table: '+fmt_u8(quantTableSelectorChroma));
this._loadQuantTable(1, ATEN_QT_CHROMA[quantTableSelectorChroma]); this._loadQuantTable(1, ATEN_QT_CHROMA[quantTableSelectorChroma]);
this._loadedQuantTables[1] = quantTableSelectorChroma; this._loadedQuantTables[1] = quantTableSelectorChroma;
changedSettings = true;
} }
if (this.subsamplingMode != 422 && this.subsamplingMode != 444) if (this.subsamplingMode != 422 && this.subsamplingMode != 444)
throw 'Unexpected value for subsamplingMode: 0x' + fmt_u16(this.subsamplingMode); 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. // The remainder of the stream is byte-swapped in four-byte chunks. BitStream takes care of this.
this._stream = new BitStream({data: data}); this._stream = new BitStream({data: data});
this._stream.skip(16); this._stream.skip(16);

View File

@ -107,6 +107,8 @@
subrects: 0, // RRE subrects: 0, // RRE
lines: 0, // RAW lines: 0, // RAW
tiles: 0, // HEXTILE tiles: 0, // HEXTILE
aten_len: -1, // ATEN
aten_type: -1, // ATEN
bytes: 0, bytes: 0,
x: 0, x: 0,
y: 0, y: 0,
@ -170,6 +172,12 @@
'wsProtocols': ['binary'], // Protocols to use in the WebSocket connection 'wsProtocols': ['binary'], // Protocols to use in the WebSocket connection
'repeaterID': '', // [UltraVNC] RepeaterID to connect to 'repeaterID': '', // [UltraVNC] RepeaterID to connect to
'viewportDrag': false, // Move the viewport on mouse drags '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 // Callback functions
'onUpdateState': function () { }, // onUpdateState(rfb, state, oldstate): connection state change 'onUpdateState': function () { }, // onUpdateState(rfb, state, oldstate): connection state change
@ -182,7 +190,8 @@
'onFBUComplete': function () { }, // onFBUComplete(rfb, fbu): RFB FBU received and processed 'onFBUComplete': function () { }, // onFBUComplete(rfb, fbu): RFB FBU received and processed
'onFBResize': function () { }, // onFBResize(rfb, width, height): frame buffer resized 'onFBResize': function () { }, // onFBResize(rfb, width, height): frame buffer resized
'onDesktopName': function () { }, // onDesktopName(rfb, name): desktop name received '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 // main setup
@ -312,12 +321,19 @@
if (this._rfb_connection_state !== 'connected' || this._view_only) { return false; } if (this._rfb_connection_state !== 'connected' || this._view_only) { return false; }
Util.Info("Sending Ctrl-Alt-Del"); Util.Info("Sending Ctrl-Alt-Del");
RFB.messages.keyEvent(this._sock, KeyTable.XK_Control_L, 1); var keyEvent;
RFB.messages.keyEvent(this._sock, KeyTable.XK_Alt_L, 1); if (this._rfb_atenikvm) {
RFB.messages.keyEvent(this._sock, KeyTable.XK_Delete, 1); keyEvent = RFB.messages.atenKeyEvent;
RFB.messages.keyEvent(this._sock, KeyTable.XK_Delete, 0); } else {
RFB.messages.keyEvent(this._sock, KeyTable.XK_Alt_L, 0); keyEvent = RFB.messages.keyEvent;
RFB.messages.keyEvent(this._sock, KeyTable.XK_Control_L, 0); }
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; return true;
}, },
@ -344,13 +360,21 @@
// followed by an up key. // followed by an up key.
sendKey: function (keysym, down) { sendKey: function (keysym, down) {
if (this._rfb_connection_state !== 'connected' || this._view_only) { return false; } 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') { if (typeof down !== 'undefined') {
Util.Info("Sending keysym (" + (down ? "down" : "up") + "): " + keysym); 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 { } else {
Util.Info("Sending keysym (down + up): " + keysym); Util.Info("Sending keysym (down + up): " + keysym);
RFB.messages.keyEvent(this._sock, keysym, 1); keyEvent(this._sock, keysym, 1);
RFB.messages.keyEvent(this._sock, keysym, 0); keyEvent(this._sock, keysym, 0);
} }
return true; return true;
}, },
@ -685,8 +709,12 @@
} }
} else { } else {
keysym = keyevent.keysym.keysym; keysym = keyevent.keysym.keysym;
if (this._rfb_atenikvm) {
RFB.messages.atenKeyEvent(this._sock, keysym, down);
} else {
RFB.messages.keyEvent(this._sock, keysym, down); RFB.messages.keyEvent(this._sock, keysym, down);
} }
}
}, },
_handleMouseButton: function (x, y, down, bmask) { _handleMouseButton: function (x, y, down, bmask) {
@ -709,8 +737,12 @@
// If the viewport didn't actually move, then treat as a mouse click event // 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 // 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) { if (!this._viewportHasMoved && !this._view_only) {
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); RFB.messages.pointerEvent(this._sock, this._display.absX(x), this._display.absY(y), bmask);
} }
}
this._viewportHasMoved = false; this._viewportHasMoved = false;
} }
} }
@ -718,7 +750,11 @@
if (this._view_only) { return; } // View only, skip mouse events if (this._view_only) { return; } // View only, skip mouse events
if (this._rfb_connection_state !== 'connected') { return; } if (this._rfb_connection_state !== 'connected') { return; }
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); RFB.messages.pointerEvent(this._sock, this._display.absX(x), this._display.absY(y), this._mouse_buttonMask);
}
}, },
_handleMouseMove: function (x, y) { _handleMouseMove: function (x, y) {
@ -745,7 +781,11 @@
if (this._view_only) { return; } // View only, skip mouse events if (this._view_only) { return; } // View only, skip mouse events
if (this._rfb_connection_state !== 'connected') { return; } if (this._rfb_connection_state !== 'connected') { return; }
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); RFB.messages.pointerEvent(this._sock, this._display.absX(x), this._display.absY(y), this._mouse_buttonMask);
}
}, },
// Message Handlers // Message Handlers
@ -1233,13 +1273,6 @@
this._pixelFormat.red_shift = 10; this._pixelFormat.red_shift = 10;
this._pixelFormat.green_shift = 5; this._pixelFormat.green_shift = 5;
this._pixelFormat.blue_shift = 0; 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) if (this._convert_color)
@ -1771,6 +1804,9 @@
['wsProtocols', 'rw', 'arr'], // Protocols to use in the WebSocket connection ['wsProtocols', 'rw', 'arr'], // Protocols to use in the WebSocket connection
['repeaterID', 'rw', 'str'], // [UltraVNC] RepeaterID to connect to ['repeaterID', 'rw', 'str'], // [UltraVNC] RepeaterID to connect to
['viewportDrag', 'rw', 'bool'], // Move the viewport on mouse drags ['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 // Callback functions
['onUpdateState', 'rw', 'func'], // onUpdateState(rfb, state, oldstate): connection state change ['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 ['onFBUComplete', 'rw', 'func'], // onFBUComplete(rfb, fbu): RFB FBU received and processed
['onFBResize', 'rw', 'func'], // onFBResize(rfb, width, height): frame buffer resized ['onFBResize', 'rw', 'func'], // onFBResize(rfb, width, height): frame buffer resized
['onDesktopName', 'rw', 'func'], // onDesktopName(rfb, name): desktop name received ['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) { RFB.prototype.set_local_cursor = function (cursor) {
@ -2974,6 +3012,7 @@
} }
if (!this._aten_ast2100_dec) { if (!this._aten_ast2100_dec) {
var _rfb = this;
var display = this._display; var display = this._display;
this._aten_ast2100_dec = new Ast2100Decoder({ this._aten_ast2100_dec = new Ast2100Decoder({
width: this._FBU.width, width: this._FBU.width,
@ -2985,6 +3024,9 @@
// the block to be enqueued instead of being blitted right // the block to be enqueued instead of being blitted right
// away via a call to _rgbxImageData(). // away via a call to _rgbxImageData().
display.blitRgbxImage(x, y, width, height, buf, 0, true); display.blitRgbxImage(x, y, width, height, buf, 0, true);
},
videoSettingsChangedCallback: function (settings) {
_rfb._ast2100_onVideoSettingsChanged(settings);
} }
}); });
} }

View File

@ -237,6 +237,38 @@
</li> </li>
</ul></div> </ul></div>
</li> </li>
<!--
AST2100 (0x57) video encoding only:
- Video quality slider (quantization table selector)
- Subsampling mode
-->
<div id="noVNC_ast2100_settings" class="">
<li><hr></li>
<li>
<div class="noVNC_expander">AST2100 Settings</div>
<div><ul>
<li>
<label>Video quality</label>
<div class="slider-area">
low
<input type="range" min="0" max="11" class="slider-bar" id="noVNC_setting_ast2100_quality" />
high
</div>
</li>
<li><hr></li>
<li>
<label>Subsampling mode</label>
<select id="noVNC_setting_ast2100_subsampling">
<option value="444">4:4:4 (no subsampling; higher quality)</option>
<option value="422">4:2:0 (chroma subsampling; lower quality)</option>
</select>
</li>
</ul></div>
</li>
</div>
<li><hr></li> <li><hr></li>
<li> <li>
<input type="button" id="noVNC_settings_apply" value="Apply" class="noVNC_submit" /> <input type="button" id="noVNC_settings_apply" value="Apply" class="noVNC_submit" />

View File

@ -267,6 +267,8 @@
'shared': WebUtil.getConfigVar('shared', true), 'shared': WebUtil.getConfigVar('shared', true),
'view_only': WebUtil.getConfigVar('view_only', false), 'view_only': WebUtil.getConfigVar('view_only', false),
'onNotification': notification, 'onNotification': notification,
'ast2100_quality': WebUtil.getConfigVar('ast2100_quality', -1),
'ast2100_subsamplingMode': WebUtil.getConfigVar('ast2100_subsamplingMode', -1),
'onUpdateState': updateState, 'onUpdateState': updateState,
'onDisconnected': disconnected, 'onDisconnected': disconnected,
'onXvpInit': xvpInit, 'onXvpInit': xvpInit,