Add noVNC_setting_crop_rect to show only a region of the fb

This commit is contained in:
Daniel Hammerschmidt 2025-09-08 22:44:41 +02:00
parent d49d2b366a
commit 4d4b29a14f
3 changed files with 130 additions and 19 deletions

View File

@ -179,6 +179,7 @@ const UI = {
UI.initSetting('autoconnect', false); UI.initSetting('autoconnect', false);
UI.initSetting('view_clip', false); UI.initSetting('view_clip', false);
UI.initSetting('resize', 'off'); UI.initSetting('resize', 'off');
UI.initSetting('crop_rect');
UI.initSetting('quality', 6); UI.initSetting('quality', 6);
UI.initSetting('compression', 2); UI.initSetting('compression', 2);
UI.initSetting('shared', true); UI.initSetting('shared', true);
@ -360,6 +361,8 @@ const UI = {
UI.addSettingChangeHandler('resize'); UI.addSettingChangeHandler('resize');
UI.addSettingChangeHandler('resize', UI.applyResizeMode); UI.addSettingChangeHandler('resize', UI.applyResizeMode);
UI.addSettingChangeHandler('resize', UI.updateViewClip); UI.addSettingChangeHandler('resize', UI.updateViewClip);
UI.addSettingChangeHandler('crop_rect');
UI.addSettingChangeHandler('crop_rect', UI.updateCropRect);
UI.addSettingChangeHandler('quality'); UI.addSettingChangeHandler('quality');
UI.addSettingChangeHandler('quality', UI.updateQuality); UI.addSettingChangeHandler('quality', UI.updateQuality);
UI.addSettingChangeHandler('compression'); UI.addSettingChangeHandler('compression');
@ -464,6 +467,17 @@ const UI = {
.classList.remove('noVNC_open'); .classList.remove('noVNC_open');
}, },
showConnectedStatus(e) {
let msg;
if (UI.getSetting('encrypt')) {
msg = _("Connected (encrypted) to ") + UI.desktopName;
} else {
msg = _("Connected (unencrypted) to ") + UI.desktopName;
}
msg += ' [' + UI.rfb.cropRect + ']';
UI.showStatus(msg);
},
showStatus(text, statusType, time) { showStatus(text, statusType, time) {
const statusElem = document.getElementById('noVNC_status'); const statusElem = document.getElementById('noVNC_status');
@ -1095,9 +1109,11 @@ const UI = {
UI.rfb.addEventListener("clipboard", UI.clipboardReceive); UI.rfb.addEventListener("clipboard", UI.clipboardReceive);
UI.rfb.addEventListener("bell", UI.bell); UI.rfb.addEventListener("bell", UI.bell);
UI.rfb.addEventListener("desktopname", UI.updateDesktopName); UI.rfb.addEventListener("desktopname", UI.updateDesktopName);
UI.rfb.addEventListener("croprectchanged", UI.showConnectedStatus);
UI.rfb.clipViewport = UI.getSetting('view_clip'); UI.rfb.clipViewport = UI.getSetting('view_clip');
UI.rfb.scaleViewport = UI.getSetting('resize') === 'scale'; UI.rfb.scaleViewport = UI.getSetting('resize') === 'scale';
UI.rfb.resizeSession = UI.getSetting('resize') === 'remote'; UI.rfb.resizeSession = UI.getSetting('resize') === 'remote';
UI.rfb.cropRect = UI.getSetting('crop_rect');
UI.rfb.qualityLevel = parseInt(UI.getSetting('quality')); UI.rfb.qualityLevel = parseInt(UI.getSetting('quality'));
UI.rfb.compressionLevel = parseInt(UI.getSetting('compression')); UI.rfb.compressionLevel = parseInt(UI.getSetting('compression'));
UI.rfb.showDotCursor = UI.getSetting('show_dot'); UI.rfb.showDotCursor = UI.getSetting('show_dot');
@ -1144,14 +1160,7 @@ const UI = {
connectFinished(e) { connectFinished(e) {
UI.connected = true; UI.connected = true;
UI.inhibitReconnect = false; UI.inhibitReconnect = false;
UI.showConnectedStatus();
let msg;
if (UI.getSetting('encrypt')) {
msg = _("Connected (encrypted) to ") + UI.desktopName;
} else {
msg = _("Connected (unencrypted) to ") + UI.desktopName;
}
UI.showStatus(msg);
UI.updateVisualState('connected'); UI.updateVisualState('connected');
// Do this last because it can only be used on rendered elements // Do this last because it can only be used on rendered elements
@ -1438,6 +1447,12 @@ const UI = {
viewDragButton.disabled = !UI.rfb.clippingViewport; viewDragButton.disabled = !UI.rfb.clippingViewport;
}, },
updateCropRect() {
if (!UI.connected) return;
UI.rfb.cropRect = UI.getSetting('crop_rect');
},
/* ------^------- /* ------^-------
* /VIEWDRAG * /VIEWDRAG
* ============== * ==============

View File

@ -87,6 +87,15 @@ const extendedClipboardActionPeek = 1 << 26;
const extendedClipboardActionNotify = 1 << 27; const extendedClipboardActionNotify = 1 << 27;
const extendedClipboardActionProvide = 1 << 28; const extendedClipboardActionProvide = 1 << 28;
// [GEOMETRY SPECIFICATIONS](https://www.x.org/releases/X11R7.7/doc/man/man7/X.7.xhtml#heading7)
const geometryRegExp = new RegExp([
'^',
'(?<width>0|[1-9][0-9]*)', 'x', '(?<height>0|[1-9][0-9]*)',
'(?:', '[+](?<left>0|[1-9][0-9]*)', '|', '[-](?<right>0|[1-9][0-9]*)', ')',
'(?:', '[+](?<top>0|[1-9][0-9]*)', '|', '[-](?<bottom>0|[1-9][0-9]*)', ')',
'$',
].join(''));
export default class RFB extends EventTargetMixin { export default class RFB extends EventTargetMixin {
constructor(target, urlOrChannel, options) { constructor(target, urlOrChannel, options) {
if (!target) { if (!target) {
@ -136,6 +145,19 @@ export default class RFB extends EventTargetMixin {
this._fbWidth = 0; this._fbWidth = 0;
this._fbHeight = 0; this._fbHeight = 0;
this._cropRect = {
geometry: undefined,
width: 0,
height: 0,
left: 0,
right: undefined,
top: 0,
bottom: undefined,
// we keep a redundant copy of fbWidth and fbHeight here
// to avoid conflicts with existing code
fbWidth: undefined,
fbHeight: undefined,
};
this._fbName = ""; this._fbName = "";
@ -356,6 +378,45 @@ export default class RFB extends EventTargetMixin {
} }
} }
get cropRect() {
const { width, height, left, right, top, bottom, fbWidth, fbHeight } = this._cropRect;
return `${width}x${height}${
!right ? `+${left}` : `-${right}`
}${
!bottom ? `+${top}` : `-${bottom}`
}${
width === fbWidth && height === fbHeight ? '' : ` (${fbWidth}x${fbHeight})`
}`;
}
set cropRect(geometry) {
const rect = Object.assign( this._cropRect, { geometry } );
const { fbWidth, fbHeight } = rect;
if (geometry && (geometry = geometry.match(geometryRegExp)?.groups)) {
Object.assign(rect, Object.fromEntries(
Object.entries(geometry).map(([k, v]) => ([k, v === undefined ? undefined : +v]))
));
} else { // empty or invalid geometry
Object.assign(rect, {
width: 0,
height: 0,
left: 0,
right: undefined,
top: 0,
bottom: undefined,
});
}
const { width, height, left, top } = this._updateCropRect(fbWidth, fbHeight);
if (width && height) {
this._resize(width, height);
if (this._rfbConnectionState === 'connected') {
RFB.messages.fbUpdateRequest(this._sock, false, left, top, width, height);
this.dispatchEvent(new CustomEvent('croprectchanged', {
detail: this.cropRect,
}));
}
}
}
get resizeSession() { return this._resizeSession; } get resizeSession() { return this._resizeSession; }
set resizeSession(resize) { set resizeSession(resize) {
this._resizeSession = resize; this._resizeSession = resize;
@ -783,6 +844,35 @@ export default class RFB extends EventTargetMixin {
this._fixScrollbars(); this._fixScrollbars();
} }
_updateCropRect(fbWidth, fbHeight) {
const rect = this._cropRect;
const { fbWidth: prevFbWidth, fbHeight: prevFbHeight, geometry } = rect;
let { width, height, left, right, top, bottom } = Object.assign(rect, { fbWidth, fbHeight });
function compute(width, left, right, maxWidth) {
if (width === 0 || width > maxWidth) { width = maxWidth; }
if (right === undefined) {
if (left + width > maxWidth) {
left = maxWidth - width;
}
} else {
if (right + width > maxWidth) {
right = 0;
left = 0;
} else {
left = maxWidth - (right + width);
}
}
return [ width, left, right ];
}
[ width, left, right ] = compute(width, left, right, fbWidth);
[ height, top, bottom ] = compute(height, top, bottom, fbHeight);
Object.assign(rect, { width, height, left, right, top, bottom });
if (prevFbWidth !== fbWidth || prevFbHeight !== fbHeight) {
this.cropRect = geometry;
}
return rect;
}
// Requests a change of remote desktop size. This message is an extension // Requests a change of remote desktop size. This message is an extension
// and may only be sent if we have received an ExtendedDesktopSize message // and may only be sent if we have received an ExtendedDesktopSize message
_requestRemoteResize() { _requestRemoteResize() {
@ -2141,8 +2231,9 @@ export default class RFB extends EventTargetMixin {
if (this._sock.rQwait("server initialization", 24)) { return false; } if (this._sock.rQwait("server initialization", 24)) { return false; }
/* Screen size */ /* Screen size */
const width = this._sock.rQshift16(); const fbWidth = this._sock.rQshift16();
const height = this._sock.rQshift16(); const fbHeight = this._sock.rQshift16();
const { width, height, left, top } = this._updateCropRect(fbWidth, fbHeight);
/* PIXEL_FORMAT */ /* PIXEL_FORMAT */
const bpp = this._sock.rQshift8(); const bpp = this._sock.rQshift8();
@ -2219,7 +2310,7 @@ export default class RFB extends EventTargetMixin {
RFB.messages.pixelFormat(this._sock, this._fbDepth, true); RFB.messages.pixelFormat(this._sock, this._fbDepth, true);
this._sendEncodings(); this._sendEncodings();
RFB.messages.fbUpdateRequest(this._sock, false, 0, 0, this._fbWidth, this._fbHeight); RFB.messages.fbUpdateRequest(this._sock, false, left, top, width, height);
this._updateConnectionState('connected'); this._updateConnectionState('connected');
return true; return true;
@ -2569,8 +2660,8 @@ export default class RFB extends EventTargetMixin {
case 0: // FramebufferUpdate case 0: // FramebufferUpdate
ret = this._framebufferUpdate(); ret = this._framebufferUpdate();
if (ret && !this._enabledContinuousUpdates) { if (ret && !this._enabledContinuousUpdates) {
RFB.messages.fbUpdateRequest(this._sock, true, 0, 0, const { left, top, width, height } = this._cropRect;
this._fbWidth, this._fbHeight); RFB.messages.fbUpdateRequest(this._sock, true, left, top, width, height);
} }
return ret; return ret;
@ -2641,8 +2732,9 @@ export default class RFB extends EventTargetMixin {
if (this._sock.rQwait("rect header", 12)) { return false; } if (this._sock.rQwait("rect header", 12)) { return false; }
/* New FramebufferUpdate */ /* New FramebufferUpdate */
this._FBU.x = this._sock.rQshift16(); const { left: offsetX, top: offsetY } = this._cropRect;
this._FBU.y = this._sock.rQshift16(); this._FBU.x = this._sock.rQshift16() - offsetX;
this._FBU.y = this._sock.rQshift16() - offsetY;
this._FBU.width = this._sock.rQshift16(); this._FBU.width = this._sock.rQshift16();
this._FBU.height = this._sock.rQshift16(); this._FBU.height = this._sock.rQshift16();
this._FBU.encoding = this._sock.rQshift32(); this._FBU.encoding = this._sock.rQshift32();
@ -2683,7 +2775,7 @@ export default class RFB extends EventTargetMixin {
return this._handleDesktopName(); return this._handleDesktopName();
case encodings.pseudoEncodingDesktopSize: case encodings.pseudoEncodingDesktopSize:
this._resize(this._FBU.width, this._FBU.height); this._updateCropRect(this._FBU.width, this._FBU.height);
return true; return true;
case encodings.pseudoEncodingExtendedDesktopSize: case encodings.pseudoEncodingExtendedDesktopSize:
@ -2994,9 +3086,9 @@ export default class RFB extends EventTargetMixin {
_updateContinuousUpdates() { _updateContinuousUpdates() {
if (!this._enabledContinuousUpdates) { return; } if (!this._enabledContinuousUpdates) { return; }
// TODO: test required
RFB.messages.enableContinuousUpdates(this._sock, true, 0, 0, const { left, top, width, height } = this._cropRect;
this._fbWidth, this._fbHeight); RFB.messages.enableContinuousUpdates(this._sock, true, left, top, width, height);
} }
// Handle resize-messages from the server // Handle resize-messages from the server

View File

@ -235,6 +235,10 @@
<option value="remote">Remote resizing</option> <option value="remote">Remote resizing</option>
</select> </select>
</li> </li>
<li>
<label for="noVNC_setting_crop_rect">Crop:</label>
<input id="noVNC_setting_crop_rect">
</li>
<li><hr></li> <li><hr></li>
<li> <li>
<div class="noVNC_expander">Advanced</div> <div class="noVNC_expander">Advanced</div>