Re-apply https://github.com/replit/noVNC/pull/3 on top of v1.5.0
This commit is contained in:
parent
7fcf9dcfe0
commit
dfe8e64078
|
|
@ -0,0 +1,92 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
width="25"
|
||||||
|
height="25"
|
||||||
|
viewBox="0 0 25 25"
|
||||||
|
id="svg2"
|
||||||
|
version="1.1"
|
||||||
|
inkscape:version="1.0.1 (3bc2e813f5, 2020-09-07)"
|
||||||
|
sodipodi:docname="audio.svg"
|
||||||
|
inkscape:export-filename="/home/ossman/devel/noVNC/images/keyboard.png"
|
||||||
|
inkscape:export-xdpi="90"
|
||||||
|
inkscape:export-ydpi="90">
|
||||||
|
<defs
|
||||||
|
id="defs4" />
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="base"
|
||||||
|
pagecolor="#717171"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1.0"
|
||||||
|
inkscape:pageopacity="0"
|
||||||
|
inkscape:pageshadow="2"
|
||||||
|
inkscape:zoom="22.627417"
|
||||||
|
inkscape:cx="-14.015068"
|
||||||
|
inkscape:cy="2.7882772"
|
||||||
|
inkscape:document-units="px"
|
||||||
|
inkscape:current-layer="layer1"
|
||||||
|
showgrid="false"
|
||||||
|
units="px"
|
||||||
|
inkscape:snap-bbox="true"
|
||||||
|
inkscape:bbox-paths="true"
|
||||||
|
inkscape:bbox-nodes="true"
|
||||||
|
inkscape:snap-bbox-edge-midpoints="true"
|
||||||
|
inkscape:snap-bbox-midpoints="false"
|
||||||
|
inkscape:window-width="2488"
|
||||||
|
inkscape:window-height="1376"
|
||||||
|
inkscape:window-x="72"
|
||||||
|
inkscape:window-y="27"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:object-paths="true"
|
||||||
|
inkscape:snap-intersection-paths="true"
|
||||||
|
inkscape:object-nodes="true"
|
||||||
|
inkscape:snap-midpoints="true"
|
||||||
|
inkscape:snap-smooth-nodes="true"
|
||||||
|
inkscape:document-rotation="0"
|
||||||
|
showguides="true"
|
||||||
|
inkscape:guide-bbox="true">
|
||||||
|
<inkscape:grid
|
||||||
|
type="xygrid"
|
||||||
|
id="grid4136" />
|
||||||
|
</sodipodi:namedview>
|
||||||
|
<metadata
|
||||||
|
id="metadata7">
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work
|
||||||
|
rdf:about="">
|
||||||
|
<dc:format>image/svg+xml</dc:format>
|
||||||
|
<dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||||
|
<dc:title></dc:title>
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
<g
|
||||||
|
inkscape:label="Layer 1"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer1"
|
||||||
|
transform="translate(0,-1027.3622)">
|
||||||
|
<path
|
||||||
|
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-variant-east-asian:normal;font-feature-settings:normal;font-variation-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;shape-margin:0;inline-size:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.58527;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate;stop-color:#000000;stop-opacity:1;opacity:1"
|
||||||
|
d="M 17.726562 1033.3848 A 0.79263502 0.79263502 0 0 0 17.142578 1033.6172 A 0.79263502 0.79263502 0 0 0 17.142578 1034.7383 C 18.501685 1036.0976 19.263676 1037.9389 19.263672 1039.8613 C 19.263677 1041.7837 18.501684 1043.627 17.142578 1044.9863 A 0.79263502 0.79263502 0 0 0 17.142578 1046.1074 A 0.79263502 0.79263502 0 0 0 18.263672 1046.1074 C 19.919622 1044.4512 20.849615 1042.2035 20.849609 1039.8613 C 20.849615 1037.5192 19.919622 1035.2734 18.263672 1033.6172 A 0.79263502 0.79263502 0 0 0 17.726562 1033.3848 z "
|
||||||
|
id="path1015" />
|
||||||
|
<path
|
||||||
|
id="rect854"
|
||||||
|
style="fill:#ffffff;stroke-width:0;stroke-dasharray:none"
|
||||||
|
d="m 11.211844,1034.336 a 0.79268394,0.79268394 0 0 0 -0.469698,0.1673 c -0.0022,0 -0.0059,9e-4 -0.0082,0 a 0.79268394,0.79268394 0 0 0 -0.0082,0.01 l -3.876241,3.2177 H 4.9546099 a 0.79268394,0.79268394 0 0 1 -0.0027,0 0.79268394,0.79268394 0 0 0 -0.0081,0 0.79268394,0.79268394 0 0 0 -0.7936268,0.7909 0.79268394,0.79268394 0 0 0 0,0.014 v 2.6455 c 0,0 0,0 0,0 a 0.79268394,0.79268394 0 0 0 0,0.01 0.79268394,0.79268394 0 0 0 0.7936268,0.7936 0.79268394,0.79268394 0 0 0 0.013503,0 h 1.8922867 l 3.7494814,3.1124 0.0054,0.01 a 0.79268394,0.79268394 0 0 0 0.610068,0.2862 0.79268394,0.79268394 0 0 0 0.790926,-0.7586 c 0.0026,-0.015 0.01355,-0.03 0.01355,-0.046 v -9.4561 a 0.79268394,0.79268394 0 0 0 -0.793627,-0.7936 0.79268394,0.79268394 0 0 0 -0.01355,0 z" />
|
||||||
|
<path
|
||||||
|
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-variant-east-asian:normal;font-feature-settings:normal;font-variation-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;shape-margin:0;inline-size:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.58527;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate;stop-color:#000000;stop-opacity:1;opacity:1"
|
||||||
|
d="M 15.794922 1035.3164 A 0.79263502 0.79263502 0 0 0 15.210938 1035.5488 A 0.79263502 0.79263502 0 0 0 15.210938 1036.6699 C 16.057983 1037.517 16.533213 1038.6634 16.533203 1039.8613 C 16.53319 1041.0592 16.057962 1042.2077 15.210938 1043.0547 A 0.79263502 0.79263502 0 0 0 15.210938 1044.1758 A 0.79263502 0.79263502 0 0 0 16.332031 1044.1758 C 17.475916 1043.0319 18.119123 1041.479 18.119141 1039.8613 C 18.119154 1038.2436 17.475944 1036.6928 16.332031 1035.5488 A 0.79263502 0.79263502 0 0 0 15.794922 1035.3164 z "
|
||||||
|
id="path1019" />
|
||||||
|
<path
|
||||||
|
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-variant-east-asian:normal;font-feature-settings:normal;font-variation-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;shape-margin:0;inline-size:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.58527;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate;stop-color:#000000;stop-opacity:1;opacity:1"
|
||||||
|
d="M 13.808594 1037.3027 A 0.79263502 0.79263502 0 0 0 13.224609 1037.5352 A 0.79263502 0.79263502 0 0 0 13.224609 1038.6562 C 13.544806 1038.9763 13.724616 1039.4089 13.724609 1039.8613 C 13.724598 1040.3137 13.544789 1040.7483 13.224609 1041.0684 A 0.79263502 0.79263502 0 0 0 13.224609 1042.1895 A 0.79263502 0.79263502 0 0 0 14.345703 1042.1895 C 14.962816 1041.5727 15.310525 1040.7338 15.310547 1039.8613 C 15.31056 1038.9888 14.962848 1038.152 14.345703 1037.5352 A 0.79263502 0.79263502 0 0 0 13.808594 1037.3027 z "
|
||||||
|
id="path1023" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 8.7 KiB |
46
app/ui.js
46
app/ui.js
|
|
@ -232,6 +232,9 @@ const UI = {
|
||||||
document.getElementById("noVNC_view_drag_button")
|
document.getElementById("noVNC_view_drag_button")
|
||||||
.addEventListener('click', UI.toggleViewDrag);
|
.addEventListener('click', UI.toggleViewDrag);
|
||||||
|
|
||||||
|
document.getElementById("noVNC_audio_button")
|
||||||
|
.addEventListener('click', UI.toggleEnableAudio);
|
||||||
|
|
||||||
document.getElementById("noVNC_control_bar_handle")
|
document.getElementById("noVNC_control_bar_handle")
|
||||||
.addEventListener('mousedown', UI.controlbarHandleMouseDown);
|
.addEventListener('mousedown', UI.controlbarHandleMouseDown);
|
||||||
document.getElementById("noVNC_control_bar_handle")
|
document.getElementById("noVNC_control_bar_handle")
|
||||||
|
|
@ -448,7 +451,7 @@ const UI = {
|
||||||
UI.enableSetting('port');
|
UI.enableSetting('port');
|
||||||
UI.enableSetting('path');
|
UI.enableSetting('path');
|
||||||
UI.enableSetting('repeaterID');
|
UI.enableSetting('repeaterID');
|
||||||
UI.updatePowerButton();
|
UI.updateCapabilities();
|
||||||
UI.keepControlbar();
|
UI.keepControlbar();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -891,6 +894,24 @@ const UI = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
updateCapabilities() {
|
||||||
|
UI.updatePowerButton();
|
||||||
|
UI.updateAudioButton();
|
||||||
|
},
|
||||||
|
|
||||||
|
updateAudioButton() {
|
||||||
|
if (UI.connected &&
|
||||||
|
UI.rfb.capabilities.audio) {
|
||||||
|
document.getElementById('noVNC_audio_button')
|
||||||
|
.classList.remove("noVNC_hidden");
|
||||||
|
document.getElementById('noVNC_audio_button')
|
||||||
|
.classList.remove("noVNC_selected");
|
||||||
|
} else {
|
||||||
|
document.getElementById('noVNC_audio_button')
|
||||||
|
.classList.add("noVNC_hidden");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
/* ------^-------
|
/* ------^-------
|
||||||
* /SETTINGS
|
* /SETTINGS
|
||||||
* ==============
|
* ==============
|
||||||
|
|
@ -1059,7 +1080,7 @@ const UI = {
|
||||||
UI.rfb.addEventListener("credentialsrequired", UI.credentials);
|
UI.rfb.addEventListener("credentialsrequired", UI.credentials);
|
||||||
UI.rfb.addEventListener("securityfailure", UI.securityFailed);
|
UI.rfb.addEventListener("securityfailure", UI.securityFailed);
|
||||||
UI.rfb.addEventListener("clippingviewport", UI.updateViewDrag);
|
UI.rfb.addEventListener("clippingviewport", UI.updateViewDrag);
|
||||||
UI.rfb.addEventListener("capabilities", UI.updatePowerButton);
|
UI.rfb.addEventListener("capabilities", UI.updateCapabilities);
|
||||||
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);
|
||||||
|
|
@ -1722,6 +1743,27 @@ const UI = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
toggleEnableAudio() {
|
||||||
|
if (!UI.rfb) return;
|
||||||
|
|
||||||
|
if (!document.getElementById('noVNC_audio_button')
|
||||||
|
.classList.contains("noVNC_selected")) {
|
||||||
|
UI.rfb.enableAudio(
|
||||||
|
2,
|
||||||
|
MediaSource.isTypeSupported('audio/webm;codecs=opus') ?
|
||||||
|
RFB.audioCodecs.OpusWebM :
|
||||||
|
RFB.audioCodecs.MP3,
|
||||||
|
32 * 1024 // 32kbps
|
||||||
|
);
|
||||||
|
document.getElementById('noVNC_audio_button')
|
||||||
|
.classList.add("noVNC_selected");
|
||||||
|
} else {
|
||||||
|
UI.rfb.disableAudio();
|
||||||
|
document.getElementById('noVNC_audio_button')
|
||||||
|
.classList.remove("noVNC_selected");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
updateShowDotCursor() {
|
updateShowDotCursor() {
|
||||||
if (!UI.rfb) return;
|
if (!UI.rfb) return;
|
||||||
UI.rfb.showDotCursor = UI.getSetting('show_dot');
|
UI.rfb.showDotCursor = UI.getSetting('show_dot');
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ export const encodings = {
|
||||||
pseudoEncodingContinuousUpdates: -313,
|
pseudoEncodingContinuousUpdates: -313,
|
||||||
pseudoEncodingCompressLevel9: -247,
|
pseudoEncodingCompressLevel9: -247,
|
||||||
pseudoEncodingCompressLevel0: -256,
|
pseudoEncodingCompressLevel0: -256,
|
||||||
|
pseudoEncodingReplitAudio: 0x52706c41,
|
||||||
pseudoEncodingVMwareCursor: 0x574d5664,
|
pseudoEncodingVMwareCursor: 0x574d5664,
|
||||||
pseudoEncodingExtendedClipboard: 0xc0a1e5ce
|
pseudoEncodingExtendedClipboard: 0xc0a1e5ce
|
||||||
};
|
};
|
||||||
|
|
|
||||||
157
core/rfb.js
157
core/rfb.js
|
|
@ -13,6 +13,7 @@ import { encodeUTF8, decodeUTF8 } from './util/strings.js';
|
||||||
import { dragThreshold } from './util/browser.js';
|
import { dragThreshold } from './util/browser.js';
|
||||||
import { clientToElement } from './util/element.js';
|
import { clientToElement } from './util/element.js';
|
||||||
import { setCapture } from './util/events.js';
|
import { setCapture } from './util/events.js';
|
||||||
|
import AudioStream from './util/audio.js';
|
||||||
import EventTargetMixin from './util/eventtarget.js';
|
import EventTargetMixin from './util/eventtarget.js';
|
||||||
import Display from "./display.js";
|
import Display from "./display.js";
|
||||||
import Inflator from "./inflator.js";
|
import Inflator from "./inflator.js";
|
||||||
|
|
@ -137,7 +138,7 @@ export default class RFB extends EventTargetMixin {
|
||||||
|
|
||||||
this._fbName = "";
|
this._fbName = "";
|
||||||
|
|
||||||
this._capabilities = { power: false };
|
this._capabilities = { power: false, audio: false };
|
||||||
|
|
||||||
this._supportsFence = false;
|
this._supportsFence = false;
|
||||||
|
|
||||||
|
|
@ -149,6 +150,8 @@ export default class RFB extends EventTargetMixin {
|
||||||
this._screenFlags = 0;
|
this._screenFlags = 0;
|
||||||
|
|
||||||
this._qemuExtKeyEventSupported = false;
|
this._qemuExtKeyEventSupported = false;
|
||||||
|
this._replitAudioSupported = false;
|
||||||
|
this._replitAudioServerVersion = -1;
|
||||||
|
|
||||||
this._clipboardText = null;
|
this._clipboardText = null;
|
||||||
this._clipboardServerCapabilitiesActions = {};
|
this._clipboardServerCapabilitiesActions = {};
|
||||||
|
|
@ -195,6 +198,11 @@ export default class RFB extends EventTargetMixin {
|
||||||
this._gestureLastMagnitudeX = 0;
|
this._gestureLastMagnitudeX = 0;
|
||||||
this._gestureLastMagnitudeY = 0;
|
this._gestureLastMagnitudeY = 0;
|
||||||
|
|
||||||
|
// Audio state
|
||||||
|
this._audioEnabled = false;
|
||||||
|
this._audioMimeType = null;
|
||||||
|
this._audioStream = null;
|
||||||
|
|
||||||
// Bound event handlers
|
// Bound event handlers
|
||||||
this._eventHandlers = {
|
this._eventHandlers = {
|
||||||
focusCanvas: this._focusCanvas.bind(this),
|
focusCanvas: this._focusCanvas.bind(this),
|
||||||
|
|
@ -541,6 +549,25 @@ export default class RFB extends EventTargetMixin {
|
||||||
return this._display.toBlob(callback, type, quality);
|
return this._display.toBlob(callback, type, quality);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enableAudio(channels, codec, kbps) {
|
||||||
|
if (this._audioEnabled) { return; }
|
||||||
|
|
||||||
|
this._audioEnabled = true;
|
||||||
|
if (codec == RFB.audioCodecs.OpusWebM) {
|
||||||
|
this._audioMimeType = 'audio/webm;codecs=opus';
|
||||||
|
} else if (codec == RFB.audioCodecs.MP3) {
|
||||||
|
this._audioMimeType = 'audio/mpeg';
|
||||||
|
}
|
||||||
|
RFB.messages.ReplitAudioStartEncoder(this._sock, true, channels, codec, kbps);
|
||||||
|
}
|
||||||
|
|
||||||
|
disableAudio() {
|
||||||
|
if (!this._audioEnabled) { return; }
|
||||||
|
|
||||||
|
this._audioEnabled = false;
|
||||||
|
RFB.messages.ReplitAudioStartEncoder(this._sock, false, 0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
// ===== PRIVATE METHODS =====
|
// ===== PRIVATE METHODS =====
|
||||||
|
|
||||||
_connect() {
|
_connect() {
|
||||||
|
|
@ -2132,6 +2159,7 @@ export default class RFB extends EventTargetMixin {
|
||||||
encs.push(encodings.pseudoEncodingLastRect);
|
encs.push(encodings.pseudoEncodingLastRect);
|
||||||
encs.push(encodings.pseudoEncodingQEMUExtendedKeyEvent);
|
encs.push(encodings.pseudoEncodingQEMUExtendedKeyEvent);
|
||||||
encs.push(encodings.pseudoEncodingQEMULedEvent);
|
encs.push(encodings.pseudoEncodingQEMULedEvent);
|
||||||
|
encs.push(encodings.pseudoEncodingReplitAudio);
|
||||||
encs.push(encodings.pseudoEncodingExtendedDesktopSize);
|
encs.push(encodings.pseudoEncodingExtendedDesktopSize);
|
||||||
encs.push(encodings.pseudoEncodingXvp);
|
encs.push(encodings.pseudoEncodingXvp);
|
||||||
encs.push(encodings.pseudoEncodingFence);
|
encs.push(encodings.pseudoEncodingFence);
|
||||||
|
|
@ -2411,6 +2439,54 @@ export default class RFB extends EventTargetMixin {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_handleReplitAudioPseudoEncodingMsg() {
|
||||||
|
if (this._sock.rQwait("Repl.it audio message", 3, 1)) { return false; }
|
||||||
|
const submessage = this._sock.rQshift8();
|
||||||
|
const length = this._sock.rQshift16();
|
||||||
|
if (this._sock.rQwait("Repl.it audio message", length, 4)) { return false; }
|
||||||
|
|
||||||
|
switch (submessage) {
|
||||||
|
case 0: { // StartCapture response.
|
||||||
|
const enabled = this._sock.rQshift8() == 1;
|
||||||
|
|
||||||
|
if (enabled) {
|
||||||
|
this._audioStream = new AudioStream(this._audioMimeType);
|
||||||
|
RFB.messages.ReplitAudioEnableContinuousUpdate(this._sock);
|
||||||
|
} else if (this._audioStream != null) {
|
||||||
|
this._audioStream.close();
|
||||||
|
this._audioStream = null;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 1: { // AudioFrame response.
|
||||||
|
const keyframeAndTimestamp = this._sock.rQshift32();
|
||||||
|
const keyframe = (keyframeAndTimestamp & 0x80000000) != 0;
|
||||||
|
const timestamp = keyframeAndTimestamp & 0x7fffffff;
|
||||||
|
const data = this._sock.rQshiftBytes(length - 4);
|
||||||
|
if (this._audioStream != null) {
|
||||||
|
this._audioStream.queueAudioFrame(timestamp / 1000, keyframe, data);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 2: { // StartContinuousUpdates response.
|
||||||
|
const enabled = this._sock.rQshift8() == 1;
|
||||||
|
if (!enabled && this._audioStream != null) {
|
||||||
|
this._audioStream.close();
|
||||||
|
this._audioStream = null;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
this._fail("Illegal server Repl.it audio message (msg: " + submessage + ")");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
_handleXvpMsg() {
|
_handleXvpMsg() {
|
||||||
if (this._sock.rQwait("XVP version and message", 3, 1)) { return false; }
|
if (this._sock.rQwait("XVP version and message", 3, 1)) { return false; }
|
||||||
this._sock.rQskipBytes(1); // Padding
|
this._sock.rQskipBytes(1); // Padding
|
||||||
|
|
@ -2479,6 +2555,9 @@ export default class RFB extends EventTargetMixin {
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
|
case 245: // Repl.it audio message
|
||||||
|
return this._handleReplitAudioPseudoEncodingMsg();
|
||||||
|
|
||||||
case 248: // ServerFence
|
case 248: // ServerFence
|
||||||
return this._handleServerFenceMsg();
|
return this._handleServerFenceMsg();
|
||||||
|
|
||||||
|
|
@ -2557,6 +2636,9 @@ export default class RFB extends EventTargetMixin {
|
||||||
this._qemuExtKeyEventSupported = true;
|
this._qemuExtKeyEventSupported = true;
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
|
case encodings.pseudoEncodingReplitAudio:
|
||||||
|
return this._handleReplitAudioPseudoEncoding();
|
||||||
|
|
||||||
case encodings.pseudoEncodingDesktopName:
|
case encodings.pseudoEncodingDesktopName:
|
||||||
return this._handleDesktopName();
|
return this._handleDesktopName();
|
||||||
|
|
||||||
|
|
@ -2728,6 +2810,25 @@ export default class RFB extends EventTargetMixin {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_handleReplitAudioPseudoEncoding() {
|
||||||
|
if (this._sock.rQwait("Repl.it audio", 4)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const version = this._sock.rQshift16();
|
||||||
|
const codecs = this._sock.rQshift16();
|
||||||
|
|
||||||
|
if (this._sock.rQwait("Repl.it audio", 2 * codecs, 4)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
this._sock.rQshiftStr(2 * codecs);
|
||||||
|
|
||||||
|
this._replitAudioSupported = true;
|
||||||
|
this._replitAudioServerVersion = version;
|
||||||
|
this._setCapability("audio", true);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
_handleDesktopName() {
|
_handleDesktopName() {
|
||||||
if (this._sock.rQwait("DesktopName", 4)) {
|
if (this._sock.rQwait("DesktopName", 4)) {
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -2935,6 +3036,12 @@ export default class RFB extends EventTargetMixin {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Audio codecs
|
||||||
|
RFB.audioCodecs = {
|
||||||
|
OpusWebM: 0,
|
||||||
|
MP3: 1,
|
||||||
|
};
|
||||||
|
|
||||||
// Class Methods
|
// Class Methods
|
||||||
RFB.messages = {
|
RFB.messages = {
|
||||||
keyEvent(sock, keysym, down) {
|
keyEvent(sock, keysym, down) {
|
||||||
|
|
@ -2948,6 +3055,54 @@ RFB.messages = {
|
||||||
sock.flush();
|
sock.flush();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
ReplitAudioStartEncoder(sock, enabled, channels, codec, kbps) {
|
||||||
|
const buff = sock._sQ;
|
||||||
|
const offset = sock._sQlen;
|
||||||
|
|
||||||
|
buff[offset] = 245; // msg-type
|
||||||
|
buff[offset + 1] = 0; // sub msg-type
|
||||||
|
buff[offset + 2] = 0;
|
||||||
|
buff[offset + 3] = 6; // length
|
||||||
|
|
||||||
|
buff[offset + 4] = enabled ? 1 : 0; // enabled
|
||||||
|
buff[offset + 5] = channels;
|
||||||
|
|
||||||
|
buff[offset + 6] = codec >> 8;
|
||||||
|
buff[offset + 7] = codec;
|
||||||
|
|
||||||
|
buff[offset + 8] = kbps >> 8;
|
||||||
|
buff[offset + 9] = kbps;
|
||||||
|
|
||||||
|
sock._sQlen += 10;
|
||||||
|
sock.flush();
|
||||||
|
},
|
||||||
|
|
||||||
|
ReplitAudioRequestFrame(sock, channels, codec, kbps) {
|
||||||
|
const buff = sock._sQ;
|
||||||
|
const offset = sock._sQlen;
|
||||||
|
|
||||||
|
buff[offset] = 245; // msg-type
|
||||||
|
buff[offset + 1] = 1; // sub msg-type
|
||||||
|
buff[offset + 2] = 0;
|
||||||
|
buff[offset + 3] = 0; // length
|
||||||
|
|
||||||
|
sock._sQlen += 4;
|
||||||
|
sock.flush();
|
||||||
|
},
|
||||||
|
|
||||||
|
ReplitAudioEnableContinuousUpdate(sock) {
|
||||||
|
const buff = sock._sQ;
|
||||||
|
const offset = sock._sQlen;
|
||||||
|
|
||||||
|
buff[offset] = 245; // msg-type
|
||||||
|
buff[offset + 1] = 2; // sub msg-type
|
||||||
|
buff[offset + 2] = 0;
|
||||||
|
buff[offset + 3] = 0; // length
|
||||||
|
|
||||||
|
sock._sQlen += 4;
|
||||||
|
sock.flush();
|
||||||
|
},
|
||||||
|
|
||||||
QEMUExtendedKeyEvent(sock, keysym, down, keycode) {
|
QEMUExtendedKeyEvent(sock, keysym, down, keycode) {
|
||||||
function getRFBkeycode(xtScanCode) {
|
function getRFBkeycode(xtScanCode) {
|
||||||
const upperByte = (keycode >> 8);
|
const upperByte = (keycode >> 8);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,201 @@
|
||||||
|
/*
|
||||||
|
* noVNC: HTML5 VNC client
|
||||||
|
* Copyright (C) 2021 The noVNC Authors
|
||||||
|
* Licensed under MPL 2.0 (see LICENSE.txt)
|
||||||
|
*
|
||||||
|
* See README.md for usage and integration instructions.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// The maximum allowable de-sync, in seconds. If the time between the last
|
||||||
|
// received timestamp and the current audio playback timestamp exceeds this
|
||||||
|
// value, the audio stream will be seeked to the most current timestamp
|
||||||
|
// possible.
|
||||||
|
const MAX_ALLOWABLE_DESYNC = 0.5;
|
||||||
|
|
||||||
|
// The amount of time, in seconds, to keep in the audio buffer while seeking.
|
||||||
|
// Whenever a de-sync event happens and we need to seek to a future
|
||||||
|
// timestamp, we skip to the last buffered time minus this amount, so that the
|
||||||
|
// browser has this amount of time worth of buffered audio data. This is done
|
||||||
|
// to avoid having the browser enter a buffering state just after seeking.
|
||||||
|
const SEEK_BUFFER_LENGTH = 0.2;
|
||||||
|
|
||||||
|
// An audio stream built upon Media Stream Extensions.
|
||||||
|
export default class AudioStream {
|
||||||
|
constructor(codec) {
|
||||||
|
this._codec = codec;
|
||||||
|
this._reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
_reset() {
|
||||||
|
// Instantiate a media source and audio buffer/queue.
|
||||||
|
this._mediaSource = new MediaSource();
|
||||||
|
this._audioBuffer = null;
|
||||||
|
this._audioQ = [];
|
||||||
|
|
||||||
|
// Create a hidden audio element.
|
||||||
|
this._audio = document.createElement("audio");
|
||||||
|
this._audio.src = window.URL.createObjectURL(this._mediaSource);
|
||||||
|
|
||||||
|
// When data is queued, start playing.
|
||||||
|
this._audio.autoplay = true;
|
||||||
|
this._mediaSource.addEventListener(
|
||||||
|
"sourceopen",
|
||||||
|
this._onSourceOpen.bind(this),
|
||||||
|
false
|
||||||
|
);
|
||||||
|
this._audio.addEventListener(
|
||||||
|
"error",
|
||||||
|
(ev) => {
|
||||||
|
console.error("Audio element error", ev);
|
||||||
|
},
|
||||||
|
false
|
||||||
|
);
|
||||||
|
this._audio.addEventListener("canplay", () => {
|
||||||
|
try {
|
||||||
|
this._audio.play();
|
||||||
|
} catch (e) {
|
||||||
|
// Firefox and Chrome are totally cool with playing this
|
||||||
|
// the moment we can do it, but Safari throws an exception
|
||||||
|
// since play() is not called in a stack that ran a user
|
||||||
|
// event handler.
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_onSourceOpen(e) {
|
||||||
|
if (this._audioBuffer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._audioBuffer = this._mediaSource.addSourceBuffer(this._codec);
|
||||||
|
this._audioBuffer.mode = "segments";
|
||||||
|
this._audioBuffer.addEventListener(
|
||||||
|
"updateend",
|
||||||
|
this._onUpdateBuffer.bind(this)
|
||||||
|
);
|
||||||
|
this._audioBuffer.addEventListener("error", (ev) => {
|
||||||
|
console.error("AudioBuffer error", ev);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_onUpdateBuffer() {
|
||||||
|
if (
|
||||||
|
!this._audioBuffer ||
|
||||||
|
this._audioBuffer.updating ||
|
||||||
|
this._audio.error
|
||||||
|
) {
|
||||||
|
// The audio buffer is not yet ready to accept any new data.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!this._audioQ.length) {
|
||||||
|
// There's nothing to append.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const timestamp = this._audioQ[0][0];
|
||||||
|
if (this._audioQ.length === 1) {
|
||||||
|
this._appendChunk(timestamp, this._audioQ.pop()[1]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there is more than one chunk in the queue, they are coalesced
|
||||||
|
// into a single buffer. This is because following appendBuffer(),
|
||||||
|
// the audio buffer changes to an "updating" state for a small amount
|
||||||
|
// of time and any new chunks won't be able to be appended immediately.
|
||||||
|
// Since the internal queue is used when the browser is trying to catch
|
||||||
|
// up with the server, we want to have the audio buffer unappendable
|
||||||
|
// for a smaller amount of time.
|
||||||
|
let chunkLength = 0;
|
||||||
|
for (let i = 0; i < this._audioQ.length; ++i) {
|
||||||
|
chunkLength += this._audioQ[i][1].byteLength;
|
||||||
|
}
|
||||||
|
const chunk = new Uint8Array(chunkLength);
|
||||||
|
let offset = 0;
|
||||||
|
for (let i = 0; i < this._audioQ.length; ++i) {
|
||||||
|
chunk.set(new Uint8Array(this._audioQ[i][1]), offset);
|
||||||
|
offset += this._audioQ[i][1].byteLength;
|
||||||
|
}
|
||||||
|
this._audioQ.splice(0, this._audioQ.length);
|
||||||
|
this._appendChunk(timestamp, chunk);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append a chunk into the AudioBuffer. The caller should ensure that
|
||||||
|
// the AudioBuffer is ready to receive the chunk. If the difference
|
||||||
|
// between the current playback position of the audio and the timestamp
|
||||||
|
// exceeds the maximum allowable desync threshold, the audio will be
|
||||||
|
// seeked to the latest possible position that doesn't trigger buffering
|
||||||
|
// to avoid an arbitrarily large desync between video and audio.
|
||||||
|
_appendChunk(timestamp, chunk) {
|
||||||
|
this._audioBuffer.appendBuffer(chunk);
|
||||||
|
if (
|
||||||
|
timestamp - this._audio.currentTime > MAX_ALLOWABLE_DESYNC &&
|
||||||
|
(this._audio.seekable.length || this._audio.buffered.length)
|
||||||
|
) {
|
||||||
|
console.debug("maximum allowable desync reached", {
|
||||||
|
readyState: this._audio.readyState,
|
||||||
|
buffered: (
|
||||||
|
(this._audio.buffered &&
|
||||||
|
this._audio.buffered.length &&
|
||||||
|
this._audio.buffered.end(
|
||||||
|
this._audio.buffered.length - 1
|
||||||
|
)) ||
|
||||||
|
0
|
||||||
|
).toFixed(2),
|
||||||
|
seekable: (
|
||||||
|
(this._audio.seekable &&
|
||||||
|
this._audio.seekable.length &&
|
||||||
|
this._audio.seekable.end(
|
||||||
|
this._audio.seekable.length - 1
|
||||||
|
)) ||
|
||||||
|
0
|
||||||
|
).toFixed(2),
|
||||||
|
time: this._audio.currentTime.toFixed(2),
|
||||||
|
delta: (timestamp - this._audio.currentTime).toFixed(2)
|
||||||
|
});
|
||||||
|
if (this._audio.buffered && this._audio.buffered.length) {
|
||||||
|
this._audio.currentTime =
|
||||||
|
this._audio.buffered.end(this._audio.buffered.length - 1) -
|
||||||
|
SEEK_BUFFER_LENGTH;
|
||||||
|
} else {
|
||||||
|
this._audio.currentTime =
|
||||||
|
this._audio.seekable.end(this._audio.seekable.length - 1) -
|
||||||
|
SEEK_BUFFER_LENGTH;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Queues an audio chunk at a particular timestamp.
|
||||||
|
queueAudioFrame(timestamp, keyframe, chunk) {
|
||||||
|
// If the MSE audio buffer is not ready to receive the chunk or
|
||||||
|
// there are some other chunks waiting to be appended, we save
|
||||||
|
// a copy of it into our own internal queue. Eventually,
|
||||||
|
// when it becomes ready, we append all pending chunks at once.
|
||||||
|
if (
|
||||||
|
this._audioBuffer === null ||
|
||||||
|
this._audioBuffer.updating ||
|
||||||
|
this._audio.error ||
|
||||||
|
this._audioQ.length
|
||||||
|
) {
|
||||||
|
// We need to make a copy, since `chunk` is a view of the underlying
|
||||||
|
// buffer owned by Websock, and will be mutated once we return.
|
||||||
|
// TODO: `keyframe` can be used to decide when to drop a chunk if
|
||||||
|
// there's enough backpressure.
|
||||||
|
const copy = new ArrayBuffer(chunk.byteLength);
|
||||||
|
new Uint8Array(copy).set(new Uint8Array(chunk));
|
||||||
|
this._audioQ.push([timestamp, copy]);
|
||||||
|
this._onUpdateBuffer();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._appendChunk(timestamp, chunk);
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
if (this._audio) {
|
||||||
|
this._audio.pause();
|
||||||
|
}
|
||||||
|
this._mediaSource = null;
|
||||||
|
this._audioBuffer = null;
|
||||||
|
this._audioQ = [];
|
||||||
|
this._audio = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
5
vnc.html
5
vnc.html
|
|
@ -108,6 +108,11 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Audio -->
|
||||||
|
<input type="image" alt="Toggle Audio" src="app/images/audio.svg"
|
||||||
|
id="noVNC_audio_button" class="noVNC_button noVNC_hidden"
|
||||||
|
title="Toggle Audio">
|
||||||
|
|
||||||
<!-- Shutdown/Reboot -->
|
<!-- Shutdown/Reboot -->
|
||||||
<input type="image" alt="Shutdown/Reboot" src="app/images/power.svg"
|
<input type="image" alt="Shutdown/Reboot" src="app/images/power.svg"
|
||||||
id="noVNC_power_button" class="noVNC_button"
|
id="noVNC_power_button" class="noVNC_button"
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue