Skip to content

Commit

Permalink
#122 add offscreen decoding
Browse files Browse the repository at this point in the history
  • Loading branch information
totaam committed Jan 7, 2022
1 parent d0d62d5 commit 311a176
Show file tree
Hide file tree
Showing 7 changed files with 459 additions and 20 deletions.
3 changes: 3 additions & 0 deletions html5/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@
<script type="text/javascript" src="js/Notifications.js"></script>
<script type="text/javascript" src="js/Utilities.js"></script>
<script type="text/javascript" src="js/MediaSourceUtil.js"></script>
<script type="text/javascript" src="js/ImageDecoder.js"></script>
<script type="text/javascript" src="js/VideoDecoder.js"></script>
<script type="text/javascript" src="js/OffscreenDecodeWorkerHelper.js"></script>
<script type="text/javascript" src="js/Client.js"></script>

<link rel="stylesheet" type="text/css" href="css/menu.css"/>
Expand Down
48 changes: 29 additions & 19 deletions html5/js/Client.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ XpraClient.prototype.init_settings = function(container) {
this.supported_encodings = ["jpeg", "png", "rgb", "rgb32", "rgb24"];
//extra encodings we enable if validated via the decode worker:
//(we also validate jpeg and png as a sanity check)
this.check_encodings = ["webp", "jpeg", "png"]; //"h264", "vp8+webm", "h264+mp4", "mpeg4+mp4"];
this.check_encodings = []; //"webp", "jpeg", "png"]; //"h264", "vp8+webm", "h264+mp4", "mpeg4+mp4"];
this.debug_categories = [];
this.start_new_session = null;
this.clipboard_enabled = false;
Expand All @@ -91,6 +91,7 @@ XpraClient.prototype.init_settings = function(container) {
this.PING_FREQUENCY = 5000;
this.INFO_FREQUENCY = 1000;
this.uuid = Utilities.getHexUUID();
this.offscreen_api = XpraOffscreenWorker.isAvailable();
};

XpraClient.prototype.init_state = function(container) {
Expand Down Expand Up @@ -431,7 +432,12 @@ XpraClient.prototype.initialize_workers = function() {
this.decode_worker = false;
return;
}
const decode_worker = new Worker('js/DecodeWorker.js');
let decode_worker;
if (this.offscreen_api) {
decode_worker = new Worker('js/OffscreenDecodeWorker.js');
} else {
decode_worker = new Worker('js/DecodeWorker.js');
}
decode_worker.addEventListener('message', function(e) {
const data = e.data;
if (data['draw']) {
Expand Down Expand Up @@ -3245,31 +3251,35 @@ XpraClient.prototype.do_process_draw = function(packet, start) {
function send_damage_sequence(decode_time, message) {
me.do_send_damage_sequence(packet_sequence, wid, width, height, decode_time, message);
}
function decode_result(error) {
const flush = options["flush"] || 0;
let decode_time = -1;
if(flush==0) {
me.request_redraw(win);
}
if (error || start==0) {
me.request_redraw(win);
}
else {
decode_time = Math.round(1000*performance.now() - 1000*start);
}
me.debug("draw", "decode time for ", coding, " sequence ", packet_sequence, ": ", decode_time, ", flush=", flush);
send_damage_sequence(decode_time, error || "");
}
if (!win) {
this.debug("draw", 'cannot paint, window not found:', wid);
send_damage_sequence(-1, "window "+wid+" not found");
return;
}
if (coding=="offscreen-painted") {
//we're done!
decode_result(0);
return;
}
try {
win.paint(x, y,
width, height,
coding, data, packet_sequence, rowstride, options,
function (error) {
const flush = options["flush"] || 0;
let decode_time = -1;
if(flush==0) {
me.request_redraw(win);
}
if (error || start==0) {
me.request_redraw(win);
}
else {
decode_time = Math.round(1000*performance.now() - 1000*start);
}
me.debug("draw", "decode time for ", coding, " sequence ", packet_sequence, ": ", decode_time, ", flush=", flush);
send_damage_sequence(decode_time, error || "");
}
);
coding, data, packet_sequence, rowstride, options, decode_result);
}
catch(e) {
me.exc(e, "error painting", coding, "sequence no", packet_sequence);
Expand Down
50 changes: 50 additions & 0 deletions html5/js/ImageDecoder.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
* This file is part of Xpra.
* Copyright (C) 2021 Tijs van der Zwaan <tijzwa@vpo.nl>
* Licensed under MPL 2.0, see:
* http://www.mozilla.org/MPL/2.0/
*
*/

/*
* Receives native image packages and decode them via ImageDecoder.
* https://developer.mozilla.org/en-US/docs/Web/API/ImageDecoder
* ImageDecoder is only working in Chrome 94+ and Android
*
*/

const XpraImageDecoderLoader = {
hasNativeDecoder: function () {
return typeof ImageDecoder !== "undefined";
}
}

function XpraImageDecoder() {
this.on_frame_decoded = null;
}

XpraImageDecoder.prototype.queue_frame = function (packet, start) {
const width = packet[4];
const height = packet[5];
const coding = packet[6];
if (coding.startsWith("rgb")) {
// TODO: Figure out how to decode rgb with ImageDecoder API;
const data = decode_rgb(packet);
createImageBitmap(new ImageData(new Uint8ClampedArray(data.buffer), width, height), 0, 0, width, height).then((bitmap) => {
packet[6] = "bitmap";
packet[7] = bitmap;
this.on_frame_decoded(packet, start);
});
} else {
const decoder = new ImageDecoder({
type: "image/" + coding,
data: packet[7]
});
decoder.decode({ frameIndex: 0 }).then((result) => {
packet[6] = "frame";
packet[7] = result;
decoder.close();
this.on_frame_decoded(packet, start);
});
}
};
185 changes: 185 additions & 0 deletions html5/js/OffscreenDecodeWorker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
/*
* This file is part of Xpra.
* Copyright (C) 2021 Tijs van der Zwaan <tijzwa@vpo.nl>
* Licensed under MPL 2.0, see:
* http://www.mozilla.org/MPL/2.0/
*
*/

/*
* Worker for offscreen decoding and painting.
* Requires Chrome 94+ or Android and a secure (SSL or localhost) context.
*/

importScripts("./lib/zlib.js");
importScripts("./lib/lz4.js");
importScripts("./lib/broadway/Decoder.js");
importScripts("./VideoDecoder.js");
importScripts("./ImageDecoder.js");
importScripts("./RgbHelpers.js");

// Array of offscreen canvases and video decoders we have control over
const offscreen_canvas = [];
const image_decoders = [];
const video_decoders = [];

function add_decoder_for_window(wid, canvas) {
// Canvas
offscreen_canvas[wid] = [];
offscreen_canvas[wid]["c"] = canvas;
offscreen_canvas[wid]["ctx"] = canvas.getContext("2d");
offscreen_canvas[wid]["ctx"].imageSmoothingEnabled = false;

// Decoders
image_decoders[wid] = new XpraImageDecoder();
image_decoders[wid].on_frame_decoded = ((packet, start) => {
const wid = packet[1],
x = packet[2],
y = packet[3],
width = packet[4],
height = packet[5],
coding = packet[6],
data = packet[7];

let ctx = offscreen_canvas[wid]["ctx"];
if (coding == "bitmap") {
// RGB is transformed to bitmap
ctx.clearRect(x, y, width, height);
ctx.drawImage(data, x, y, width, height);
} else {
// All others are transformed to VideoFrame
ctx.clearRect(x, y, width, height);
ctx.drawImage(data.image, x, y, width, height);
data.image.close();
}
// Replace the coding & drop data
packet[6] = "offscreen-painted";
packet[7] = null;
self.postMessage({ 'draw': packet, 'start': start }, []);
});

video_decoders[wid] = new XpraVideoDecoder();
video_decoders[wid].on_frame_decoded = ((packet, start) => {
const wid = packet[1],
x = packet[2],
y = packet[3],
w = packet[4],
h = packet[5],
coding = packet[6],
data = packet[7];
let options = packet[10].length > 10 ? packet[10] : {};

let enc_width = w;
let enc_height = h;
const scaled_size = options["scaled_size"];
if (scaled_size) {
enc_width = scaled_size[0];
enc_height = scaled_size[1];
}

let ctx = offscreen_canvas[wid]["ctx"];
if (coding == "frame") {
ctx.drawImage(data, x, y, enc_width, enc_height);
data.close();
packet[6] = "offscreen-painted";
packet[7] = null;
self.postMessage({ 'draw': packet, 'start': start }, []);
} else {
// Encoding throttle is used to slow down frame input
// TODO: Relal error handling
const timeout = coding == "throttle" ? 500 : 0;
setTimeout(() => {
packet[6] = "offscreen-painted";
packet[7] = null;
self.postMessage({ 'draw': packet, 'start': start }, []);
}, timeout);
}
});

}

function decode_draw_packet(packet, start) {
const image_coding = ["rgb", "rgb32", "rgb24", "jpeg", "png", "webp"];
const video_coding = ["h264"];
const wid = packet[1];
const coding = packet[6];

if (image_coding.includes(coding)) {
// Add to image queue
let decoder = image_decoders[wid];
decoder.queue_frame(packet, start);

} else if (video_coding.includes(coding)) {
// Add to video queue
let decoder = video_decoders[wid];
if (!decoder.initialized) {
// Init with width and heigth of this packet.
// TODO: Use video max-size? It does not seem to matter.
decoder.init(packet[4], packet[5]);
}
decoder.queue_frame(packet, start);
}
else if (coding == "scroll") {
const data = packet[7];
const canvas = offscreen_canvas[wid]["c"];;
const ctx = offscreen_canvas[wid]["ctx"];;
for (let i = 0, j = data.length; i < j; ++i) {
const scroll_data = data[i];
const sx = scroll_data[0],
sy = scroll_data[1],
sw = scroll_data[2],
sh = scroll_data[3],
xdelta = scroll_data[4],
ydelta = scroll_data[5];

ctx.drawImage(canvas, sx, sy, sw, sh, sx + xdelta, sy + ydelta, sw, sh);
}
packet[6] = "offscreen-painted";
packet[7] = null;
self.postMessage({ 'draw': packet, 'start': start }, []);
}
else {
// We dont know, pass trough
self.postMessage({ 'draw': packet, 'start': start }, []);
}
}

function close_video(wid) {
try {
video_decoders[wid]._close();
} catch {
// TODO: Handle error.
}
}

onmessage = function (e) {
const data = e.data;
switch (data.cmd) {
case 'check':
// We do not check. We are here because we support native decoding.
// TODO: Reconsider this. It might be a good thing to do some testing, just for sanity??
const encodings = data.encodings;
self.postMessage({ 'result': true, 'formats': encodings });
break;
case 'eos':
close_video(data.wid);
break;
case 'decode':
decode_draw_packet(data.packet, data.start);
break
case 'canvas':
add_decoder_for_window(data.wid, data.canvas)
break;
case 'canvas-geo':
if (offscreen_canvas[data.wid]) {
const canvas = offscreen_canvas[data.wid]["c"];
if (canvas.width != data.w || canvas.height != data.h) {
canvas.width = data.w;
canvas.height = data.h;
}
}
break;
default:
console.error("Offscreen decode worker got unknown message: " + data.cmd);
}
}
23 changes: 23 additions & 0 deletions html5/js/OffscreenDecodeWorkerHelper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*
* This file is part of Xpra.
* Copyright (C) 2021 Tijs van der Zwaan <tijzwa@vpo.nl>
* Licensed under MPL 2.0, see:
* http://www.mozilla.org/MPL/2.0/
*
*/

/*
* Helper for offscreen decoding and painting.
* Requires Chrome 94+ or Android and a secure (SSL or localhost) context.
*/

const XpraOffscreenWorker = {
isAvailable: function () {
if (XpraImageDecoderLoader.hasNativeDecoder() && XpraVideoDecoderLoader.hasNativeDecoder && typeof OffscreenCanvas !== "undefined") {
return true;
} else {
console.warn("Offscreen decoding is not available. Please consider using Google Chrome 94+ in a secure (SSL or localhost) context for better performance.");
return false;
}
}
}
Loading

0 comments on commit 311a176

Please sign in to comment.