diff --git a/src/common/common/recording.h b/src/common/common/recording.h index b0278f01..e090f1de 100644 --- a/src/common/common/recording.h +++ b/src/common/common/recording.h @@ -71,6 +71,15 @@ typedef struct guac_common_recording { */ int include_mouse; + /** + * Non-zero if multi-touch events should be included in the session + * recording, zero otherwise. Depending on whether the remote desktop will + * automatically provide graphical feedback for touches, including touch + * events may be necessary for multi-touch interactions to be rendered in + * any resulting video. + */ + int include_touch; + /** * Non-zero if keys pressed and released should be included in the session * recording, zero otherwise. Including key events within the recording may @@ -119,6 +128,13 @@ typedef struct guac_common_recording { * otherwise. Including mouse state is necessary for the mouse cursor to be * rendered in any resulting video. * + * @param include_touch + * Non-zero if touch events should be included in the session recording, + * zero otherwise. Depending on whether the remote desktop will + * automatically provide graphical feedback for touches, including touch + * events may be necessary for multi-touch interactions to be rendered in + * any resulting video. + * * @param include_keys * Non-zero if keys pressed and released should be included in the session * recording, zero otherwise. Including key events within the recording may @@ -133,7 +149,8 @@ typedef struct guac_common_recording { */ guac_common_recording* guac_common_recording_create(guac_client* client, const char* path, const char* name, int create_path, - int include_output, int include_mouse, int include_keys); + int include_output, int include_mouse, int include_touch, + int include_keys); /** * Frees the resources associated with the given in-progress recording. Note @@ -174,6 +191,44 @@ void guac_common_recording_free(guac_common_recording* recording); void guac_common_recording_report_mouse(guac_common_recording* recording, int x, int y, int button_mask); +/** + * Reports the current state of a touch contact within the recording. + * + * @param recording + * The guac_common_recording associated with the touch contact that + * has changed state. + * + * @param id + * An arbitrary integer ID which uniquely identifies this contact relative + * to other active contacts. + * + * @param x + * The X coordinate of the center of the touch contact. + * + * @param y + * The Y coordinate of the center of the touch contact. + * + * @param x_radius + * The X radius of the ellipse covering the general area of the touch + * contact, in pixels. + * + * @param y_radius + * The Y radius of the ellipse covering the general area of the touch + * contact, in pixels. + * + * @param angle + * The rough angle of clockwise rotation of the general area of the touch + * contact, in degrees. + * + * @param force + * The relative force exerted by the touch contact, where 0 is no force + * (the touch has been lifted) and 1 is maximum force (the maximum amount + * of force representable by the device). + */ +void guac_common_recording_report_touch(guac_common_recording* recording, + int id, int x, int y, int x_radius, int y_radius, + double angle, double force); + /** * Reports a change in the state of an individual key within the recording. * diff --git a/src/common/common/surface.h b/src/common/common/surface.h index c7788837..ca8b3106 100644 --- a/src/common/common/surface.h +++ b/src/common/common/surface.h @@ -120,6 +120,12 @@ typedef struct guac_common_surface { */ guac_socket* socket; + /** + * The number of simultaneous touches that this surface can accept, where 0 + * indicates that the surface does not support touch events at all. + */ + int touches; + /** * The X coordinate of the upper-left corner of this layer, in pixels, * relative to its parent layer. This is only applicable to visible @@ -486,5 +492,23 @@ void guac_common_surface_flush(guac_common_surface* surface); void guac_common_surface_dup(guac_common_surface* surface, guac_user* user, guac_socket* socket); +/** + * Declares that the given surface should receive touch events. By default, + * surfaces are assumed to not expect touch events. This value is advisory, and + * the client is not required to honor the declared level of touch support. + * Implementations are expected to safely handle or ignore any received touch + * events, regardless of the level of touch support declared. regardless of + * the level of touch support declared. + * + * @param surface + * The surface to modify. + * + * @param touches + * The number of simultaneous touches that this surface can accept, where 0 + * indicates that the surface does not support touch events at all. + */ +void guac_common_surface_set_multitouch(guac_common_surface* surface, + int touches); + #endif diff --git a/src/common/recording.c b/src/common/recording.c index b4ad2193..01a212b7 100644 --- a/src/common/recording.c +++ b/src/common/recording.c @@ -137,7 +137,8 @@ static int guac_common_recording_open(const char* path, guac_common_recording* guac_common_recording_create(guac_client* client, const char* path, const char* name, int create_path, - int include_output, int include_mouse, int include_keys) { + int include_output, int include_mouse, int include_touch, + int include_keys) { char filename[GUAC_COMMON_RECORDING_MAX_NAME_LENGTH]; @@ -165,6 +166,7 @@ guac_common_recording* guac_common_recording_create(guac_client* client, recording->socket = guac_socket_open(fd); recording->include_output = include_output; recording->include_mouse = include_mouse; + recording->include_touch = include_touch; recording->include_keys = include_keys; /* Replace client socket with wrapped recording socket only if including @@ -203,6 +205,17 @@ void guac_common_recording_report_mouse(guac_common_recording* recording, } +void guac_common_recording_report_touch(guac_common_recording* recording, + int id, int x, int y, int x_radius, int y_radius, + double angle, double force) { + + /* Report touches only if recording should contain touch events */ + if (recording->include_touch) + guac_protocol_send_touch(recording->socket, id, x, y, + x_radius, y_radius, angle, force, guac_timestamp_current()); + +} + void guac_common_recording_report_key(guac_common_recording* recording, int keysym, int pressed) { diff --git a/src/common/surface.c b/src/common/surface.c index c86ca806..183ae11e 100644 --- a/src/common/surface.c +++ b/src/common/surface.c @@ -103,6 +103,19 @@ */ #define GUAC_SURFACE_WEBP_BLOCK_SIZE 8 +void guac_common_surface_set_multitouch(guac_common_surface* surface, + int touches) { + + pthread_mutex_lock(&surface->_lock); + + surface->touches = touches; + guac_protocol_send_set_int(surface->socket, surface->layer, + GUAC_PROTOCOL_LAYER_PARAMETER_MULTI_TOUCH, touches); + + pthread_mutex_unlock(&surface->_lock); + +} + void guac_common_surface_move(guac_common_surface* surface, int x, int y) { pthread_mutex_lock(&surface->_lock); @@ -1981,6 +1994,11 @@ void guac_common_surface_dup(guac_common_surface* surface, guac_user* user, guac_protocol_send_move(socket, surface->layer, surface->parent, surface->x, surface->y, surface->z); + /* Synchronize multi-touch support level */ + guac_protocol_send_set_int(surface->socket, surface->layer, + GUAC_PROTOCOL_LAYER_PARAMETER_MULTI_TOUCH, + surface->touches); + } /* Sync size to new socket */ diff --git a/src/libguac/guacamole/protocol-constants.h b/src/libguac/guacamole/protocol-constants.h index 5bd91b07..13d7f680 100644 --- a/src/libguac/guacamole/protocol-constants.h +++ b/src/libguac/guacamole/protocol-constants.h @@ -49,5 +49,20 @@ */ #define GUAC_PROTOCOL_BLOB_MAX_LENGTH 6048 +/** + * The name of the layer parameter defining the number of simultaneous points + * of contact supported by a layer. This parameter should be set to a non-zero + * value if the associated layer should receive touch events ("touch" + * instructions). + * + * This value specified for this parameter is advisory, and the client is not + * required to honor the declared level of touch support. Implementations are + * expected to safely handle or ignore any received touch events, regardless of + * the level of touch support declared. + * + * @see guac_protocol_send_set_int() + */ +#define GUAC_PROTOCOL_LAYER_PARAMETER_MULTI_TOUCH "multi-touch" + #endif diff --git a/src/libguac/guacamole/protocol.h b/src/libguac/guacamole/protocol.h index c6dbed29..362351c5 100644 --- a/src/libguac/guacamole/protocol.h +++ b/src/libguac/guacamole/protocol.h @@ -209,6 +209,53 @@ int vguac_protocol_send_log(guac_socket* socket, const char* format, int guac_protocol_send_mouse(guac_socket* socket, int x, int y, int button_mask, guac_timestamp timestamp); +/** + * Sends a touch instruction over the given guac_socket connection. + * + * If an error occurs sending the instruction, a non-zero value is + * returned, and guac_error is set appropriately. + * + * @param socket + * The guac_socket connection to use. + * + * @param id + * An arbitrary integer ID which uniquely identifies this contact relative + * to other active contacts. + * + * @param x + * The X coordinate of the center of the touch contact. + * + * @param y + * The Y coordinate of the center of the touch contact. + * + * @param x_radius + * The X radius of the ellipse covering the general area of the touch + * contact, in pixels. + * + * @param y_radius + * The Y radius of the ellipse covering the general area of the touch + * contact, in pixels. + * + * @param angle + * The rough angle of clockwise rotation of the general area of the touch + * contact, in degrees. + * + * @param force + * The relative force exerted by the touch contact, where 0 is no force + * (the touch has been lifted) and 1 is maximum force (the maximum amount + * of force representable by the device). + * + * @param timestamp + * The server timestamp (in milliseconds) at the point in time this touch + * event was acknowledged. + * + * @return + * Zero on success, non-zero on error. + */ +int guac_protocol_send_touch(guac_socket* socket, int id, int x, int y, + int x_radius, int y_radius, double angle, double force, + guac_timestamp timestamp); + /** * Sends a nest instruction over the given guac_socket connection. * @@ -271,6 +318,32 @@ int guac_protocol_send_ready(guac_socket* socket, const char* id); int guac_protocol_send_set(guac_socket* socket, const guac_layer* layer, const char* name, const char* value); +/** + * Sends a set instruction over the given guac_socket connection. This function + * behavies identically to guac_protocol_send_set() except that the provided + * parameter value is an integer, rather than a string. + * + * If an error occurs sending the instruction, a non-zero value is + * returned, and guac_error is set appropriately. + * + * @param socket + * The guac_socket connection to use. + * + * @param layer + * The layer to set the parameter of. + * + * @param name + * The name of the parameter to set. + * + * @param value + * The value to set the parameter to. + * + * @return + * Zero on success, non-zero on error. + */ +int guac_protocol_send_set_int(guac_socket* socket, const guac_layer* layer, + const char* name, int value); + /** * Sends a select instruction over the given guac_socket connection. * diff --git a/src/libguac/guacamole/user-fntypes.h b/src/libguac/guacamole/user-fntypes.h index d8689322..95f099e9 100644 --- a/src/libguac/guacamole/user-fntypes.h +++ b/src/libguac/guacamole/user-fntypes.h @@ -95,6 +95,51 @@ typedef void* guac_user_callback(guac_user* user, void* data); typedef int guac_user_mouse_handler(guac_user* user, int x, int y, int button_mask); +/** + * Handler for Guacamole touch events, invoked when a "touch" instruction has + * been received from a user. + * + * @param user + * The user that sent the touch event. + * + * @param id + * An arbitrary integer ID which uniquely identifies this contact relative + * to other active contacts. + * + * @param x + * The X coordinate of the center of the touch contact within the display + * when the event occurred, in pixels. This value is not guaranteed to be + * within the bounds of the display area. + * + * @param y + * The Y coordinate of the center of the touch contact within the display + * when the event occurred, in pixels. This value is not guaranteed to be + * within the bounds of the display area. + * + * @param x_radius + * The X radius of the ellipse covering the general area of the touch + * contact, in pixels. + * + * @param y_radius + * The Y radius of the ellipse covering the general area of the touch + * contact, in pixels. + * + * @param angle + * The rough angle of clockwise rotation of the general area of the touch + * contact, in degrees. + * + * @param force + * The relative force exerted by the touch contact, where 0 is no force + * (the touch has been lifted) and 1 is maximum force (the maximum amount + * of force representable by the device). + * + * @return + * Zero if the touch event was handled successfully, or non-zero if an + * error occurred. + */ +typedef int guac_user_touch_handler(guac_user* user, int id, int x, int y, + int x_radius, int y_radius, double angle, double force); + /** * Handler for Guacamole key events, invoked when a "key" event has been * received from a user. diff --git a/src/libguac/guacamole/user.h b/src/libguac/guacamole/user.h index de04d2a9..963dbe68 100644 --- a/src/libguac/guacamole/user.h +++ b/src/libguac/guacamole/user.h @@ -509,6 +509,27 @@ struct guac_user { */ guac_user_argv_handler* argv_handler; + /** + * Handler for touch events sent by the Guacamole web-client. + * + * The handler takes the integer X and Y coordinates representing the + * center of the touch contact, as well as several parameters describing + * the general shape of the contact area. The force parameter indicates the + * amount of force exerted by the contact, including whether the contact + * has been lifted. + * + * Example: + * @code + * int touch_handler(guac_user* user, int id, int x, int y, + * int x_radius, int y_radius, double angle, double force); + * + * int guac_user_init(guac_user* user, int argc, char** argv) { + * user->touch_handler = touch_handler; + * } + * @endcode + */ + guac_user_touch_handler* touch_handler; + }; /** diff --git a/src/libguac/protocol.c b/src/libguac/protocol.c index 59a46d5b..1c53c200 100644 --- a/src/libguac/protocol.c +++ b/src/libguac/protocol.c @@ -820,6 +820,37 @@ int guac_protocol_send_mouse(guac_socket* socket, int x, int y, } +int guac_protocol_send_touch(guac_socket* socket, int id, int x, int y, + int x_radius, int y_radius, double angle, double force, + guac_timestamp timestamp) { + + int ret_val; + + guac_socket_instruction_begin(socket); + ret_val = + guac_socket_write_string(socket, "5.touch,") + || __guac_socket_write_length_int(socket, id) + || guac_socket_write_string(socket, ",") + || __guac_socket_write_length_int(socket, x) + || guac_socket_write_string(socket, ",") + || __guac_socket_write_length_int(socket, y) + || guac_socket_write_string(socket, ",") + || __guac_socket_write_length_int(socket, x_radius) + || guac_socket_write_string(socket, ",") + || __guac_socket_write_length_int(socket, y_radius) + || guac_socket_write_string(socket, ",") + || __guac_socket_write_length_double(socket, angle) + || guac_socket_write_string(socket, ",") + || __guac_socket_write_length_double(socket, force) + || guac_socket_write_string(socket, ",") + || __guac_socket_write_length_int(socket, timestamp) + || guac_socket_write_string(socket, ";"); + + guac_socket_instruction_end(socket); + return ret_val; + +} + int guac_protocol_send_move(guac_socket* socket, const guac_layer* layer, const guac_layer* parent, int x, int y, int z) { @@ -1057,6 +1088,26 @@ int guac_protocol_send_set(guac_socket* socket, const guac_layer* layer, } +int guac_protocol_send_set_int(guac_socket* socket, const guac_layer* layer, + const char* name, int value) { + + int ret_val; + + guac_socket_instruction_begin(socket); + ret_val = + guac_socket_write_string(socket, "3.set,") + || __guac_socket_write_length_int(socket, layer->index) + || guac_socket_write_string(socket, ",") + || __guac_socket_write_length_string(socket, name) + || guac_socket_write_string(socket, ",") + || __guac_socket_write_length_int(socket, value) + || guac_socket_write_string(socket, ";"); + + guac_socket_instruction_end(socket); + return ret_val; + +} + int guac_protocol_send_select(guac_socket* socket, const char* protocol) { int ret_val; diff --git a/src/libguac/user-handlers.c b/src/libguac/user-handlers.c index f64fd299..c5cad9df 100644 --- a/src/libguac/user-handlers.c +++ b/src/libguac/user-handlers.c @@ -37,6 +37,7 @@ __guac_instruction_handler_mapping __guac_instruction_handler_map[] = { {"sync", __guac_handle_sync}, + {"touch", __guac_handle_touch}, {"mouse", __guac_handle_mouse}, {"key", __guac_handle_key}, {"clipboard", __guac_handle_clipboard}, @@ -150,6 +151,21 @@ int __guac_handle_sync(guac_user* user, int argc, char** argv) { return 0; } +int __guac_handle_touch(guac_user* user, int argc, char** argv) { + if (user->touch_handler) + return user->touch_handler( + user, + atoi(argv[0]), /* id */ + atoi(argv[1]), /* x */ + atoi(argv[2]), /* y */ + atoi(argv[3]), /* x_radius */ + atoi(argv[4]), /* y_radius */ + atof(argv[5]), /* angle */ + atof(argv[6]) /* force */ + ); + return 0; +} + int __guac_handle_mouse(guac_user* user, int argc, char** argv) { if (user->mouse_handler) return user->mouse_handler( diff --git a/src/libguac/user-handlers.h b/src/libguac/user-handlers.h index 263c2928..a51a3f89 100644 --- a/src/libguac/user-handlers.h +++ b/src/libguac/user-handlers.h @@ -85,6 +85,13 @@ __guac_instruction_handler __guac_handle_sync; */ __guac_instruction_handler __guac_handle_mouse; +/** + * Internal initial handler for the touch instruction. When a touch instruction + * is received, this handler will be called. The client's touch handler will + * be invoked if defined. + */ +__guac_instruction_handler __guac_handle_touch; + /** * Internal initial handler for the key instruction. When a key instruction * is received, this handler will be called. The client's key handler will diff --git a/src/protocols/kubernetes/kubernetes.c b/src/protocols/kubernetes/kubernetes.c index 7158a989..1998d13b 100644 --- a/src/protocols/kubernetes/kubernetes.c +++ b/src/protocols/kubernetes/kubernetes.c @@ -234,6 +234,7 @@ void* guac_kubernetes_client_thread(void* data) { settings->create_recording_path, !settings->recording_exclude_output, !settings->recording_exclude_mouse, + 0, /* Touch events not supported */ settings->recording_include_keys); } diff --git a/src/protocols/rdp/Makefile.am b/src/protocols/rdp/Makefile.am index e8ddfeb4..2612bdff 100644 --- a/src/protocols/rdp/Makefile.am +++ b/src/protocols/rdp/Makefile.am @@ -56,6 +56,7 @@ libguac_client_rdp_la_SOURCES = \ channels/rdpdr/rdpdr-messages.c \ channels/rdpdr/rdpdr-printer.c \ channels/rdpdr/rdpdr.c \ + channels/rdpei.c \ channels/rdpsnd/rdpsnd-messages.c \ channels/rdpsnd/rdpsnd.c \ client.c \ @@ -101,6 +102,7 @@ noinst_HEADERS = \ channels/rdpdr/rdpdr-messages.h \ channels/rdpdr/rdpdr-printer.h \ channels/rdpdr/rdpdr.h \ + channels/rdpei.h \ channels/rdpsnd/rdpsnd-messages.h \ channels/rdpsnd/rdpsnd.h \ client.h \ diff --git a/src/protocols/rdp/channels/rdpei.c b/src/protocols/rdp/channels/rdpei.c new file mode 100644 index 00000000..f1c5d09e --- /dev/null +++ b/src/protocols/rdp/channels/rdpei.c @@ -0,0 +1,170 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +#include "channels/rdpei.h" +#include "common/surface.h" +#include "plugins/channels.h" +#include "rdp.h" +#include "settings.h" + +#include +#include +#include +#include +#include + +#include +#include + +guac_rdp_rdpei* guac_rdp_rdpei_alloc() { + + guac_rdp_rdpei* rdpei = malloc(sizeof(guac_rdp_rdpei)); + + /* Not yet connected */ + rdpei->rdpei = NULL; + + /* No active touches */ + for (int i = 0; i < GUAC_RDP_RDPEI_MAX_TOUCHES; i++) + rdpei->touch[i].active = 0; + + return rdpei; + +} + +void guac_rdp_rdpei_free(guac_rdp_rdpei* rdpei) { + free(rdpei); +} + +/** + * Callback which associates handlers specific to Guacamole with the + * RdpeiClientContext instance allocated by FreeRDP to deal with received + * RDPEI (multi-touch input) messages. + * + * This function is called whenever a channel connects via the PubSub event + * system within FreeRDP, but only has any effect if the connected channel is + * the RDPEI channel. This specific callback is registered with the + * PubSub system of the relevant rdpContext when guac_rdp_rdpei_load_plugin() is + * called. + * + * @param context + * The rdpContext associated with the active RDP session. + * + * @param e + * Event-specific arguments, mainly the name of the channel, and a + * reference to the associated plugin loaded for that channel by FreeRDP. + */ +static void guac_rdp_rdpei_channel_connected(rdpContext* context, + ChannelConnectedEventArgs* e) { + + guac_client* client = ((rdp_freerdp_context*) context)->client; + guac_rdp_client* rdp_client = (guac_rdp_client*) client->data; + guac_rdp_rdpei* guac_rdpei = rdp_client->rdpei; + + /* Ignore connection event if it's not for the RDPEI channel */ + if (strcmp(e->name, RDPEI_DVC_CHANNEL_NAME) != 0) + return; + + /* Store reference to the RDPEI plugin once it's connected */ + RdpeiClientContext* rdpei = (RdpeiClientContext*) e->pInterface; + guac_rdpei->rdpei = rdpei; + + /* Declare level of multi-touch support */ + guac_common_surface_set_multitouch(rdp_client->display->default_surface, + GUAC_RDP_RDPEI_MAX_TOUCHES); + + guac_client_log(client, GUAC_LOG_DEBUG, "RDPEI channel will be used for " + "multi-touch support."); + +} + +void guac_rdp_rdpei_load_plugin(rdpContext* context) { + + /* Subscribe to and handle channel connected events */ + PubSub_SubscribeChannelConnected(context->pubSub, + (pChannelConnectedEventHandler) guac_rdp_rdpei_channel_connected); + + /* Add "rdpei" channel */ + guac_freerdp_dynamic_channel_collection_add(context->settings, "rdpei", NULL); + +} + +int guac_rdp_rdpei_touch_update(guac_rdp_rdpei* rdpei, int id, int x, int y, + double force) { + + int contact_id; /* Ignored */ + + /* Track touches only if channel is connected */ + RdpeiClientContext* context = rdpei->rdpei; + if (context == NULL) + return 1; + + /* Locate active touch having provided ID */ + guac_rdp_rdpei_touch* touch = NULL; + for (int i = 0; i < GUAC_RDP_RDPEI_MAX_TOUCHES; i++) { + if (rdpei->touch[i].active && rdpei->touch[i].id == id) { + touch = &rdpei->touch[i]; + break; + } + } + + /* If no such touch exists, add it */ + if (touch == NULL) { + for (int i = 0; i < GUAC_RDP_RDPEI_MAX_TOUCHES; i++) { + if (!rdpei->touch[i].active) { + touch = &rdpei->touch[i]; + touch->id = id; + break; + } + } + } + + /* If the touch couldn't be added, we're already at maximum touch capacity. + * Drop the event. */ + if (touch == NULL) + return 1; + + /* Signal the end of an established touch if touch force has become zero + * (this should be a safe comparison, as zero has an exact representation + * in floating point, and the client side will use an exact value to + * represent the absence of a touch) */ + if (force == 0.0) { + + /* Ignore release of touches that we aren't tracking */ + if (!touch->active) + return 1; + + context->TouchEnd(context, id, x, y, &contact_id); + touch->active = 0; + + } + + /* Signal the start of a touch if this is the first we've seen it */ + else if (!touch->active) { + context->TouchBegin(context, id, x, y, &contact_id); + touch->active = 1; + } + + /* Established touches need only be updated */ + else + context->TouchUpdate(context, id, x, y, &contact_id); + + return 0; + +} + diff --git a/src/protocols/rdp/channels/rdpei.h b/src/protocols/rdp/channels/rdpei.h new file mode 100644 index 00000000..5ca10c30 --- /dev/null +++ b/src/protocols/rdp/channels/rdpei.h @@ -0,0 +1,154 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +#ifndef GUAC_RDP_CHANNELS_RDPEI_H +#define GUAC_RDP_CHANNELS_RDPEI_H + +#include "settings.h" + +#include +#include +#include +#include + +/** + * The maximum number of simultaneously-tracked touches. + */ +#define GUAC_RDP_RDPEI_MAX_TOUCHES 10 + +/** + * A single, tracked touch contact. + */ +typedef struct guac_rdp_rdpei_touch { + + /** + * Whether this touch is active (1) or inactive (0). An active touch is + * being tracked, while an inactive touch is simple an empty space awaiting + * use by some future touch event. + */ + int active; + + /** + * The unique ID representing this touch contact. + */ + int id; + + /** + * The X-coordinate of this touch, in pixels. + */ + int x; + + /** + * The Y-coordinate of this touch, in pixels. + */ + int y; + +} guac_rdp_rdpei_touch; + +/** + * Multi-touch input module. + */ +typedef struct guac_rdp_rdpei { + + /** + * RDPEI control interface. + */ + RdpeiClientContext* rdpei; + + /** + * All currently-tracked touches. + */ + guac_rdp_rdpei_touch touch[GUAC_RDP_RDPEI_MAX_TOUCHES]; + +} guac_rdp_rdpei; + +/** + * Allocates a new RDPEI module, which will ultimately control the RDPEI + * channel once connected. The RDPEI channel allows multi-touch input + * events to be sent to the RDP server. + * + * @return + * A newly-allocated RDPEI module. + */ +guac_rdp_rdpei* guac_rdp_rdpei_alloc(); + +/** + * Frees the resources associated with support for the RDPEI channel. Only + * resources specific to Guacamole are freed. Resources specific to FreeRDP's + * handling of the RDPEI channel will be freed by FreeRDP. If no resources are + * currently allocated for RDPEI, this function has no effect. + * + * @param rdpei + * The RDPEI module to free. + */ +void guac_rdp_rdpei_free(guac_rdp_rdpei* rdpei); + +/** + * Adds FreeRDP's "rdpei" plugin to the list of dynamic virtual channel plugins + * to be loaded by FreeRDP's "drdynvc" plugin. The context of the plugin will + * automatically be assicated with the guac_rdp_rdpei instance pointed to by the + * current guac_rdp_client. The plugin will only be loaded once the "drdynvc" + * plugin is loaded. The "rdpei" plugin ultimately adds support for multi-touch + * input via the RDPEI channel. + * + * If failures occur, messages noting the specifics of those failures will be + * logged, and the RDP side of multi-touch support will not be functional. + * + * This MUST be called within the PreConnect callback of the freerdp instance + * for multi-touch support to be loaded. + * + * @param context + * The rdpContext associated with the active RDP session. + */ +void guac_rdp_rdpei_load_plugin(rdpContext* context); + +/** + * Reports to the RDP server that the status of a single touch contact has + * changed. Depending on the amount of force associated with the touch and + * whether the touch has been encountered before, this will result a new touch + * contact, updates to an existing contact, or removal of an existing contact. + * If the RDPEI channel has not yet been connected, touches will be ignored and + * dropped until it is connected. + * + * @param rdpei + * The RDPEI module associated with the RDP session. + * + * @param id + * An arbitrary integer ID unique to the touch being updated. + * + * @param x + * The X-coordinate of the touch, in pixels. + * + * @param y + * The Y-coordinate of the touch, in pixels. + * + * @param force + * The amount of force currently being exerted on the device by the touch + * contact in question, where 1.0 is the maximum amount of force + * representable and 0.0 indicates the contact has been lifted. + * + * @return + * Zero if the touch event was successfully processed, non-zero if the + * touch event had to be dropped. + */ +int guac_rdp_rdpei_touch_update(guac_rdp_rdpei* rdpei, int id, int x, int y, + double force); + +#endif + diff --git a/src/protocols/rdp/client.c b/src/protocols/rdp/client.c index 07c90a33..50808dcf 100644 --- a/src/protocols/rdp/client.c +++ b/src/protocols/rdp/client.c @@ -147,6 +147,9 @@ int guac_client_init(guac_client* client, int argc, char** argv) { /* Init display update module */ rdp_client->disp = guac_rdp_disp_alloc(); + /* Init multi-touch support module (RDPEI) */ + rdp_client->rdpei = guac_rdp_rdpei_alloc(); + /* Redirect FreeRDP log messages to guac_client_log() */ guac_rdp_redirect_wlog(client); @@ -187,6 +190,9 @@ int guac_rdp_client_free_handler(guac_client* client) { /* Free display update module */ guac_rdp_disp_free(rdp_client->disp); + /* Free multi-touch support module (RDPEI) */ + guac_rdp_rdpei_free(rdp_client->rdpei); + /* Clean up filesystem, if allocated */ if (rdp_client->filesystem != NULL) guac_rdp_fs_free(rdp_client->filesystem); diff --git a/src/protocols/rdp/input.c b/src/protocols/rdp/input.c index cb9bb10e..0c36d614 100644 --- a/src/protocols/rdp/input.c +++ b/src/protocols/rdp/input.c @@ -18,6 +18,7 @@ */ #include "channels/disp.h" +#include "channels/rdpei.h" #include "common/cursor.h" #include "common/display.h" #include "common/recording.h" @@ -122,6 +123,33 @@ complete: return 0; } +int guac_rdp_user_touch_handler(guac_user* user, int id, int x, int y, + int x_radius, int y_radius, double angle, double force) { + + guac_client* client = user->client; + guac_rdp_client* rdp_client = (guac_rdp_client*) client->data; + + pthread_rwlock_rdlock(&(rdp_client->lock)); + + /* Skip if not yet connected */ + freerdp* rdp_inst = rdp_client->rdp_inst; + if (rdp_inst == NULL) + goto complete; + + /* Report touch event within recording */ + if (rdp_client->recording != NULL) + guac_common_recording_report_touch(rdp_client->recording, id, x, y, + x_radius, y_radius, angle, force); + + /* Forward touch event along RDPEI channel */ + guac_rdp_rdpei_touch_update(rdp_client->rdpei, id, x, y, force); + +complete: + pthread_rwlock_unlock(&(rdp_client->lock)); + + return 0; +} + int guac_rdp_user_key_handler(guac_user* user, int keysym, int pressed) { guac_client* client = user->client; diff --git a/src/protocols/rdp/input.h b/src/protocols/rdp/input.h index 60ef0641..eb9e4825 100644 --- a/src/protocols/rdp/input.h +++ b/src/protocols/rdp/input.h @@ -27,6 +27,11 @@ */ guac_user_mouse_handler guac_rdp_user_mouse_handler; +/** + * Handler for Guacamole user touch events. + */ +guac_user_touch_handler guac_rdp_user_touch_handler; + /** * Handler for Guacamole user key events. */ diff --git a/src/protocols/rdp/rdp.c b/src/protocols/rdp/rdp.c index ce6a2796..db62bc4b 100644 --- a/src/protocols/rdp/rdp.c +++ b/src/protocols/rdp/rdp.c @@ -27,6 +27,7 @@ #include "channels/pipe-svc.h" #include "channels/rail.h" #include "channels/rdpdr/rdpdr.h" +#include "channels/rdpei.h" #include "channels/rdpsnd/rdpsnd.h" #include "client.h" #include "color.h" @@ -100,6 +101,10 @@ BOOL rdp_freerdp_pre_connect(freerdp* instance) { if (settings->resize_method == GUAC_RESIZE_DISPLAY_UPDATE) guac_rdp_disp_load_plugin(context); + /* Load "rdpei" plugin for multi-touch support */ + if (settings->enable_touch) + guac_rdp_rdpei_load_plugin(context); + /* Load "AUDIO_INPUT" plugin for audio input*/ if (settings->enable_audio_input) { rdp_client->audio_input = guac_rdp_audio_buffer_alloc(); @@ -421,6 +426,7 @@ static int guac_rdp_handle_connection(guac_client* client) { settings->create_recording_path, !settings->recording_exclude_output, !settings->recording_exclude_mouse, + !settings->recording_exclude_touch, settings->recording_include_keys); } diff --git a/src/protocols/rdp/rdp.h b/src/protocols/rdp/rdp.h index e65f702c..cc537966 100644 --- a/src/protocols/rdp/rdp.h +++ b/src/protocols/rdp/rdp.h @@ -23,6 +23,7 @@ #include "channels/audio-input/audio-buffer.h" #include "channels/cliprdr.h" #include "channels/disp.h" +#include "channels/rdpei.h" #include "common/clipboard.h" #include "common/display.h" #include "common/list.h" @@ -148,6 +149,11 @@ typedef struct guac_rdp_client { */ guac_rdp_disp* disp; + /** + * Multi-touch support module (RDPEI). + */ + guac_rdp_rdpei* rdpei; + /** * List of all available static virtual channels. */ diff --git a/src/protocols/rdp/settings.c b/src/protocols/rdp/settings.c index e12e8c12..f834a7fa 100644 --- a/src/protocols/rdp/settings.c +++ b/src/protocols/rdp/settings.c @@ -104,10 +104,12 @@ const char* GUAC_RDP_CLIENT_ARGS[] = { "recording-name", "recording-exclude-output", "recording-exclude-mouse", + "recording-exclude-touch", "recording-include-keys", "create-recording-path", "resize-method", "enable-audio-input", + "enable-touch", "read-only", "gateway-hostname", @@ -499,6 +501,13 @@ enum RDP_ARGS_IDX { */ IDX_RECORDING_EXCLUDE_MOUSE, + /** + * Whether changes to touch contact state should NOT be included in the + * session recording. Touch state is included by default, as it may be + * necessary for touch interactions to be rendered in any resulting video. + */ + IDX_RECORDING_EXCLUDE_TOUCH, + /** * Whether keys pressed and released should be included in the session * recording. Key events are NOT included by default within the recording, @@ -527,6 +536,12 @@ enum RDP_ARGS_IDX { */ IDX_ENABLE_AUDIO_INPUT, + /** + * "true" if multi-touch support should be enabled for the RDP connection, + * "false" or blank otherwise. + */ + IDX_ENABLE_TOUCH, + /** * "true" if this connection should be read-only (user input should be * dropped), "false" or blank otherwise. @@ -1050,6 +1065,11 @@ guac_rdp_settings* guac_rdp_parse_args(guac_user* user, guac_user_parse_args_boolean(user, GUAC_RDP_CLIENT_ARGS, argv, IDX_RECORDING_EXCLUDE_MOUSE, 0); + /* Parse touch exclusion flag */ + settings->recording_exclude_touch = + guac_user_parse_args_boolean(user, GUAC_RDP_CLIENT_ARGS, argv, + IDX_RECORDING_EXCLUDE_TOUCH, 0); + /* Parse key event inclusion flag */ settings->recording_include_keys = guac_user_parse_args_boolean(user, GUAC_RDP_CLIENT_ARGS, argv, @@ -1085,6 +1105,11 @@ guac_rdp_settings* guac_rdp_parse_args(guac_user* user, settings->resize_method = GUAC_RESIZE_NONE; } + /* Multi-touch input enable/disable */ + settings->enable_touch = + guac_user_parse_args_boolean(user, GUAC_RDP_CLIENT_ARGS, argv, + IDX_ENABLE_TOUCH, 0); + /* Audio input enable/disable */ settings->enable_audio_input = guac_user_parse_args_boolean(user, GUAC_RDP_CLIENT_ARGS, argv, diff --git a/src/protocols/rdp/settings.h b/src/protocols/rdp/settings.h index 323ba5d3..c540fcc8 100644 --- a/src/protocols/rdp/settings.h +++ b/src/protocols/rdp/settings.h @@ -505,6 +505,14 @@ typedef struct guac_rdp_settings { */ int recording_exclude_mouse; + /** + * Non-zero if changes to touch state should NOT be included in the session + * recording, zero otherwise. Touch state is included by default, as it may + * be necessary for touch interactions to be rendered in any resulting + * video. + */ + int recording_exclude_touch; + /** * Non-zero if keys pressed and released should be included in the session * recording, zero otherwise. Key events are NOT included by default within @@ -525,6 +533,11 @@ typedef struct guac_rdp_settings { */ int enable_audio_input; + /** + * Whether multi-touch support is enabled. + */ + int enable_touch; + /** * The hostname of the remote desktop gateway that should be used as an * intermediary for the remote desktop connection. If no gateway should diff --git a/src/protocols/rdp/user.c b/src/protocols/rdp/user.c index 351c67e6..992e551b 100644 --- a/src/protocols/rdp/user.c +++ b/src/protocols/rdp/user.c @@ -105,6 +105,10 @@ int guac_rdp_user_join_handler(guac_user* user, int argc, char** argv) { user->mouse_handler = guac_rdp_user_mouse_handler; user->key_handler = guac_rdp_user_key_handler; + /* Multi-touch events */ + if (settings->enable_touch) + user->touch_handler = guac_rdp_user_touch_handler; + /* Inbound (client to server) clipboard transfer */ if (!settings->disable_paste) user->clipboard_handler = guac_rdp_clipboard_handler; diff --git a/src/protocols/ssh/ssh.c b/src/protocols/ssh/ssh.c index aaa5a8eb..81fb0855 100644 --- a/src/protocols/ssh/ssh.c +++ b/src/protocols/ssh/ssh.c @@ -234,6 +234,7 @@ void* ssh_client_thread(void* data) { settings->create_recording_path, !settings->recording_exclude_output, !settings->recording_exclude_mouse, + 0, /* Touch events not supported */ settings->recording_include_keys); } diff --git a/src/protocols/telnet/telnet.c b/src/protocols/telnet/telnet.c index b2b3106e..f6ea46b4 100644 --- a/src/protocols/telnet/telnet.c +++ b/src/protocols/telnet/telnet.c @@ -581,6 +581,7 @@ void* guac_telnet_client_thread(void* data) { settings->create_recording_path, !settings->recording_exclude_output, !settings->recording_exclude_mouse, + 0, /* Touch events not supported */ settings->recording_include_keys); } diff --git a/src/protocols/vnc/vnc.c b/src/protocols/vnc/vnc.c index eadaa8b6..ade8278b 100644 --- a/src/protocols/vnc/vnc.c +++ b/src/protocols/vnc/vnc.c @@ -427,6 +427,7 @@ void* guac_vnc_client_thread(void* data) { settings->create_recording_path, !settings->recording_exclude_output, !settings->recording_exclude_mouse, + 0, /* Touch events not supported */ settings->recording_include_keys); }