update remoteDesktop

This commit is contained in:
lxsang
2020-12-29 19:36:35 +00:00
parent b31cd4302d
commit 0a6f8c0f8c
23 changed files with 757 additions and 34 deletions

74
libwvnc/README.md Normal file
View File

@ -0,0 +1,74 @@
# libwvnc
Overview about WVNC: [https://blog.lxsang.me/r/id/23](https://blog.lxsang.me/r/id/23)
**libwvnc** is the client side protocol API for my [Antd's **wvnc**](https://github.com/lxsang/antd-wvnc-plugin) server side plugin. It allows to acess VNC server from the web using websocket (via the **wvnc** server plugin).
Since the **wvnc** plugin offers data compression using JPEG, **wvnc.js** depends on the **libjpeg** package and **web worker** to speed up the data decoding process, thus speed up the screen rendering on HTML canvas.
## Example
It is straight forward to use the api:
Html code:
```html
...
<canvas id = "screen"></canvas>
```
Javascript:
```javascript
var args, client;
args = {
// the canvas element
element: 'screen',
// The websocket uri to the wvnc server side plugin
ws: 'wss://localhost/wvnc',
// the decoder worker
libjpeg: 'path/to/jpg.js'
};
client = new WVNC(args);
// This function responds to a VNC server password request
// should return a promise
client.onpassword = function() {
return new Promise(function(r, e) {
return r('password');
});
};
// this function responds to the remote OS username and password request
// should return a promise
client.oncredential = function() {
return new Promise(function(r, e) {
return r('username', 'password');
});
};
// event fired when a text is copied on
// the remote computer
client.oncopy = function(text) {
console.log(text);
};
// init the WVNC client
client.init()
.then(function() {
client.connect(
// VNC server
"192.168.1.20:5901",
{
// bits per pixel
bbp: 32,
// data compression flag
// 1 is for both JPEG
// 0 is for raw data
flag: 1,
// JPEG quality %
quality: 50
});
})
.catch(function(m, s) {
return console.error(m, s);
});
```

View File

@ -0,0 +1,74 @@
# libwvnc
Overview about WVNC: [https://blog.lxsang.me/r/id/23](https://blog.lxsang.me/r/id/23)
**libwvnc** is the client side protocol API for my [Antd's **wvnc**](https://github.com/lxsang/antd-wvnc-plugin) server side plugin. It allows to acess VNC server from the web using websocket (via the **wvnc** server plugin).
Since the **wvnc** plugin offers data compression using JPEG, **wvnc.js** depends on the **libjpeg** package and **web worker** to speed up the data decoding process, thus speed up the screen rendering on HTML canvas.
## Example
It is straight forward to use the api:
Html code:
```html
...
<canvas id = "screen"></canvas>
```
Javascript:
```javascript
var args, client;
args = {
// the canvas element
element: 'screen',
// The websocket uri to the wvnc server side plugin
ws: 'wss://localhost/wvnc',
// the decoder worker
libjpeg: 'path/to/jpg.js'
};
client = new WVNC(args);
// This function responds to a VNC server password request
// should return a promise
client.onpassword = function() {
return new Promise(function(r, e) {
return r('password');
});
};
// this function responds to the remote OS username and password request
// should return a promise
client.oncredential = function() {
return new Promise(function(r, e) {
return r('username', 'password');
});
};
// event fired when a text is copied on
// the remote computer
client.oncopy = function(text) {
console.log(text);
};
// init the WVNC client
client.init()
.then(function() {
client.connect(
// VNC server
"192.168.1.20:5901",
{
// bits per pixel
bbp: 32,
// data compression flag
// 1 is for both JPEG
// 0 is for raw data
flag: 1,
// JPEG quality %
quality: 50
});
})
.catch(function(m, s) {
return console.error(m, s);
});
```

View File

@ -0,0 +1,112 @@
let resolution;
decode_jpeg = (raw, msg) =>
{
if(!JpegImage)
{
console.error("The JPEG library is not available");
return 0;
}
let jpeg = new JpegImage()
jpeg.parse(raw)
if(jpeg.width != msg.w || jpeg.height != msg.h)
{
console.error("Incorrect data size: expect " + msg.w*msg.h*4 + " got " + jpeg.width*jpeg.height*4);
return 0;
}
msg.pixels = new Uint8Array(msg.w*msg.h*4);
let data = jpeg.getData(msg.w, msg.h);
for(let j = 0; j < msg.h; j++)
{
for(let i = 0; i < msg.w; i++)
{
let index = j*msg.w*4 + i*4;
msg.pixels[index] = data[j*msg.w*3 + i*3];
msg.pixels[index+1] = data[j*msg.w*3 + i*3 + 1];
msg.pixels[index+2] = data[j*msg.w*3 + i*3 + 2];
msg.pixels[index+3] = 255;
}
}
return msg.pixels.length;
}
decode_raw = (raw, msg) =>
{
let size =raw.length;
if(resolution.depth == 32)
{
msg.pixels = new Uint8Array(raw);
return size;
}
if(resolution.depth != 16)
{
console.error("Unsupported depth" + resolution.depth);
return 0;
}
let bytes = resolution.depth/8;
let npixels = size / bytes;
let outsize = npixels*4;
if(outsize != msg.w*msg.h*4)
{
console.error("Incorrect data size: expect " + msg.w*msg.h*4 + " got " + outsize);
return 0;
}
msg.pixels = new Uint8Array(outsize);
let value = 0;
let px = {};
for(let i = 0; i < npixels; i++)
{
value = 0;
for(let j=0; j < bytes; j++ )
value = (raw[i * bytes + j] << (j*8)) | value ;
// lookup for pixel
msg.pixels[i*4] = (value & 0x1F) * (255 / 31);
msg.pixels[i*4+1] = ((value >> 5) & 0x3F) * (255 / 63);
msg.pixels[i*4+2] = ((value >> 11) & 0x1F) * (255 / 31);
msg.pixels[i*4+3] = 255;
}
return outsize;
}
decode = (arr) =>
{
let datain = new Uint8Array(arr);
let msg = {
x: datain[1] | (datain[2] << 8),
y: datain[3] | (datain[4] << 8),
w: datain[5] | (datain[6] << 8),
h: datain[7] | (datain[8] << 8),
flag: datain[9],
pixels: undefined
}
switch (msg.flag) {
case 0x0:
decode_raw(datain.subarray(10), msg);
break;
case 0x1:
decode_jpeg(datain.subarray(10), msg);
break;
default:
console.error("Unknow flag: " + msg.flag);
}
if(msg.pixels)
{
msg.pixels = msg.pixels.buffer;
postMessage(msg, [msg.pixels]);
}
}
onmessage = (e) => {
if(e.data.depth)
{
resolution = e.data;
}
else if(e.data.libjpeg)
{
importScripts(e.data.libjpeg);
}
else
{
decode(e.data);
}
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,15 @@
{
"pkgname": "libwvnc",
"name":"libwvnc",
"description":"libwvnc: client side VNC protocol via websocket",
"info":{
"author": "",
"email": ""
},
"version":"0.1.2-a",
"category":"Library",
"iconclass":"fa fa-cog",
"mimes":["none"],
"dependencies":["libjpeg@0.1.1-a"],
"locale": {}
}

Binary file not shown.

112
libwvnc/decoder.js Normal file
View File

@ -0,0 +1,112 @@
let resolution;
decode_jpeg = (raw, msg) =>
{
if(!JpegImage)
{
console.error("The JPEG library is not available");
return 0;
}
let jpeg = new JpegImage()
jpeg.parse(raw)
if(jpeg.width != msg.w || jpeg.height != msg.h)
{
console.error("Incorrect data size: expect " + msg.w*msg.h*4 + " got " + jpeg.width*jpeg.height*4);
return 0;
}
msg.pixels = new Uint8Array(msg.w*msg.h*4);
let data = jpeg.getData(msg.w, msg.h);
for(let j = 0; j < msg.h; j++)
{
for(let i = 0; i < msg.w; i++)
{
let index = j*msg.w*4 + i*4;
msg.pixels[index] = data[j*msg.w*3 + i*3];
msg.pixels[index+1] = data[j*msg.w*3 + i*3 + 1];
msg.pixels[index+2] = data[j*msg.w*3 + i*3 + 2];
msg.pixels[index+3] = 255;
}
}
return msg.pixels.length;
}
decode_raw = (raw, msg) =>
{
let size =raw.length;
if(resolution.depth == 32)
{
msg.pixels = new Uint8Array(raw);
return size;
}
if(resolution.depth != 16)
{
console.error("Unsupported depth" + resolution.depth);
return 0;
}
let bytes = resolution.depth/8;
let npixels = size / bytes;
let outsize = npixels*4;
if(outsize != msg.w*msg.h*4)
{
console.error("Incorrect data size: expect " + msg.w*msg.h*4 + " got " + outsize);
return 0;
}
msg.pixels = new Uint8Array(outsize);
let value = 0;
let px = {};
for(let i = 0; i < npixels; i++)
{
value = 0;
for(let j=0; j < bytes; j++ )
value = (raw[i * bytes + j] << (j*8)) | value ;
// lookup for pixel
msg.pixels[i*4] = (value & 0x1F) * (255 / 31);
msg.pixels[i*4+1] = ((value >> 5) & 0x3F) * (255 / 63);
msg.pixels[i*4+2] = ((value >> 11) & 0x1F) * (255 / 31);
msg.pixels[i*4+3] = 255;
}
return outsize;
}
decode = (arr) =>
{
let datain = new Uint8Array(arr);
let msg = {
x: datain[1] | (datain[2] << 8),
y: datain[3] | (datain[4] << 8),
w: datain[5] | (datain[6] << 8),
h: datain[7] | (datain[8] << 8),
flag: datain[9],
pixels: undefined
}
switch (msg.flag) {
case 0x0:
decode_raw(datain.subarray(10), msg);
break;
case 0x1:
decode_jpeg(datain.subarray(10), msg);
break;
default:
console.error("Unknow flag: " + msg.flag);
}
if(msg.pixels)
{
msg.pixels = msg.pixels.buffer;
postMessage(msg, [msg.pixels]);
}
}
onmessage = (e) => {
if(e.data.depth)
{
resolution = e.data;
}
else if(e.data.libjpeg)
{
importScripts(e.data.libjpeg);
}
else
{
decode(e.data);
}
}

315
libwvnc/main.coffee Normal file
View File

@ -0,0 +1,315 @@
class WVNC
constructor: (args) ->
@socket = undefined
@ws = undefined
@canvas = undefined
worker = "pkg://libwvnc/decoder.js".asFileHandle().getlink()
@scale = 1.0
@ws = args.ws if args.ws
@canvas = args.element
@canvas = document.getElementById @canvas if typeof @canvas is 'string'
libjpeg = args.libjpeg if args.libjpeg
@decoder = new Worker worker
@enableEvent = true
me = @
@mouseMask = 0
@decoder.postMessage {libjpeg: libjpeg}
@decoder.onmessage = (e) ->
me.process e.data
init: () ->
me = @
return new Promise (r, e) ->
return e('Canvas is not set') if not me.canvas
# fix keyboard event problem
$(me.canvas).attr 'tabindex', '1'
me.initInputEvent()
r()
initInputEvent: () ->
me = @
return unless @canvas
getMousePos = (e) ->
rect = me.canvas.getBoundingClientRect()
pos=
x: Math.floor((e.clientX - rect.left) / me.scale)
y: Math.floor((e.clientY - rect.top) / me.scale)
return pos
sendMouseLocation = (e) ->
return unless me.enableEvent
p = getMousePos e
me.sendPointEvent p.x, p.y, me.mouseMask
return unless me.canvas
me.canvas.oncontextmenu = (e) ->
e.preventDefault()
return false
me.canvas.onmousemove = (e) -> sendMouseLocation e
me.canvas.onmousedown = (e) ->
state = 1 << e.button
me.mouseMask = me.mouseMask | state
sendMouseLocation e
#e.preventDefault()
me.canvas.onmouseup = (e) ->
state = 1 << e.button
me.mouseMask = me.mouseMask & (~state)
sendMouseLocation e
#e.preventDefault()
me.canvas.onkeydown = me.canvas.onkeyup = (e) ->
# get the key code
keycode = e.keyCode
#console.log e
switch keycode
when 8 then code = 0xFF08 #back space
when 9 then code = 0xff89 #0xFF09 # tab ?
when 13 then code = 0xFF0D # return
when 27 then code = 0xFF1B # esc
when 46 then code = 0xFFFF # delete to verify
when 38 then code = 0xFF52 # up
when 40 then code = 0xFF54 # down
when 37 then code = 0xFF51 # left
when 39 then code = 0xFF53 # right
when 91 then code = 0xFFE7 # meta left
when 93 then code = 0xFFE8 # meta right
when 16 then code = 0xFFE1 # shift left
when 17 then code = 0xFFE3 # ctrl left
when 18 then code = 0xFFE9 # alt left
when 20 then code = 0xFFE5 # capslock
when 113 then code = 0xFFBF # f2
when 112 then code = 0xFFBE # f1
when 114 then code = 0xFFC0 # f3
when 115 then code = 0xFFC1 # f4
when 116 then code = 0xFFC2 # f5
when 117 then code = 0xFFC3 # f6
when 118 then code = 0xFFC4 # f7
when 119 then code = 0xFFC5 # f8
when 120 then code = 0xFFC6 # f9
when 121 then code = 0xFFC7 # f10
when 122 then code = 0xFFC8 # f11
when 123 then code = 0xFFC9 # f12
else
code = e.key.charCodeAt(0) #if not e.ctrlKey and not e.altKey
#if ((keycode > 47 and keycode < 58) or (keycode > 64 and keycode < 91) or (keycode > 95 and keycode < 112) or (keycode > 185 and keycode < 193) or (keycode > 218 && keycode < 223))
# code = e.key.charCodeAt(0)
#else
# code = keycode
e.preventDefault()
return unless code
if e.type is "keydown"
me.sendKeyEvent code, 1
else if e.type is "keyup"
me.sendKeyEvent code, 0
# mouse wheel event
@canvas.addEventListener 'wheel', (e) ->
return unless me.enableEvent
#if (e.deltaY < 0) # up
p = getMousePos e
e.preventDefault()
if e.deltaY < 0
me.sendPointEvent p.x, p.y, 8
me.sendPointEvent p.x, p.y, 0
return
me.sendPointEvent p.x, p.y, 16
me.sendPointEvent p.x, p.y, 0
# paste event
@canvas.onpaste = (e) ->
return unless me.enableEvent
pastedText = undefined
if window.clipboardData and window.clipboardData.getData #IE
pastedText = window.clipboardData.getData 'Text'
else if e.clipboardData and e.clipboardData.getData
pastedText = e.clipboardData.getData 'text/plain'
return false unless pastedText
e.preventDefault()
me.sendTextAsClipboard pastedText
# global event
fn = (e) =>
@disconnect(true)
window.addEventListener "unload", fn
window.addEventListener "beforeunload", fn
initCanvas: (w, h , d) ->
me = @
@depth = d
@canvas.width = w
@canvas.height = h
@resolution =
w: w,
h: h,
depth: @depth
@decoder.postMessage @resolution
me.canvas.style.cursor = "none"
@setScale @scale
process: (msg) ->
if not @socket
return
data = new Uint8Array msg.pixels
#w = @buffer.width * @scale
#h = @buffer.height * @scale
ctx = @canvas.getContext "2d", { alpha: false }
imgData = ctx.createImageData msg.w, msg.h
imgData.data.set data
ctx.putImageData imgData, msg.x, msg.y
setScale: (n) ->
@scale = n
return unless @canvas
@canvas.style.transformOrigin = '0 0'
@canvas.style.transform = 'scale(' + n + ')'
connect: (url, args) ->
me = @
@socket.close() if @socket
return unless @ws
@socket = new WebSocket @ws
@socket.binaryType = "arraybuffer"
@socket.onopen = () ->
console.log "socket opened"
me.initConnection(url, args)
@socket.onmessage = (e) ->
me.consume e
@socket.onclose = () ->
me.socket = null
me.canvas.style.cursor = "auto"
me.canvas.getContext('2d').clearRect 0,0, me.resolution.w, me.resolution.h if me.canvas and me.resolution
console.log "socket closed"
disconnect: (close_worker) ->
@socket.close() if @socket
@decoder.terminate() if @decoder and close_worker
initConnection: (vncserver, params) ->
#vncserver = "192.168.1.20:5901"
data = new Uint8Array vncserver.length + 3
data[0] = 32 # bbp
###
flag:
0: raw data no compress
1: jpeg no compress
###
data[1] = 1
data[2] = 50 # jpeg quality
if params
data[0] = params.bbp if params.bbp
data[1] = params.flag if params.flag
data[2] = params.quality if params.quality
## rate in milisecond
data.set (new TextEncoder()).encode(vncserver), 3
@socket.send(@buildCommand 0x01, data)
sendPointEvent: (x, y, mask) ->
return unless @socket
data = new Uint8Array 5
data[0] = x & 0xFF
data[1] = x >> 8
data[2] = y & 0xFF
data[3] = y >> 8
data[4] = mask
@socket.send( @buildCommand 0x05, data )
sendKeyEvent: (code, v) ->
#console.log code, v
return unless @socket
return unless @enableEvent
data = new Uint8Array 3
data[0] = code & 0xFF
data[1] = code >> 8
data[2] = v
@socket.send( @buildCommand 0x06, data )
buildCommand: (hex, o) ->
data = undefined
switch typeof o
when 'string'
data = (new TextEncoder()).encode(o)
when 'number'
data = new Uint8Array [o]
else
data = o
cmd = new Uint8Array data.length + 3
cmd[0] = hex
cmd[2] = data.length >> 8
cmd[1] = data.length & 0x0F
cmd.set data, 3
#console.log "the command is", cmd.buffer
return cmd.buffer
oncopy: (text) ->
console.log "Get clipboard text: " + text
onpassword: () ->
return new Promise (resolve, reject) ->
reject("onpassword is not implemented")
sendTextAsClipboard: (text) ->
return unless @socket
console.log "send ", text
@socket.send (@buildCommand 0x07, text)
oncredential: () ->
return new Promise (resolve, reject) ->
reject("oncredential is not implemented")
onerror: (m) ->
console.log "Error", m
onresize: () ->
console.log "resize"
consume: (e) ->
data = new Uint8Array e.data
cmd = data[0]
me = @
switch cmd
when 0xFE #error
data = data.subarray 1, data.length - 1
dec = new TextDecoder("utf-8")
@onerror dec.decode(data)
when 0x81
console.log "Request for password"
@enableEvent = false
@onpassword().then (pass) ->
me.socket.send (me.buildCommand 0x02, pass)
me.enableEvent = true
when 0x82
console.log "Request for login"
@enableEvent = false
@oncredential().then (user, pass) ->
arr = new Uint8Array user.length + pass.length + 1
arr.set (new TextEncoder()).encode(user), 0
arr.set ['\0'], user.length
arr.set (new TextEncoder()).encode(pass), user.length + 1
me.socket.send(me.buildCommand 0x03, arr)
me.enableEvent = true
when 0x83
w = data[1] | (data[2]<<8)
h = data[3] | (data[4]<<8)
depth = data[5]
@initCanvas w, h, depth
# status command for ack
@socket.send(@buildCommand 0x04, 1)
@onresize()
when 0x84
# send data to web assembly for decoding
@decoder.postMessage data.buffer, [data.buffer]
when 0x85
# clipboard data from server
data = data.subarray 1
dec = new TextDecoder "utf-8"
@oncopy dec.decode data
@socket.send(@buildCommand 0x04, 1)
else
console.log cmd
window.WVNC = WVNC

15
libwvnc/package.json Normal file
View File

@ -0,0 +1,15 @@
{
"pkgname": "libwvnc",
"name":"libwvnc",
"description":"libwvnc: client side VNC protocol via websocket",
"info":{
"author": "",
"email": ""
},
"version":"0.1.2-a",
"category":"Library",
"iconclass":"fa fa-cog",
"mimes":["none"],
"dependencies":["libjpeg@0.1.1-a"],
"locale": {}
}

7
libwvnc/project.json Normal file
View File

@ -0,0 +1,7 @@
{
"name": "libwvnc",
"css": [],
"javascripts": [],
"coffees": ["main.coffee"],
"copies": [ "decoder.js","package.json", "README.md"]
}