diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 1defc9c5..d1caf896 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -400,6 +400,16 @@ rustfmt:
- cargo fmt --version
- cargo fmt -- --color=always --check
+gstwebrtc-api lint:
+ image: node:lts
+ stage: "lint"
+ rules:
+ - when: 'always'
+ script:
+ - cd net/webrtc/gstwebrtc-api
+ - npm install
+ - npm run check
+
check commits:
extends: .img-stable
stage: "lint"
diff --git a/net/webrtc/README.md b/net/webrtc/README.md
index bfee180b..e14b8202 100644
--- a/net/webrtc/README.md
+++ b/net/webrtc/README.md
@@ -1,13 +1,19 @@
-# webrtcsink
+# webrtcsink and webrtcsrc
-All-batteries included GStreamer WebRTC producer, that tries its best to do The Right Thing™.
+All-batteries included GStreamer WebRTC producer and consumer, that try their
+best to do The Right Thing™.
+
+It also provides a flexible and all-purposes WebRTC signalling server
+([gst-webrtc-signalling-server](signalling/src/bin/server.rs)) and a Javascript
+API ([gstwebrtc-api](gstwebrtc-api)) to produce and consume compatible WebRTC
+streams from a web browser.
## Use case
-The [webrtcbin] element in GStreamer is extremely flexible and powerful, but using
-it can be a difficult exercise. When all you want to do is serve a fixed set of streams
-to any number of consumers, `webrtcsink` (which wraps `webrtcbin` internally) can be a
-useful alternative.
+The [webrtcbin] element in GStreamer is extremely flexible and powerful, but
+using it can be a difficult exercise. When all you want to do is serve a fixed
+set of streams to any number of consumers, `webrtcsink` (which wraps
+`webrtcbin` internally) can be a useful alternative.
[webrtcbin]: https://gstreamer.freedesktop.org/documentation/webrtc/index.html
@@ -15,25 +21,27 @@ useful alternative.
`webrtcsink` implements the following features:
-* Built-in signaller: when using the default signalling server, this element will
- perform signalling without requiring application interaction.
+* Built-in signaller: when using the default signalling server, this element
+ will perform signalling without requiring application interaction.
This makes it usable directly from `gst-launch`.
-* Application-provided signalling: `webrtcsink` can be instantiated by an application
- with a custom signaller. That signaller must be a GObject, and must implement the
- `Signallable` interface as defined [here](src/webrtcsink/mod.rs). The
- [default signaller](src/signaller/mod.rs) can be used as an example.
+* Application-provided signalling: `webrtcsink` can be instantiated by an
+ application with a custom signaller. That signaller must be a GObject, and
+ must implement the `Signallable` interface as defined
+ [here](src/webrtcsink/mod.rs). The [default signaller](src/signaller/mod.rs)
+ can be used as an example.
- An [example project] is also available to use as a boilerplate for implementing
- and using a custom signaller.
+ An [example project] is also available to use as a boilerplate for
+ implementing and using a custom signaller.
-* Sandboxed consumers: when a consumer is added, its encoder / payloader / webrtcbin
- elements run in a separately managed pipeline. This provides a certain level of
- sandboxing, as opposed to having those elements running inside the element itself.
+* Sandboxed consumers: when a consumer is added, its encoder / payloader /
+ webrtcbin elements run in a separately managed pipeline. This provides a
+ certain level of sandboxing, as opposed to having those elements running
+ inside the element itself.
- It is important to note that at this moment, encoding is not shared between consumers.
- While this is not on the roadmap at the moment, nothing in the design prevents
- implementing this optimization.
+ It is important to note that at this moment, encoding is not shared between
+ consumers. While this is not on the roadmap at the moment, nothing in the
+ design prevents implementing this optimization.
* Congestion control: the element leverages transport-wide congestion control
feedback messages in order to adapt the bitrate of individual consumers' video
@@ -45,20 +53,20 @@ useful alternative.
* Packet loss mitigation: webrtcsink now supports sending protection packets for
Forward Error Correction, modulating the amount as a function of the available
- bandwidth, and can honor retransmission requests. Both features can be disabled
- via properties.
+ bandwidth, and can honor retransmission requests. Both features can be
+ disabled via properties.
It is important to note that full control over the individual elements used by
-`webrtcsink` is *not* on the roadmap, as it will act as a black box in that respect,
-for example `webrtcsink` wants to reserve control over the bitrate for congestion
-control.
+`webrtcsink` is *not* on the roadmap, as it will act as a black box in that
+respect, for example `webrtcsink` wants to reserve control over the bitrate for
+congestion control.
A signal is now available however for the application to provide the initial
configuration for the encoders `webrtcsink` instantiates.
-If more granular control is required, applications should use `webrtcbin` directly,
-`webrtcsink` will focus on trying to just do the right thing, although it might
-expose more interfaces to guide and tune the heuristics it employs.
+If more granular control is required, applications should use `webrtcbin`
+directly, `webrtcsink` will focus on trying to just do the right thing, although
+it might expose more interfaces to guide and tune the heuristics it employs.
[example project]: https://github.com/centricular/webrtcsink-custom-signaller
@@ -74,38 +82,49 @@ cargo build
## Usage
-Open three terminals. In the first, run:
+Open three terminals. In the first one, run the signalling server:
``` shell
WEBRTCSINK_SIGNALLING_SERVER_LOG=debug cargo run --bin gst-webrtc-signalling-server
```
-In the second, run:
+In the second one, run a web browser client (can produce and consume streams):
``` shell
-python3 -m http.server -d www/
+cd gstwebrtc-api
+npm install
+npm start
```
-In the third, run:
+In the third one, run a webrtcsink producer from a GStreamer pipeline:
``` shell
export GST_PLUGIN_PATH=$PWD/target/debug:$GST_PLUGIN_PATH
-gst-launch-1.0 webrtcsink name=ws videotestsrc ! ws. audiotestsrc ! ws.
+gst-launch-1.0 webrtcsink name=ws meta="meta,name=gst-stream" videotestsrc ! ws. audiotestsrc ! ws.
```
-When the pipeline above is running successfully, open a browser and
-point it to the http server:
+The webrtcsink produced stream will appear in the former web page
+(automatically opened at https://localhost:9090) under the name "gst-stream",
+if you click on it you should see a test video stream and hear a test tone.
+
+You can also produce WebRTC streams from the web browser and consume them with
+a GStreamer pipeline. Click on the "Start Capture" button and copy the
+"Client ID" value.
+
+Then open a new terminal and run:
``` shell
-gio open http://127.0.0.1:8000
+export GST_PLUGIN_PATH=$PWD/target/debug:$GST_PLUGIN_PATH
+gst-launch-1.0 playbin uri=gstwebrtc://127.0.0.1:8443?peer-id=[Client ID]
```
-You should see an identifier listed in the left-hand panel, click on
-it. You should see a test video stream, and hear a test tone.
+Replacing the "peer-id" value with the previously copied "Client ID" value. You
+should see the playbin element opening a window and showing you the content
+produced by the web page.
## Configuration
-The element itself can be configured through its properties, see
+The webrtcsink element itself can be configured through its properties, see
`gst-inspect-1.0 webrtcsink` for more information about that, in addition the
default signaller also exposes properties for configuring it, in
particular setting the signalling server address, those properties
@@ -122,15 +141,15 @@ gst-launch-1.0 webrtcsink signaller::address="ws://127.0.0.1:8443" ..
with the content, for example move with your mouse, entering keys with the
keyboard, etc... On top of that a `WebRTCDataChannel` based protocol has been
implemented and can be activated with the `enable-data-channel-navigation=true`
-property. The [demo](www/) implements the protocol and you can easily test this
-feature, using the [`wpesrc`] for example.
+property. The [gstwebrtc-api](gstwebrtc-api) implements the protocol and you
+can easily test this feature using the [`wpesrc`] for example.
As an example, the following pipeline allows you to navigate the GStreamer
-documentation inside the video running within your web browser (in
-http://127.0.0.1:8000 if you followed previous steps of that readme):
+documentation inside the video running within your web browser (at
+https://127.0.0.1:9090 if you followed previous steps in that readme):
``` shell
-gst-launch-1.0 wpesrc location=https://gstreamer.freedesktop.org/documentation/ ! webrtcsink enable-data-channel-navigation=true
+gst-launch-1.0 wpesrc location=https://gstreamer.freedesktop.org/documentation/ ! queue ! webrtcsink enable-data-channel-navigation=true meta="meta,name=web-stream"
```
[`GstNavigation`]: https://gstreamer.freedesktop.org/documentation/video/gstnavigation.html
@@ -139,16 +158,16 @@ gst-launch-1.0 wpesrc location=https://gstreamer.freedesktop.org/documentation/
## Testing congestion control
For the purpose of testing congestion in a reproducible manner, a
-[simple tool] has been used, I only used it on Linux but it is documented
-as usable on MacOS too. I had to run the client browser on a separate
-machine on my local network for congestion to actually be applied, I didn't
-look into why that was necessary.
+[simple tool] has been used, it has been used on Linux exclusively but it is
+also documented as usable on MacOS too. Client web browser has to be launched
+on a separate machine on the LAN to test for congestion, although specific
+configurations may allow to run it on the same machine.
-My testing procedure was:
+Testing procedure was:
-* identify the server machine network interface (eg with `ifconfig` on Linux)
+* identify the server machine network interface (e.g. with `ifconfig` on Linux)
-* identify the client machine IP address (eg with `ifconfig` on Linux)
+* identify the client machine IP address (e.g. with `ifconfig` on Linux)
* start the various services as explained in the Usage section (use
`GST_DEBUG=webrtcsink:7` to get detailed logs about congestion control)
@@ -171,7 +190,7 @@ My testing procedure was:
$HOME/go/bin/comcast --device=$SERVER_INTERFACE --stop
```
-For comparison, the congestion control property can be set to disabled on
+For comparison, the congestion control property can be set to "disabled" on
webrtcsink, then the above procedure applied again, the expected result is
for playback to simply crawl down to a halt until the bandwidth limitation
is lifted:
@@ -184,18 +203,18 @@ gst-launch-1.0 webrtcsink congestion-control=disabled
## Monitoring tool
-An example server / client application for monitoring per-consumer stats
+An example of client/server application for monitoring per-consumer stats
can be found [here].
[here]: https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/tree/main/net/webrtc/examples
## License
-All the rust code in this repository is licensed under the [Mozilla Public License Version 2.0].
+All the rust code in this repository is licensed under the
+[Mozilla Public License Version 2.0].
-Parts of the JavaScript code in the www/ example are licensed under the [Apache License, Version 2.0],
-the rest is licensed under the [Mozilla Public License Version 2.0] unless advertised in the
-header.
+Code in [gstwebrtc-api](gstwebrtc-api) is also licensed under the
+[Mozilla Public License Version 2.0].
## Using the AWS KVS signaller
@@ -212,4 +231,3 @@ AWS_ACCESS_KEY_ID="XXX" AWS_SECRET_ACCESS_KEY="XXX" gst-launch-1.0 videotestsrc
* Connect a viewer @
[Mozilla Public License Version 2.0]: http://opensource.org/licenses/MPL-2.0
-[Apache License, Version 2.0]: https://www.apache.org/licenses/LICENSE-2.1
diff --git a/net/webrtc/gstwebrtc-api/.editorconfig b/net/webrtc/gstwebrtc-api/.editorconfig
new file mode 100644
index 00000000..4960585e
--- /dev/null
+++ b/net/webrtc/gstwebrtc-api/.editorconfig
@@ -0,0 +1,9 @@
+root = true
+
+[*]
+end_of_line = lf
+insert_final_newline = true
+trim_trailing_whitespace = true
+charset = utf-8
+indent_style = space
+indent_size = 2
diff --git a/net/webrtc/gstwebrtc-api/.eslintrc.json b/net/webrtc/gstwebrtc-api/.eslintrc.json
new file mode 100644
index 00000000..b804e8ca
--- /dev/null
+++ b/net/webrtc/gstwebrtc-api/.eslintrc.json
@@ -0,0 +1,61 @@
+{
+ "root": true,
+ "parserOptions": {
+ "ecmaVersion": 2017,
+ "sourceType": "module"
+ },
+ "env": {
+ "browser": true,
+ "es6": true
+ },
+ "extends": "eslint:recommended",
+ "rules": {
+ "getter-return": "error",
+ "no-await-in-loop": "error",
+ "no-console": "off",
+ "no-extra-parens": "off",
+ "no-template-curly-in-string": "error",
+ "consistent-return": "error",
+ "curly": "error",
+ "eqeqeq": "error",
+ "no-eval": "error",
+ "no-extra-bind": "error",
+ "no-invalid-this": "error",
+ "no-labels": "error",
+ "no-lone-blocks": "error",
+ "no-loop-func": "error",
+ "no-multi-spaces": "error",
+ "no-return-assign": "error",
+ "no-return-await": "error",
+ "no-self-compare": "error",
+ "no-throw-literal": "error",
+ "no-unused-expressions": "error",
+ "no-useless-call": "error",
+ "no-useless-concat": "error",
+ "no-useless-return": "error",
+ "no-void": "error",
+ "no-shadow": "error",
+ "block-spacing": "error",
+ "brace-style": [
+ "error",
+ "1tbs",
+ {
+ "allowSingleLine": true
+ }
+ ],
+ "camelcase": "error",
+ "comma-dangle": "error",
+ "eol-last": "error",
+ "indent": [
+ "error",
+ 2
+ ],
+ "linebreak-style": "error",
+ "new-parens": "error",
+ "no-lonely-if": "error",
+ "no-multiple-empty-lines": "error",
+ "no-trailing-spaces": "error",
+ "quotes": "error",
+ "semi": "error"
+ }
+}
diff --git a/net/webrtc/gstwebrtc-api/.gitattributes b/net/webrtc/gstwebrtc-api/.gitattributes
new file mode 100644
index 00000000..6313b56c
--- /dev/null
+++ b/net/webrtc/gstwebrtc-api/.gitattributes
@@ -0,0 +1 @@
+* text=auto eol=lf
diff --git a/net/webrtc/gstwebrtc-api/.gitignore b/net/webrtc/gstwebrtc-api/.gitignore
new file mode 100644
index 00000000..929fa012
--- /dev/null
+++ b/net/webrtc/gstwebrtc-api/.gitignore
@@ -0,0 +1,4 @@
+/dist/
+/docs/
+/node_modules/
+/*.tgz
diff --git a/net/webrtc/gstwebrtc-api/.npmrc b/net/webrtc/gstwebrtc-api/.npmrc
new file mode 100644
index 00000000..f225f8cd
--- /dev/null
+++ b/net/webrtc/gstwebrtc-api/.npmrc
@@ -0,0 +1,5 @@
+engine-strict = true
+fund = false
+git-tag-version = false
+package-lock = false
+save-exact = true
diff --git a/net/webrtc/gstwebrtc-api/LICENSE-MPL-2.0 b/net/webrtc/gstwebrtc-api/LICENSE-MPL-2.0
new file mode 120000
index 00000000..d8394f20
--- /dev/null
+++ b/net/webrtc/gstwebrtc-api/LICENSE-MPL-2.0
@@ -0,0 +1 @@
+../../../LICENSE-MPL-2.0
\ No newline at end of file
diff --git a/net/webrtc/gstwebrtc-api/README.md b/net/webrtc/gstwebrtc-api/README.md
new file mode 100644
index 00000000..26174f1a
--- /dev/null
+++ b/net/webrtc/gstwebrtc-api/README.md
@@ -0,0 +1,150 @@
+# gstwebrtc-api
+
+[![License: MPL 2.0](https://img.shields.io/badge/License-MPL_2.0-brightgreen.svg)](https://opensource.org/licenses/MPL-2.0)
+
+Javascript API used to integrate GStreamer WebRTC streams produced and consumed by webrtcsink and webrtcsrc elements
+into a web browser or a mobile WebView.
+
+This API allows a complete 360º interconnection between GStreamer and web interfaces for realtime streaming using the
+WebRTC protocol.
+
+This API is released under the Mozilla Public License Version 2.0 (MPL-2.0) that can be found in the LICENSE-MPL-2.0
+file or at https://opensource.org/licenses/MPL-2.0
+
+Copyright (C) 2022 Igalia S.L. <>
+Author: Loïc Le Page <>
+
+It includes external source code from [webrtc-adapter](https://github.com/webrtcHacks/adapter) that is embedded with
+the API. The webrtc-adapter BSD 3-Clause license is available at
+https://github.com/webrtcHacks/adapter/blob/master/LICENSE.md
+
+Webrtc-adapter is Copyright (c) 2014, The WebRTC project authors, All rights reserved.
+Copyright (c) 2018, The adapter.js project authors, All rights reserved.
+
+It also includes Keyboard.js source code from the Apache [guacamole-client](https://github.com/apache/guacamole-client)
+that is embedded with the API to enable remote control of the webrtcsink producer.
+
+Keyboard.js is released under the Apache 2.0 license available at:
+https://github.com/apache/guacamole-client/blob/master/LICENSE
+
+## Building the API
+
+The GstWebRTC API uses [Webpack](https://webpack.js.org/) to bundle all source files and dependencies together.
+
+You only need to install [Node.js](https://nodejs.org/en/) to run all commands.
+
+On first time, install the dependencies by calling:
+```shell
+$ npm install
+```
+
+Then build the bundle by calling:
+```shell
+$ npm run make
+```
+
+It will build and compress the code into the *dist/* folder, there you will find 2 files:
+- *gstwebrtc-api-[version].min.js* which is the only file you need to include into your web application to use the API.
+ It already embeds all dependencies.
+- *gstwebrtc-api-[version].min.js.map* which is useful for debugging the API code, you need to put it in the same
+ folder as the API script on your web server if you want to allow debugging, else you can just ignore it.
+
+The API documentation is created into the *docs/* folder. It is automatically created when building the whole API.
+
+If you want to build the documentation only, you can call:
+```shell
+$ npm run docs
+```
+
+If you only want to build the API without the documentation, you can call:
+```shell
+$ npm run build
+```
+
+## Packaging the API
+
+You can create a portable package of the API by calling:
+```shell
+$ npm pack
+```
+
+It will create a *gstwebrtc-api-[version].tgz* file that contains all source code, documentation and built API. This
+portable package can be installed as a dependency in any Node.js project by calling:
+```shell
+$ npm install gstwebrtc-api-[version].tgz
+```
+
+## Testing and debugging the API
+
+To easily test and debug the GstWebRTC API, you just need to:
+1. launch the webrtc signalling server by calling (from the repository *gst-plugins-rs* root folder):
+ ```shell
+ $ cargo run --bin gst-webrtc-signalling-server
+ ```
+2. launch the GstWebRTC API server by calling (from the *net/webrtc/gstwebrtc-api* sub-folder):
+ ```shell
+ $ npm start
+ ```
+
+It will launch a local HTTPS server listening on port 9090 and using an automatically generated self-signed
+certificate.
+
+With this server you can test the reference example shipped in *index.html* from a web browser on your local computer
+or a mobile device.
+
+## Interconnect with GStreamer pipelines
+
+Once the signalling and gstwebrtc-api servers launched, you can interconnect the streams produced and consumed from
+the web browser with GStreamer pipelines using the webrtcsink and webrtcsrc elements.
+
+### Consume a WebRTC stream produced by the gstwebrtc-api
+
+On the web browser side, click on the *Start Capture* button and give access to the webcam. The gstwebrtc-api will
+start producing a video stream.
+
+The signalling server logs will show the registration of a new producer with the same *peer_id* as the *Client ID*
+that appears on the webpage.
+
+Then launch the following GStreamer pipeline:
+```shell
+$ gst-launch-1.0 playbin uri=gstwebrtc://[signalling server]?peer-id=[client ID of the producer]
+```
+
+Using the local signalling server, it will look like this:
+```shell
+$ gst-launch-1.0 playbin uri=gstwebrtc://127.0.0.1:8443?peer-id=e54e5d6b-f597-4e8f-bc96-2cc3765b6567
+```
+
+The underlying *uridecodebin* element recognizes the *gstwebrtc://* scheme as a WebRTC stream compatible with the
+gstwebrtc-api and will correctly use a *webrtcsrc* element to manage this stream.
+
+The *gstwebrtc://* scheme is used for normal WebSocket connections to the signalling server, and the *gstwebrtcs://*
+scheme for secured connections over SSL or TLS.
+
+### Produce a GStreamer WebRTC stream consumed by the gstwebrtc-api
+
+Launch the following GStreamer pipeline:
+```shell
+$ gst-launch-1.0 videotestsrc ! agingtv ! webrtcsink meta="meta,name=native-stream"
+```
+
+By default *webrtcsink* element uses *ws://127.0.0.1:8443* for the signalling server address, so there is no need
+for more arguments. If you're hosting the signalling server elsewhere, you can specify its address by adding
+`signaller::address="ws[s]://[signalling server]"` to the list of *webrtcsink* properties.
+
+Once the GStreamer pipeline launched, you will see the registration of a new producer in the logs of the signalling
+server and a new remote stream, with the name *native-stream*, will appear on the webpage.
+
+You just need to click on the corresponding entry to connect as a consumer to the remote native stream.
+
+### Produce a GStreamer interactive WebRTC stream with remote control
+
+Launch the following GStreamer pipeline:
+```shell
+$ gst-launch-1.0 wpesrc location=https://gstreamer.freedesktop.org/documentation ! queue ! webrtcsink enable-data-channel-navigation=true meta="meta,name=web-stream"
+```
+
+Once the GStreamer pipeline launched, you will see a new producer with the name *web-stream*. When connecting to this
+producer you will see the remote rendering of the web page. You can interact remotely with this web page, controls are
+sent through a special WebRTC data channel while the rendering is done remotely by the
+[wpesrc](https://gstreamer.freedesktop.org/documentation/wpe/wpesrc.html) element.
diff --git a/net/webrtc/gstwebrtc-api/index.html b/net/webrtc/gstwebrtc-api/index.html
new file mode 100644
index 00000000..8fe2a643
--- /dev/null
+++ b/net/webrtc/gstwebrtc-api/index.html
@@ -0,0 +1,456 @@
+
+
+
+
+
+
+ GstWebRTC API
+
+
+
+
+
+
+
You can override default values by defining configuration before receiving the DOMContentLoaded event.
+ * Once the DOMContentLoaded event triggered, changing configuration will have no effect.
+ * @typedef {object} gstWebRTCConfig
+ * @property {object} meta=null - Client free-form information that will be exchanged with all peers through the
+ * signaling meta property, its content depends on your application.
+ * @property {string} signalingServerUrl=ws://127.0.0.1:8443 - The WebRTC signaling server URL.
+ * @property {number} reconnectionTimeout=2500 - Timeout in milliseconds to reconnect to the signaling server in
+ * case of an unexpected disconnection.
+ * @property {object} webrtcConfig={iceServers...} - The WebRTC peer connection configuration passed to
+ * {@link external:RTCPeerConnection}. Default configuration only includes a list of free STUN servers
+ * (stun[0-4].l.google.com:19302).
+ */
+const defaultConfig = Object.freeze({
+ meta: null,
+ signalingServerUrl: "ws://127.0.0.1:8443",
+ reconnectionTimeout: 2500,
+ webrtcConfig: {
+ iceServers: [
+ {
+ urls: [
+ "stun:stun.l.google.com:19302",
+ "stun:stun1.l.google.com:19302",
+ "stun:stun2.l.google.com:19302",
+ "stun:stun3.l.google.com:19302",
+ "stun:stun4.l.google.com:19302"
+ ]
+ }
+ ]
+ }
+});
+
+export { defaultConfig as default };
diff --git a/net/webrtc/gstwebrtc-api/src/consumer-session.js b/net/webrtc/gstwebrtc-api/src/consumer-session.js
new file mode 100644
index 00000000..f000ed62
--- /dev/null
+++ b/net/webrtc/gstwebrtc-api/src/consumer-session.js
@@ -0,0 +1,236 @@
+/*
+ * gstwebrtc-api
+ *
+ * Copyright (C) 2022 Igalia S.L.
+ * Author: Loïc Le Page
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+import WebRTCSession from "./webrtc-session";
+import SessionState from "./session-state";
+import RemoteController from "./remote-controller";
+
+/**
+ * Event name: "streamsChanged".
+ * Triggered when the underlying media streams of a {@link gstWebRTCAPI.ConsumerSession} change.
+ * @event gstWebRTCAPI#StreamsChangedEvent
+ * @type {external:Event}
+ * @see gstWebRTCAPI.ConsumerSession#streams
+ */
+/**
+ * Event name: "remoteControllerChanged".
+ * Triggered when the underlying remote controller of a {@link gstWebRTCAPI.ConsumerSession} changes.
+ * @event gstWebRTCAPI#RemoteControllerChangedEvent
+ * @type {external:Event}
+ * @see gstWebRTCAPI.ConsumerSession#remoteController
+ */
+
+/**
+ * @class gstWebRTCAPI.ConsumerSession
+ * @hideconstructor
+ * @classdesc Consumer session managing a peer-to-peer WebRTC channel between a remote producer and this client
+ * instance.
+ *
Call {@link gstWebRTCAPI#createConsumerSession} to create a ConsumerSession instance.
+ * @extends {gstWebRTCAPI.WebRTCSession}
+ * @fires {@link gstWebRTCAPI#event:StreamsChangedEvent}
+ * @fires {@link gstWebRTCAPI#event:RemoteControllerChangedEvent}
+ */
+export default class ConsumerSession extends WebRTCSession {
+ constructor(peerId, comChannel) {
+ super(peerId, comChannel);
+ this._streams = [];
+ this._remoteController = null;
+
+ this.addEventListener("closed", () => {
+ this._streams = [];
+
+ if (this._remoteController) {
+ this._remoteController.close();
+ }
+ });
+ }
+
+ /**
+ * The array of remote media streams consumed locally through this WebRTC channel.
+ * @member {external:MediaStream[]} gstWebRTCAPI.ConsumerSession#streams
+ * @readonly
+ */
+ get streams() {
+ return this._streams;
+ }
+
+ /**
+ * The remote controller associated with this WebRTC consumer session. Value may be null if consumer session
+ * has no remote controller.
+ * @member {gstWebRTCAPI.RemoteController} gstWebRTCAPI.ConsumerSession#remoteController
+ * @readonly
+ */
+ get remoteController() {
+ return this._remoteController;
+ }
+
+ /**
+ * Connects the consumer session to its remote producer.
+ * This method must be called after creating the consumer session in order to start receiving the remote streams.
+ * It registers this consumer session to the signaling server and gets ready to receive audio/video streams.
+ *
Even on success, streaming can fail later if any error occurs during or after connection. In order to know
+ * the effective streaming state, you should be listening to the [error]{@link gstWebRTCAPI#event:ErrorEvent},
+ * [stateChanged]{@link gstWebRTCAPI#event:StateChangedEvent} and/or [closed]{@link gstWebRTCAPI#event:ClosedEvent}
+ * events.
+ * @method gstWebRTCAPI.ConsumerSession#connect
+ * @returns {boolean} true in case of success (may fail later during or after connection) or false in case of
+ * immediate error (wrong session state or no connection to the signaling server).
+ */
+ connect() {
+ if (!this._comChannel || (this._state === SessionState.closed)) {
+ return false;
+ }
+
+ if (this._state !== SessionState.idle) {
+ return true;
+ }
+
+ const msg = {
+ type: "startSession",
+ peerId: this._peerId
+ };
+ if (!this._comChannel.send(msg)) {
+ this.dispatchEvent(new ErrorEvent("error", {
+ message: "cannot connect consumer session",
+ error: new Error("cannot send startSession message to signaling server")
+ }));
+
+ this.close();
+ return false;
+ }
+
+ this._state = SessionState.connecting;
+ this.dispatchEvent(new Event("stateChanged"));
+ return true;
+ }
+
+ onSessionStarted(peerId, sessionId) {
+ if ((this._peerId === peerId) && (this._state === SessionState.connecting) && !this._sessionId) {
+ this._sessionId = sessionId;
+ }
+ }
+
+ onSessionPeerMessage(msg) {
+ if ((this._state === SessionState.closed) || !this._comChannel || !this._sessionId) {
+ return;
+ }
+
+ if (!this._rtcPeerConnection) {
+ const connection = new RTCPeerConnection(this._comChannel.webrtcConfig);
+ this._rtcPeerConnection = connection;
+
+ connection.ontrack = (event) => {
+ if ((this._rtcPeerConnection === connection) && event.streams && (event.streams.length > 0)) {
+ if (this._state === SessionState.connecting) {
+ this._state = SessionState.streaming;
+ this.dispatchEvent(new Event("stateChanged"));
+ }
+
+ let streamsChanged = false;
+ for (const stream of event.streams) {
+ if (!this._streams.includes(stream)) {
+ this._streams.push(stream);
+ streamsChanged = true;
+ }
+ }
+
+ if (streamsChanged) {
+ this.dispatchEvent(new Event("streamsChanged"));
+ }
+ }
+ };
+
+ connection.ondatachannel = (event) => {
+ const rtcDataChannel = event.channel;
+ if (rtcDataChannel && (rtcDataChannel.label === "input")) {
+ if (this._remoteController) {
+ const previousController = this._remoteController;
+ this._remoteController = null;
+ previousController.close();
+ }
+
+ const remoteController = new RemoteController(rtcDataChannel, this);
+ this._remoteController = remoteController;
+ this.dispatchEvent(new Event("remoteControllerChanged"));
+
+ remoteController.addEventListener("closed", () => {
+ if (this._remoteController === remoteController) {
+ this._remoteController = null;
+ this.dispatchEvent(new Event("remoteControllerChanged"));
+ }
+ });
+ }
+ };
+
+ connection.onicecandidate = (event) => {
+ if ((this._rtcPeerConnection === connection) && event.candidate && this._comChannel) {
+ this._comChannel.send({
+ type: "peer",
+ sessionId: this._sessionId,
+ ice: event.candidate.toJSON()
+ });
+ }
+ };
+
+ this.dispatchEvent(new Event("rtcPeerConnectionChanged"));
+ }
+
+ if (msg.sdp) {
+ this._rtcPeerConnection.setRemoteDescription(msg.sdp).then(() => {
+ if (this._rtcPeerConnection) {
+ return this._rtcPeerConnection.createAnswer();
+ } else {
+ return null;
+ }
+ }).then((desc) => {
+ if (this._rtcPeerConnection && desc) {
+ return this._rtcPeerConnection.setLocalDescription(desc);
+ } else {
+ return null;
+ }
+ }).then(() => {
+ if (this._rtcPeerConnection && this._comChannel) {
+ const sdp = {
+ type: "peer",
+ sessionId: this._sessionId,
+ sdp: this._rtcPeerConnection.localDescription.toJSON()
+ };
+ if (!this._comChannel.send(sdp)) {
+ throw new Error("cannot send local SDP configuration to WebRTC peer");
+ }
+ }
+ }).catch((ex) => {
+ if (this._state !== SessionState.closed) {
+ this.dispatchEvent(new ErrorEvent("error", {
+ message: "an unrecoverable error occurred during SDP handshake",
+ error: ex
+ }));
+
+ this.close();
+ }
+ });
+ } else if (msg.ice) {
+ const candidate = new RTCIceCandidate(msg.ice);
+ this._rtcPeerConnection.addIceCandidate(candidate).catch((ex) => {
+ if (this._state !== SessionState.closed) {
+ this.dispatchEvent(new ErrorEvent("error", {
+ message: "an unrecoverable error occurred during ICE handshake",
+ error: ex
+ }));
+
+ this.close();
+ }
+ });
+ } else {
+ throw new Error(`invalid empty peer message received from consumer session ${this._sessionId}`);
+ }
+ }
+}
diff --git a/net/webrtc/gstwebrtc-api/src/gstwebrtc-api.js b/net/webrtc/gstwebrtc-api/src/gstwebrtc-api.js
new file mode 100644
index 00000000..23d36d7f
--- /dev/null
+++ b/net/webrtc/gstwebrtc-api/src/gstwebrtc-api.js
@@ -0,0 +1,379 @@
+/*
+ * gstwebrtc-api
+ *
+ * Copyright (C) 2022 Igalia S.L.
+ * Author: Loïc Le Page
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+import defaultConfig from "./config";
+import ComChannel from "./com-channel";
+import SessionState from "./session-state";
+
+const apiState = {
+ config: null,
+ channel: null,
+ producers: {},
+ connectionListeners: [],
+ producersListeners: []
+};
+
+/**
+ * @interface gstWebRTCAPI.ConnectionListener
+ */
+/**
+ * Callback method called when this client connects to the WebRTC signaling server.
+ * The callback implementation should not throw any exception.
+ * @method gstWebRTCAPI.ConnectionListener#connected
+ * @abstract
+ * @param {string} clientId - The unique identifier of this WebRTC client. This identifier is provided by the
+ * signaling server to uniquely identify each connected peer.
+ */
+/**
+ * Callback method called when this client disconnects from the WebRTC signaling server.
+ * The callback implementation should not throw any exception.
+ * @method gstWebRTCAPI.ConnectionListener#disconnected
+ * @abstract
+ */
+
+/**
+ * Registers a connection listener that will be called each time the WebRTC API connects to or disconnects from the
+ * signaling server.
+ * @function
+ * @memberof gstWebRTCAPI
+ * @instance
+ * @param {gstWebRTCAPI.ConnectionListener} listener - The connection listener to register.
+ * @returns {boolean} true in case of success (or if the listener was already registered), or false if the listener
+ * doesn't implement all callback functions and cannot be registered.
+ */
+function registerConnectionListener(listener) {
+ if (!listener || (typeof (listener) !== "object") ||
+ (typeof (listener.connected) !== "function") ||
+ (typeof (listener.disconnected) !== "function")) {
+ return false;
+ }
+
+ if (!apiState.connectionListeners.includes(listener)) {
+ apiState.connectionListeners.push(listener);
+ }
+
+ return true;
+}
+
+/**
+ * Unregisters a connection listener.
+ * The removed listener will never be called again and can be garbage collected.
+ * @function
+ * @memberof gstWebRTCAPI
+ * @instance
+ * @param {gstWebRTCAPI.ConnectionListener} listener - The connection listener to unregister.
+ * @returns {boolean} true if the listener is found and unregistered, or false if the listener was not previously
+ * registered.
+ */
+function unregisterConnectionListener(listener) {
+ const idx = apiState.connectionListeners.indexOf(listener);
+ if (idx >= 0) {
+ apiState.connectionListeners.splice(idx, 1);
+ return true;
+ }
+
+ return false;
+}
+
+/**
+ * Unregisters all previously registered connection listeners.
+ * @function
+ * @memberof gstWebRTCAPI
+ * @instance
+ */
+function unregisterAllConnectionListeners() {
+ apiState.connectionListeners = [];
+}
+
+/**
+ * Creates a new producer session.
+ *
You can only create a producer session at once.
+ * To request streaming from a new stream you will first need to close the previous producer session.
+ *
You can only request a producer session while you are connected to the signaling server. You can use the
+ * {@link gstWebRTCAPI.ConnectionListener} interface and {@link gstWebRTCAPI#registerConnectionListener} function to
+ * listen to the connection state.
+ * @function
+ * @memberof gstWebRTCAPI
+ * @instance
+ * @param {external:MediaStream} stream - The audio/video stream to offer as a producer through WebRTC.
+ * @returns {gstWebRTCAPI.ProducerSession} The created producer session or null in case of error. To start streaming,
+ * you still need to call {@link gstWebRTCAPI.ProducerSession#start} after adding on the returned session all the event
+ * listeners you may need.
+ */
+function createProducerSession(stream) {
+ if (apiState.channel) {
+ return apiState.channel.createProducerSession(stream);
+ } else {
+ return null;
+ }
+}
+
+/**
+ * Information about a remote producer registered by the signaling server.
+ * @typedef {object} gstWebRTCAPI.Producer
+ * @readonly
+ * @property {string} id - The remote producer unique identifier set by the signaling server (always non-empty).
+ * @property {object} meta - Free-form object containing extra information about the remote producer (always non-null,
+ * but may be empty). Its content depends on your application.
+ */
+
+/**
+ * Gets the list of all remote WebRTC producers available on the signaling server.
+ *
The remote producers list is only populated once you've connected to the signaling server. You can use the
+ * {@link gstWebRTCAPI.ConnectionListener} interface and {@link gstWebRTCAPI#registerConnectionListener} function to
+ * listen to the connection state.
+ * @function
+ * @memberof gstWebRTCAPI
+ * @instance
+ * @returns {gstWebRTCAPI.Producer[]} The list of remote WebRTC producers available.
+ */
+function getAvailableProducers() {
+ return Object.values(apiState.producers);
+}
+
+/**
+ * @interface gstWebRTCAPI.ProducersListener
+ */
+/**
+ * Callback method called when a remote producer is added on the signaling server.
+ * The callback implementation should not throw any exception.
+ * @method gstWebRTCAPI.ProducersListener#producerAdded
+ * @abstract
+ * @param {gstWebRTCAPI.Producer} producer - The remote producer added on server-side.
+ */
+/**
+ * Callback method called when a remote producer is removed from the signaling server.
+ * The callback implementation should not throw any exception.
+ * @method gstWebRTCAPI.ProducersListener#producerRemoved
+ * @abstract
+ * @param {gstWebRTCAPI.Producer} producer - The remote producer removed on server-side.
+ */
+
+/**
+ * Registers a producers listener that will be called each time a producer is added or removed on the signaling
+ * server.
+ * @function
+ * @memberof gstWebRTCAPI
+ * @instance
+ * @param {gstWebRTCAPI.ProducersListener} listener - The producer listener to register.
+ * @returns {boolean} true in case of success (or if the listener was already registered), or false if the listener
+ * doesn't implement all callback functions and cannot be registered.
+ */
+function registerProducersListener(listener) {
+ if (!listener || (typeof (listener) !== "object") ||
+ (typeof (listener.producerAdded) !== "function") ||
+ (typeof (listener.producerRemoved) !== "function")) {
+ return false;
+ }
+
+ if (!apiState.producersListeners.includes(listener)) {
+ apiState.producersListeners.push(listener);
+ }
+
+ return true;
+}
+
+/**
+ * Unregisters a producers listener.
+ * The removed listener will never be called again and can be garbage collected.
+ * @function
+ * @memberof gstWebRTCAPI
+ * @instance
+ * @param {gstWebRTCAPI.ProducersListener} listener - The producers listener to unregister.
+ * @returns {boolean} true if the listener is found and unregistered, or false if the listener was not previously
+ * registered.
+ */
+function unregisterProducersListener(listener) {
+ const idx = apiState.producersListeners.indexOf(listener);
+ if (idx >= 0) {
+ apiState.producersListeners.splice(idx, 1);
+ return true;
+ }
+
+ return false;
+}
+
+/**
+ * Unregisters all previously registered producers listeners.
+ * @function
+ * @memberof gstWebRTCAPI
+ * @instance
+ */
+function unregisterAllProducersListeners() {
+ apiState.producersListeners = [];
+}
+
+/**
+ * Creates a consumer session by connecting the local client to a remote WebRTC producer.
+ *
You can only create one consumer session per remote producer.
+ *
You can only request a new consumer session while you are connected to the signaling server. You can use the
+ * {@link gstWebRTCAPI.ConnectionListener} interface and {@link gstWebRTCAPI#registerConnectionListener} function to
+ * listen to the connection state.
Call {@link gstWebRTCAPI#createProducerSession} to create a ProducerSession instance.
+ * @extends {external:EventTarget}
+ * @fires {@link gstWebRTCAPI#event:ErrorEvent}
+ * @fires {@link gstWebRTCAPI#event:StateChangedEvent}
+ * @fires {@link gstWebRTCAPI#event:ClosedEvent}
+ * @fires {@link gstWebRTCAPI#event:ClientConsumerAddedEvent}
+ * @fires {@link gstWebRTCAPI#event:ClientConsumerRemovedEvent}
+ */
+export default class ProducerSession extends EventTarget {
+ constructor(comChannel, stream) {
+ super();
+
+ this._comChannel = comChannel;
+ this._stream = stream;
+ this._state = SessionState.idle;
+ this._clientSessions = {};
+ }
+
+ /**
+ * The local stream produced out by this session.
+ * @member {external:MediaStream} gstWebRTCAPI.ProducerSession#stream
+ * @readonly
+ */
+ get stream() {
+ return this._stream;
+ }
+
+ /**
+ * The current producer session state.
+ * @member {gstWebRTCAPI.SessionState} gstWebRTCAPI.ProducerSession#state
+ * @readonly
+ */
+ get state() {
+ return this._state;
+ }
+
+ /**
+ * Starts the producer session.
+ * This method must be called after creating the producer session in order to start streaming. It registers this
+ * producer session to the signaling server and gets ready to serve peer requests from consumers.
+ *
Even on success, streaming can fail later if any error occurs during or after connection. In order to know
+ * the effective streaming state, you should be listening to the [error]{@link gstWebRTCAPI#event:ErrorEvent},
+ * [stateChanged]{@link gstWebRTCAPI#event:StateChangedEvent} and/or [closed]{@link gstWebRTCAPI#event:ClosedEvent}
+ * events.
+ * @method gstWebRTCAPI.ProducerSession#start
+ * @returns {boolean} true in case of success (may fail later during or after connection) or false in case of
+ * immediate error (wrong session state or no connection to the signaling server).
+ */
+ start() {
+ if (!this._comChannel || (this._state === SessionState.closed)) {
+ return false;
+ }
+
+ if (this._state !== SessionState.idle) {
+ return true;
+ }
+
+ const msg = {
+ type: "setPeerStatus",
+ roles: ["listener", "producer"],
+ meta: this._comChannel.meta
+ };
+ if (!this._comChannel.send(msg)) {
+ this.dispatchEvent(new ErrorEvent("error", {
+ message: "cannot start producer session",
+ error: new Error("cannot register producer to signaling server")
+ }));
+
+ this.close();
+ return false;
+ }
+
+ this._state = SessionState.connecting;
+ this.dispatchEvent(new Event("stateChanged"));
+ return true;
+ }
+
+ /**
+ * Terminates the producer session.
+ * It immediately disconnects all peer consumers attached to this producer session and unregisters the producer
+ * from the signaling server.
+ * @method gstWebRTCAPI.ProducerSession#close
+ */
+ close() {
+ if (this._state !== SessionState.closed) {
+ for (const track of this._stream.getTracks()) {
+ track.stop();
+ }
+
+ if ((this._state !== SessionState.idle) && this._comChannel) {
+ this._comChannel.send({
+ type: "setPeerStatus",
+ roles: ["listener"],
+ meta: this._comChannel.meta
+ });
+ }
+
+ this._state = SessionState.closed;
+ this.dispatchEvent(new Event("stateChanged"));
+
+ this._comChannel = null;
+ this._stream = null;
+
+ for (const clientSession of Object.values(this._clientSessions)) {
+ clientSession.close();
+ }
+ this._clientSessions = {};
+
+ this.dispatchEvent(new Event("closed"));
+ }
+ }
+
+ onProducerRegistered() {
+ if (this._state === SessionState.connecting) {
+ this._state = SessionState.streaming;
+ this.dispatchEvent(new Event("stateChanged"));
+ }
+ }
+
+ onStartSessionMessage(msg) {
+ if (this._comChannel && this._stream && !(msg.sessionId in this._clientSessions)) {
+ const session = new ClientSession(msg.peerId, msg.sessionId, this._comChannel, this._stream);
+ this._clientSessions[msg.sessionId] = session;
+
+ session.addEventListener("closed", (event) => {
+ const sessionId = event.target.sessionId;
+ if ((sessionId in this._clientSessions) && (this._clientSessions[sessionId] === session)) {
+ delete this._clientSessions[sessionId];
+ this.dispatchEvent(new CustomEvent("clientConsumerRemoved", { detail: session }));
+ }
+ });
+
+ session.addEventListener("error", (event) => {
+ this.dispatchEvent(new ErrorEvent("error", {
+ message: `error from client consumer ${event.target.peerId}: ${event.message}`,
+ error: event.error
+ }));
+ });
+
+ this.dispatchEvent(new CustomEvent("clientConsumerAdded", { detail: session }));
+ }
+ }
+
+ onEndSessionMessage(msg) {
+ if (msg.sessionId in this._clientSessions) {
+ this._clientSessions[msg.sessionId].close();
+ }
+ }
+
+ onSessionPeerMessage(msg) {
+ if (msg.sessionId in this._clientSessions) {
+ this._clientSessions[msg.sessionId].onSessionPeerMessage(msg);
+ }
+ }
+}
diff --git a/net/webrtc/gstwebrtc-api/src/remote-controller.js b/net/webrtc/gstwebrtc-api/src/remote-controller.js
new file mode 100644
index 00000000..e7c07bd8
--- /dev/null
+++ b/net/webrtc/gstwebrtc-api/src/remote-controller.js
@@ -0,0 +1,360 @@
+/*
+ * gstwebrtc-api
+ *
+ * Copyright (C) 2022 Igalia S.L.
+ * Author: Loïc Le Page
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+import Guacamole from "../third-party/Keyboard";
+import getKeysymString from "./keysyms";
+
+const eventsNames = Object.freeze([
+ "wheel",
+ "contextmenu",
+ "mousemove",
+ "mousedown",
+ "mouseup",
+ "touchstart",
+ "touchend",
+ "touchmove",
+ "touchcancel"
+]);
+
+const mouseEventsNames = Object.freeze({
+ mousemove: "MouseMove",
+ mousedown: "MouseButtonPress",
+ mouseup: "MouseButtonRelease"
+});
+
+const touchEventsNames = Object.freeze({
+ touchstart: "TouchDown",
+ touchend: "TouchUp",
+ touchmove: "TouchMotion",
+ touchcancel: "TouchUp"
+});
+
+function getModifiers(event) {
+ const modifiers = [];
+ if (event.altKey) {
+ modifiers.push("alt-mask");
+ }
+
+ if (event.ctrlKey) {
+ modifiers.push("control-mask");
+ }
+
+ if (event.metaKey) {
+ modifiers.push("meta-mask");
+ }
+
+ if (event.shiftKey) {
+ modifiers.push("shift-mask");
+ }
+
+ return modifiers.join("+");
+}
+
+/**
+ * @class gstWebRTCAPI.RemoteController
+ * @hideconstructor
+ * @classdesc Manages a specific WebRTC data channel created by a remote GStreamer webrtcsink producer and offering
+ * remote control of the producer through
+ * [GstNavigation]{@link https://gstreamer.freedesktop.org/documentation/video/gstnavigation.html} events.
+ *
The remote control data channel is created by the GStreamer webrtcsink element on the producer side. Then it is
+ * announced through the consumer session thanks to the {@link gstWebRTCAPI#event:RemoteControllerChangedEvent}
+ * event.
+ *
You can attach an {@link external:HTMLVideoElement} to the remote controller, then all mouse and keyboard events
+ * emitted by this element will be automatically relayed to the remote producer.
+ * @extends {external:EventTarget}
+ * @fires {@link gstWebRTCAPI#event:ErrorEvent}
+ * @fires {@link gstWebRTCAPI#event:ClosedEvent}
+ * @see gstWebRTCAPI.ConsumerSession#remoteController
+ * @see gstWebRTCAPI.RemoteController#attachVideoElement
+ * @see https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/tree/main/net/webrtc/gstwebrtc-api#produce-a-gstreamer-interactive-webrtc-stream-with-remote-control
+ */
+export default class RemoteController extends EventTarget {
+ constructor(rtcDataChannel, consumerSession) {
+ super();
+
+ this._rtcDataChannel = rtcDataChannel;
+ this._consumerSession = consumerSession;
+
+ this._videoElement = null;
+ this._videoElementComputedStyle = null;
+ this._videoElementKeyboard = null;
+ this._lastTouchEventTimestamp = 0;
+
+ rtcDataChannel.addEventListener("close", () => {
+ if (this._rtcDataChannel === rtcDataChannel) {
+ this.close();
+ }
+ });
+
+ rtcDataChannel.addEventListener("error", (event) => {
+ if (this._rtcDataChannel === rtcDataChannel) {
+ const error = event.error;
+ this.dispatchEvent(new ErrorEvent("error", {
+ message: (error && error.message) || "Remote controller error",
+ error: error || new Error("unknown error on the remote controller data channel")
+ }));
+ }
+ });
+ }
+
+ /**
+ * The underlying WebRTC data channel connected to a remote GStreamer webrtcsink producer offering remote control.
+ * The value may be null if the remote controller has been closed.
+ * @member {external:RTCDataChannel} gstWebRTCAPI.RemoteController#rtcDataChannel
+ * @readonly
+ */
+ get rtcDataChannel() {
+ return this._rtcDataChannel;
+ }
+
+ /**
+ * The consumer session associated with this remote controller.
+ * @member {gstWebRTCAPI.ConsumerSession} gstWebRTCAPI.RemoteController#consumerSession
+ * @readonly
+ */
+ get consumerSession() {
+ return this._consumerSession;
+ }
+
+ /**
+ * The video element that is currently used to send all mouse and keyboard events to the remote producer. Value may
+ * be null if no video element is attached.
+ * @member {external:HTMLVideoElement} gstWebRTCAPI.RemoteController#videoElement
+ * @readonly
+ * @see gstWebRTCAPI.RemoteController#attachVideoElement
+ */
+ get videoElement() {
+ return this._videoElement;
+ }
+
+ /**
+ * Associates a video element with this remote controller.
+ * When a video element is attached to this remote controller, all mouse and keyboard events emitted by this
+ * element will be sent to the remote GStreamer webrtcink producer.
+ * @method gstWebRTCAPI.RemoteController#attachVideoElement
+ * @param {external:HTMLVideoElement|null} element - the video element to use to relay mouse and keyboard events,
+ * or null to detach any previously attached element. If the provided element parameter is not null and not a
+ * valid instance of an {@link external:HTMLVideoElement}, then the method does nothing.
+ */
+ attachVideoElement(element) {
+ if ((element instanceof HTMLVideoElement) && (element !== this._videoElement)) {
+ if (this._videoElement) {
+ this.attachVideoElement(null);
+ }
+
+ this._videoElement = element;
+ this._videoElementComputedStyle = window.getComputedStyle(element);
+
+ this._videoElementKeyboard = new Guacamole.Keyboard(element);
+ this._videoElementKeyboard.onkeydown = (keysym, modifierState) => {
+ this._sendGstNavigationEvent({
+ event: "KeyPress",
+ key: getKeysymString(keysym),
+ modifier_state: modifierState // eslint-disable-line camelcase
+ });
+ };
+ this._videoElementKeyboard.onkeyup = (keysym, modifierState) => {
+ this._sendGstNavigationEvent({
+ event: "KeyRelease",
+ key: getKeysymString(keysym),
+ modifier_state: modifierState // eslint-disable-line camelcase
+ });
+ };
+
+ for (const eventName of eventsNames) {
+ element.addEventListener(eventName, this);
+ }
+
+ element.setAttribute("tabindex", "0");
+ } else if ((element === null) && this._videoElement) {
+ const previousElement = this._videoElement;
+ previousElement.removeAttribute("tabindex");
+
+ this._videoElement = null;
+ this._videoElementComputedStyle = null;
+
+ this._videoElementKeyboard.onkeydown = null;
+ this._videoElementKeyboard.onkeyup = null;
+ this._videoElementKeyboard.reset();
+ this._videoElementKeyboard = null;
+
+ this._lastTouchEventTimestamp = 0;
+
+ for (const eventName of eventsNames) {
+ previousElement.removeEventListener(eventName, this);
+ }
+ }
+ }
+
+ /**
+ * Closes the remote controller channel.
+ * It immediately shuts down the underlying WebRTC data channel connected to a remote GStreamer webrtcsink
+ * producer and detaches any video element that may be used to relay mouse and keyboard events.
+ * @method gstWebRTCAPI.RemoteController#close
+ */
+ close() {
+ this.attachVideoElement(null);
+
+ const rtcDataChannel = this._rtcDataChannel;
+ this._rtcDataChannel = null;
+
+ if (rtcDataChannel) {
+ rtcDataChannel.close();
+ this.dispatchEvent(new Event("closed"));
+ }
+ }
+
+ _sendGstNavigationEvent(data) {
+ try {
+ if (!data || (typeof (data) !== "object")) {
+ throw new Error("invalid GstNavigation event");
+ }
+
+ if (!this._rtcDataChannel) {
+ throw new Error("remote controller data channel is closed");
+ }
+
+ this._rtcDataChannel.send(JSON.stringify(data));
+ } catch (ex) {
+ this.dispatchEvent(new ErrorEvent("error", {
+ message: `cannot send GstNavigation event over session ${this._consumerSession.sessionId} remote controller`,
+ error: ex
+ }));
+ }
+ }
+
+ _computeVideoMousePosition(event) {
+ const mousePos = { x: 0, y: 0 };
+ if (!this._videoElement || (this._videoElement.videoWidth <= 0) || (this._videoElement.videoHeight <= 0)) {
+ return mousePos;
+ }
+
+ const padding = {
+ left: parseFloat(this._videoElementComputedStyle.paddingLeft),
+ right: parseFloat(this._videoElementComputedStyle.paddingRight),
+ top: parseFloat(this._videoElementComputedStyle.paddingTop),
+ bottom: parseFloat(this._videoElementComputedStyle.paddingBottom)
+ };
+
+ if (("offsetX" in event) && ("offsetY" in event)) {
+ mousePos.x = event.offsetX - padding.left;
+ mousePos.y = event.offsetY - padding.top;
+ } else {
+ const clientRect = this._videoElement.getBoundingClientRect();
+ const border = {
+ left: parseFloat(this._videoElementComputedStyle.borderLeftWidth),
+ top: parseFloat(this._videoElementComputedStyle.borderTopWidth)
+ };
+ mousePos.x = event.clientX - clientRect.left - border.left - padding.left;
+ mousePos.y = event.clientY - clientRect.top - border.top - padding.top;
+ }
+
+ const videoOffset = {
+ x: this._videoElement.clientWidth - (padding.left + padding.right),
+ y: this._videoElement.clientHeight - (padding.top + padding.bottom)
+ };
+
+ const ratio = Math.min(videoOffset.x / this._videoElement.videoWidth, videoOffset.y / this._videoElement.videoHeight);
+ videoOffset.x = Math.max(0.5 * (videoOffset.x - this._videoElement.videoWidth * ratio), 0);
+ videoOffset.y = Math.max(0.5 * (videoOffset.y - this._videoElement.videoHeight * ratio), 0);
+
+ const invRatio = (ratio !== 0) ? (1 / ratio) : 0;
+ mousePos.x = (mousePos.x - videoOffset.x) * invRatio;
+ mousePos.y = (mousePos.y - videoOffset.y) * invRatio;
+
+ mousePos.x = Math.min(Math.max(mousePos.x, 0), this._videoElement.videoWidth);
+ mousePos.y = Math.min(Math.max(mousePos.y, 0), this._videoElement.videoHeight);
+
+ return mousePos;
+ }
+
+ handleEvent(event) {
+ if (!this._videoElement) {
+ return;
+ }
+
+ switch (event.type) {
+ case "wheel":
+ event.preventDefault();
+ {
+ const mousePos = this._computeVideoMousePosition(event);
+ this._sendGstNavigationEvent({
+ event: "MouseScroll",
+ x: mousePos.x,
+ y: mousePos.y,
+ delta_x: -event.deltaX, // eslint-disable-line camelcase
+ delta_y: -event.deltaY, // eslint-disable-line camelcase
+ modifier_state: getModifiers(event) // eslint-disable-line camelcase
+ });
+ }
+ break;
+
+ case "contextmenu":
+ event.preventDefault();
+ break;
+
+ case "mousemove":
+ case "mousedown":
+ case "mouseup":
+ event.preventDefault();
+ {
+ const mousePos = this._computeVideoMousePosition(event);
+ const data = {
+ event: mouseEventsNames[event.type],
+ x: mousePos.x,
+ y: mousePos.y,
+ modifier_state: getModifiers(event) // eslint-disable-line camelcase
+ };
+
+ if (event.type !== "mousemove") {
+ data.button = event.button + 1;
+
+ if ((event.type === "mousedown") && (event.button === 0)) {
+ this._videoElement.focus();
+ }
+ }
+
+ this._sendGstNavigationEvent(data);
+ }
+ break;
+
+ case "touchstart":
+ case "touchend":
+ case "touchmove":
+ case "touchcancel":
+ for (const touch of event.changedTouches) {
+ const mousePos = this._computeVideoMousePosition(touch);
+ const data = {
+ event: touchEventsNames[event.type],
+ identifier: touch.identifier,
+ x: mousePos.x,
+ y: mousePos.y,
+ modifier_state: getModifiers(event) // eslint-disable-line camelcase
+ };
+
+ if (("force" in touch) && ((event.type === "touchstart") || (event.type === "touchmove"))) {
+ data.pressure = touch.force;
+ }
+
+ this._sendGstNavigationEvent(data);
+ }
+
+ if (event.timeStamp > this._lastTouchEventTimestamp) {
+ this._lastTouchEventTimestamp = event.timeStamp;
+ this._sendGstNavigationEvent({
+ event: "TouchFrame",
+ modifier_state: getModifiers(event) // eslint-disable-line camelcase
+ });
+ }
+ break;
+ }
+ }
+}
diff --git a/net/webrtc/gstwebrtc-api/src/session-state.js b/net/webrtc/gstwebrtc-api/src/session-state.js
new file mode 100644
index 00000000..075307d4
--- /dev/null
+++ b/net/webrtc/gstwebrtc-api/src/session-state.js
@@ -0,0 +1,32 @@
+/*
+ * gstwebrtc-api
+ *
+ * Copyright (C) 2022 Igalia S.L.
+ * Author: Loïc Le Page
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+/**
+ * Session states enumeration.
+ * Session state always increases from idle to closed and never switches backwards.
+ * @typedef {enum} gstWebRTCAPI.SessionState
+ * @readonly
+ * @property {0} idle - Default state when creating a new session, goes to connecting when starting
+ * the session.
+ * @property {1} connecting - Session is trying to connect to remote peers, goes to streaming in case of
+ * success or closed in case of error.
+ * @property {2} streaming - Session is correctly connected to remote peers and currently streaming audio/video, goes
+ * to closed when any peer closes the session.
+ * @property {3} closed - Session is closed and can be garbage collected, state will not change anymore.
+ */
+const SessionState = Object.freeze({
+ idle: 0,
+ connecting: 1,
+ streaming: 2,
+ closed: 3
+});
+
+export { SessionState as default };
diff --git a/net/webrtc/gstwebrtc-api/src/webrtc-session.js b/net/webrtc/gstwebrtc-api/src/webrtc-session.js
new file mode 100644
index 00000000..71c99a19
--- /dev/null
+++ b/net/webrtc/gstwebrtc-api/src/webrtc-session.js
@@ -0,0 +1,148 @@
+/*
+ * gstwebrtc-api
+ *
+ * Copyright (C) 2022 Igalia S.L.
+ * Author: Loïc Le Page
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+import SessionState from "./session-state";
+
+/**
+ * Event name: "error".
+ * Triggered when any kind of error occurs.
+ *
When emitted by a session, it is in general an unrecoverable error. Normally, the session is automatically closed
+ * but in the specific case of a {@link gstWebRTCAPI.ProducerSession}, when the error occurs on an underlying
+ * {@link gstWebRTCAPI.ClientSession} between the producer session and a remote client consuming the streamed media,
+ * then only the failing {@link gstWebRTCAPI.ClientSession} is closed. The producer session can keep on serving the
+ * other consumer peers.
+ * @event gstWebRTCAPI#ErrorEvent
+ * @type {external:ErrorEvent}
+ * @property {string} message - The error message.
+ * @property {external:Error} error - The error exception.
+ * @see gstWebRTCAPI.WebRTCSession
+ * @see gstWebRTCAPI.RemoteController
+ */
+/**
+ * Event name: "stateChanged".
+ * Triggered each time a session state changes.
+ * @event gstWebRTCAPI#StateChangedEvent
+ * @type {external:Event}
+ * @see gstWebRTCAPI.WebRTCSession#state
+ */
+/**
+ * Event name: "rtcPeerConnectionChanged".
+ * Triggered each time a session internal {@link external:RTCPeerConnection} changes. This can occur during the session
+ * connecting state when the peer-to-peer WebRTC connection is established, and when closing the
+ * {@link gstWebRTCAPI.WebRTCSession}.
+ * @event gstWebRTCAPI#RTCPeerConnectionChangedEvent
+ * @type {external:Event}
+ * @see gstWebRTCAPI.WebRTCSession#rtcPeerConnection
+ */
+/**
+ * Event name: "closed".
+ * Triggered when a session is definitively closed (then it can be garbage collected as session instances are not
+ * reusable).
+ * @event gstWebRTCAPI#ClosedEvent
+ * @type {external:Event}
+ */
+
+/**
+ * @class gstWebRTCAPI.WebRTCSession
+ * @hideconstructor
+ * @classdesc Manages a WebRTC session between a producer and a consumer (peer-to-peer channel).
+ * @extends {external:EventTarget}
+ * @fires {@link gstWebRTCAPI#event:ErrorEvent}
+ * @fires {@link gstWebRTCAPI#event:StateChangedEvent}
+ * @fires {@link gstWebRTCAPI#event:RTCPeerConnectionChangedEvent}
+ * @fires {@link gstWebRTCAPI#event:ClosedEvent}
+ * @see gstWebRTCAPI.ConsumerSession
+ * @see gstWebRTCAPI.ProducerSession
+ * @see gstWebRTCAPI.ClientSession
+ */
+export default class WebRTCSession extends EventTarget {
+ constructor(peerId, comChannel) {
+ super();
+
+ this._peerId = peerId;
+ this._sessionId = "";
+ this._comChannel = comChannel;
+ this._state = SessionState.idle;
+ this._rtcPeerConnection = null;
+ }
+
+ /**
+ * Unique identifier of the remote peer to which this session is connected.
+ * @member {string} gstWebRTCAPI.WebRTCSession#peerId
+ * @readonly
+ */
+ get peerId() {
+ return this._peerId;
+ }
+
+ /**
+ * Unique identifier of this session (defined by the signaling server).
+ * The local session ID equals "" until it is created on server side. This is done during the connection handshake.
+ * The local session ID is guaranteed to be valid and to correctly reflect the signaling server value once
+ * session state has switched to {@link gstWebRTCAPI.SessionState#streaming}.
+ * @member {string} gstWebRTCAPI.WebRTCSession#sessionId
+ * @readonly
+ */
+ get sessionId() {
+ return this._sessionId;
+ }
+
+ /**
+ * The current WebRTC session state.
+ * @member {gstWebRTCAPI.SessionState} gstWebRTCAPI.WebRTCSession#state
+ * @readonly
+ */
+ get state() {
+ return this._state;
+ }
+
+ /**
+ * The internal {@link external:RTCPeerConnection} used to manage the underlying WebRTC connnection with session
+ * peer. Value may be null if session has no active WebRTC connection. You can listen to the
+ * {@link gstWebRTCAPI#event:RTCPeerConnectionChangedEvent} event to be informed when the connection is established
+ * or destroyed.
+ * @member {external:RTCPeerConnection} gstWebRTCAPI.WebRTCSession#rtcPeerConnection
+ * @readonly
+ */
+ get rtcPeerConnection() {
+ return this._rtcPeerConnection;
+ }
+
+ /**
+ * Terminates the WebRTC session.
+ * It immediately disconnects the remote peer attached to this session and unregisters the session from the
+ * signaling server.
+ * @method gstWebRTCAPI.WebRTCSession#close
+ */
+ close() {
+ if (this._state !== SessionState.closed) {
+ if ((this._state !== SessionState.idle) && this._comChannel && this._sessionId) {
+ this._comChannel.send({
+ type: "endSession",
+ sessionId: this._sessionId
+ });
+ }
+
+ this._state = SessionState.closed;
+ this.dispatchEvent(new Event("stateChanged"));
+
+ this._comChannel = null;
+
+ if (this._rtcPeerConnection) {
+ this._rtcPeerConnection.close();
+ this._rtcPeerConnection = null;
+ this.dispatchEvent(new Event("rtcPeerConnectionChanged"));
+ }
+
+ this.dispatchEvent(new Event("closed"));
+ }
+ }
+}
diff --git a/net/webrtc/gstwebrtc-api/third-party/Keyboard.js b/net/webrtc/gstwebrtc-api/third-party/Keyboard.js
new file mode 100644
index 00000000..e55541e5
--- /dev/null
+++ b/net/webrtc/gstwebrtc-api/third-party/Keyboard.js
@@ -0,0 +1,1511 @@
+/*
+ * 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.
+ */
+
+const Guacamole = {};
+export { Guacamole as default };
+
+/**
+ * Provides cross-browser and cross-keyboard keyboard for a specific element.
+ * Browser and keyboard layout variation is abstracted away, providing events
+ * which represent keys as their corresponding X11 keysym.
+ *
+ * @constructor
+ * @param {Element|Document} [element]
+ * The Element to use to provide keyboard events. If omitted, at least one
+ * Element must be manually provided through the listenTo() function for
+ * the Guacamole.Keyboard instance to have any effect.
+ */
+Guacamole.Keyboard = function Keyboard(element) {
+
+ /**
+ * Reference to this Guacamole.Keyboard.
+ *
+ * @private
+ * @type {!Guacamole.Keyboard}
+ */
+ var guac_keyboard = this;
+
+ /**
+ * An integer value which uniquely identifies this Guacamole.Keyboard
+ * instance with respect to other Guacamole.Keyboard instances.
+ *
+ * @private
+ * @type {!number}
+ */
+ var guacKeyboardID = Guacamole.Keyboard._nextID++;
+
+ /**
+ * The name of the property which is added to event objects via markEvent()
+ * to note that they have already been handled by this Guacamole.Keyboard.
+ *
+ * @private
+ * @constant
+ * @type {!string}
+ */
+ var EVENT_MARKER = '_GUAC_KEYBOARD_HANDLED_BY_' + guacKeyboardID;
+
+ /**
+ * Fired whenever the user presses a key with the element associated
+ * with this Guacamole.Keyboard in focus.
+ *
+ * @event
+ * @param {!number} keysym
+ * The keysym of the key being pressed.
+ *
+ * @return {!boolean}
+ * true if the key event should be allowed through to the browser,
+ * false otherwise.
+ */
+ this.onkeydown = null;
+
+ /**
+ * Fired whenever the user releases a key with the element associated
+ * with this Guacamole.Keyboard in focus.
+ *
+ * @event
+ * @param {!number} keysym
+ * The keysym of the key being released.
+ */
+ this.onkeyup = null;
+
+ /**
+ * Set of known platform-specific or browser-specific quirks which must be
+ * accounted for to properly interpret key events, even if the only way to
+ * reliably detect that quirk is to platform/browser-sniff.
+ *
+ * @private
+ * @type {!Object.}
+ */
+ var quirks = {
+
+ /**
+ * Whether keyup events are universally unreliable.
+ *
+ * @type {!boolean}
+ */
+ keyupUnreliable: false,
+
+ /**
+ * Whether the Alt key is actually a modifier for typable keys and is
+ * thus never used for keyboard shortcuts.
+ *
+ * @type {!boolean}
+ */
+ altIsTypableOnly: false,
+
+ /**
+ * Whether we can rely on receiving a keyup event for the Caps Lock
+ * key.
+ *
+ * @type {!boolean}
+ */
+ capsLockKeyupUnreliable: false
+
+ };
+
+ // Set quirk flags depending on platform/browser, if such information is
+ // available
+ if (navigator && navigator.platform) {
+
+ // All keyup events are unreliable on iOS (sadly)
+ if (navigator.platform.match(/ipad|iphone|ipod/i))
+ quirks.keyupUnreliable = true;
+
+ // The Alt key on Mac is never used for keyboard shortcuts, and the
+ // Caps Lock key never dispatches keyup events
+ else if (navigator.platform.match(/^mac/i)) {
+ quirks.altIsTypableOnly = true;
+ quirks.capsLockKeyupUnreliable = true;
+ }
+
+ }
+
+ /**
+ * A key event having a corresponding timestamp. This event is non-specific.
+ * Its subclasses should be used instead when recording specific key
+ * events.
+ *
+ * @private
+ * @constructor
+ * @param {KeyboardEvent} [orig]
+ * The relevant DOM keyboard event.
+ */
+ var KeyEvent = function KeyEvent(orig) {
+
+ /**
+ * Reference to this key event.
+ *
+ * @private
+ * @type {!KeyEvent}
+ */
+ var key_event = this;
+
+ /**
+ * The JavaScript key code of the key pressed. For most events (keydown
+ * and keyup), this is a scancode-like value related to the position of
+ * the key on the US English "Qwerty" keyboard. For keypress events,
+ * this is the Unicode codepoint of the character that would be typed
+ * by the key pressed.
+ *
+ * @type {!number}
+ */
+ this.keyCode = orig ? (orig.which || orig.keyCode) : 0;
+
+ /**
+ * The legacy DOM3 "keyIdentifier" of the key pressed, as defined at:
+ * http://www.w3.org/TR/2009/WD-DOM-Level-3-Events-20090908/#events-Events-KeyboardEvent
+ *
+ * @type {!string}
+ */
+ this.keyIdentifier = orig && orig.keyIdentifier;
+
+ /**
+ * The standard name of the key pressed, as defined at:
+ * http://www.w3.org/TR/DOM-Level-3-Events/#events-KeyboardEvent
+ *
+ * @type {!string}
+ */
+ this.key = orig && orig.key;
+
+ /**
+ * The location on the keyboard corresponding to the key pressed, as
+ * defined at:
+ * http://www.w3.org/TR/DOM-Level-3-Events/#events-KeyboardEvent
+ *
+ * @type {!number}
+ */
+ this.location = orig ? getEventLocation(orig) : 0;
+
+ /**
+ * The state of all local keyboard modifiers at the time this event was
+ * received.
+ *
+ * @type {!Guacamole.Keyboard.ModifierState}
+ */
+ this.modifiers = orig ? Guacamole.Keyboard.ModifierState.fromKeyboardEvent(orig) : new Guacamole.Keyboard.ModifierState();
+
+ /**
+ * An arbitrary timestamp in milliseconds, indicating this event's
+ * position in time relative to other events.
+ *
+ * @type {!number}
+ */
+ this.timestamp = new Date().getTime();
+
+ /**
+ * Whether the default action of this key event should be prevented.
+ *
+ * @type {!boolean}
+ */
+ this.defaultPrevented = false;
+
+ /**
+ * The keysym of the key associated with this key event, as determined
+ * by a best-effort guess using available event properties and keyboard
+ * state.
+ *
+ * @type {number}
+ */
+ this.keysym = null;
+
+ /**
+ * Whether the keysym value of this key event is known to be reliable.
+ * If false, the keysym may still be valid, but it's only a best guess,
+ * and future key events may be a better source of information.
+ *
+ * @type {!boolean}
+ */
+ this.reliable = false;
+
+ /**
+ * Returns the number of milliseconds elapsed since this event was
+ * received.
+ *
+ * @return {!number}
+ * The number of milliseconds elapsed since this event was
+ * received.
+ */
+ this.getAge = function() {
+ return new Date().getTime() - key_event.timestamp;
+ };
+
+ };
+
+ /**
+ * Information related to the pressing of a key, which need not be a key
+ * associated with a printable character. The presence or absence of any
+ * information within this object is browser-dependent.
+ *
+ * @private
+ * @constructor
+ * @augments Guacamole.Keyboard.KeyEvent
+ * @param {!KeyboardEvent} orig
+ * The relevant DOM "keydown" event.
+ */
+ var KeydownEvent = function KeydownEvent(orig) {
+
+ // We extend KeyEvent
+ KeyEvent.call(this, orig);
+
+ // If key is known from keyCode or DOM3 alone, use that
+ this.keysym = keysym_from_key_identifier(this.key, this.location)
+ || keysym_from_keycode(this.keyCode, this.location);
+
+ /**
+ * Whether the keyup following this keydown event is known to be
+ * reliable. If false, we cannot rely on the keyup event to occur.
+ *
+ * @type {!boolean}
+ */
+ this.keyupReliable = !quirks.keyupUnreliable;
+
+ // DOM3 and keyCode are reliable sources if the corresponding key is
+ // not a printable key
+ if (this.keysym && !isPrintable(this.keysym))
+ this.reliable = true;
+
+ // Use legacy keyIdentifier as a last resort, if it looks sane
+ if (!this.keysym && key_identifier_sane(this.keyCode, this.keyIdentifier))
+ this.keysym = keysym_from_key_identifier(this.keyIdentifier, this.location, this.modifiers.shift);
+
+ // If a key is pressed while meta is held down, the keyup will
+ // never be sent in Chrome (bug #108404)
+ if (this.modifiers.meta && this.keysym !== 0xFFE7 && this.keysym !== 0xFFE8)
+ this.keyupReliable = false;
+
+ // We cannot rely on receiving keyup for Caps Lock on certain platforms
+ else if (this.keysym === 0xFFE5 && quirks.capsLockKeyupUnreliable)
+ this.keyupReliable = false;
+
+ // Determine whether default action for Alt+combinations must be prevented
+ var prevent_alt = !this.modifiers.ctrl && !quirks.altIsTypableOnly;
+
+ // Determine whether default action for Ctrl+combinations must be prevented
+ var prevent_ctrl = !this.modifiers.alt;
+
+ // We must rely on the (potentially buggy) keyIdentifier if preventing
+ // the default action is important
+ if ((prevent_ctrl && this.modifiers.ctrl)
+ || (prevent_alt && this.modifiers.alt)
+ || this.modifiers.meta
+ || this.modifiers.hyper)
+ this.reliable = true;
+
+ // Record most recently known keysym by associated key code
+ recentKeysym[this.keyCode] = this.keysym;
+
+ };
+
+ KeydownEvent.prototype = new KeyEvent();
+
+ /**
+ * Information related to the pressing of a key, which MUST be
+ * associated with a printable character. The presence or absence of any
+ * information within this object is browser-dependent.
+ *
+ * @private
+ * @constructor
+ * @augments Guacamole.Keyboard.KeyEvent
+ * @param {!KeyboardEvent} orig
+ * The relevant DOM "keypress" event.
+ */
+ var KeypressEvent = function KeypressEvent(orig) {
+
+ // We extend KeyEvent
+ KeyEvent.call(this, orig);
+
+ // Pull keysym from char code
+ this.keysym = keysym_from_charcode(this.keyCode);
+
+ // Keypress is always reliable
+ this.reliable = true;
+
+ };
+
+ KeypressEvent.prototype = new KeyEvent();
+
+ /**
+ * Information related to the releasing of a key, which need not be a key
+ * associated with a printable character. The presence or absence of any
+ * information within this object is browser-dependent.
+ *
+ * @private
+ * @constructor
+ * @augments Guacamole.Keyboard.KeyEvent
+ * @param {!KeyboardEvent} orig
+ * The relevant DOM "keyup" event.
+ */
+ var KeyupEvent = function KeyupEvent(orig) {
+
+ // We extend KeyEvent
+ KeyEvent.call(this, orig);
+
+ // If key is known from keyCode or DOM3 alone, use that (keyCode is
+ // still more reliable for keyup when dead keys are in use)
+ this.keysym = keysym_from_keycode(this.keyCode, this.location)
+ || keysym_from_key_identifier(this.key, this.location);
+
+ // Fall back to the most recently pressed keysym associated with the
+ // keyCode if the inferred key doesn't seem to actually be pressed
+ if (!guac_keyboard.pressed[this.keysym])
+ this.keysym = recentKeysym[this.keyCode] || this.keysym;
+
+ // Keyup is as reliable as it will ever be
+ this.reliable = true;
+
+ };
+
+ KeyupEvent.prototype = new KeyEvent();
+
+ /**
+ * An array of recorded events, which can be instances of the private
+ * KeydownEvent, KeypressEvent, and KeyupEvent classes.
+ *
+ * @private
+ * @type {!KeyEvent[]}
+ */
+ var eventLog = [];
+
+ /**
+ * Map of known JavaScript keycodes which do not map to typable characters
+ * to their X11 keysym equivalents.
+ *
+ * @private
+ * @type {!Object.}
+ */
+ var keycodeKeysyms = {
+ 8: [0xFF08], // backspace
+ 9: [0xFF09], // tab
+ 12: [0xFF0B, 0xFF0B, 0xFF0B, 0xFFB5], // clear / KP 5
+ 13: [0xFF0D], // enter
+ 16: [0xFFE1, 0xFFE1, 0xFFE2], // shift
+ 17: [0xFFE3, 0xFFE3, 0xFFE4], // ctrl
+ 18: [0xFFE9, 0xFFE9, 0xFE03], // alt
+ 19: [0xFF13], // pause/break
+ 20: [0xFFE5], // caps lock
+ 27: [0xFF1B], // escape
+ 32: [0x0020], // space
+ 33: [0xFF55, 0xFF55, 0xFF55, 0xFFB9], // page up / KP 9
+ 34: [0xFF56, 0xFF56, 0xFF56, 0xFFB3], // page down / KP 3
+ 35: [0xFF57, 0xFF57, 0xFF57, 0xFFB1], // end / KP 1
+ 36: [0xFF50, 0xFF50, 0xFF50, 0xFFB7], // home / KP 7
+ 37: [0xFF51, 0xFF51, 0xFF51, 0xFFB4], // left arrow / KP 4
+ 38: [0xFF52, 0xFF52, 0xFF52, 0xFFB8], // up arrow / KP 8
+ 39: [0xFF53, 0xFF53, 0xFF53, 0xFFB6], // right arrow / KP 6
+ 40: [0xFF54, 0xFF54, 0xFF54, 0xFFB2], // down arrow / KP 2
+ 45: [0xFF63, 0xFF63, 0xFF63, 0xFFB0], // insert / KP 0
+ 46: [0xFFFF, 0xFFFF, 0xFFFF, 0xFFAE], // delete / KP decimal
+ 91: [0xFFE7], // left windows/command key (meta_l)
+ 92: [0xFFE8], // right window/command key (meta_r)
+ 93: [0xFF67], // menu key
+ 96: [0xFFB0], // KP 0
+ 97: [0xFFB1], // KP 1
+ 98: [0xFFB2], // KP 2
+ 99: [0xFFB3], // KP 3
+ 100: [0xFFB4], // KP 4
+ 101: [0xFFB5], // KP 5
+ 102: [0xFFB6], // KP 6
+ 103: [0xFFB7], // KP 7
+ 104: [0xFFB8], // KP 8
+ 105: [0xFFB9], // KP 9
+ 106: [0xFFAA], // KP multiply
+ 107: [0xFFAB], // KP add
+ 109: [0xFFAD], // KP subtract
+ 110: [0xFFAE], // KP decimal
+ 111: [0xFFAF], // KP divide
+ 112: [0xFFBE], // f1
+ 113: [0xFFBF], // f2
+ 114: [0xFFC0], // f3
+ 115: [0xFFC1], // f4
+ 116: [0xFFC2], // f5
+ 117: [0xFFC3], // f6
+ 118: [0xFFC4], // f7
+ 119: [0xFFC5], // f8
+ 120: [0xFFC6], // f9
+ 121: [0xFFC7], // f10
+ 122: [0xFFC8], // f11
+ 123: [0xFFC9], // f12
+ 144: [0xFF7F], // num lock
+ 145: [0xFF14], // scroll lock
+ 225: [0xFE03] // altgraph (iso_level3_shift)
+ };
+
+ /**
+ * Map of known JavaScript keyidentifiers which do not map to typable
+ * characters to their unshifted X11 keysym equivalents.
+ *
+ * @private
+ * @type {!Object.}
+ */
+ var keyidentifier_keysym = {
+ "Again": [0xFF66],
+ "AllCandidates": [0xFF3D],
+ "Alphanumeric": [0xFF30],
+ "Alt": [0xFFE9, 0xFFE9, 0xFE03],
+ "Attn": [0xFD0E],
+ "AltGraph": [0xFE03],
+ "ArrowDown": [0xFF54],
+ "ArrowLeft": [0xFF51],
+ "ArrowRight": [0xFF53],
+ "ArrowUp": [0xFF52],
+ "Backspace": [0xFF08],
+ "CapsLock": [0xFFE5],
+ "Cancel": [0xFF69],
+ "Clear": [0xFF0B],
+ "Convert": [0xFF21],
+ "Copy": [0xFD15],
+ "Crsel": [0xFD1C],
+ "CrSel": [0xFD1C],
+ "CodeInput": [0xFF37],
+ "Compose": [0xFF20],
+ "Control": [0xFFE3, 0xFFE3, 0xFFE4],
+ "ContextMenu": [0xFF67],
+ "Delete": [0xFFFF],
+ "Down": [0xFF54],
+ "End": [0xFF57],
+ "Enter": [0xFF0D],
+ "EraseEof": [0xFD06],
+ "Escape": [0xFF1B],
+ "Execute": [0xFF62],
+ "Exsel": [0xFD1D],
+ "ExSel": [0xFD1D],
+ "F1": [0xFFBE],
+ "F2": [0xFFBF],
+ "F3": [0xFFC0],
+ "F4": [0xFFC1],
+ "F5": [0xFFC2],
+ "F6": [0xFFC3],
+ "F7": [0xFFC4],
+ "F8": [0xFFC5],
+ "F9": [0xFFC6],
+ "F10": [0xFFC7],
+ "F11": [0xFFC8],
+ "F12": [0xFFC9],
+ "F13": [0xFFCA],
+ "F14": [0xFFCB],
+ "F15": [0xFFCC],
+ "F16": [0xFFCD],
+ "F17": [0xFFCE],
+ "F18": [0xFFCF],
+ "F19": [0xFFD0],
+ "F20": [0xFFD1],
+ "F21": [0xFFD2],
+ "F22": [0xFFD3],
+ "F23": [0xFFD4],
+ "F24": [0xFFD5],
+ "Find": [0xFF68],
+ "GroupFirst": [0xFE0C],
+ "GroupLast": [0xFE0E],
+ "GroupNext": [0xFE08],
+ "GroupPrevious": [0xFE0A],
+ "FullWidth": null,
+ "HalfWidth": null,
+ "HangulMode": [0xFF31],
+ "Hankaku": [0xFF29],
+ "HanjaMode": [0xFF34],
+ "Help": [0xFF6A],
+ "Hiragana": [0xFF25],
+ "HiraganaKatakana": [0xFF27],
+ "Home": [0xFF50],
+ "Hyper": [0xFFED, 0xFFED, 0xFFEE],
+ "Insert": [0xFF63],
+ "JapaneseHiragana": [0xFF25],
+ "JapaneseKatakana": [0xFF26],
+ "JapaneseRomaji": [0xFF24],
+ "JunjaMode": [0xFF38],
+ "KanaMode": [0xFF2D],
+ "KanjiMode": [0xFF21],
+ "Katakana": [0xFF26],
+ "Left": [0xFF51],
+ "Meta": [0xFFE7, 0xFFE7, 0xFFE8],
+ "ModeChange": [0xFF7E],
+ "NumLock": [0xFF7F],
+ "PageDown": [0xFF56],
+ "PageUp": [0xFF55],
+ "Pause": [0xFF13],
+ "Play": [0xFD16],
+ "PreviousCandidate": [0xFF3E],
+ "PrintScreen": [0xFF61],
+ "Redo": [0xFF66],
+ "Right": [0xFF53],
+ "RomanCharacters": null,
+ "Scroll": [0xFF14],
+ "Select": [0xFF60],
+ "Separator": [0xFFAC],
+ "Shift": [0xFFE1, 0xFFE1, 0xFFE2],
+ "SingleCandidate": [0xFF3C],
+ "Super": [0xFFEB, 0xFFEB, 0xFFEC],
+ "Tab": [0xFF09],
+ "UIKeyInputDownArrow": [0xFF54],
+ "UIKeyInputEscape": [0xFF1B],
+ "UIKeyInputLeftArrow": [0xFF51],
+ "UIKeyInputRightArrow": [0xFF53],
+ "UIKeyInputUpArrow": [0xFF52],
+ "Up": [0xFF52],
+ "Undo": [0xFF65],
+ "Win": [0xFFE7, 0xFFE7, 0xFFE8],
+ "Zenkaku": [0xFF28],
+ "ZenkakuHankaku": [0xFF2A]
+ };
+
+ /**
+ * All keysyms which should not repeat when held down.
+ *
+ * @private
+ * @type {!Object.}
+ */
+ var no_repeat = {
+ 0xFE03: true, // ISO Level 3 Shift (AltGr)
+ 0xFFE1: true, // Left shift
+ 0xFFE2: true, // Right shift
+ 0xFFE3: true, // Left ctrl
+ 0xFFE4: true, // Right ctrl
+ 0xFFE5: true, // Caps Lock
+ 0xFFE7: true, // Left meta
+ 0xFFE8: true, // Right meta
+ 0xFFE9: true, // Left alt
+ 0xFFEA: true, // Right alt
+ 0xFFEB: true, // Left super/hyper
+ 0xFFEC: true // Right super/hyper
+ };
+
+ /**
+ * All modifiers and their states.
+ *
+ * @type {!Guacamole.Keyboard.ModifierState}
+ */
+ this.modifiers = new Guacamole.Keyboard.ModifierState();
+
+ /**
+ * The state of every key, indexed by keysym. If a particular key is
+ * pressed, the value of pressed for that keysym will be true. If a key
+ * is not currently pressed, it will not be defined.
+ *
+ * @type {!Object.}
+ */
+ this.pressed = {};
+
+ /**
+ * The state of every key, indexed by keysym, for strictly those keys whose
+ * status has been indirectly determined thorugh observation of other key
+ * events. If a particular key is implicitly pressed, the value of
+ * implicitlyPressed for that keysym will be true. If a key
+ * is not currently implicitly pressed (the key is not pressed OR the state
+ * of the key is explicitly known), it will not be defined.
+ *
+ * @private
+ * @type {!Object.}
+ */
+ var implicitlyPressed = {};
+
+ /**
+ * The last result of calling the onkeydown handler for each key, indexed
+ * by keysym. This is used to prevent/allow default actions for key events,
+ * even when the onkeydown handler cannot be called again because the key
+ * is (theoretically) still pressed.
+ *
+ * @private
+ * @type {!Object.}
+ */
+ var last_keydown_result = {};
+
+ /**
+ * The keysym most recently associated with a given keycode when keydown
+ * fired. This object maps keycodes to keysyms.
+ *
+ * @private
+ * @type {!Object.}
+ */
+ var recentKeysym = {};
+
+ /**
+ * Timeout before key repeat starts.
+ *
+ * @private
+ * @type {number}
+ */
+ var key_repeat_timeout = null;
+
+ /**
+ * Interval which presses and releases the last key pressed while that
+ * key is still being held down.
+ *
+ * @private
+ * @type {number}
+ */
+ var key_repeat_interval = null;
+
+ /**
+ * Given an array of keysyms indexed by location, returns the keysym
+ * for the given location, or the keysym for the standard location if
+ * undefined.
+ *
+ * @private
+ * @param {number[]} keysyms
+ * An array of keysyms, where the index of the keysym in the array is
+ * the location value.
+ *
+ * @param {!number} location
+ * The location on the keyboard corresponding to the key pressed, as
+ * defined at: http://www.w3.org/TR/DOM-Level-3-Events/#events-KeyboardEvent
+ */
+ var get_keysym = function get_keysym(keysyms, location) {
+
+ if (!keysyms)
+ return null;
+
+ return keysyms[location] || keysyms[0];
+ };
+
+ /**
+ * Returns true if the given keysym corresponds to a printable character,
+ * false otherwise.
+ *
+ * @param {!number} keysym
+ * The keysym to check.
+ *
+ * @returns {!boolean}
+ * true if the given keysym corresponds to a printable character,
+ * false otherwise.
+ */
+ var isPrintable = function isPrintable(keysym) {
+
+ // Keysyms with Unicode equivalents are printable
+ return (keysym >= 0x00 && keysym <= 0xFF)
+ || (keysym & 0xFFFF0000) === 0x01000000;
+
+ };
+
+ function keysym_from_key_identifier(identifier, location, shifted) {
+
+ if (!identifier)
+ return null;
+
+ var typedCharacter;
+
+ // If identifier is U+xxxx, decode Unicode character
+ var unicodePrefixLocation = identifier.indexOf("U+");
+ if (unicodePrefixLocation >= 0) {
+ var hex = identifier.substring(unicodePrefixLocation+2);
+ typedCharacter = String.fromCharCode(parseInt(hex, 16));
+ }
+
+ // If single character and not keypad, use that as typed character
+ else if (identifier.length === 1 && location !== 3)
+ typedCharacter = identifier;
+
+ // Otherwise, look up corresponding keysym
+ else
+ return get_keysym(keyidentifier_keysym[identifier], location);
+
+ // Alter case if necessary
+ if (shifted === true)
+ typedCharacter = typedCharacter.toUpperCase();
+ else if (shifted === false)
+ typedCharacter = typedCharacter.toLowerCase();
+
+ // Get codepoint
+ var codepoint = typedCharacter.charCodeAt(0);
+ return keysym_from_charcode(codepoint);
+
+ }
+
+ function isControlCharacter(codepoint) {
+ return codepoint <= 0x1F || (codepoint >= 0x7F && codepoint <= 0x9F);
+ }
+
+ function keysym_from_charcode(codepoint) {
+
+ // Keysyms for control characters
+ if (isControlCharacter(codepoint)) return 0xFF00 | codepoint;
+
+ // Keysyms for ASCII chars
+ if (codepoint >= 0x0000 && codepoint <= 0x00FF)
+ return codepoint;
+
+ // Keysyms for Unicode
+ if (codepoint >= 0x0100 && codepoint <= 0x10FFFF)
+ return 0x01000000 | codepoint;
+
+ return null;
+
+ }
+
+ function keysym_from_keycode(keyCode, location) {
+ return get_keysym(keycodeKeysyms[keyCode], location);
+ }
+
+ /**
+ * Heuristically detects if the legacy keyIdentifier property of
+ * a keydown/keyup event looks incorrectly derived. Chrome, and
+ * presumably others, will produce the keyIdentifier by assuming
+ * the keyCode is the Unicode codepoint for that key. This is not
+ * correct in all cases.
+ *
+ * @private
+ * @param {!number} keyCode
+ * The keyCode from a browser keydown/keyup event.
+ *
+ * @param {string} keyIdentifier
+ * The legacy keyIdentifier from a browser keydown/keyup event.
+ *
+ * @returns {!boolean}
+ * true if the keyIdentifier looks sane, false if the keyIdentifier
+ * appears incorrectly derived or is missing entirely.
+ */
+ var key_identifier_sane = function key_identifier_sane(keyCode, keyIdentifier) {
+
+ // Missing identifier is not sane
+ if (!keyIdentifier)
+ return false;
+
+ // Assume non-Unicode keyIdentifier values are sane
+ var unicodePrefixLocation = keyIdentifier.indexOf("U+");
+ if (unicodePrefixLocation === -1)
+ return true;
+
+ // If the Unicode codepoint isn't identical to the keyCode,
+ // then the identifier is likely correct
+ var codepoint = parseInt(keyIdentifier.substring(unicodePrefixLocation+2), 16);
+ if (keyCode !== codepoint)
+ return true;
+
+ // The keyCodes for A-Z and 0-9 are actually identical to their
+ // Unicode codepoints
+ if ((keyCode >= 65 && keyCode <= 90) || (keyCode >= 48 && keyCode <= 57))
+ return true;
+
+ // The keyIdentifier does NOT appear sane
+ return false;
+
+ };
+
+ /**
+ * Marks a key as pressed, firing the keydown event if registered. Key
+ * repeat for the pressed key will start after a delay if that key is
+ * not a modifier. The return value of this function depends on the
+ * return value of the keydown event handler, if any.
+ *
+ * @param {number} keysym
+ * The keysym of the key to press.
+ *
+ * @return {boolean}
+ * true if event should NOT be canceled, false otherwise.
+ */
+ this.press = function(keysym) {
+
+ // Don't bother with pressing the key if the key is unknown
+ if (keysym === null) return;
+
+ // Only press if released
+ if (!guac_keyboard.pressed[keysym]) {
+
+ // Mark key as pressed
+ guac_keyboard.pressed[keysym] = true;
+
+ // Send key event
+ if (guac_keyboard.onkeydown) {
+ var result = guac_keyboard.onkeydown(keysym);
+ last_keydown_result[keysym] = result;
+
+ // Stop any current repeat
+ window.clearTimeout(key_repeat_timeout);
+ window.clearInterval(key_repeat_interval);
+
+ // Repeat after a delay as long as pressed
+ if (!no_repeat[keysym])
+ key_repeat_timeout = window.setTimeout(function() {
+ key_repeat_interval = window.setInterval(function() {
+ guac_keyboard.onkeyup(keysym);
+ guac_keyboard.onkeydown(keysym);
+ }, 50);
+ }, 500);
+
+ return result;
+ }
+ }
+
+ // Return the last keydown result by default, resort to false if unknown
+ return last_keydown_result[keysym] || false;
+
+ };
+
+ /**
+ * Marks a key as released, firing the keyup event if registered.
+ *
+ * @param {number} keysym
+ * The keysym of the key to release.
+ */
+ this.release = function(keysym) {
+
+ // Only release if pressed
+ if (guac_keyboard.pressed[keysym]) {
+
+ // Mark key as released
+ delete guac_keyboard.pressed[keysym];
+ delete implicitlyPressed[keysym];
+
+ // Stop repeat
+ window.clearTimeout(key_repeat_timeout);
+ window.clearInterval(key_repeat_interval);
+
+ // Send key event
+ if (keysym !== null && guac_keyboard.onkeyup)
+ guac_keyboard.onkeyup(keysym);
+
+ }
+
+ };
+
+ /**
+ * Presses and releases the keys necessary to type the given string of
+ * text.
+ *
+ * @param {!string} str
+ * The string to type.
+ */
+ this.type = function type(str) {
+
+ // Press/release the key corresponding to each character in the string
+ for (var i = 0; i < str.length; i++) {
+
+ // Determine keysym of current character
+ var codepoint = str.codePointAt ? str.codePointAt(i) : str.charCodeAt(i);
+ var keysym = keysym_from_charcode(codepoint);
+
+ // Press and release key for current character
+ guac_keyboard.press(keysym);
+ guac_keyboard.release(keysym);
+
+ }
+
+ };
+
+ /**
+ * Resets the state of this keyboard, releasing all keys, and firing keyup
+ * events for each released key.
+ */
+ this.reset = function() {
+
+ // Release all pressed keys
+ for (var keysym in guac_keyboard.pressed)
+ guac_keyboard.release(parseInt(keysym));
+
+ // Clear event log
+ eventLog = [];
+
+ };
+
+ /**
+ * Resynchronizes the remote state of the given modifier with its
+ * corresponding local modifier state, as dictated by
+ * {@link KeyEvent#modifiers} within the given key event, by pressing or
+ * releasing keysyms.
+ *
+ * @private
+ * @param {!string} modifier
+ * The name of the {@link Guacamole.Keyboard.ModifierState} property
+ * being updated.
+ *
+ * @param {!number[]} keysyms
+ * The keysyms which represent the modifier being updated.
+ *
+ * @param {!KeyEvent} keyEvent
+ * Guacamole's current best interpretation of the key event being
+ * processed.
+ */
+ var updateModifierState = function updateModifierState(modifier,
+ keysyms, keyEvent) {
+
+ var localState = keyEvent.modifiers[modifier];
+ var remoteState = guac_keyboard.modifiers[modifier];
+
+ var i;
+
+ // Do not trust changes in modifier state for events directly involving
+ // that modifier: (1) the flag may erroneously be cleared despite
+ // another version of the same key still being held and (2) the change
+ // in flag may be due to the current event being processed, thus
+ // updating things here is at best redundant and at worst incorrect
+ if (keysyms.indexOf(keyEvent.keysym) !== -1)
+ return;
+
+ // Release all related keys if modifier is implicitly released
+ if (remoteState && localState === false) {
+ for (i = 0; i < keysyms.length; i++) {
+ guac_keyboard.release(keysyms[i]);
+ }
+ }
+
+ // Press if modifier is implicitly pressed
+ else if (!remoteState && localState) {
+
+ // Verify that modifier flag isn't already pressed or already set
+ // due to another version of the same key being held down
+ for (i = 0; i < keysyms.length; i++) {
+ if (guac_keyboard.pressed[keysyms[i]])
+ return;
+ }
+
+ // Mark as implicitly pressed only if there is other information
+ // within the key event relating to a different key. Some
+ // platforms, such as iOS, will send essentially empty key events
+ // for modifier keys, using only the modifier flags to signal the
+ // identity of the key.
+ var keysym = keysyms[0];
+ if (keyEvent.keysym)
+ implicitlyPressed[keysym] = true;
+
+ guac_keyboard.press(keysym);
+
+ }
+
+ };
+
+ /**
+ * Given a keyboard event, updates the remote key state to match the local
+ * modifier state and remote based on the modifier flags within the event.
+ * This function pays no attention to keycodes.
+ *
+ * @private
+ * @param {!KeyEvent} keyEvent
+ * Guacamole's current best interpretation of the key event being
+ * processed.
+ */
+ var syncModifierStates = function syncModifierStates(keyEvent) {
+
+ // Resync state of alt
+ updateModifierState('alt', [
+ 0xFFE9, // Left alt
+ 0xFFEA, // Right alt
+ 0xFE03 // AltGr
+ ], keyEvent);
+
+ // Resync state of shift
+ updateModifierState('shift', [
+ 0xFFE1, // Left shift
+ 0xFFE2 // Right shift
+ ], keyEvent);
+
+ // Resync state of ctrl
+ updateModifierState('ctrl', [
+ 0xFFE3, // Left ctrl
+ 0xFFE4 // Right ctrl
+ ], keyEvent);
+
+ // Resync state of meta
+ updateModifierState('meta', [
+ 0xFFE7, // Left meta
+ 0xFFE8 // Right meta
+ ], keyEvent);
+
+ // Resync state of hyper
+ updateModifierState('hyper', [
+ 0xFFEB, // Left super/hyper
+ 0xFFEC // Right super/hyper
+ ], keyEvent);
+
+ // Update state
+ guac_keyboard.modifiers = keyEvent.modifiers;
+
+ };
+
+ /**
+ * Returns whether all currently pressed keys were implicitly pressed. A
+ * key is implicitly pressed if its status was inferred indirectly from
+ * inspection of other key events.
+ *
+ * @private
+ * @returns {!boolean}
+ * true if all currently pressed keys were implicitly pressed, false
+ * otherwise.
+ */
+ var isStateImplicit = function isStateImplicit() {
+
+ for (var keysym in guac_keyboard.pressed) {
+ if (!implicitlyPressed[keysym])
+ return false;
+ }
+
+ return true;
+
+ };
+
+ /**
+ * Reads through the event log, removing events from the head of the log
+ * when the corresponding true key presses are known (or as known as they
+ * can be).
+ *
+ * @private
+ * @return {boolean}
+ * Whether the default action of the latest event should be prevented.
+ */
+ function interpret_events() {
+
+ // Do not prevent default if no event could be interpreted
+ var handled_event = interpret_event();
+ if (!handled_event)
+ return false;
+
+ // Interpret as much as possible
+ var last_event;
+ do {
+ last_event = handled_event;
+ handled_event = interpret_event();
+ } while (handled_event !== null);
+
+ // Reset keyboard state if we cannot expect to receive any further
+ // keyup events
+ if (isStateImplicit())
+ guac_keyboard.reset();
+
+ return last_event.defaultPrevented;
+
+ }
+
+ /**
+ * Releases Ctrl+Alt, if both are currently pressed and the given keysym
+ * looks like a key that may require AltGr.
+ *
+ * @private
+ * @param {!number} keysym
+ * The key that was just pressed.
+ */
+ var release_simulated_altgr = function release_simulated_altgr(keysym) {
+
+ // Both Ctrl+Alt must be pressed if simulated AltGr is in use
+ if (!guac_keyboard.modifiers.ctrl || !guac_keyboard.modifiers.alt)
+ return;
+
+ // Assume [A-Z] never require AltGr
+ if (keysym >= 0x0041 && keysym <= 0x005A)
+ return;
+
+ // Assume [a-z] never require AltGr
+ if (keysym >= 0x0061 && keysym <= 0x007A)
+ return;
+
+ // Release Ctrl+Alt if the keysym is printable
+ if (keysym <= 0xFF || (keysym & 0xFF000000) === 0x01000000) {
+ guac_keyboard.release(0xFFE3); // Left ctrl
+ guac_keyboard.release(0xFFE4); // Right ctrl
+ guac_keyboard.release(0xFFE9); // Left alt
+ guac_keyboard.release(0xFFEA); // Right alt
+ }
+
+ };
+
+ /**
+ * Reads through the event log, interpreting the first event, if possible,
+ * and returning that event. If no events can be interpreted, due to a
+ * total lack of events or the need for more events, null is returned. Any
+ * interpreted events are automatically removed from the log.
+ *
+ * @private
+ * @return {KeyEvent}
+ * The first key event in the log, if it can be interpreted, or null
+ * otherwise.
+ */
+ var interpret_event = function interpret_event() {
+
+ // Peek at first event in log
+ var first = eventLog[0];
+ if (!first)
+ return null;
+
+ // Keydown event
+ if (first instanceof KeydownEvent) {
+
+ var keysym = null;
+ var accepted_events = [];
+
+ // Defer handling of Meta until it is known to be functioning as a
+ // modifier (it may otherwise actually be an alternative method for
+ // pressing a single key, such as Meta+Left for Home on ChromeOS)
+ if (first.keysym === 0xFFE7 || first.keysym === 0xFFE8) {
+
+ // Defer handling until further events exist to provide context
+ if (eventLog.length === 1)
+ return null;
+
+ // Drop keydown if it turns out Meta does not actually apply
+ if (eventLog[1].keysym !== first.keysym) {
+ if (!eventLog[1].modifiers.meta)
+ return eventLog.shift();
+ }
+
+ // Drop duplicate keydown events while waiting to determine
+ // whether to acknowledge Meta (browser may repeat keydown
+ // while the key is held)
+ else if (eventLog[1] instanceof KeydownEvent)
+ return eventLog.shift();
+
+ }
+
+ // If event itself is reliable, no need to wait for other events
+ if (first.reliable) {
+ keysym = first.keysym;
+ accepted_events = eventLog.splice(0, 1);
+ }
+
+ // If keydown is immediately followed by a keypress, use the indicated character
+ else if (eventLog[1] instanceof KeypressEvent) {
+ keysym = eventLog[1].keysym;
+ accepted_events = eventLog.splice(0, 2);
+ }
+
+ // If keydown is immediately followed by anything else, then no
+ // keypress can possibly occur to clarify this event, and we must
+ // handle it now
+ else if (eventLog[1]) {
+ keysym = first.keysym;
+ accepted_events = eventLog.splice(0, 1);
+ }
+
+ // Fire a key press if valid events were found
+ if (accepted_events.length > 0) {
+
+ syncModifierStates(first);
+
+ if (keysym) {
+
+ // Fire event
+ release_simulated_altgr(keysym);
+ var defaultPrevented = !guac_keyboard.press(keysym);
+ recentKeysym[first.keyCode] = keysym;
+
+ // Release the key now if we cannot rely on the associated
+ // keyup event
+ if (!first.keyupReliable)
+ guac_keyboard.release(keysym);
+
+ // Record whether default was prevented
+ for (var i=0; i, Author: Loïc Le Page */\n" +
+ "/*! Contains embedded adapter from webrtc-adapter (https://github.com/webrtcHacks/adapter), BSD 3-Clause License, Copyright (c) 2014, The WebRTC project authors. All rights reserved. Copyright (c) 2018, The adapter.js project authors. All rights reserved. */\n" +
+ "/*! Contains embedded Keyboard.js from guacamole-client (https://github.com/apache/guacamole-client), Apache 2.0 License */"
+ }
+ }
+ })
+ ]
+ },
+
+ plugins: [new webpack.ProgressPlugin()]
+};
+
+if (isDevServer) {
+ config.plugins.push(new HtmlWebpackPlugin({
+ template: "./index.html",
+ inject: "head",
+ minify: false
+ }));
+}
+
+module.exports = config; // eslint-disable-line no-undef
diff --git a/net/webrtc/protocol/src/lib.rs b/net/webrtc/protocol/src/lib.rs
index 99717a86..7d565c25 100644
--- a/net/webrtc/protocol/src/lib.rs
+++ b/net/webrtc/protocol/src/lib.rs
@@ -17,6 +17,7 @@ pub struct Peer {
/// Messages sent from the server to peers
pub enum OutgoingMessage {
/// Welcoming message, sets the Peer ID linked to a new connection
+ #[serde(rename_all = "camelCase")]
Welcome { peer_id: String },
/// Notifies listeners that a peer status has changed
PeerStatusChanged(PeerStatus),
@@ -27,13 +28,14 @@ pub enum OutgoingMessage {
#[serde(rename_all = "camelCase")]
SessionStarted { peer_id: String, session_id: String },
/// Signals that the session the peer was in was ended
- #[serde(rename_all = "camelCase")]
EndSession(EndSessionMessage),
/// Messages directly forwarded from one peer to another
Peer(PeerMessage),
/// Provides the current list of consumer peers
+ #[serde(rename_all = "camelCase")]
List { producers: Vec },
/// Notifies that an error occurred with the peer's current session
+ #[serde(rename_all = "camelCase")]
Error { details: String },
}
@@ -42,10 +44,8 @@ pub enum OutgoingMessage {
/// Register with a peer type
pub enum PeerRole {
/// Register as a producer
- #[serde(rename_all = "camelCase")]
Producer,
/// Register as a listener
- #[serde(rename_all = "camelCase")]
Listener,
}
@@ -83,11 +83,13 @@ pub struct StartSessionMessage {
/// Conveys a SDP
pub enum SdpMessage {
/// Conveys an offer
+ #[serde(rename_all = "camelCase")]
Offer {
/// The SDP
sdp: String,
},
/// Conveys an answer
+ #[serde(rename_all = "camelCase")]
Answer {
/// The SDP
sdp: String,
diff --git a/net/webrtc/www/index.html b/net/webrtc/www/index.html
deleted file mode 100644
index e533c4ff..00000000
--- a/net/webrtc/www/index.html
+++ /dev/null
@@ -1,38 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/net/webrtc/www/input.js b/net/webrtc/www/input.js
deleted file mode 100644
index 805f3557..00000000
--- a/net/webrtc/www/input.js
+++ /dev/null
@@ -1,482 +0,0 @@
-/**
- * Copyright 2019 Google LLC
- *
- * Licensed 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.
- */
-
-/*global GamepadManager*/
-/*eslint no-unused-vars: ["error", { "vars": "local" }]*/
-
-
-class Input {
- /**
- * Input handling for WebRTC web app
- *
- * @constructor
- * @param {Element} [element]
- * Video element to attach events to
- * @param {function} [send]
- * Function used to send input events to server.
- */
- constructor(element, send) {
- /**
- * @type {Element}
- */
- this.element = element;
-
- /**
- * @type {function}
- */
- this.send = send;
-
- /**
- * @type {boolean}
- */
- this.mouseRelative = false;
-
- /**
- * @type {Object}
- */
- this.m = null;
-
- /**
- * @type {Keyboard}
- */
- this.keyboard = null;
-
- /**
- * @type {GamepadManager}
- */
- this.gamepadManager = null;
-
- /**
- * @type {Integer}
- */
- this.x = 0;
-
- /**
- * @type {Integer}
- */
- this.y = 0;
-
- /**
- * @type {Integer}
- */
- this.lastTouch = 0;
-
- /**
- * @type {function}
- */
- this.ongamepadconnected = null;
-
- /**
- * @type {function}
- */
- this.ongamepaddisconneceted = null;
-
- /**
- * List of attached listeners, record keeping used to detach all.
- * @type {Array}
- */
- this.listeners = [];
-
- /**
- * @type {function}
- */
- this.onresizeend = null;
-
- // internal variables used by resize start/end functions.
- this._rtime = null;
- this._rtimeout = false;
- this._rdelta = 200;
- }
-
- /**
- * Handles mouse button and motion events and sends them to WebRTC app.
- * @param {MouseEvent} event
- */
- _mouseButtonMovement(event) {
- const down = (event.type === 'mousedown' ? 1 : 0);
- var data = {};
-
- if (event.type === 'mousemove' && !this.m) return;
-
- if (!document.pointerLockElement) {
- if (this.mouseRelative)
- event.target.requestPointerLock();
- }
-
- // Hotkey to enable pointer lock, CTRL-SHIFT-LeftButton
- if (down && event.button === 0 && event.ctrlKey && event.shiftKey) {
- event.target.requestPointerLock();
- return;
- }
-
- if (document.pointerLockElement) {
- // FIXME - mark as relative!
- console.warn("FIXME: Make event relative!")
- this.x = event.movementX;
- this.y = event.movementY;
- } else if (event.type === 'mousemove') {
- this.x = this._clientToServerX(event.clientX);
- this.y = this._clientToServerY(event.clientY);
- data["event"] = "MouseMove"
- }
-
- if (event.type === 'mousedown') {
- data["event"] = "MouseButtonPress";
- } else if (event.type === 'mouseup') {
- data["event"] = "MouseButtonRelease";
- }
-
- if (event.type === 'mousedown' || event.type === 'mouseup') {
- data["button"] = event.button + 1;
- }
-
- data["x"] = this.x;
- data["y"] = this.y;
- data["modifier_state"] = this._modifierState(event);
-
- this.send(data);
- }
-
- /**
- * Handles touch events and sends them to WebRTC app.
- * @param {TouchEvent} event
- */
- _touch(event) {
- var mod_state = this._modifierState(event);
-
- // Use TouchUp for cancelled touch points
- if (event.type === 'touchcancel') {
- let data = {};
-
- data["event"] = "TouchUp";
- data["identifier"] = event.changedTouches[0].identifier;
- data["x"] = this._clientToServerX(event.changedTouches[0].clientX);
- data["y"] = this._clientToServerY(event.changedTouches[0].clientY);
- data["modifier_state"] = mod_state;
-
- this.send(data);
- return;
- }
-
- if (event.type === 'touchstart') {
- var event_name = "TouchDown";
- } else if (event.type === 'touchmove') {
- var event_name = "TouchMotion";
- } else if (event.type === 'touchend') {
- var event_name = "TouchUp";
- }
-
- for (let touch of event.changedTouches) {
- let data = {};
-
- data["event"] = event_name;
- data["identifier"] = touch.identifier;
- data["x"] = this._clientToServerX(touch.clientX);
- data["y"] = this._clientToServerY(touch.clientY);
- data["modifier_state"] = mod_state;
-
- if (event.type !== 'touchend') {
- if ('force' in touch) {
- data["pressure"] = touch.force;
- } else {
- data["pressure"] = NaN;
- }
- }
-
- this.send(data);
- }
-
- if (event.timeStamp > this.lastTouch) {
- let data = {};
-
- data["event"] = "TouchFrame";
- data["modifier_state"] = mod_state;
-
- this.send(data);
- this.lastTouch = event.timeStamp;
- }
-
- event.preventDefault();
- }
-
- /**
- * Handles mouse wheel events and sends them to WebRTC app.
- * @param {MouseEvent} event
- */
- _wheel(event) {
- let data = {
- "event": "MouseScroll",
- "x": this.x,
- "y": this.y,
- "delta_x": -event.deltaX,
- "delta_y": -event.deltaY,
- "modifier_state": this._modifierState(event),
- };
-
- this.send(data);
-
- event.preventDefault();
- }
-
- /**
- * Captures mouse context menu (right-click) event and prevents event propagation.
- * @param {MouseEvent} event
- */
- _contextMenu(event) {
- event.preventDefault();
- }
-
- /**
- * Sends WebRTC app command to hide the remote pointer when exiting pointer lock.
- */
- _exitPointerLock() {
- document.exitPointerLock();
- }
-
- /**
- * constructs the string representation for the active modifiers on the event
- */
- _modifierState(event) {
- let masks = []
- if (event.altKey) masks.push("alt-mask");
- if (event.ctrlKey) masks.push("control-mask");
- if (event.metaKey) masks.push("meta-mask");
- if (event.shiftKey) masks.push("shift-mask");
- return masks.join('+')
- }
-
- /**
- * Captures display and video dimensions required for computing mouse pointer position.
- * This should be fired whenever the window size changes.
- */
- _windowMath() {
- const windowW = this.element.offsetWidth;
- const windowH = this.element.offsetHeight;
- const frameW = this.element.videoWidth;
- const frameH = this.element.videoHeight;
-
- const multi = Math.min(windowW / frameW, windowH / frameH);
- const vpWidth = frameW * multi;
- const vpHeight = (frameH * multi);
-
- var elem = this.element;
- var offsetLeft = 0;
- var offsetTop = 0;
- do {
- if (!isNaN(elem.offsetLeft)) {
- offsetLeft += elem.offsetLeft;
- }
-
- if (!isNaN(elem.offsetTop)) {
- offsetTop += elem.offsetTop;
- }
- } while (elem = elem.offsetParent);
-
- this.m = {
- mouseMultiX: frameW / vpWidth,
- mouseMultiY: frameH / vpHeight,
- mouseOffsetX: Math.max((windowW - vpWidth) / 2.0, 0),
- mouseOffsetY: Math.max((windowH - vpHeight) / 2.0, 0),
- offsetLeft: offsetLeft,
- offsetTop: offsetTop,
- scrollX: window.scrollX,
- scrollY: window.scrollY,
- frameW,
- frameH,
- };
- }
-
- /**
- * Translates pointer position X based on current window math.
- * @param {Integer} clientX
- */
- _clientToServerX(clientX) {
- var serverX = Math.round((clientX - this.m.mouseOffsetX - this.m.offsetLeft + this.m.scrollX) * this.m.mouseMultiX);
-
- if (serverX === this.m.frameW - 1) serverX = this.m.frameW;
- if (serverX > this.m.frameW) serverX = this.m.frameW;
- if (serverX < 0) serverX = 0;
-
- return serverX;
- }
-
- /**
- * Translates pointer position Y based on current window math.
- * @param {Integer} clientY
- */
- _clientToServerY(clientY) {
- let serverY = Math.round((clientY - this.m.mouseOffsetY - this.m.offsetTop + this.m.scrollY) * this.m.mouseMultiY);
-
- if (serverY === this.m.frameH - 1) serverY = this.m.frameH;
- if (serverY > this.m.frameH) serverY = this.m.frameH;
- if (serverY < 0) serverY = 0;
-
- return serverY;
- }
-
- /**
- * When fullscreen is entered, request keyboard and pointer lock.
- */
- _onFullscreenChange() {
- if (document.fullscreenElement !== null) {
- // Enter fullscreen
- this.requestKeyboardLock();
- this.element.requestPointerLock();
- }
- // Reset local keyboard. When holding to exit full-screen the escape key can get stuck.
- this.keyboard.reset();
-
- // Reset stuck keys on server side.
- // FIXME: How to implement resetting keyboard with the GstNavigation interface
- // this.send("kr");
- }
-
- /**
- * Called when window is being resized, used to detect when resize ends so new resolution can be sent.
- */
- _resizeStart() {
- this._rtime = new Date();
- if (this._rtimeout === false) {
- this._rtimeout = true;
- setTimeout(() => { this._resizeEnd() }, this._rdelta);
- }
- }
-
- /**
- * Called in setTimeout loop to detect if window is done being resized.
- */
- _resizeEnd() {
- if (new Date() - this._rtime < this._rdelta) {
- setTimeout(() => { this._resizeEnd() }, this._rdelta);
- } else {
- this._rtimeout = false;
- if (this.onresizeend !== null) {
- this.onresizeend();
- }
- }
- }
-
- /**
- * Attaches input event handles to docuemnt, window and element.
- */
- attach() {
- this.listeners.push(addListener(this.element, 'resize', this._windowMath, this));
- this.listeners.push(addListener(this.element, 'wheel', this._wheel, this));
- this.listeners.push(addListener(this.element, 'contextmenu', this._contextMenu, this));
- this.listeners.push(addListener(this.element.parentElement, 'fullscreenchange', this._onFullscreenChange, this));
- this.listeners.push(addListener(window, 'resize', this._windowMath, this));
- this.listeners.push(addListener(window, 'resize', this._resizeStart, this));
-
- if ('ontouchstart' in window) {
- console.warning("FIXME: Enabling mouse pointer display for touch devices.");
- } else {
- this.listeners.push(addListener(this.element, 'mousemove', this._mouseButtonMovement, this));
- this.listeners.push(addListener(this.element, 'mousedown', this._mouseButtonMovement, this));
- this.listeners.push(addListener(this.element, 'mouseup', this._mouseButtonMovement, this));
- }
-
- this.listeners.push(addListener(this.element, 'touchstart', this._touch, this));
- this.listeners.push(addListener(this.element, 'touchend', this._touch, this));
- this.listeners.push(addListener(this.element, 'touchmove', this._touch, this));
- this.listeners.push(addListener(this.element, 'touchcancel', this._touch, this));
-
- // Adjust for scroll offset
- this.listeners.push(addListener(window, 'scroll', () => {
- this.m.scrollX = window.scrollX;
- this.m.scrollY = window.scrollY;
- }, this));
-
- // Using guacamole keyboard because it has the keysym translations.
- this.keyboard = new Keyboard(this.element);
- this.keyboard.onkeydown = (keysym, state) => {
- this.send({"event": "KeyPress", "key": keysym, "modifier_state": state});
- };
- this.keyboard.onkeyup = (keysym, state) => {
- this.send({"event": "KeyRelease", "key": keysym, "modifier_state": state});
- };
-
- this._windowMath();
- }
-
- detach() {
- removeListeners(this.listeners);
- this._exitPointerLock();
- if (this.keyboard) {
- this.keyboard.onkeydown = null;
- this.keyboard.onkeyup = null;
- this.keyboard.reset();
- delete this.keyboard;
- // FIXME: How to implement resetting keyboard with the GstNavigation interface
- // this.send("kr");
- }
- }
-
- /**
- * Request keyboard lock, must be in fullscreen mode to work.
- */
- requestKeyboardLock() {
- // event codes: https://www.w3.org/TR/uievents-code/#key-alphanumeric-writing-system
- const keys = [
- "AltLeft",
- "AltRight",
- "Tab",
- "Escape",
- "ContextMenu",
- "MetaLeft",
- "MetaRight"
- ];
- console.log("requesting keyboard lock");
- navigator.keyboard.lock(keys).then(
- () => {
- console.log("keyboard lock success");
- }
- ).catch(
- (e) => {
- console.log("keyboard lock failed: ", e);
- }
- )
- }
-
- getWindowResolution() {
- return [
- parseInt(this.element.offsetWidth * window.devicePixelRatio),
- parseInt(this.element.offsetHeight * window.devicePixelRatio)
- ];
- }
-}
-
-/**
- * Helper function to keep track of attached event listeners.
- * @param {Object} obj
- * @param {string} name
- * @param {function} func
- * @param {Object} ctx
- */
-function addListener(obj, name, func, ctx) {
- const newFunc = ctx ? func.bind(ctx) : func;
- obj.addEventListener(name, newFunc);
-
- return [obj, name, newFunc];
-}
-
-/**
- * Helper function to remove all attached event listeners.
- * @param {Array} listeners
- */
-function removeListeners(listeners) {
- for (const listener of listeners)
- listener[0].removeEventListener(listener[1], listener[2]);
-}
diff --git a/net/webrtc/www/keyboard.js b/net/webrtc/www/keyboard.js
deleted file mode 100644
index 4305cd1a..00000000
--- a/net/webrtc/www/keyboard.js
+++ /dev/null
@@ -1,3302 +0,0 @@
-/*
- * 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.
- */
-
-/**
- * Provides cross-browser and cross-keyboard keyboard for a specific element.
- * Browser and keyboard layout variation is abstracted away, providing events
- * which represent keys as their corresponding X11 keysym.
- *
- * @constructor
- * @param {Element} element The Element to use to provide keyboard events.
- */
-Keyboard = function(element) {
-
- /**
- * Reference to this Keyboard.
- * @private
- */
- var guac_keyboard = this;
-
- /**
- * Fired whenever the user presses a key with the element associated
- * with this Keyboard in focus.
- *
- * @event
- * @param {Number} keysym The keysym of the key being pressed.
- * @param {String} modifier_state The string representation of
- * all active modifiers.
- * @return {Boolean} true if the key event should be allowed through to the
- * browser, false otherwise.
- */
- this.onkeydown = null;
-
- /**
- * Fired whenever the user releases a key with the element associated
- * with this Keyboard in focus.
- *
- * @event
- * @param {Number} keysym The keysym of the key being released.
- * @param {String} modifier_state The string representation of
- * all active modifiers.
- */
- this.onkeyup = null;
-
- /**
- * A key event having a corresponding timestamp. This event is non-specific.
- * Its subclasses should be used instead when recording specific key
- * events.
- *
- * @private
- * @constructor
- */
- var KeyEvent = function() {
-
- /**
- * Reference to this key event.
- */
- var key_event = this;
-
- /**
- * An arbitrary timestamp in milliseconds, indicating this event's
- * position in time relative to other events.
- *
- * @type {Number}
- */
- this.timestamp = new Date().getTime();
-
- /**
- * Whether the default action of this key event should be prevented.
- *
- * @type {Boolean}
- */
- this.defaultPrevented = false;
-
- /**
- * The keysym of the key associated with this key event, as determined
- * by a best-effort guess using available event properties and keyboard
- * state.
- *
- * @type {Number}
- */
- this.keysym = null;
-
- /**
- * Whether the keysym value of this key event is known to be reliable.
- * If false, the keysym may still be valid, but it's only a best guess,
- * and future key events may be a better source of information.
- *
- * @type {Boolean}
- */
- this.reliable = false;
-
- /**
- * Returns the number of milliseconds elapsed since this event was
- * received.
- *
- * @return {Number} The number of milliseconds elapsed since this
- * event was received.
- */
- this.getAge = function() {
- return new Date().getTime() - key_event.timestamp;
- };
-
- };
-
- /**
- * Information related to the pressing of a key, which need not be a key
- * associated with a printable character. The presence or absence of any
- * information within this object is browser-dependent.
- *
- * @private
- * @constructor
- * @augments Keyboard.KeyEvent
- * @param {Number} keyCode The JavaScript key code of the key pressed.
- * @param {String} keyIdentifier The legacy DOM3 "keyIdentifier" of the key
- * pressed, as defined at:
- * http://www.w3.org/TR/2009/WD-DOM-Level-3-Events-20090908/#events-Events-KeyboardEvent
- * @param {String} key The standard name of the key pressed, as defined at:
- * http://www.w3.org/TR/DOM-Level-3-Events/#events-KeyboardEvent
- * @param {Number} location The location on the keyboard corresponding to
- * the key pressed, as defined at:
- * http://www.w3.org/TR/DOM-Level-3-Events/#events-KeyboardEvent
- */
- var KeydownEvent = function(keyCode, keyIdentifier, key, location) {
-
- // We extend KeyEvent
- KeyEvent.apply(this);
-
- /**
- * The JavaScript key code of the key pressed.
- *
- * @type {Number}
- */
- this.keyCode = keyCode;
-
- /**
- * The legacy DOM3 "keyIdentifier" of the key pressed, as defined at:
- * http://www.w3.org/TR/2009/WD-DOM-Level-3-Events-20090908/#events-Events-KeyboardEvent
- *
- * @type {String}
- */
- this.keyIdentifier = keyIdentifier;
-
- /**
- * The standard name of the key pressed, as defined at:
- * http://www.w3.org/TR/DOM-Level-3-Events/#events-KeyboardEvent
- *
- * @type {String}
- */
- this.key = key;
-
- /**
- * The location on the keyboard corresponding to the key pressed, as
- * defined at:
- * http://www.w3.org/TR/DOM-Level-3-Events/#events-KeyboardEvent
- *
- * @type {Number}
- */
- this.location = location;
-
- // If key is known from keyCode or DOM3 alone, use that
- this.keysym = keysym_from_key_identifier(key, location)
- || keysym_from_keycode(keyCode, location);
-
- // DOM3 and keyCode are reliable sources if the corresponding key is
- // not a printable key
- if (this.keysym && !isPrintable(this.keysym))
- this.reliable = true;
-
- // Use legacy keyIdentifier as a last resort, if it looks sane
- if (!this.keysym && key_identifier_sane(keyCode, keyIdentifier))
- this.keysym = keysym_from_key_identifier(keyIdentifier, location, guac_keyboard.modifiers.shift);
-
- // Determine whether default action for Alt+combinations must be prevented
- var prevent_alt = !guac_keyboard.modifiers.ctrl
- && !(navigator && navigator.platform && navigator.platform.match(/^mac/i));
-
- // Determine whether default action for Ctrl+combinations must be prevented
- var prevent_ctrl = !guac_keyboard.modifiers.alt;
-
- // We must rely on the (potentially buggy) keyIdentifier if preventing
- // the default action is important
- if ((prevent_ctrl && guac_keyboard.modifiers.ctrl)
- || (prevent_alt && guac_keyboard.modifiers.alt)
- || guac_keyboard.modifiers.meta
- || guac_keyboard.modifiers.hyper)
- this.reliable = true;
-
- // Record most recently known keysym by associated key code
- recentKeysym[keyCode] = this.keysym;
-
- };
-
- KeydownEvent.prototype = new KeyEvent();
-
- /**
- * Information related to the pressing of a key, which MUST be
- * associated with a printable character. The presence or absence of any
- * information within this object is browser-dependent.
- *
- * @private
- * @constructor
- * @augments Keyboard.KeyEvent
- * @param {Number} charCode The Unicode codepoint of the character that
- * would be typed by the key pressed.
- */
- var KeypressEvent = function(charCode) {
-
- // We extend KeyEvent
- KeyEvent.apply(this);
-
- /**
- * The Unicode codepoint of the character that would be typed by the
- * key pressed.
- *
- * @type {Number}
- */
- this.charCode = charCode;
-
- // Pull keysym from char code
- this.keysym = keysym_from_charcode(charCode);
-
- // Keypress is always reliable
- this.reliable = true;
-
- };
-
- KeypressEvent.prototype = new KeyEvent();
-
- /**
- * Information related to the pressing of a key, which need not be a key
- * associated with a printable character. The presence or absence of any
- * information within this object is browser-dependent.
- *
- * @private
- * @constructor
- * @augments Keyboard.KeyEvent
- * @param {Number} keyCode The JavaScript key code of the key released.
- * @param {String} keyIdentifier The legacy DOM3 "keyIdentifier" of the key
- * released, as defined at:
- * http://www.w3.org/TR/2009/WD-DOM-Level-3-Events-20090908/#events-Events-KeyboardEvent
- * @param {String} key The standard name of the key released, as defined at:
- * http://www.w3.org/TR/DOM-Level-3-Events/#events-KeyboardEvent
- * @param {Number} location The location on the keyboard corresponding to
- * the key released, as defined at:
- * http://www.w3.org/TR/DOM-Level-3-Events/#events-KeyboardEvent
- */
- var KeyupEvent = function(keyCode, keyIdentifier, key, location) {
-
- // We extend KeyEvent
- KeyEvent.apply(this);
-
- /**
- * The JavaScript key code of the key released.
- *
- * @type {Number}
- */
- this.keyCode = keyCode;
-
- /**
- * The legacy DOM3 "keyIdentifier" of the key released, as defined at:
- * http://www.w3.org/TR/2009/WD-DOM-Level-3-Events-20090908/#events-Events-KeyboardEvent
- *
- * @type {String}
- */
- this.keyIdentifier = keyIdentifier;
-
- /**
- * The standard name of the key released, as defined at:
- * http://www.w3.org/TR/DOM-Level-3-Events/#events-KeyboardEvent
- *
- * @type {String}
- */
- this.key = key;
-
- /**
- * The location on the keyboard corresponding to the key released, as
- * defined at:
- * http://www.w3.org/TR/DOM-Level-3-Events/#events-KeyboardEvent
- *
- * @type {Number}
- */
- this.location = location;
-
- // If key is known from keyCode or DOM3 alone, use that
- this.keysym = recentKeysym[keyCode]
- || keysym_from_keycode(keyCode, location)
- || keysym_from_key_identifier(key, location); // keyCode is still more reliable for keyup when dead keys are in use
-
- // Keyup is as reliable as it will ever be
- this.reliable = true;
-
- };
-
- KeyupEvent.prototype = new KeyEvent();
-
- /**
- * An array of recorded events, which can be instances of the private
- * KeydownEvent, KeypressEvent, and KeyupEvent classes.
- *
- * @private
- * @type {KeyEvent[]}
- */
- var eventLog = [];
-
- /**
- * Map of known JavaScript keycodes which do not map to typable characters
- * to their X11 keysym equivalents.
- * @private
- */
- var keycodeKeysyms = {
- 8: [0xFF08], // backspace
- 9: [0xFF09], // tab
- 12: [0xFF0B, 0xFF0B, 0xFF0B, 0xFFB5], // clear / KP 5
- 13: [0xFF0D], // enter
- 16: [0xFFE1, 0xFFE1, 0xFFE2], // shift
- 17: [0xFFE3, 0xFFE3, 0xFFE4], // ctrl
- 18: [0xFFE9, 0xFFE9, 0xFE03], // alt
- 19: [0xFF13], // pause/break
- 20: [0xFFE5], // caps lock
- 27: [0xFF1B], // escape
- 32: [0x0020], // space
- 33: [0xFF55, 0xFF55, 0xFF55, 0xFFB9], // page up / KP 9
- 34: [0xFF56, 0xFF56, 0xFF56, 0xFFB3], // page down / KP 3
- 35: [0xFF57, 0xFF57, 0xFF57, 0xFFB1], // end / KP 1
- 36: [0xFF50, 0xFF50, 0xFF50, 0xFFB7], // home / KP 7
- 37: [0xFF51, 0xFF51, 0xFF51, 0xFFB4], // left arrow / KP 4
- 38: [0xFF52, 0xFF52, 0xFF52, 0xFFB8], // up arrow / KP 8
- 39: [0xFF53, 0xFF53, 0xFF53, 0xFFB6], // right arrow / KP 6
- 40: [0xFF54, 0xFF54, 0xFF54, 0xFFB2], // down arrow / KP 2
- 45: [0xFF63, 0xFF63, 0xFF63, 0xFFB0], // insert / KP 0
- 46: [0xFFFF, 0xFFFF, 0xFFFF, 0xFFAE], // delete / KP decimal
- 91: [0xFFEB], // left window key (hyper_l)
- 92: [0xFF67], // right window key (menu key?)
- 93: null, // select key
- 96: [0xFFB0], // KP 0
- 97: [0xFFB1], // KP 1
- 98: [0xFFB2], // KP 2
- 99: [0xFFB3], // KP 3
- 100: [0xFFB4], // KP 4
- 101: [0xFFB5], // KP 5
- 102: [0xFFB6], // KP 6
- 103: [0xFFB7], // KP 7
- 104: [0xFFB8], // KP 8
- 105: [0xFFB9], // KP 9
- 106: [0xFFAA], // KP multiply
- 107: [0xFFAB], // KP add
- 109: [0xFFAD], // KP subtract
- 110: [0xFFAE], // KP decimal
- 111: [0xFFAF], // KP divide
- 112: [0xFFBE], // f1
- 113: [0xFFBF], // f2
- 114: [0xFFC0], // f3
- 115: [0xFFC1], // f4
- 116: [0xFFC2], // f5
- 117: [0xFFC3], // f6
- 118: [0xFFC4], // f7
- 119: [0xFFC5], // f8
- 120: [0xFFC6], // f9
- 121: [0xFFC7], // f10
- 122: [0xFFC8], // f11
- 123: [0xFFC9], // f12
- 144: [0xFF7F], // num lock
- 145: [0xFF14], // scroll lock
- 225: [0xFE03] // altgraph (iso_level3_shift)
- };
-
- /**
- * Map of known JavaScript keyidentifiers which do not map to typable
- * characters to their unshifted X11 keysym equivalents.
- * @private
- */
- var keyidentifier_keysym = {
- "Again": [0xFF66],
- "AllCandidates": [0xFF3D],
- "Alphanumeric": [0xFF30],
- "Alt": [0xFFE9, 0xFFE9, 0xFE03],
- "Attn": [0xFD0E],
- "AltGraph": [0xFE03],
- "ArrowDown": [0xFF54],
- "ArrowLeft": [0xFF51],
- "ArrowRight": [0xFF53],
- "ArrowUp": [0xFF52],
- "Backspace": [0xFF08],
- "CapsLock": [0xFFE5],
- "Cancel": [0xFF69],
- "Clear": [0xFF0B],
- "Convert": [0xFF21],
- "Copy": [0xFD15],
- "Crsel": [0xFD1C],
- "CrSel": [0xFD1C],
- "CodeInput": [0xFF37],
- "Compose": [0xFF20],
- "Control": [0xFFE3, 0xFFE3, 0xFFE4],
- "ContextMenu": [0xFF67],
- "DeadGrave": [0xFE50],
- "DeadAcute": [0xFE51],
- "DeadCircumflex": [0xFE52],
- "DeadTilde": [0xFE53],
- "DeadMacron": [0xFE54],
- "DeadBreve": [0xFE55],
- "DeadAboveDot": [0xFE56],
- "DeadUmlaut": [0xFE57],
- "DeadAboveRing": [0xFE58],
- "DeadDoubleacute": [0xFE59],
- "DeadCaron": [0xFE5A],
- "DeadCedilla": [0xFE5B],
- "DeadOgonek": [0xFE5C],
- "DeadIota": [0xFE5D],
- "DeadVoicedSound": [0xFE5E],
- "DeadSemivoicedSound": [0xFE5F],
- "Delete": [0xFFFF],
- "Down": [0xFF54],
- "End": [0xFF57],
- "Enter": [0xFF0D],
- "EraseEof": [0xFD06],
- "Escape": [0xFF1B],
- "Execute": [0xFF62],
- "Exsel": [0xFD1D],
- "ExSel": [0xFD1D],
- "F1": [0xFFBE],
- "F2": [0xFFBF],
- "F3": [0xFFC0],
- "F4": [0xFFC1],
- "F5": [0xFFC2],
- "F6": [0xFFC3],
- "F7": [0xFFC4],
- "F8": [0xFFC5],
- "F9": [0xFFC6],
- "F10": [0xFFC7],
- "F11": [0xFFC8],
- "F12": [0xFFC9],
- "F13": [0xFFCA],
- "F14": [0xFFCB],
- "F15": [0xFFCC],
- "F16": [0xFFCD],
- "F17": [0xFFCE],
- "F18": [0xFFCF],
- "F19": [0xFFD0],
- "F20": [0xFFD1],
- "F21": [0xFFD2],
- "F22": [0xFFD3],
- "F23": [0xFFD4],
- "F24": [0xFFD5],
- "Find": [0xFF68],
- "GroupFirst": [0xFE0C],
- "GroupLast": [0xFE0E],
- "GroupNext": [0xFE08],
- "GroupPrevious": [0xFE0A],
- "FullWidth": null,
- "HalfWidth": null,
- "HangulMode": [0xFF31],
- "Hankaku": [0xFF29],
- "HanjaMode": [0xFF34],
- "Help": [0xFF6A],
- "Hiragana": [0xFF25],
- "HiraganaKatakana": [0xFF27],
- "Home": [0xFF50],
- "Hyper": [0xFFED, 0xFFED, 0xFFEE],
- "Insert": [0xFF63],
- "JapaneseHiragana": [0xFF25],
- "JapaneseKatakana": [0xFF26],
- "JapaneseRomaji": [0xFF24],
- "JunjaMode": [0xFF38],
- "KanaMode": [0xFF2D],
- "KanjiMode": [0xFF21],
- "Katakana": [0xFF26],
- "Left": [0xFF51],
- "Meta": [0xFFE7, 0xFFE7, 0xFFE8],
- "ModeChange": [0xFF7E],
- "NumLock": [0xFF7F],
- "PageDown": [0xFF56],
- "PageUp": [0xFF55],
- "Pause": [0xFF13],
- "Play": [0xFD16],
- "PreviousCandidate": [0xFF3E],
- "PrintScreen": [0xFD1D],
- "Redo": [0xFF66],
- "Right": [0xFF53],
- "RomanCharacters": null,
- "Scroll": [0xFF14],
- "Select": [0xFF60],
- "Separator": [0xFFAC],
- "Shift": [0xFFE1, 0xFFE1, 0xFFE2],
- "SingleCandidate": [0xFF3C],
- "Super": [0xFFEB, 0xFFEB, 0xFFEC],
- "Tab": [0xFF09],
- "Up": [0xFF52],
- "Undo": [0xFF65],
- "Win": [0xFFEB],
- "Zenkaku": [0xFF28],
- "ZenkakuHankaku": [0xFF2A]
- };
-
- const keysym_to_string = {
- 0xFFFFFF: "VoidSymbol",
- 0xFF08: "BackSpace",
- 0xFF09: "Tab",
- 0xFF0A: "Linefeed",
- 0xFF0B: "Clear",
- 0xFF0D: "Return",
- 0xFF13: "Pause",
- 0xFF14: "Scroll_Lock",
- 0xFF15: "Sys_Req",
- 0xFF1B: "Escape",
- 0xFFFF: "Delete",
- 0xFF20: "Multi_key",
- 0xFF37: "Codeinput",
- 0xFF3C: "SingleCandidate",
- 0xFF3D: "MultipleCandidate",
- 0xFF3E: "PreviousCandidate",
- 0xFF21: "Kanji",
- 0xFF22: "Muhenkan",
- 0xFF23: "Henkan_Mode",
- 0xFF23: "Henkan",
- 0xFF24: "Romaji",
- 0xFF25: "Hiragana",
- 0xFF26: "Katakana",
- 0xFF27: "Hiragana_Katakana",
- 0xFF28: "Zenkaku",
- 0xFF29: "Hankaku",
- 0xFF2A: "Zenkaku_Hankaku",
- 0xFF2B: "Touroku",
- 0xFF2C: "Massyo",
- 0xFF2D: "Kana_Lock",
- 0xFF2E: "Kana_Shift",
- 0xFF2F: "Eisu_Shift",
- 0xFF30: "Eisu_toggle",
- 0xFF37: "Kanji_Bangou",
- 0xFF3D: "Zen_Koho",
- 0xFF3E: "Mae_Koho",
- 0xFF50: "Home",
- 0xFF51: "Left",
- 0xFF52: "Up",
- 0xFF53: "Right",
- 0xFF54: "Down",
- 0xFF55: "Prior",
- 0xFF55: "Page_Up",
- 0xFF56: "Next",
- 0xFF56: "Page_Down",
- 0xFF57: "End",
- 0xFF58: "Begin",
- 0xFF60: "Select",
- 0xFF61: "Print",
- 0xFF62: "Execute",
- 0xFF63: "Insert",
- 0xFF65: "Undo",
- 0xFF66: "Redo",
- 0xFF67: "Menu",
- 0xFF68: "Find",
- 0xFF69: "Cancel",
- 0xFF6A: "Help",
- 0xFF6B: "Break",
- 0xFF7E: "Mode_switch",
- 0xFF7E: "script_switch",
- 0xFF7F: "Num_Lock",
- 0xFF80: "KP_Space",
- 0xFF89: "KP_Tab",
- 0xFF8D: "KP_Enter",
- 0xFF91: "KP_F1",
- 0xFF92: "KP_F2",
- 0xFF93: "KP_F3",
- 0xFF94: "KP_F4",
- 0xFF95: "KP_Home",
- 0xFF96: "KP_Left",
- 0xFF97: "KP_Up",
- 0xFF98: "KP_Right",
- 0xFF99: "KP_Down",
- 0xFF9A: "KP_Prior",
- 0xFF9A: "KP_Page_Up",
- 0xFF9B: "KP_Next",
- 0xFF9B: "KP_Page_Down",
- 0xFF9C: "KP_End",
- 0xFF9D: "KP_Begin",
- 0xFF9E: "KP_Insert",
- 0xFF9F: "KP_Delete",
- 0xFFBD: "KP_Equal",
- 0xFFAA: "KP_Multiply",
- 0xFFAB: "KP_Add",
- 0xFFAC: "KP_Separator",
- 0xFFAD: "KP_Subtract",
- 0xFFAE: "KP_Decimal",
- 0xFFAF: "KP_Divide",
- 0xFFB0: "KP_0",
- 0xFFB1: "KP_1",
- 0xFFB2: "KP_2",
- 0xFFB3: "KP_3",
- 0xFFB4: "KP_4",
- 0xFFB5: "KP_5",
- 0xFFB6: "KP_6",
- 0xFFB7: "KP_7",
- 0xFFB8: "KP_8",
- 0xFFB9: "KP_9",
- 0xFFBE: "F1",
- 0xFFBF: "F2",
- 0xFFC0: "F3",
- 0xFFC1: "F4",
- 0xFFC2: "F5",
- 0xFFC3: "F6",
- 0xFFC4: "F7",
- 0xFFC5: "F8",
- 0xFFC6: "F9",
- 0xFFC7: "F10",
- 0xFFC8: "F11",
- 0xFFC8: "L1",
- 0xFFC9: "F12",
- 0xFFC9: "L2",
- 0xFFCA: "F13",
- 0xFFCA: "L3",
- 0xFFCB: "F14",
- 0xFFCB: "L4",
- 0xFFCC: "F15",
- 0xFFCC: "L5",
- 0xFFCD: "F16",
- 0xFFCD: "L6",
- 0xFFCE: "F17",
- 0xFFCE: "L7",
- 0xFFCF: "F18",
- 0xFFCF: "L8",
- 0xFFD0: "F19",
- 0xFFD0: "L9",
- 0xFFD1: "F20",
- 0xFFD1: "L10",
- 0xFFD2: "F21",
- 0xFFD2: "R1",
- 0xFFD3: "F22",
- 0xFFD3: "R2",
- 0xFFD4: "F23",
- 0xFFD4: "R3",
- 0xFFD5: "F24",
- 0xFFD5: "R4",
- 0xFFD6: "F25",
- 0xFFD6: "R5",
- 0xFFD7: "F26",
- 0xFFD7: "R6",
- 0xFFD8: "F27",
- 0xFFD8: "R7",
- 0xFFD9: "F28",
- 0xFFD9: "R8",
- 0xFFDA: "F29",
- 0xFFDA: "R9",
- 0xFFDB: "F30",
- 0xFFDB: "R10",
- 0xFFDC: "F31",
- 0xFFDC: "R11",
- 0xFFDD: "F32",
- 0xFFDD: "R12",
- 0xFFDE: "F33",
- 0xFFDE: "R13",
- 0xFFDF: "F34",
- 0xFFDF: "R14",
- 0xFFE0: "F35",
- 0xFFE0: "R15",
- 0xFFE1: "Shift_L",
- 0xFFE2: "Shift_R",
- 0xFFE3: "Control_L",
- 0xFFE4: "Control_R",
- 0xFFE5: "Caps_Lock",
- 0xFFE6: "Shift_Lock",
- 0xFFE7: "Meta_L",
- 0xFFE8: "Meta_R",
- 0xFFE9: "Alt_L",
- 0xFFEA: "Alt_R",
- 0xFFEB: "Super_L",
- 0xFFEC: "Super_R",
- 0xFFED: "Hyper_L",
- 0xFFEE: "Hyper_R",
- 0xFE01: "ISO_Lock",
- 0xFE02: "ISO_Level2_Latch",
- 0xFE03: "ISO_Level3_Shift",
- 0xFE04: "ISO_Level3_Latch",
- 0xFE05: "ISO_Level3_Lock",
- 0xFE11: "ISO_Level5_Shift",
- 0xFE12: "ISO_Level5_Latch",
- 0xFE13: "ISO_Level5_Lock",
- 0xFF7E: "ISO_Group_Shift",
- 0xFE06: "ISO_Group_Latch",
- 0xFE07: "ISO_Group_Lock",
- 0xFE08: "ISO_Next_Group",
- 0xFE09: "ISO_Next_Group_Lock",
- 0xFE0A: "ISO_Prev_Group",
- 0xFE0B: "ISO_Prev_Group_Lock",
- 0xFE0C: "ISO_First_Group",
- 0xFE0D: "ISO_First_Group_Lock",
- 0xFE0E: "ISO_Last_Group",
- 0xFE0F: "ISO_Last_Group_Lock",
- 0xFE20: "ISO_Left_Tab",
- 0xFE21: "ISO_Move_Line_Up",
- 0xFE22: "ISO_Move_Line_Down",
- 0xFE23: "ISO_Partial_Line_Up",
- 0xFE24: "ISO_Partial_Line_Down",
- 0xFE25: "ISO_Partial_Space_Left",
- 0xFE26: "ISO_Partial_Space_Right",
- 0xFE27: "ISO_Set_Margin_Left",
- 0xFE28: "ISO_Set_Margin_Right",
- 0xFE29: "ISO_Release_Margin_Left",
- 0xFE2A: "ISO_Release_Margin_Right",
- 0xFE2B: "ISO_Release_Both_Margins",
- 0xFE2C: "ISO_Fast_Cursor_Left",
- 0xFE2D: "ISO_Fast_Cursor_Right",
- 0xFE2E: "ISO_Fast_Cursor_Up",
- 0xFE2F: "ISO_Fast_Cursor_Down",
- 0xFE30: "ISO_Continuous_Underline",
- 0xFE31: "ISO_Discontinuous_Underline",
- 0xFE32: "ISO_Emphasize",
- 0xFE33: "ISO_Center_Object",
- 0xFE34: "ISO_Enter",
- 0xFE50: "dead_grave",
- 0xFE51: "dead_acute",
- 0xFE52: "dead_circumflex",
- 0xFE53: "dead_tilde",
- 0xFE53: "dead_perispomeni",
- 0xFE54: "dead_macron",
- 0xFE55: "dead_breve",
- 0xFE56: "dead_abovedot",
- 0xFE57: "dead_diaeresis",
- 0xFE58: "dead_abovering",
- 0xFE59: "dead_doubleacute",
- 0xFE5A: "dead_caron",
- 0xFE5B: "dead_cedilla",
- 0xFE5C: "dead_ogonek",
- 0xFE5D: "dead_iota",
- 0xFE5E: "dead_voiced_sound",
- 0xFE5F: "dead_semivoiced_sound",
- 0xFE60: "dead_belowdot",
- 0xFE61: "dead_hook",
- 0xFE62: "dead_horn",
- 0xFE63: "dead_stroke",
- 0xFE64: "dead_abovecomma",
- 0xFE64: "dead_psili",
- 0xFE65: "dead_abovereversedcomma",
- 0xFE65: "dead_dasia",
- 0xFE66: "dead_doublegrave",
- 0xFE67: "dead_belowring",
- 0xFE68: "dead_belowmacron",
- 0xFE69: "dead_belowcircumflex",
- 0xFE6A: "dead_belowtilde",
- 0xFE6B: "dead_belowbreve",
- 0xFE6C: "dead_belowdiaeresis",
- 0xFE6D: "dead_invertedbreve",
- 0xFE6E: "dead_belowcomma",
- 0xFE6F: "dead_currency",
- 0xFE90: "dead_lowline",
- 0xFE91: "dead_aboveverticalline",
- 0xFE92: "dead_belowverticalline",
- 0xFE93: "dead_longsolidusoverlay",
- 0xFE80: "dead_a",
- 0xFE81: "dead_A",
- 0xFE82: "dead_e",
- 0xFE83: "dead_E",
- 0xFE84: "dead_i",
- 0xFE85: "dead_I",
- 0xFE86: "dead_o",
- 0xFE87: "dead_O",
- 0xFE88: "dead_u",
- 0xFE89: "dead_U",
- 0xFE8A: "dead_small_schwa",
- 0xFE8B: "dead_capital_schwa",
- 0xFE8C: "dead_greek",
- 0xFED0: "First_Virtual_Screen",
- 0xFED1: "Prev_Virtual_Screen",
- 0xFED2: "Next_Virtual_Screen",
- 0xFED4: "Last_Virtual_Screen",
- 0xFED5: "Terminate_Server",
- 0xFE70: "AccessX_Enable",
- 0xFE71: "AccessX_Feedback_Enable",
- 0xFE72: "RepeatKeys_Enable",
- 0xFE73: "SlowKeys_Enable",
- 0xFE74: "BounceKeys_Enable",
- 0xFE75: "StickyKeys_Enable",
- 0xFE76: "MouseKeys_Enable",
- 0xFE77: "MouseKeys_Accel_Enable",
- 0xFE78: "Overlay1_Enable",
- 0xFE79: "Overlay2_Enable",
- 0xFE7A: "AudibleBell_Enable",
- 0xFEE0: "Pointer_Left",
- 0xFEE1: "Pointer_Right",
- 0xFEE2: "Pointer_Up",
- 0xFEE3: "Pointer_Down",
- 0xFEE4: "Pointer_UpLeft",
- 0xFEE5: "Pointer_UpRight",
- 0xFEE6: "Pointer_DownLeft",
- 0xFEE7: "Pointer_DownRight",
- 0xFEE8: "Pointer_Button_Dflt",
- 0xFEE9: "Pointer_Button1",
- 0xFEEA: "Pointer_Button2",
- 0xFEEB: "Pointer_Button3",
- 0xFEEC: "Pointer_Button4",
- 0xFEED: "Pointer_Button5",
- 0xFEEE: "Pointer_DblClick_Dflt",
- 0xFEEF: "Pointer_DblClick1",
- 0xFEF0: "Pointer_DblClick2",
- 0xFEF1: "Pointer_DblClick3",
- 0xFEF2: "Pointer_DblClick4",
- 0xFEF3: "Pointer_DblClick5",
- 0xFEF4: "Pointer_Drag_Dflt",
- 0xFEF5: "Pointer_Drag1",
- 0xFEF6: "Pointer_Drag2",
- 0xFEF7: "Pointer_Drag3",
- 0xFEF8: "Pointer_Drag4",
- 0xFEFD: "Pointer_Drag5",
- 0xFEF9: "Pointer_EnableKeys",
- 0xFEFA: "Pointer_Accelerate",
- 0xFEFB: "Pointer_DfltBtnNext",
- 0xFEFC: "Pointer_DfltBtnPrev",
- 0xFEA0: "ch",
- 0xFEA1: "Ch",
- 0xFEA2: "CH",
- 0xFEA3: "c_h",
- 0xFEA4: "C_h",
- 0xFEA5: "C_H",
- 0xFD01: "3270_Duplicate",
- 0xFD02: "3270_FieldMark",
- 0xFD03: "3270_Right2",
- 0xFD04: "3270_Left2",
- 0xFD05: "3270_BackTab",
- 0xFD06: "3270_EraseEOF",
- 0xFD07: "3270_EraseInput",
- 0xFD08: "3270_Reset",
- 0xFD09: "3270_Quit",
- 0xFD0A: "3270_PA1",
- 0xFD0B: "3270_PA2",
- 0xFD0C: "3270_PA3",
- 0xFD0D: "3270_Test",
- 0xFD0E: "3270_Attn",
- 0xFD0F: "3270_CursorBlink",
- 0xFD10: "3270_AltCursor",
- 0xFD11: "3270_KeyClick",
- 0xFD12: "3270_Jump",
- 0xFD13: "3270_Ident",
- 0xFD14: "3270_Rule",
- 0xFD15: "3270_Copy",
- 0xFD16: "3270_Play",
- 0xFD17: "3270_Setup",
- 0xFD18: "3270_Record",
- 0xFD19: "3270_ChangeScreen",
- 0xFD1A: "3270_DeleteWord",
- 0xFD1B: "3270_ExSelect",
- 0xFD1C: "3270_CursorSelect",
- 0xFD1D: "3270_PrintScreen",
- 0xFD1E: "3270_Enter",
- 0x0020: "space",
- 0x0021: "exclam",
- 0x0022: "quotedbl",
- 0x0023: "numbersign",
- 0x0024: "dollar",
- 0x0025: "percent",
- 0x0026: "ampersand",
- 0x0027: "apostrophe",
- 0x0027: "quoteright",
- 0x0028: "parenleft",
- 0x0029: "parenright",
- 0x002A: "asterisk",
- 0x002B: "plus",
- 0x002C: "comma",
- 0x002D: "minus",
- 0x002E: "period",
- 0x002F: "slash",
- 0x0030: "0",
- 0x0031: "1",
- 0x0032: "2",
- 0x0033: "3",
- 0x0034: "4",
- 0x0035: "5",
- 0x0036: "6",
- 0x0037: "7",
- 0x0038: "8",
- 0x0039: "9",
- 0x003A: "colon",
- 0x003B: "semicolon",
- 0x003C: "less",
- 0x003D: "equal",
- 0x003E: "greater",
- 0x003F: "question",
- 0x0040: "at",
- 0x0041: "A",
- 0x0042: "B",
- 0x0043: "C",
- 0x0044: "D",
- 0x0045: "E",
- 0x0046: "F",
- 0x0047: "G",
- 0x0048: "H",
- 0x0049: "I",
- 0x004A: "J",
- 0x004B: "K",
- 0x004C: "L",
- 0x004D: "M",
- 0x004E: "N",
- 0x004F: "O",
- 0x0050: "P",
- 0x0051: "Q",
- 0x0052: "R",
- 0x0053: "S",
- 0x0054: "T",
- 0x0055: "U",
- 0x0056: "V",
- 0x0057: "W",
- 0x0058: "X",
- 0x0059: "Y",
- 0x005A: "Z",
- 0x005B: "bracketleft",
- 0x005C: "backslash",
- 0x005D: "bracketright",
- 0x005E: "asciicircum",
- 0x005F: "underscore",
- 0x0060: "grave",
- 0x0060: "quoteleft",
- 0x0061: "a",
- 0x0062: "b",
- 0x0063: "c",
- 0x0064: "d",
- 0x0065: "e",
- 0x0066: "f",
- 0x0067: "g",
- 0x0068: "h",
- 0x0069: "i",
- 0x006A: "j",
- 0x006B: "k",
- 0x006C: "l",
- 0x006D: "m",
- 0x006E: "n",
- 0x006F: "o",
- 0x0070: "p",
- 0x0071: "q",
- 0x0072: "r",
- 0x0073: "s",
- 0x0074: "t",
- 0x0075: "u",
- 0x0076: "v",
- 0x0077: "w",
- 0x0078: "x",
- 0x0079: "y",
- 0x007A: "z",
- 0x007B: "braceleft",
- 0x007C: "bar",
- 0x007D: "braceright",
- 0x007E: "asciitilde",
- 0x00A0: "nobreakspace",
- 0x00A1: "exclamdown",
- 0x00A2: "cent",
- 0x00A3: "sterling",
- 0x00A4: "currency",
- 0x00A5: "yen",
- 0x00A6: "brokenbar",
- 0x00A7: "section",
- 0x00A8: "diaeresis",
- 0x00A9: "copyright",
- 0x00AA: "ordfeminine",
- 0x00AB: "guillemotleft",
- 0x00AC: "notsign",
- 0x00AD: "hyphen",
- 0x00AE: "registered",
- 0x00AF: "macron",
- 0x00B0: "degree",
- 0x00B1: "plusminus",
- 0x00B2: "twosuperior",
- 0x00B3: "threesuperior",
- 0x00B4: "acute",
- 0x00B5: "mu",
- 0x00B6: "paragraph",
- 0x00B7: "periodcentered",
- 0x00B8: "cedilla",
- 0x00B9: "onesuperior",
- 0x00BA: "masculine",
- 0x00BB: "guillemotright",
- 0x00BC: "onequarter",
- 0x00BD: "onehalf",
- 0x00BE: "threequarters",
- 0x00BF: "questiondown",
- 0x00C0: "Agrave",
- 0x00C1: "Aacute",
- 0x00C2: "Acircumflex",
- 0x00C3: "Atilde",
- 0x00C4: "Adiaeresis",
- 0x00C5: "Aring",
- 0x00C6: "AE",
- 0x00C7: "Ccedilla",
- 0x00C8: "Egrave",
- 0x00C9: "Eacute",
- 0x00CA: "Ecircumflex",
- 0x00CB: "Ediaeresis",
- 0x00CC: "Igrave",
- 0x00CD: "Iacute",
- 0x00CE: "Icircumflex",
- 0x00CF: "Idiaeresis",
- 0x00D0: "ETH",
- 0x00D0: "Eth",
- 0x00D1: "Ntilde",
- 0x00D2: "Ograve",
- 0x00D3: "Oacute",
- 0x00D4: "Ocircumflex",
- 0x00D5: "Otilde",
- 0x00D6: "Odiaeresis",
- 0x00D7: "multiply",
- 0x00D8: "Oslash",
- 0x00D8: "Ooblique",
- 0x00D9: "Ugrave",
- 0x00DA: "Uacute",
- 0x00DB: "Ucircumflex",
- 0x00DC: "Udiaeresis",
- 0x00DD: "Yacute",
- 0x00DE: "THORN",
- 0x00DE: "Thorn",
- 0x00DF: "ssharp",
- 0x00E0: "agrave",
- 0x00E1: "aacute",
- 0x00E2: "acircumflex",
- 0x00E3: "atilde",
- 0x00E4: "adiaeresis",
- 0x00E5: "aring",
- 0x00E6: "ae",
- 0x00E7: "ccedilla",
- 0x00E8: "egrave",
- 0x00E9: "eacute",
- 0x00EA: "ecircumflex",
- 0x00EB: "ediaeresis",
- 0x00EC: "igrave",
- 0x00ED: "iacute",
- 0x00EE: "icircumflex",
- 0x00EF: "idiaeresis",
- 0x00F0: "eth",
- 0x00F1: "ntilde",
- 0x00F2: "ograve",
- 0x00F3: "oacute",
- 0x00F4: "ocircumflex",
- 0x00F5: "otilde",
- 0x00F6: "odiaeresis",
- 0x00F7: "division",
- 0x00F8: "oslash",
- 0x00F8: "ooblique",
- 0x00F9: "ugrave",
- 0x00FA: "uacute",
- 0x00FB: "ucircumflex",
- 0x00FC: "udiaeresis",
- 0x00FD: "yacute",
- 0x00FE: "thorn",
- 0x00FF: "ydiaeresis",
- 0x01A1: "Aogonek",
- 0x01A2: "breve",
- 0x01A3: "Lstroke",
- 0x01A5: "Lcaron",
- 0x01A6: "Sacute",
- 0x01A9: "Scaron",
- 0x01AA: "Scedilla",
- 0x01AB: "Tcaron",
- 0x01AC: "Zacute",
- 0x01AE: "Zcaron",
- 0x01AF: "Zabovedot",
- 0x01B1: "aogonek",
- 0x01B2: "ogonek",
- 0x01B3: "lstroke",
- 0x01B5: "lcaron",
- 0x01B6: "sacute",
- 0x01B7: "caron",
- 0x01B9: "scaron",
- 0x01BA: "scedilla",
- 0x01BB: "tcaron",
- 0x01BC: "zacute",
- 0x01BD: "doubleacute",
- 0x01BE: "zcaron",
- 0x01BF: "zabovedot",
- 0x01C0: "Racute",
- 0x01C3: "Abreve",
- 0x01C5: "Lacute",
- 0x01C6: "Cacute",
- 0x01C8: "Ccaron",
- 0x01CA: "Eogonek",
- 0x01CC: "Ecaron",
- 0x01CF: "Dcaron",
- 0x01D0: "Dstroke",
- 0x01D1: "Nacute",
- 0x01D2: "Ncaron",
- 0x01D5: "Odoubleacute",
- 0x01D8: "Rcaron",
- 0x01D9: "Uring",
- 0x01DB: "Udoubleacute",
- 0x01DE: "Tcedilla",
- 0x01E0: "racute",
- 0x01E3: "abreve",
- 0x01E5: "lacute",
- 0x01E6: "cacute",
- 0x01E8: "ccaron",
- 0x01EA: "eogonek",
- 0x01EC: "ecaron",
- 0x01EF: "dcaron",
- 0x01F0: "dstroke",
- 0x01F1: "nacute",
- 0x01F2: "ncaron",
- 0x01F5: "odoubleacute",
- 0x01F8: "rcaron",
- 0x01F9: "uring",
- 0x01FB: "udoubleacute",
- 0x01FE: "tcedilla",
- 0x01FF: "abovedot",
- 0x02A1: "Hstroke",
- 0x02A6: "Hcircumflex",
- 0x02A9: "Iabovedot",
- 0x02AB: "Gbreve",
- 0x02AC: "Jcircumflex",
- 0x02B1: "hstroke",
- 0x02B6: "hcircumflex",
- 0x02B9: "idotless",
- 0x02BB: "gbreve",
- 0x02BC: "jcircumflex",
- 0x02C5: "Cabovedot",
- 0x02C6: "Ccircumflex",
- 0x02D5: "Gabovedot",
- 0x02D8: "Gcircumflex",
- 0x02DD: "Ubreve",
- 0x02DE: "Scircumflex",
- 0x02E5: "cabovedot",
- 0x02E6: "ccircumflex",
- 0x02F5: "gabovedot",
- 0x02F8: "gcircumflex",
- 0x02FD: "ubreve",
- 0x02FE: "scircumflex",
- 0x03A2: "kra",
- 0x03A2: "kappa",
- 0x03A3: "Rcedilla",
- 0x03A5: "Itilde",
- 0x03A6: "Lcedilla",
- 0x03AA: "Emacron",
- 0x03AB: "Gcedilla",
- 0x03AC: "Tslash",
- 0x03B3: "rcedilla",
- 0x03B5: "itilde",
- 0x03B6: "lcedilla",
- 0x03BA: "emacron",
- 0x03BB: "gcedilla",
- 0x03BC: "tslash",
- 0x03BD: "ENG",
- 0x03BF: "eng",
- 0x03C0: "Amacron",
- 0x03C7: "Iogonek",
- 0x03CC: "Eabovedot",
- 0x03CF: "Imacron",
- 0x03D1: "Ncedilla",
- 0x03D2: "Omacron",
- 0x03D3: "Kcedilla",
- 0x03D9: "Uogonek",
- 0x03DD: "Utilde",
- 0x03DE: "Umacron",
- 0x03E0: "amacron",
- 0x03E7: "iogonek",
- 0x03EC: "eabovedot",
- 0x03EF: "imacron",
- 0x03F1: "ncedilla",
- 0x03F2: "omacron",
- 0x03F3: "kcedilla",
- 0x03F9: "uogonek",
- 0x03FD: "utilde",
- 0x03FE: "umacron",
- 0x1000174: "Wcircumflex",
- 0x1000175: "wcircumflex",
- 0x1000176: "Ycircumflex",
- 0x1000177: "ycircumflex",
- 0x1001E02: "Babovedot",
- 0x1001E03: "babovedot",
- 0x1001E0A: "Dabovedot",
- 0x1001E0B: "dabovedot",
- 0x1001E1E: "Fabovedot",
- 0x1001E1F: "fabovedot",
- 0x1001E40: "Mabovedot",
- 0x1001E41: "mabovedot",
- 0x1001E56: "Pabovedot",
- 0x1001E57: "pabovedot",
- 0x1001E60: "Sabovedot",
- 0x1001E61: "sabovedot",
- 0x1001E6A: "Tabovedot",
- 0x1001E6B: "tabovedot",
- 0x1001E80: "Wgrave",
- 0x1001E81: "wgrave",
- 0x1001E82: "Wacute",
- 0x1001E83: "wacute",
- 0x1001E84: "Wdiaeresis",
- 0x1001E85: "wdiaeresis",
- 0x1001EF2: "Ygrave",
- 0x1001EF3: "ygrave",
- 0x13BC: "OE",
- 0x13BD: "oe",
- 0x13BE: "Ydiaeresis",
- 0x047E: "overline",
- 0x04A1: "kana_fullstop",
- 0x04A2: "kana_openingbracket",
- 0x04A3: "kana_closingbracket",
- 0x04A4: "kana_comma",
- 0x04A5: "kana_conjunctive",
- 0x04A5: "kana_middledot",
- 0x04A6: "kana_WO",
- 0x04A7: "kana_a",
- 0x04A8: "kana_i",
- 0x04A9: "kana_u",
- 0x04AA: "kana_e",
- 0x04AB: "kana_o",
- 0x04AC: "kana_ya",
- 0x04AD: "kana_yu",
- 0x04AE: "kana_yo",
- 0x04AF: "kana_tsu",
- 0x04AF: "kana_tu",
- 0x04B0: "prolongedsound",
- 0x04B1: "kana_A",
- 0x04B2: "kana_I",
- 0x04B3: "kana_U",
- 0x04B4: "kana_E",
- 0x04B5: "kana_O",
- 0x04B6: "kana_KA",
- 0x04B7: "kana_KI",
- 0x04B8: "kana_KU",
- 0x04B9: "kana_KE",
- 0x04BA: "kana_KO",
- 0x04BB: "kana_SA",
- 0x04BC: "kana_SHI",
- 0x04BD: "kana_SU",
- 0x04BE: "kana_SE",
- 0x04BF: "kana_SO",
- 0x04C0: "kana_TA",
- 0x04C1: "kana_CHI",
- 0x04C1: "kana_TI",
- 0x04C2: "kana_TSU",
- 0x04C2: "kana_TU",
- 0x04C3: "kana_TE",
- 0x04C4: "kana_TO",
- 0x04C5: "kana_NA",
- 0x04C6: "kana_NI",
- 0x04C7: "kana_NU",
- 0x04C8: "kana_NE",
- 0x04C9: "kana_NO",
- 0x04CA: "kana_HA",
- 0x04CB: "kana_HI",
- 0x04CC: "kana_FU",
- 0x04CC: "kana_HU",
- 0x04CD: "kana_HE",
- 0x04CE: "kana_HO",
- 0x04CF: "kana_MA",
- 0x04D0: "kana_MI",
- 0x04D1: "kana_MU",
- 0x04D2: "kana_ME",
- 0x04D3: "kana_MO",
- 0x04D4: "kana_YA",
- 0x04D5: "kana_YU",
- 0x04D6: "kana_YO",
- 0x04D7: "kana_RA",
- 0x04D8: "kana_RI",
- 0x04D9: "kana_RU",
- 0x04DA: "kana_RE",
- 0x04DB: "kana_RO",
- 0x04DC: "kana_WA",
- 0x04DD: "kana_N",
- 0x04DE: "voicedsound",
- 0x04DF: "semivoicedsound",
- 0xFF7E: "kana_switch",
- 0x10006F0: "Farsi_0",
- 0x10006F1: "Farsi_1",
- 0x10006F2: "Farsi_2",
- 0x10006F3: "Farsi_3",
- 0x10006F4: "Farsi_4",
- 0x10006F5: "Farsi_5",
- 0x10006F6: "Farsi_6",
- 0x10006F7: "Farsi_7",
- 0x10006F8: "Farsi_8",
- 0x10006F9: "Farsi_9",
- 0x100066A: "Arabic_percent",
- 0x1000670: "Arabic_superscript_alef",
- 0x1000679: "Arabic_tteh",
- 0x100067E: "Arabic_peh",
- 0x1000686: "Arabic_tcheh",
- 0x1000688: "Arabic_ddal",
- 0x1000691: "Arabic_rreh",
- 0x05AC: "Arabic_comma",
- 0x10006D4: "Arabic_fullstop",
- 0x1000660: "Arabic_0",
- 0x1000661: "Arabic_1",
- 0x1000662: "Arabic_2",
- 0x1000663: "Arabic_3",
- 0x1000664: "Arabic_4",
- 0x1000665: "Arabic_5",
- 0x1000666: "Arabic_6",
- 0x1000667: "Arabic_7",
- 0x1000668: "Arabic_8",
- 0x1000669: "Arabic_9",
- 0x05BB: "Arabic_semicolon",
- 0x05BF: "Arabic_question_mark",
- 0x05C1: "Arabic_hamza",
- 0x05C2: "Arabic_maddaonalef",
- 0x05C3: "Arabic_hamzaonalef",
- 0x05C4: "Arabic_hamzaonwaw",
- 0x05C5: "Arabic_hamzaunderalef",
- 0x05C6: "Arabic_hamzaonyeh",
- 0x05C7: "Arabic_alef",
- 0x05C8: "Arabic_beh",
- 0x05C9: "Arabic_tehmarbuta",
- 0x05CA: "Arabic_teh",
- 0x05CB: "Arabic_theh",
- 0x05CC: "Arabic_jeem",
- 0x05CD: "Arabic_hah",
- 0x05CE: "Arabic_khah",
- 0x05CF: "Arabic_dal",
- 0x05D0: "Arabic_thal",
- 0x05D1: "Arabic_ra",
- 0x05D2: "Arabic_zain",
- 0x05D3: "Arabic_seen",
- 0x05D4: "Arabic_sheen",
- 0x05D5: "Arabic_sad",
- 0x05D6: "Arabic_dad",
- 0x05D7: "Arabic_tah",
- 0x05D8: "Arabic_zah",
- 0x05D9: "Arabic_ain",
- 0x05DA: "Arabic_ghain",
- 0x05E0: "Arabic_tatweel",
- 0x05E1: "Arabic_feh",
- 0x05E2: "Arabic_qaf",
- 0x05E3: "Arabic_kaf",
- 0x05E4: "Arabic_lam",
- 0x05E5: "Arabic_meem",
- 0x05E6: "Arabic_noon",
- 0x05E7: "Arabic_ha",
- 0x05E7: "Arabic_heh",
- 0x05E8: "Arabic_waw",
- 0x05E9: "Arabic_alefmaksura",
- 0x05EA: "Arabic_yeh",
- 0x05EB: "Arabic_fathatan",
- 0x05EC: "Arabic_dammatan",
- 0x05ED: "Arabic_kasratan",
- 0x05EE: "Arabic_fatha",
- 0x05EF: "Arabic_damma",
- 0x05F0: "Arabic_kasra",
- 0x05F1: "Arabic_shadda",
- 0x05F2: "Arabic_sukun",
- 0x1000653: "Arabic_madda_above",
- 0x1000654: "Arabic_hamza_above",
- 0x1000655: "Arabic_hamza_below",
- 0x1000698: "Arabic_jeh",
- 0x10006A4: "Arabic_veh",
- 0x10006A9: "Arabic_keheh",
- 0x10006AF: "Arabic_gaf",
- 0x10006BA: "Arabic_noon_ghunna",
- 0x10006BE: "Arabic_heh_doachashmee",
- 0x10006CC: "Farsi_yeh",
- 0x10006CC: "Arabic_farsi_yeh",
- 0x10006D2: "Arabic_yeh_baree",
- 0x10006C1: "Arabic_heh_goal",
- 0xFF7E: "Arabic_switch",
- 0x1000492: "Cyrillic_GHE_bar",
- 0x1000493: "Cyrillic_ghe_bar",
- 0x1000496: "Cyrillic_ZHE_descender",
- 0x1000497: "Cyrillic_zhe_descender",
- 0x100049A: "Cyrillic_KA_descender",
- 0x100049B: "Cyrillic_ka_descender",
- 0x100049C: "Cyrillic_KA_vertstroke",
- 0x100049D: "Cyrillic_ka_vertstroke",
- 0x10004A2: "Cyrillic_EN_descender",
- 0x10004A3: "Cyrillic_en_descender",
- 0x10004AE: "Cyrillic_U_straight",
- 0x10004AF: "Cyrillic_u_straight",
- 0x10004B0: "Cyrillic_U_straight_bar",
- 0x10004B1: "Cyrillic_u_straight_bar",
- 0x10004B2: "Cyrillic_HA_descender",
- 0x10004B3: "Cyrillic_ha_descender",
- 0x10004B6: "Cyrillic_CHE_descender",
- 0x10004B7: "Cyrillic_che_descender",
- 0x10004B8: "Cyrillic_CHE_vertstroke",
- 0x10004B9: "Cyrillic_che_vertstroke",
- 0x10004BA: "Cyrillic_SHHA",
- 0x10004BB: "Cyrillic_shha",
- 0x10004D8: "Cyrillic_SCHWA",
- 0x10004D9: "Cyrillic_schwa",
- 0x10004E2: "Cyrillic_I_macron",
- 0x10004E3: "Cyrillic_i_macron",
- 0x10004E8: "Cyrillic_O_bar",
- 0x10004E9: "Cyrillic_o_bar",
- 0x10004EE: "Cyrillic_U_macron",
- 0x10004EF: "Cyrillic_u_macron",
- 0x06A1: "Serbian_dje",
- 0x06A2: "Macedonia_gje",
- 0x06A3: "Cyrillic_io",
- 0x06A4: "Ukrainian_ie",
- 0x06A4: "Ukranian_je",
- 0x06A5: "Macedonia_dse",
- 0x06A6: "Ukrainian_i",
- 0x06A6: "Ukranian_i",
- 0x06A7: "Ukrainian_yi",
- 0x06A7: "Ukranian_yi",
- 0x06A8: "Cyrillic_je",
- 0x06A8: "Serbian_je",
- 0x06A9: "Cyrillic_lje",
- 0x06A9: "Serbian_lje",
- 0x06AA: "Cyrillic_nje",
- 0x06AA: "Serbian_nje",
- 0x06AB: "Serbian_tshe",
- 0x06AC: "Macedonia_kje",
- 0x06AD: "Ukrainian_ghe_with_upturn",
- 0x06AE: "Byelorussian_shortu",
- 0x06AF: "Cyrillic_dzhe",
- 0x06AF: "Serbian_dze",
- 0x06B0: "numerosign",
- 0x06B1: "Serbian_DJE",
- 0x06B2: "Macedonia_GJE",
- 0x06B3: "Cyrillic_IO",
- 0x06B4: "Ukrainian_IE",
- 0x06B4: "Ukranian_JE",
- 0x06B5: "Macedonia_DSE",
- 0x06B6: "Ukrainian_I",
- 0x06B6: "Ukranian_I",
- 0x06B7: "Ukrainian_YI",
- 0x06B7: "Ukranian_YI",
- 0x06B8: "Cyrillic_JE",
- 0x06B8: "Serbian_JE",
- 0x06B9: "Cyrillic_LJE",
- 0x06B9: "Serbian_LJE",
- 0x06BA: "Cyrillic_NJE",
- 0x06BA: "Serbian_NJE",
- 0x06BB: "Serbian_TSHE",
- 0x06BC: "Macedonia_KJE",
- 0x06BD: "Ukrainian_GHE_WITH_UPTURN",
- 0x06BE: "Byelorussian_SHORTU",
- 0x06BF: "Cyrillic_DZHE",
- 0x06BF: "Serbian_DZE",
- 0x06C0: "Cyrillic_yu",
- 0x06C1: "Cyrillic_a",
- 0x06C2: "Cyrillic_be",
- 0x06C3: "Cyrillic_tse",
- 0x06C4: "Cyrillic_de",
- 0x06C5: "Cyrillic_ie",
- 0x06C6: "Cyrillic_ef",
- 0x06C7: "Cyrillic_ghe",
- 0x06C8: "Cyrillic_ha",
- 0x06C9: "Cyrillic_i",
- 0x06CA: "Cyrillic_shorti",
- 0x06CB: "Cyrillic_ka",
- 0x06CC: "Cyrillic_el",
- 0x06CD: "Cyrillic_em",
- 0x06CE: "Cyrillic_en",
- 0x06CF: "Cyrillic_o",
- 0x06D0: "Cyrillic_pe",
- 0x06D1: "Cyrillic_ya",
- 0x06D2: "Cyrillic_er",
- 0x06D3: "Cyrillic_es",
- 0x06D4: "Cyrillic_te",
- 0x06D5: "Cyrillic_u",
- 0x06D6: "Cyrillic_zhe",
- 0x06D7: "Cyrillic_ve",
- 0x06D8: "Cyrillic_softsign",
- 0x06D9: "Cyrillic_yeru",
- 0x06DA: "Cyrillic_ze",
- 0x06DB: "Cyrillic_sha",
- 0x06DC: "Cyrillic_e",
- 0x06DD: "Cyrillic_shcha",
- 0x06DE: "Cyrillic_che",
- 0x06DF: "Cyrillic_hardsign",
- 0x06E0: "Cyrillic_YU",
- 0x06E1: "Cyrillic_A",
- 0x06E2: "Cyrillic_BE",
- 0x06E3: "Cyrillic_TSE",
- 0x06E4: "Cyrillic_DE",
- 0x06E5: "Cyrillic_IE",
- 0x06E6: "Cyrillic_EF",
- 0x06E7: "Cyrillic_GHE",
- 0x06E8: "Cyrillic_HA",
- 0x06E9: "Cyrillic_I",
- 0x06EA: "Cyrillic_SHORTI",
- 0x06EB: "Cyrillic_KA",
- 0x06EC: "Cyrillic_EL",
- 0x06ED: "Cyrillic_EM",
- 0x06EE: "Cyrillic_EN",
- 0x06EF: "Cyrillic_O",
- 0x06F0: "Cyrillic_PE",
- 0x06F1: "Cyrillic_YA",
- 0x06F2: "Cyrillic_ER",
- 0x06F3: "Cyrillic_ES",
- 0x06F4: "Cyrillic_TE",
- 0x06F5: "Cyrillic_U",
- 0x06F6: "Cyrillic_ZHE",
- 0x06F7: "Cyrillic_VE",
- 0x06F8: "Cyrillic_SOFTSIGN",
- 0x06F9: "Cyrillic_YERU",
- 0x06FA: "Cyrillic_ZE",
- 0x06FB: "Cyrillic_SHA",
- 0x06FC: "Cyrillic_E",
- 0x06FD: "Cyrillic_SHCHA",
- 0x06FE: "Cyrillic_CHE",
- 0x06FF: "Cyrillic_HARDSIGN",
- 0x07A1: "Greek_ALPHAaccent",
- 0x07A2: "Greek_EPSILONaccent",
- 0x07A3: "Greek_ETAaccent",
- 0x07A4: "Greek_IOTAaccent",
- 0x07A5: "Greek_IOTAdieresis",
- 0x07A5: "Greek_IOTAdiaeresis",
- 0x07A7: "Greek_OMICRONaccent",
- 0x07A8: "Greek_UPSILONaccent",
- 0x07A9: "Greek_UPSILONdieresis",
- 0x07AB: "Greek_OMEGAaccent",
- 0x07AE: "Greek_accentdieresis",
- 0x07AF: "Greek_horizbar",
- 0x07B1: "Greek_alphaaccent",
- 0x07B2: "Greek_epsilonaccent",
- 0x07B3: "Greek_etaaccent",
- 0x07B4: "Greek_iotaaccent",
- 0x07B5: "Greek_iotadieresis",
- 0x07B6: "Greek_iotaaccentdieresis",
- 0x07B7: "Greek_omicronaccent",
- 0x07B8: "Greek_upsilonaccent",
- 0x07B9: "Greek_upsilondieresis",
- 0x07BA: "Greek_upsilonaccentdieresis",
- 0x07BB: "Greek_omegaaccent",
- 0x07C1: "Greek_ALPHA",
- 0x07C2: "Greek_BETA",
- 0x07C3: "Greek_GAMMA",
- 0x07C4: "Greek_DELTA",
- 0x07C5: "Greek_EPSILON",
- 0x07C6: "Greek_ZETA",
- 0x07C7: "Greek_ETA",
- 0x07C8: "Greek_THETA",
- 0x07C9: "Greek_IOTA",
- 0x07CA: "Greek_KAPPA",
- 0x07CB: "Greek_LAMDA",
- 0x07CB: "Greek_LAMBDA",
- 0x07CC: "Greek_MU",
- 0x07CD: "Greek_NU",
- 0x07CE: "Greek_XI",
- 0x07CF: "Greek_OMICRON",
- 0x07D0: "Greek_PI",
- 0x07D1: "Greek_RHO",
- 0x07D2: "Greek_SIGMA",
- 0x07D4: "Greek_TAU",
- 0x07D5: "Greek_UPSILON",
- 0x07D6: "Greek_PHI",
- 0x07D7: "Greek_CHI",
- 0x07D8: "Greek_PSI",
- 0x07D9: "Greek_OMEGA",
- 0x07E1: "Greek_alpha",
- 0x07E2: "Greek_beta",
- 0x07E3: "Greek_gamma",
- 0x07E4: "Greek_delta",
- 0x07E5: "Greek_epsilon",
- 0x07E6: "Greek_zeta",
- 0x07E7: "Greek_eta",
- 0x07E8: "Greek_theta",
- 0x07E9: "Greek_iota",
- 0x07EA: "Greek_kappa",
- 0x07EB: "Greek_lamda",
- 0x07EB: "Greek_lambda",
- 0x07EC: "Greek_mu",
- 0x07ED: "Greek_nu",
- 0x07EE: "Greek_xi",
- 0x07EF: "Greek_omicron",
- 0x07F0: "Greek_pi",
- 0x07F1: "Greek_rho",
- 0x07F2: "Greek_sigma",
- 0x07F3: "Greek_finalsmallsigma",
- 0x07F4: "Greek_tau",
- 0x07F5: "Greek_upsilon",
- 0x07F6: "Greek_phi",
- 0x07F7: "Greek_chi",
- 0x07F8: "Greek_psi",
- 0x07F9: "Greek_omega",
- 0xFF7E: "Greek_switch",
- 0x08A1: "leftradical",
- 0x08A2: "topleftradical",
- 0x08A3: "horizconnector",
- 0x08A4: "topintegral",
- 0x08A5: "botintegral",
- 0x08A6: "vertconnector",
- 0x08A7: "topleftsqbracket",
- 0x08A8: "botleftsqbracket",
- 0x08A9: "toprightsqbracket",
- 0x08AA: "botrightsqbracket",
- 0x08AB: "topleftparens",
- 0x08AC: "botleftparens",
- 0x08AD: "toprightparens",
- 0x08AE: "botrightparens",
- 0x08AF: "leftmiddlecurlybrace",
- 0x08B0: "rightmiddlecurlybrace",
- 0x08B1: "topleftsummation",
- 0x08B2: "botleftsummation",
- 0x08B3: "topvertsummationconnector",
- 0x08B4: "botvertsummationconnector",
- 0x08B5: "toprightsummation",
- 0x08B6: "botrightsummation",
- 0x08B7: "rightmiddlesummation",
- 0x08BC: "lessthanequal",
- 0x08BD: "notequal",
- 0x08BE: "greaterthanequal",
- 0x08BF: "integral",
- 0x08C0: "therefore",
- 0x08C1: "variation",
- 0x08C2: "infinity",
- 0x08C5: "nabla",
- 0x08C8: "approximate",
- 0x08C9: "similarequal",
- 0x08CD: "ifonlyif",
- 0x08CE: "implies",
- 0x08CF: "identical",
- 0x08D6: "radical",
- 0x08DA: "includedin",
- 0x08DB: "includes",
- 0x08DC: "intersection",
- 0x08DD: "union",
- 0x08DE: "logicaland",
- 0x08DF: "logicalor",
- 0x08EF: "partialderivative",
- 0x08F6: "function",
- 0x08FB: "leftarrow",
- 0x08FC: "uparrow",
- 0x08FD: "rightarrow",
- 0x08FE: "downarrow",
- 0x09DF: "blank",
- 0x09E0: "soliddiamond",
- 0x09E1: "checkerboard",
- 0x09E2: "ht",
- 0x09E3: "ff",
- 0x09E4: "cr",
- 0x09E5: "lf",
- 0x09E8: "nl",
- 0x09E9: "vt",
- 0x09EA: "lowrightcorner",
- 0x09EB: "uprightcorner",
- 0x09EC: "upleftcorner",
- 0x09ED: "lowleftcorner",
- 0x09EE: "crossinglines",
- 0x09EF: "horizlinescan1",
- 0x09F0: "horizlinescan3",
- 0x09F1: "horizlinescan5",
- 0x09F2: "horizlinescan7",
- 0x09F3: "horizlinescan9",
- 0x09F4: "leftt",
- 0x09F5: "rightt",
- 0x09F6: "bott",
- 0x09F7: "topt",
- 0x09F8: "vertbar",
- 0x0AA1: "emspace",
- 0x0AA2: "enspace",
- 0x0AA3: "em3space",
- 0x0AA4: "em4space",
- 0x0AA5: "digitspace",
- 0x0AA6: "punctspace",
- 0x0AA7: "thinspace",
- 0x0AA8: "hairspace",
- 0x0AA9: "emdash",
- 0x0AAA: "endash",
- 0x0AAC: "signifblank",
- 0x0AAE: "ellipsis",
- 0x0AAF: "doubbaselinedot",
- 0x0AB0: "onethird",
- 0x0AB1: "twothirds",
- 0x0AB2: "onefifth",
- 0x0AB3: "twofifths",
- 0x0AB4: "threefifths",
- 0x0AB5: "fourfifths",
- 0x0AB6: "onesixth",
- 0x0AB7: "fivesixths",
- 0x0AB8: "careof",
- 0x0ABB: "figdash",
- 0x0ABC: "leftanglebracket",
- 0x0ABD: "decimalpoint",
- 0x0ABE: "rightanglebracket",
- 0x0ABF: "marker",
- 0x0AC3: "oneeighth",
- 0x0AC4: "threeeighths",
- 0x0AC5: "fiveeighths",
- 0x0AC6: "seveneighths",
- 0x0AC9: "trademark",
- 0x0ACA: "signaturemark",
- 0x0ACB: "trademarkincircle",
- 0x0ACC: "leftopentriangle",
- 0x0ACD: "rightopentriangle",
- 0x0ACE: "emopencircle",
- 0x0ACF: "emopenrectangle",
- 0x0AD0: "leftsinglequotemark",
- 0x0AD1: "rightsinglequotemark",
- 0x0AD2: "leftdoublequotemark",
- 0x0AD3: "rightdoublequotemark",
- 0x0AD4: "prescription",
- 0x0AD5: "permille",
- 0x0AD6: "minutes",
- 0x0AD7: "seconds",
- 0x0AD9: "latincross",
- 0x0ADA: "hexagram",
- 0x0ADB: "filledrectbullet",
- 0x0ADC: "filledlefttribullet",
- 0x0ADD: "filledrighttribullet",
- 0x0ADE: "emfilledcircle",
- 0x0ADF: "emfilledrect",
- 0x0AE0: "enopencircbullet",
- 0x0AE1: "enopensquarebullet",
- 0x0AE2: "openrectbullet",
- 0x0AE3: "opentribulletup",
- 0x0AE4: "opentribulletdown",
- 0x0AE5: "openstar",
- 0x0AE6: "enfilledcircbullet",
- 0x0AE7: "enfilledsqbullet",
- 0x0AE8: "filledtribulletup",
- 0x0AE9: "filledtribulletdown",
- 0x0AEA: "leftpointer",
- 0x0AEB: "rightpointer",
- 0x0AEC: "club",
- 0x0AED: "diamond",
- 0x0AEE: "heart",
- 0x0AF0: "maltesecross",
- 0x0AF1: "dagger",
- 0x0AF2: "doubledagger",
- 0x0AF3: "checkmark",
- 0x0AF4: "ballotcross",
- 0x0AF5: "musicalsharp",
- 0x0AF6: "musicalflat",
- 0x0AF7: "malesymbol",
- 0x0AF8: "femalesymbol",
- 0x0AF9: "telephone",
- 0x0AFA: "telephonerecorder",
- 0x0AFB: "phonographcopyright",
- 0x0AFC: "caret",
- 0x0AFD: "singlelowquotemark",
- 0x0AFE: "doublelowquotemark",
- 0x0AFF: "cursor",
- 0x0BA3: "leftcaret",
- 0x0BA6: "rightcaret",
- 0x0BA8: "downcaret",
- 0x0BA9: "upcaret",
- 0x0BC0: "overbar",
- 0x0BC2: "downtack",
- 0x0BC3: "upshoe",
- 0x0BC4: "downstile",
- 0x0BC6: "underbar",
- 0x0BCA: "jot",
- 0x0BCC: "quad",
- 0x0BCE: "uptack",
- 0x0BCF: "circle",
- 0x0BD3: "upstile",
- 0x0BD6: "downshoe",
- 0x0BD8: "rightshoe",
- 0x0BDA: "leftshoe",
- 0x0BDC: "lefttack",
- 0x0BFC: "righttack",
- 0x0CDF: "hebrew_doublelowline",
- 0x0CE0: "hebrew_aleph",
- 0x0CE1: "hebrew_bet",
- 0x0CE1: "hebrew_beth",
- 0x0CE2: "hebrew_gimel",
- 0x0CE2: "hebrew_gimmel",
- 0x0CE3: "hebrew_dalet",
- 0x0CE3: "hebrew_daleth",
- 0x0CE4: "hebrew_he",
- 0x0CE5: "hebrew_waw",
- 0x0CE6: "hebrew_zain",
- 0x0CE6: "hebrew_zayin",
- 0x0CE7: "hebrew_chet",
- 0x0CE7: "hebrew_het",
- 0x0CE8: "hebrew_tet",
- 0x0CE8: "hebrew_teth",
- 0x0CE9: "hebrew_yod",
- 0x0CEA: "hebrew_finalkaph",
- 0x0CEB: "hebrew_kaph",
- 0x0CEC: "hebrew_lamed",
- 0x0CED: "hebrew_finalmem",
- 0x0CEE: "hebrew_mem",
- 0x0CEF: "hebrew_finalnun",
- 0x0CF0: "hebrew_nun",
- 0x0CF1: "hebrew_samech",
- 0x0CF1: "hebrew_samekh",
- 0x0CF2: "hebrew_ayin",
- 0x0CF3: "hebrew_finalpe",
- 0x0CF4: "hebrew_pe",
- 0x0CF5: "hebrew_finalzade",
- 0x0CF5: "hebrew_finalzadi",
- 0x0CF6: "hebrew_zade",
- 0x0CF6: "hebrew_zadi",
- 0x0CF7: "hebrew_qoph",
- 0x0CF7: "hebrew_kuf",
- 0x0CF8: "hebrew_resh",
- 0x0CF9: "hebrew_shin",
- 0x0CFA: "hebrew_taw",
- 0x0CFA: "hebrew_taf",
- 0xFF7E: "Hebrew_switch",
- 0x0DA1: "Thai_kokai",
- 0x0DA2: "Thai_khokhai",
- 0x0DA3: "Thai_khokhuat",
- 0x0DA4: "Thai_khokhwai",
- 0x0DA5: "Thai_khokhon",
- 0x0DA6: "Thai_khorakhang",
- 0x0DA7: "Thai_ngongu",
- 0x0DA8: "Thai_chochan",
- 0x0DA9: "Thai_choching",
- 0x0DAA: "Thai_chochang",
- 0x0DAB: "Thai_soso",
- 0x0DAC: "Thai_chochoe",
- 0x0DAD: "Thai_yoying",
- 0x0DAE: "Thai_dochada",
- 0x0DAF: "Thai_topatak",
- 0x0DB0: "Thai_thothan",
- 0x0DB1: "Thai_thonangmontho",
- 0x0DB2: "Thai_thophuthao",
- 0x0DB3: "Thai_nonen",
- 0x0DB4: "Thai_dodek",
- 0x0DB5: "Thai_totao",
- 0x0DB6: "Thai_thothung",
- 0x0DB7: "Thai_thothahan",
- 0x0DB8: "Thai_thothong",
- 0x0DB9: "Thai_nonu",
- 0x0DBA: "Thai_bobaimai",
- 0x0DBB: "Thai_popla",
- 0x0DBC: "Thai_phophung",
- 0x0DBD: "Thai_fofa",
- 0x0DBE: "Thai_phophan",
- 0x0DBF: "Thai_fofan",
- 0x0DC0: "Thai_phosamphao",
- 0x0DC1: "Thai_moma",
- 0x0DC2: "Thai_yoyak",
- 0x0DC3: "Thai_rorua",
- 0x0DC4: "Thai_ru",
- 0x0DC5: "Thai_loling",
- 0x0DC6: "Thai_lu",
- 0x0DC7: "Thai_wowaen",
- 0x0DC8: "Thai_sosala",
- 0x0DC9: "Thai_sorusi",
- 0x0DCA: "Thai_sosua",
- 0x0DCB: "Thai_hohip",
- 0x0DCC: "Thai_lochula",
- 0x0DCD: "Thai_oang",
- 0x0DCE: "Thai_honokhuk",
- 0x0DCF: "Thai_paiyannoi",
- 0x0DD0: "Thai_saraa",
- 0x0DD1: "Thai_maihanakat",
- 0x0DD2: "Thai_saraaa",
- 0x0DD3: "Thai_saraam",
- 0x0DD4: "Thai_sarai",
- 0x0DD5: "Thai_saraii",
- 0x0DD6: "Thai_saraue",
- 0x0DD7: "Thai_sarauee",
- 0x0DD8: "Thai_sarau",
- 0x0DD9: "Thai_sarauu",
- 0x0DDA: "Thai_phinthu",
- 0x0DDE: "Thai_maihanakat_maitho",
- 0x0DDF: "Thai_baht",
- 0x0DE0: "Thai_sarae",
- 0x0DE1: "Thai_saraae",
- 0x0DE2: "Thai_sarao",
- 0x0DE3: "Thai_saraaimaimuan",
- 0x0DE4: "Thai_saraaimaimalai",
- 0x0DE5: "Thai_lakkhangyao",
- 0x0DE6: "Thai_maiyamok",
- 0x0DE7: "Thai_maitaikhu",
- 0x0DE8: "Thai_maiek",
- 0x0DE9: "Thai_maitho",
- 0x0DEA: "Thai_maitri",
- 0x0DEB: "Thai_maichattawa",
- 0x0DEC: "Thai_thanthakhat",
- 0x0DED: "Thai_nikhahit",
- 0x0DF0: "Thai_leksun",
- 0x0DF1: "Thai_leknung",
- 0x0DF2: "Thai_leksong",
- 0x0DF3: "Thai_leksam",
- 0x0DF4: "Thai_leksi",
- 0x0DF5: "Thai_lekha",
- 0x0DF6: "Thai_lekhok",
- 0x0DF7: "Thai_lekchet",
- 0x0DF8: "Thai_lekpaet",
- 0x0DF9: "Thai_lekkao",
- 0xFF31: "Hangul",
- 0xFF32: "Hangul_Start",
- 0xFF33: "Hangul_End",
- 0xFF34: "Hangul_Hanja",
- 0xFF35: "Hangul_Jamo",
- 0xFF36: "Hangul_Romaja",
- 0xFF37: "Hangul_Codeinput",
- 0xFF38: "Hangul_Jeonja",
- 0xFF39: "Hangul_Banja",
- 0xFF3A: "Hangul_PreHanja",
- 0xFF3B: "Hangul_PostHanja",
- 0xFF3C: "Hangul_SingleCandidate",
- 0xFF3D: "Hangul_MultipleCandidate",
- 0xFF3E: "Hangul_PreviousCandidate",
- 0xFF3F: "Hangul_Special",
- 0xFF7E: "Hangul_switch",
- 0x0EA1: "Hangul_Kiyeog",
- 0x0EA2: "Hangul_SsangKiyeog",
- 0x0EA3: "Hangul_KiyeogSios",
- 0x0EA4: "Hangul_Nieun",
- 0x0EA5: "Hangul_NieunJieuj",
- 0x0EA6: "Hangul_NieunHieuh",
- 0x0EA7: "Hangul_Dikeud",
- 0x0EA8: "Hangul_SsangDikeud",
- 0x0EA9: "Hangul_Rieul",
- 0x0EAA: "Hangul_RieulKiyeog",
- 0x0EAB: "Hangul_RieulMieum",
- 0x0EAC: "Hangul_RieulPieub",
- 0x0EAD: "Hangul_RieulSios",
- 0x0EAE: "Hangul_RieulTieut",
- 0x0EAF: "Hangul_RieulPhieuf",
- 0x0EB0: "Hangul_RieulHieuh",
- 0x0EB1: "Hangul_Mieum",
- 0x0EB2: "Hangul_Pieub",
- 0x0EB3: "Hangul_SsangPieub",
- 0x0EB4: "Hangul_PieubSios",
- 0x0EB5: "Hangul_Sios",
- 0x0EB6: "Hangul_SsangSios",
- 0x0EB7: "Hangul_Ieung",
- 0x0EB8: "Hangul_Jieuj",
- 0x0EB9: "Hangul_SsangJieuj",
- 0x0EBA: "Hangul_Cieuc",
- 0x0EBB: "Hangul_Khieuq",
- 0x0EBC: "Hangul_Tieut",
- 0x0EBD: "Hangul_Phieuf",
- 0x0EBE: "Hangul_Hieuh",
- 0x0EBF: "Hangul_A",
- 0x0EC0: "Hangul_AE",
- 0x0EC1: "Hangul_YA",
- 0x0EC2: "Hangul_YAE",
- 0x0EC3: "Hangul_EO",
- 0x0EC4: "Hangul_E",
- 0x0EC5: "Hangul_YEO",
- 0x0EC6: "Hangul_YE",
- 0x0EC7: "Hangul_O",
- 0x0EC8: "Hangul_WA",
- 0x0EC9: "Hangul_WAE",
- 0x0ECA: "Hangul_OE",
- 0x0ECB: "Hangul_YO",
- 0x0ECC: "Hangul_U",
- 0x0ECD: "Hangul_WEO",
- 0x0ECE: "Hangul_WE",
- 0x0ECF: "Hangul_WI",
- 0x0ED0: "Hangul_YU",
- 0x0ED1: "Hangul_EU",
- 0x0ED2: "Hangul_YI",
- 0x0ED3: "Hangul_I",
- 0x0ED4: "Hangul_J_Kiyeog",
- 0x0ED5: "Hangul_J_SsangKiyeog",
- 0x0ED6: "Hangul_J_KiyeogSios",
- 0x0ED7: "Hangul_J_Nieun",
- 0x0ED8: "Hangul_J_NieunJieuj",
- 0x0ED9: "Hangul_J_NieunHieuh",
- 0x0EDA: "Hangul_J_Dikeud",
- 0x0EDB: "Hangul_J_Rieul",
- 0x0EDC: "Hangul_J_RieulKiyeog",
- 0x0EDD: "Hangul_J_RieulMieum",
- 0x0EDE: "Hangul_J_RieulPieub",
- 0x0EDF: "Hangul_J_RieulSios",
- 0x0EE0: "Hangul_J_RieulTieut",
- 0x0EE1: "Hangul_J_RieulPhieuf",
- 0x0EE2: "Hangul_J_RieulHieuh",
- 0x0EE3: "Hangul_J_Mieum",
- 0x0EE4: "Hangul_J_Pieub",
- 0x0EE5: "Hangul_J_PieubSios",
- 0x0EE6: "Hangul_J_Sios",
- 0x0EE7: "Hangul_J_SsangSios",
- 0x0EE8: "Hangul_J_Ieung",
- 0x0EE9: "Hangul_J_Jieuj",
- 0x0EEA: "Hangul_J_Cieuc",
- 0x0EEB: "Hangul_J_Khieuq",
- 0x0EEC: "Hangul_J_Tieut",
- 0x0EED: "Hangul_J_Phieuf",
- 0x0EEE: "Hangul_J_Hieuh",
- 0x0EEF: "Hangul_RieulYeorinHieuh",
- 0x0EF0: "Hangul_SunkyeongeumMieum",
- 0x0EF1: "Hangul_SunkyeongeumPieub",
- 0x0EF2: "Hangul_PanSios",
- 0x0EF3: "Hangul_KkogjiDalrinIeung",
- 0x0EF4: "Hangul_SunkyeongeumPhieuf",
- 0x0EF5: "Hangul_YeorinHieuh",
- 0x0EF6: "Hangul_AraeA",
- 0x0EF7: "Hangul_AraeAE",
- 0x0EF8: "Hangul_J_PanSios",
- 0x0EF9: "Hangul_J_KkogjiDalrinIeung",
- 0x0EFA: "Hangul_J_YeorinHieuh",
- 0x0EFF: "Korean_Won",
- 0x1000587: "Armenian_ligature_ew",
- 0x1000589: "Armenian_full_stop",
- 0x1000589: "Armenian_verjaket",
- 0x100055D: "Armenian_separation_mark",
- 0x100055D: "Armenian_but",
- 0x100058A: "Armenian_hyphen",
- 0x100058A: "Armenian_yentamna",
- 0x100055C: "Armenian_exclam",
- 0x100055C: "Armenian_amanak",
- 0x100055B: "Armenian_accent",
- 0x100055B: "Armenian_shesht",
- 0x100055E: "Armenian_question",
- 0x100055E: "Armenian_paruyk",
- 0x1000531: "Armenian_AYB",
- 0x1000561: "Armenian_ayb",
- 0x1000532: "Armenian_BEN",
- 0x1000562: "Armenian_ben",
- 0x1000533: "Armenian_GIM",
- 0x1000563: "Armenian_gim",
- 0x1000534: "Armenian_DA",
- 0x1000564: "Armenian_da",
- 0x1000535: "Armenian_YECH",
- 0x1000565: "Armenian_yech",
- 0x1000536: "Armenian_ZA",
- 0x1000566: "Armenian_za",
- 0x1000537: "Armenian_E",
- 0x1000567: "Armenian_e",
- 0x1000538: "Armenian_AT",
- 0x1000568: "Armenian_at",
- 0x1000539: "Armenian_TO",
- 0x1000569: "Armenian_to",
- 0x100053A: "Armenian_ZHE",
- 0x100056A: "Armenian_zhe",
- 0x100053B: "Armenian_INI",
- 0x100056B: "Armenian_ini",
- 0x100053C: "Armenian_LYUN",
- 0x100056C: "Armenian_lyun",
- 0x100053D: "Armenian_KHE",
- 0x100056D: "Armenian_khe",
- 0x100053E: "Armenian_TSA",
- 0x100056E: "Armenian_tsa",
- 0x100053F: "Armenian_KEN",
- 0x100056F: "Armenian_ken",
- 0x1000540: "Armenian_HO",
- 0x1000570: "Armenian_ho",
- 0x1000541: "Armenian_DZA",
- 0x1000571: "Armenian_dza",
- 0x1000542: "Armenian_GHAT",
- 0x1000572: "Armenian_ghat",
- 0x1000543: "Armenian_TCHE",
- 0x1000573: "Armenian_tche",
- 0x1000544: "Armenian_MEN",
- 0x1000574: "Armenian_men",
- 0x1000545: "Armenian_HI",
- 0x1000575: "Armenian_hi",
- 0x1000546: "Armenian_NU",
- 0x1000576: "Armenian_nu",
- 0x1000547: "Armenian_SHA",
- 0x1000577: "Armenian_sha",
- 0x1000548: "Armenian_VO",
- 0x1000578: "Armenian_vo",
- 0x1000549: "Armenian_CHA",
- 0x1000579: "Armenian_cha",
- 0x100054A: "Armenian_PE",
- 0x100057A: "Armenian_pe",
- 0x100054B: "Armenian_JE",
- 0x100057B: "Armenian_je",
- 0x100054C: "Armenian_RA",
- 0x100057C: "Armenian_ra",
- 0x100054D: "Armenian_SE",
- 0x100057D: "Armenian_se",
- 0x100054E: "Armenian_VEV",
- 0x100057E: "Armenian_vev",
- 0x100054F: "Armenian_TYUN",
- 0x100057F: "Armenian_tyun",
- 0x1000550: "Armenian_RE",
- 0x1000580: "Armenian_re",
- 0x1000551: "Armenian_TSO",
- 0x1000581: "Armenian_tso",
- 0x1000552: "Armenian_VYUN",
- 0x1000582: "Armenian_vyun",
- 0x1000553: "Armenian_PYUR",
- 0x1000583: "Armenian_pyur",
- 0x1000554: "Armenian_KE",
- 0x1000584: "Armenian_ke",
- 0x1000555: "Armenian_O",
- 0x1000585: "Armenian_o",
- 0x1000556: "Armenian_FE",
- 0x1000586: "Armenian_fe",
- 0x100055A: "Armenian_apostrophe",
- 0x10010D0: "Georgian_an",
- 0x10010D1: "Georgian_ban",
- 0x10010D2: "Georgian_gan",
- 0x10010D3: "Georgian_don",
- 0x10010D4: "Georgian_en",
- 0x10010D5: "Georgian_vin",
- 0x10010D6: "Georgian_zen",
- 0x10010D7: "Georgian_tan",
- 0x10010D8: "Georgian_in",
- 0x10010D9: "Georgian_kan",
- 0x10010DA: "Georgian_las",
- 0x10010DB: "Georgian_man",
- 0x10010DC: "Georgian_nar",
- 0x10010DD: "Georgian_on",
- 0x10010DE: "Georgian_par",
- 0x10010DF: "Georgian_zhar",
- 0x10010E0: "Georgian_rae",
- 0x10010E1: "Georgian_san",
- 0x10010E2: "Georgian_tar",
- 0x10010E3: "Georgian_un",
- 0x10010E4: "Georgian_phar",
- 0x10010E5: "Georgian_khar",
- 0x10010E6: "Georgian_ghan",
- 0x10010E7: "Georgian_qar",
- 0x10010E8: "Georgian_shin",
- 0x10010E9: "Georgian_chin",
- 0x10010EA: "Georgian_can",
- 0x10010EB: "Georgian_jil",
- 0x10010EC: "Georgian_cil",
- 0x10010ED: "Georgian_char",
- 0x10010EE: "Georgian_xan",
- 0x10010EF: "Georgian_jhan",
- 0x10010F0: "Georgian_hae",
- 0x10010F1: "Georgian_he",
- 0x10010F2: "Georgian_hie",
- 0x10010F3: "Georgian_we",
- 0x10010F4: "Georgian_har",
- 0x10010F5: "Georgian_hoe",
- 0x10010F6: "Georgian_fi",
- 0x1001E8A: "Xabovedot",
- 0x100012C: "Ibreve",
- 0x10001B5: "Zstroke",
- 0x10001E6: "Gcaron",
- 0x10001D1: "Ocaron",
- 0x100019F: "Obarred",
- 0x1001E8B: "xabovedot",
- 0x100012D: "ibreve",
- 0x10001B6: "zstroke",
- 0x10001E7: "gcaron",
- 0x10001D2: "ocaron",
- 0x1000275: "obarred",
- 0x100018F: "SCHWA",
- 0x1000259: "schwa",
- 0x10001B7: "EZH",
- 0x1000292: "ezh",
- 0x1001E36: "Lbelowdot",
- 0x1001E37: "lbelowdot",
- 0x1001EA0: "Abelowdot",
- 0x1001EA1: "abelowdot",
- 0x1001EA2: "Ahook",
- 0x1001EA3: "ahook",
- 0x1001EA4: "Acircumflexacute",
- 0x1001EA5: "acircumflexacute",
- 0x1001EA6: "Acircumflexgrave",
- 0x1001EA7: "acircumflexgrave",
- 0x1001EA8: "Acircumflexhook",
- 0x1001EA9: "acircumflexhook",
- 0x1001EAA: "Acircumflextilde",
- 0x1001EAB: "acircumflextilde",
- 0x1001EAC: "Acircumflexbelowdot",
- 0x1001EAD: "acircumflexbelowdot",
- 0x1001EAE: "Abreveacute",
- 0x1001EAF: "abreveacute",
- 0x1001EB0: "Abrevegrave",
- 0x1001EB1: "abrevegrave",
- 0x1001EB2: "Abrevehook",
- 0x1001EB3: "abrevehook",
- 0x1001EB4: "Abrevetilde",
- 0x1001EB5: "abrevetilde",
- 0x1001EB6: "Abrevebelowdot",
- 0x1001EB7: "abrevebelowdot",
- 0x1001EB8: "Ebelowdot",
- 0x1001EB9: "ebelowdot",
- 0x1001EBA: "Ehook",
- 0x1001EBB: "ehook",
- 0x1001EBC: "Etilde",
- 0x1001EBD: "etilde",
- 0x1001EBE: "Ecircumflexacute",
- 0x1001EBF: "ecircumflexacute",
- 0x1001EC0: "Ecircumflexgrave",
- 0x1001EC1: "ecircumflexgrave",
- 0x1001EC2: "Ecircumflexhook",
- 0x1001EC3: "ecircumflexhook",
- 0x1001EC4: "Ecircumflextilde",
- 0x1001EC5: "ecircumflextilde",
- 0x1001EC6: "Ecircumflexbelowdot",
- 0x1001EC7: "ecircumflexbelowdot",
- 0x1001EC8: "Ihook",
- 0x1001EC9: "ihook",
- 0x1001ECA: "Ibelowdot",
- 0x1001ECB: "ibelowdot",
- 0x1001ECC: "Obelowdot",
- 0x1001ECD: "obelowdot",
- 0x1001ECE: "Ohook",
- 0x1001ECF: "ohook",
- 0x1001ED0: "Ocircumflexacute",
- 0x1001ED1: "ocircumflexacute",
- 0x1001ED2: "Ocircumflexgrave",
- 0x1001ED3: "ocircumflexgrave",
- 0x1001ED4: "Ocircumflexhook",
- 0x1001ED5: "ocircumflexhook",
- 0x1001ED6: "Ocircumflextilde",
- 0x1001ED7: "ocircumflextilde",
- 0x1001ED8: "Ocircumflexbelowdot",
- 0x1001ED9: "ocircumflexbelowdot",
- 0x1001EDA: "Ohornacute",
- 0x1001EDB: "ohornacute",
- 0x1001EDC: "Ohorngrave",
- 0x1001EDD: "ohorngrave",
- 0x1001EDE: "Ohornhook",
- 0x1001EDF: "ohornhook",
- 0x1001EE0: "Ohorntilde",
- 0x1001EE1: "ohorntilde",
- 0x1001EE2: "Ohornbelowdot",
- 0x1001EE3: "ohornbelowdot",
- 0x1001EE4: "Ubelowdot",
- 0x1001EE5: "ubelowdot",
- 0x1001EE6: "Uhook",
- 0x1001EE7: "uhook",
- 0x1001EE8: "Uhornacute",
- 0x1001EE9: "uhornacute",
- 0x1001EEA: "Uhorngrave",
- 0x1001EEB: "uhorngrave",
- 0x1001EEC: "Uhornhook",
- 0x1001EED: "uhornhook",
- 0x1001EEE: "Uhorntilde",
- 0x1001EEF: "uhorntilde",
- 0x1001EF0: "Uhornbelowdot",
- 0x1001EF1: "uhornbelowdot",
- 0x1001EF4: "Ybelowdot",
- 0x1001EF5: "ybelowdot",
- 0x1001EF6: "Yhook",
- 0x1001EF7: "yhook",
- 0x1001EF8: "Ytilde",
- 0x1001EF9: "ytilde",
- 0x10001A0: "Ohorn",
- 0x10001A1: "ohorn",
- 0x10001AF: "Uhorn",
- 0x10001B0: "uhorn",
- 0x10020A0: "EcuSign",
- 0x10020A1: "ColonSign",
- 0x10020A2: "CruzeiroSign",
- 0x10020A3: "FFrancSign",
- 0x10020A4: "LiraSign",
- 0x10020A5: "MillSign",
- 0x10020A6: "NairaSign",
- 0x10020A7: "PesetaSign",
- 0x10020A8: "RupeeSign",
- 0x10020A9: "WonSign",
- 0x10020AA: "NewSheqelSign",
- 0x10020AB: "DongSign",
- 0x20AC: "EuroSign",
- 0x1002070: "zerosuperior",
- 0x1002074: "foursuperior",
- 0x1002075: "fivesuperior",
- 0x1002076: "sixsuperior",
- 0x1002077: "sevensuperior",
- 0x1002078: "eightsuperior",
- 0x1002079: "ninesuperior",
- 0x1002080: "zerosubscript",
- 0x1002081: "onesubscript",
- 0x1002082: "twosubscript",
- 0x1002083: "threesubscript",
- 0x1002084: "foursubscript",
- 0x1002085: "fivesubscript",
- 0x1002086: "sixsubscript",
- 0x1002087: "sevensubscript",
- 0x1002088: "eightsubscript",
- 0x1002089: "ninesubscript",
- 0x1002202: "partdifferential",
- 0x1002205: "emptyset",
- 0x1002208: "elementof",
- 0x1002209: "notelementof",
- 0x100220B: "containsas",
- 0x100221A: "squareroot",
- 0x100221B: "cuberoot",
- 0x100221C: "fourthroot",
- 0x100222C: "dintegral",
- 0x100222D: "tintegral",
- 0x1002235: "because",
- 0x1002248: "approxeq",
- 0x1002247: "notapproxeq",
- 0x1002262: "notidentical",
- 0x1002263: "stricteq",
- 0xFFF1: "braille_dot_1",
- 0xFFF2: "braille_dot_2",
- 0xFFF3: "braille_dot_3",
- 0xFFF4: "braille_dot_4",
- 0xFFF5: "braille_dot_5",
- 0xFFF6: "braille_dot_6",
- 0xFFF7: "braille_dot_7",
- 0xFFF8: "braille_dot_8",
- 0xFFF9: "braille_dot_9",
- 0xFFFA: "braille_dot_10",
- 0x1002800: "braille_blank",
- 0x1002801: "braille_dots_1",
- 0x1002802: "braille_dots_2",
- 0x1002803: "braille_dots_12",
- 0x1002804: "braille_dots_3",
- 0x1002805: "braille_dots_13",
- 0x1002806: "braille_dots_23",
- 0x1002807: "braille_dots_123",
- 0x1002808: "braille_dots_4",
- 0x1002809: "braille_dots_14",
- 0x100280A: "braille_dots_24",
- 0x100280B: "braille_dots_124",
- 0x100280C: "braille_dots_34",
- 0x100280D: "braille_dots_134",
- 0x100280E: "braille_dots_234",
- 0x100280F: "braille_dots_1234",
- 0x1002810: "braille_dots_5",
- 0x1002811: "braille_dots_15",
- 0x1002812: "braille_dots_25",
- 0x1002813: "braille_dots_125",
- 0x1002814: "braille_dots_35",
- 0x1002815: "braille_dots_135",
- 0x1002816: "braille_dots_235",
- 0x1002817: "braille_dots_1235",
- 0x1002818: "braille_dots_45",
- 0x1002819: "braille_dots_145",
- 0x100281A: "braille_dots_245",
- 0x100281B: "braille_dots_1245",
- 0x100281C: "braille_dots_345",
- 0x100281D: "braille_dots_1345",
- 0x100281E: "braille_dots_2345",
- 0x100281F: "braille_dots_12345",
- 0x1002820: "braille_dots_6",
- 0x1002821: "braille_dots_16",
- 0x1002822: "braille_dots_26",
- 0x1002823: "braille_dots_126",
- 0x1002824: "braille_dots_36",
- 0x1002825: "braille_dots_136",
- 0x1002826: "braille_dots_236",
- 0x1002827: "braille_dots_1236",
- 0x1002828: "braille_dots_46",
- 0x1002829: "braille_dots_146",
- 0x100282A: "braille_dots_246",
- 0x100282B: "braille_dots_1246",
- 0x100282C: "braille_dots_346",
- 0x100282D: "braille_dots_1346",
- 0x100282E: "braille_dots_2346",
- 0x100282F: "braille_dots_12346",
- 0x1002830: "braille_dots_56",
- 0x1002831: "braille_dots_156",
- 0x1002832: "braille_dots_256",
- 0x1002833: "braille_dots_1256",
- 0x1002834: "braille_dots_356",
- 0x1002835: "braille_dots_1356",
- 0x1002836: "braille_dots_2356",
- 0x1002837: "braille_dots_12356",
- 0x1002838: "braille_dots_456",
- 0x1002839: "braille_dots_1456",
- 0x100283A: "braille_dots_2456",
- 0x100283B: "braille_dots_12456",
- 0x100283C: "braille_dots_3456",
- 0x100283D: "braille_dots_13456",
- 0x100283E: "braille_dots_23456",
- 0x100283F: "braille_dots_123456",
- 0x1002840: "braille_dots_7",
- 0x1002841: "braille_dots_17",
- 0x1002842: "braille_dots_27",
- 0x1002843: "braille_dots_127",
- 0x1002844: "braille_dots_37",
- 0x1002845: "braille_dots_137",
- 0x1002846: "braille_dots_237",
- 0x1002847: "braille_dots_1237",
- 0x1002848: "braille_dots_47",
- 0x1002849: "braille_dots_147",
- 0x100284A: "braille_dots_247",
- 0x100284B: "braille_dots_1247",
- 0x100284C: "braille_dots_347",
- 0x100284D: "braille_dots_1347",
- 0x100284E: "braille_dots_2347",
- 0x100284F: "braille_dots_12347",
- 0x1002850: "braille_dots_57",
- 0x1002851: "braille_dots_157",
- 0x1002852: "braille_dots_257",
- 0x1002853: "braille_dots_1257",
- 0x1002854: "braille_dots_357",
- 0x1002855: "braille_dots_1357",
- 0x1002856: "braille_dots_2357",
- 0x1002857: "braille_dots_12357",
- 0x1002858: "braille_dots_457",
- 0x1002859: "braille_dots_1457",
- 0x100285A: "braille_dots_2457",
- 0x100285B: "braille_dots_12457",
- 0x100285C: "braille_dots_3457",
- 0x100285D: "braille_dots_13457",
- 0x100285E: "braille_dots_23457",
- 0x100285F: "braille_dots_123457",
- 0x1002860: "braille_dots_67",
- 0x1002861: "braille_dots_167",
- 0x1002862: "braille_dots_267",
- 0x1002863: "braille_dots_1267",
- 0x1002864: "braille_dots_367",
- 0x1002865: "braille_dots_1367",
- 0x1002866: "braille_dots_2367",
- 0x1002867: "braille_dots_12367",
- 0x1002868: "braille_dots_467",
- 0x1002869: "braille_dots_1467",
- 0x100286A: "braille_dots_2467",
- 0x100286B: "braille_dots_12467",
- 0x100286C: "braille_dots_3467",
- 0x100286D: "braille_dots_13467",
- 0x100286E: "braille_dots_23467",
- 0x100286F: "braille_dots_123467",
- 0x1002870: "braille_dots_567",
- 0x1002871: "braille_dots_1567",
- 0x1002872: "braille_dots_2567",
- 0x1002873: "braille_dots_12567",
- 0x1002874: "braille_dots_3567",
- 0x1002875: "braille_dots_13567",
- 0x1002876: "braille_dots_23567",
- 0x1002877: "braille_dots_123567",
- 0x1002878: "braille_dots_4567",
- 0x1002879: "braille_dots_14567",
- 0x100287A: "braille_dots_24567",
- 0x100287B: "braille_dots_124567",
- 0x100287C: "braille_dots_34567",
- 0x100287D: "braille_dots_134567",
- 0x100287E: "braille_dots_234567",
- 0x100287F: "braille_dots_1234567",
- 0x1002880: "braille_dots_8",
- 0x1002881: "braille_dots_18",
- 0x1002882: "braille_dots_28",
- 0x1002883: "braille_dots_128",
- 0x1002884: "braille_dots_38",
- 0x1002885: "braille_dots_138",
- 0x1002886: "braille_dots_238",
- 0x1002887: "braille_dots_1238",
- 0x1002888: "braille_dots_48",
- 0x1002889: "braille_dots_148",
- 0x100288A: "braille_dots_248",
- 0x100288B: "braille_dots_1248",
- 0x100288C: "braille_dots_348",
- 0x100288D: "braille_dots_1348",
- 0x100288E: "braille_dots_2348",
- 0x100288F: "braille_dots_12348",
- 0x1002890: "braille_dots_58",
- 0x1002891: "braille_dots_158",
- 0x1002892: "braille_dots_258",
- 0x1002893: "braille_dots_1258",
- 0x1002894: "braille_dots_358",
- 0x1002895: "braille_dots_1358",
- 0x1002896: "braille_dots_2358",
- 0x1002897: "braille_dots_12358",
- 0x1002898: "braille_dots_458",
- 0x1002899: "braille_dots_1458",
- 0x100289A: "braille_dots_2458",
- 0x100289B: "braille_dots_12458",
- 0x100289C: "braille_dots_3458",
- 0x100289D: "braille_dots_13458",
- 0x100289E: "braille_dots_23458",
- 0x100289F: "braille_dots_123458",
- 0x10028A0: "braille_dots_68",
- 0x10028A1: "braille_dots_168",
- 0x10028A2: "braille_dots_268",
- 0x10028A3: "braille_dots_1268",
- 0x10028A4: "braille_dots_368",
- 0x10028A5: "braille_dots_1368",
- 0x10028A6: "braille_dots_2368",
- 0x10028A7: "braille_dots_12368",
- 0x10028A8: "braille_dots_468",
- 0x10028A9: "braille_dots_1468",
- 0x10028AA: "braille_dots_2468",
- 0x10028AB: "braille_dots_12468",
- 0x10028AC: "braille_dots_3468",
- 0x10028AD: "braille_dots_13468",
- 0x10028AE: "braille_dots_23468",
- 0x10028AF: "braille_dots_123468",
- 0x10028B0: "braille_dots_568",
- 0x10028B1: "braille_dots_1568",
- 0x10028B2: "braille_dots_2568",
- 0x10028B3: "braille_dots_12568",
- 0x10028B4: "braille_dots_3568",
- 0x10028B5: "braille_dots_13568",
- 0x10028B6: "braille_dots_23568",
- 0x10028B7: "braille_dots_123568",
- 0x10028B8: "braille_dots_4568",
- 0x10028B9: "braille_dots_14568",
- 0x10028BA: "braille_dots_24568",
- 0x10028BB: "braille_dots_124568",
- 0x10028BC: "braille_dots_34568",
- 0x10028BD: "braille_dots_134568",
- 0x10028BE: "braille_dots_234568",
- 0x10028BF: "braille_dots_1234568",
- 0x10028C0: "braille_dots_78",
- 0x10028C1: "braille_dots_178",
- 0x10028C2: "braille_dots_278",
- 0x10028C3: "braille_dots_1278",
- 0x10028C4: "braille_dots_378",
- 0x10028C5: "braille_dots_1378",
- 0x10028C6: "braille_dots_2378",
- 0x10028C7: "braille_dots_12378",
- 0x10028C8: "braille_dots_478",
- 0x10028C9: "braille_dots_1478",
- 0x10028CA: "braille_dots_2478",
- 0x10028CB: "braille_dots_12478",
- 0x10028CC: "braille_dots_3478",
- 0x10028CD: "braille_dots_13478",
- 0x10028CE: "braille_dots_23478",
- 0x10028CF: "braille_dots_123478",
- 0x10028D0: "braille_dots_578",
- 0x10028D1: "braille_dots_1578",
- 0x10028D2: "braille_dots_2578",
- 0x10028D3: "braille_dots_12578",
- 0x10028D4: "braille_dots_3578",
- 0x10028D5: "braille_dots_13578",
- 0x10028D6: "braille_dots_23578",
- 0x10028D7: "braille_dots_123578",
- 0x10028D8: "braille_dots_4578",
- 0x10028D9: "braille_dots_14578",
- 0x10028DA: "braille_dots_24578",
- 0x10028DB: "braille_dots_124578",
- 0x10028DC: "braille_dots_34578",
- 0x10028DD: "braille_dots_134578",
- 0x10028DE: "braille_dots_234578",
- 0x10028DF: "braille_dots_1234578",
- 0x10028E0: "braille_dots_678",
- 0x10028E1: "braille_dots_1678",
- 0x10028E2: "braille_dots_2678",
- 0x10028E3: "braille_dots_12678",
- 0x10028E4: "braille_dots_3678",
- 0x10028E5: "braille_dots_13678",
- 0x10028E6: "braille_dots_23678",
- 0x10028E7: "braille_dots_123678",
- 0x10028E8: "braille_dots_4678",
- 0x10028E9: "braille_dots_14678",
- 0x10028EA: "braille_dots_24678",
- 0x10028EB: "braille_dots_124678",
- 0x10028EC: "braille_dots_34678",
- 0x10028ED: "braille_dots_134678",
- 0x10028EE: "braille_dots_234678",
- 0x10028EF: "braille_dots_1234678",
- 0x10028F0: "braille_dots_5678",
- 0x10028F1: "braille_dots_15678",
- 0x10028F2: "braille_dots_25678",
- 0x10028F3: "braille_dots_125678",
- 0x10028F4: "braille_dots_35678",
- 0x10028F5: "braille_dots_135678",
- 0x10028F6: "braille_dots_235678",
- 0x10028F7: "braille_dots_1235678",
- 0x10028F8: "braille_dots_45678",
- 0x10028F9: "braille_dots_145678",
- 0x10028FA: "braille_dots_245678",
- 0x10028FB: "braille_dots_1245678",
- 0x10028FC: "braille_dots_345678",
- 0x10028FD: "braille_dots_1345678",
- 0x10028FE: "braille_dots_2345678",
- 0x10028FF: "braille_dots_12345678",
- 0x1000D82: "Sinh_ng",
- 0x1000D83: "Sinh_h2",
- 0x1000D85: "Sinh_a",
- 0x1000D86: "Sinh_aa",
- 0x1000D87: "Sinh_ae",
- 0x1000D88: "Sinh_aee",
- 0x1000D89: "Sinh_i",
- 0x1000D8A: "Sinh_ii",
- 0x1000D8B: "Sinh_u",
- 0x1000D8C: "Sinh_uu",
- 0x1000D8D: "Sinh_ri",
- 0x1000D8E: "Sinh_rii",
- 0x1000D8F: "Sinh_lu",
- 0x1000D90: "Sinh_luu",
- 0x1000D91: "Sinh_e",
- 0x1000D92: "Sinh_ee",
- 0x1000D93: "Sinh_ai",
- 0x1000D94: "Sinh_o",
- 0x1000D95: "Sinh_oo",
- 0x1000D96: "Sinh_au",
- 0x1000D9A: "Sinh_ka",
- 0x1000D9B: "Sinh_kha",
- 0x1000D9C: "Sinh_ga",
- 0x1000D9D: "Sinh_gha",
- 0x1000D9E: "Sinh_ng2",
- 0x1000D9F: "Sinh_nga",
- 0x1000DA0: "Sinh_ca",
- 0x1000DA1: "Sinh_cha",
- 0x1000DA2: "Sinh_ja",
- 0x1000DA3: "Sinh_jha",
- 0x1000DA4: "Sinh_nya",
- 0x1000DA5: "Sinh_jnya",
- 0x1000DA6: "Sinh_nja",
- 0x1000DA7: "Sinh_tta",
- 0x1000DA8: "Sinh_ttha",
- 0x1000DA9: "Sinh_dda",
- 0x1000DAA: "Sinh_ddha",
- 0x1000DAB: "Sinh_nna",
- 0x1000DAC: "Sinh_ndda",
- 0x1000DAD: "Sinh_tha",
- 0x1000DAE: "Sinh_thha",
- 0x1000DAF: "Sinh_dha",
- 0x1000DB0: "Sinh_dhha",
- 0x1000DB1: "Sinh_na",
- 0x1000DB3: "Sinh_ndha",
- 0x1000DB4: "Sinh_pa",
- 0x1000DB5: "Sinh_pha",
- 0x1000DB6: "Sinh_ba",
- 0x1000DB7: "Sinh_bha",
- 0x1000DB8: "Sinh_ma",
- 0x1000DB9: "Sinh_mba",
- 0x1000DBA: "Sinh_ya",
- 0x1000DBB: "Sinh_ra",
- 0x1000DBD: "Sinh_la",
- 0x1000DC0: "Sinh_va",
- 0x1000DC1: "Sinh_sha",
- 0x1000DC2: "Sinh_ssha",
- 0x1000DC3: "Sinh_sa",
- 0x1000DC4: "Sinh_ha",
- 0x1000DC5: "Sinh_lla",
- 0x1000DC6: "Sinh_fa",
- 0x1000DCA: "Sinh_al",
- 0x1000DCF: "Sinh_aa2",
- 0x1000DD0: "Sinh_ae2",
- 0x1000DD1: "Sinh_aee2",
- 0x1000DD2: "Sinh_i2",
- 0x1000DD3: "Sinh_ii2",
- 0x1000DD4: "Sinh_u2",
- 0x1000DD6: "Sinh_uu2",
- 0x1000DD8: "Sinh_ru2",
- 0x1000DD9: "Sinh_e2",
- 0x1000DDA: "Sinh_ee2",
- 0x1000DDB: "Sinh_ai2",
- 0x1000DDC: "Sinh_o2",
- 0x1000DDD: "Sinh_oo2",
- 0x1000DDE: "Sinh_au2",
- 0x1000DDF: "Sinh_lu2",
- 0x1000DF2: "Sinh_ruu2",
- 0x1000DF3: "Sinh_luu2",
- 0x1000DF4: "Sinh_kunddaliya",
- };
-
- /**
- * All keysyms which should not repeat when held down.
- * @private
- */
- var no_repeat = {
- 0xFE03: true, // ISO Level 3 Shift (AltGr)
- 0xFFE1: true, // Left shift
- 0xFFE2: true, // Right shift
- 0xFFE3: true, // Left ctrl
- 0xFFE4: true, // Right ctrl
- 0xFFE7: true, // Left meta
- 0xFFE8: true, // Right meta
- 0xFFE9: true, // Left alt
- 0xFFEA: true, // Right alt
- 0xFFEB: true, // Left hyper
- 0xFFEC: true // Right hyper
- };
-
- /**
- * All modifiers and their states.
- */
- this.modifiers = new Keyboard.ModifierState();
-
- /**
- * The state of every key, indexed by keysym. If a particular key is
- * pressed, the value of pressed for that keysym will be true. If a key
- * is not currently pressed, it will not be defined.
- */
- this.pressed = {};
-
- /**
- * The last result of calling the onkeydown handler for each key, indexed
- * by keysym. This is used to prevent/allow default actions for key events,
- * even when the onkeydown handler cannot be called again because the key
- * is (theoretically) still pressed.
- *
- * @private
- */
- var last_keydown_result = {};
-
- /**
- * The keysym most recently associated with a given keycode when keydown
- * fired. This object maps keycodes to keysyms.
- *
- * @private
- * @type {Object.}
- */
- var recentKeysym = {};
-
- /**
- * Timeout before key repeat starts.
- * @private
- */
- var key_repeat_timeout = null;
-
- /**
- * Interval which presses and releases the last key pressed while that
- * key is still being held down.
- * @private
- */
- var key_repeat_interval = null;
-
- /**
- * Given an array of keysyms indexed by location, returns the keysym
- * for the given location, or the keysym for the standard location if
- * undefined.
- *
- * @private
- * @param {Number[]} keysyms
- * An array of keysyms, where the index of the keysym in the array is
- * the location value.
- *
- * @param {Number} location
- * The location on the keyboard corresponding to the key pressed, as
- * defined at: http://www.w3.org/TR/DOM-Level-3-Events/#events-KeyboardEvent
- */
- var get_keysym = function get_keysym(keysyms, location) {
-
- if (!keysyms)
- return null;
-
- return keysyms[location] || keysyms[0];
- };
-
- /**
- * Returns true if the given keysym corresponds to a printable character,
- * false otherwise.
- *
- * @param {Number} keysym
- * The keysym to check.
- *
- * @returns {Boolean}
- * true if the given keysym corresponds to a printable character,
- * false otherwise.
- */
- var isPrintable = function isPrintable(keysym) {
-
- // Keysyms with Unicode equivalents are printable
- return (keysym >= 0x00 && keysym <= 0xFF)
- || (keysym & 0xFFFF0000) === 0x01000000;
-
- };
-
- function keysym_from_key_identifier(identifier, location, shifted) {
-
- if (!identifier)
- return null;
-
- var typedCharacter;
-
- // If identifier is U+xxxx, decode Unicode character
- var unicodePrefixLocation = identifier.indexOf("U+");
- if (unicodePrefixLocation >= 0) {
- var hex = identifier.substring(unicodePrefixLocation+2);
- typedCharacter = String.fromCharCode(parseInt(hex, 16));
- }
-
- // If single character and not keypad, use that as typed character
- else if (identifier.length === 1 && location !== 3)
- typedCharacter = identifier;
-
- // Otherwise, look up corresponding keysym
- else
- return get_keysym(keyidentifier_keysym[identifier], location);
-
- // Alter case if necessary
- if (shifted === true)
- typedCharacter = typedCharacter.toUpperCase();
- else if (shifted === false)
- typedCharacter = typedCharacter.toLowerCase();
-
- // Get codepoint
- var codepoint = typedCharacter.charCodeAt(0);
- return keysym_from_charcode(codepoint);
-
- }
-
- function isControlCharacter(codepoint) {
- return codepoint <= 0x1F || (codepoint >= 0x7F && codepoint <= 0x9F);
- }
-
- function keysym_from_charcode(codepoint) {
-
- // Keysyms for control characters
- if (isControlCharacter(codepoint)) return 0xFF00 | codepoint;
-
- // Keysyms for ASCII chars
- if (codepoint >= 0x0000 && codepoint <= 0x00FF)
- return codepoint;
-
- // Keysyms for Unicode
- if (codepoint >= 0x0100 && codepoint <= 0x10FFFF)
- return 0x01000000 | codepoint;
-
- return null;
-
- }
-
- function keysym_from_keycode(keyCode, location) {
- return get_keysym(keycodeKeysyms[keyCode], location);
- }
-
- /**
- * Heuristically detects if the legacy keyIdentifier property of
- * a keydown/keyup event looks incorrectly derived. Chrome, and
- * presumably others, will produce the keyIdentifier by assuming
- * the keyCode is the Unicode codepoint for that key. This is not
- * correct in all cases.
- *
- * @private
- * @param {Number} keyCode
- * The keyCode from a browser keydown/keyup event.
- *
- * @param {String} keyIdentifier
- * The legacy keyIdentifier from a browser keydown/keyup event.
- *
- * @returns {Boolean}
- * true if the keyIdentifier looks sane, false if the keyIdentifier
- * appears incorrectly derived or is missing entirely.
- */
- var key_identifier_sane = function key_identifier_sane(keyCode, keyIdentifier) {
-
- // Missing identifier is not sane
- if (!keyIdentifier)
- return false;
-
- // Assume non-Unicode keyIdentifier values are sane
- var unicodePrefixLocation = keyIdentifier.indexOf("U+");
- if (unicodePrefixLocation === -1)
- return true;
-
- // If the Unicode codepoint isn't identical to the keyCode,
- // then the identifier is likely correct
- var codepoint = parseInt(keyIdentifier.substring(unicodePrefixLocation+2), 16);
- if (keyCode !== codepoint)
- return true;
-
- // The keyCodes for A-Z and 0-9 are actually identical to their
- // Unicode codepoints
- if ((keyCode >= 65 && keyCode <= 90) || (keyCode >= 48 && keyCode <= 57))
- return true;
-
- // The keyIdentifier does NOT appear sane
- return false;
-
- };
-
- /**
- * Marks a key as pressed, firing the keydown event if registered. Key
- * repeat for the pressed key will start after a delay if that key is
- * not a modifier. The return value of this function depends on the
- * return value of the keydown event handler, if any.
- *
- * @param {Number} keysym The keysym of the key to press.
- * @return {Boolean} true if event should NOT be canceled, false otherwise.
- */
- this.press = function(keysym) {
-
- // Don't bother with pressing the key if the key is unknown
- if (keysym === null) return;
-
- // Only press if released
- if (!guac_keyboard.pressed[keysym]) {
-
- // Mark key as pressed
- guac_keyboard.pressed[keysym] = true;
-
- // Send key event
- if (guac_keyboard.onkeydown) {
-
- var result = guac_keyboard.onkeydown(keysym_to_string[keysym],
- modifier_state_to_str());
- last_keydown_result[keysym] = result;
-
- // Stop any current repeat
- window.clearTimeout(key_repeat_timeout);
- window.clearInterval(key_repeat_interval);
-
- // Repeat after a delay as long as pressed
- if (!no_repeat[keysym])
- key_repeat_timeout = window.setTimeout(function() {
- key_repeat_interval = window.setInterval(function() {
- var mods = modifier_state_to_str();
- guac_keyboard.onkeyup(keysym_to_string[keysym], mods);
- guac_keyboard.onkeydown(keysym_to_string[keysym], mods);
- }, 50);
- }, 500);
-
- return result;
- }
- }
-
- // Return the last keydown result by default, resort to false if unknown
- return last_keydown_result[keysym] || false;
-
- };
-
- /**
- * Marks a key as released, firing the keyup event if registered.
- *
- * @param {Number} keysym The keysym of the key to release.
- */
- this.release = function(keysym) {
-
- // Only release if pressed
- if (guac_keyboard.pressed[keysym]) {
-
- // Mark key as released
- delete guac_keyboard.pressed[keysym];
-
- // Stop repeat
- window.clearTimeout(key_repeat_timeout);
- window.clearInterval(key_repeat_interval);
-
- // Send key event
- if (keysym !== null && guac_keyboard.onkeyup)
- guac_keyboard.onkeyup(keysym_to_string[keysym],
- modifier_state_to_str());
-
- }
-
- };
-
- /**
- * Resets the state of this keyboard, releasing all keys, and firing keyup
- * events for each released key.
- */
- this.reset = function() {
-
- // Release all pressed keys
- for (var keysym in guac_keyboard.pressed)
- guac_keyboard.release(parseInt(keysym));
-
- // Clear event log
- eventLog = [];
-
- };
-
- /**
- * Given a keyboard event, updates the local modifier state and remote
- * key state based on the modifier flags within the event. This function
- * pays no attention to keycodes.
- *
- * @private
- * @param {KeyboardEvent} e
- * The keyboard event containing the flags to update.
- */
- var update_modifier_state = function update_modifier_state(e) {
-
- // Get state
- var state = Keyboard.ModifierState.fromKeyboardEvent(e);
-
- // Release alt if implicitly released
- if (guac_keyboard.modifiers.alt && state.alt === false) {
- guac_keyboard.release(0xFFE9); // Left alt
- guac_keyboard.release(0xFFEA); // Right alt
- guac_keyboard.release(0xFE03); // AltGr
- }
-
- // Release shift if implicitly released
- if (guac_keyboard.modifiers.shift && state.shift === false) {
- guac_keyboard.release(0xFFE1); // Left shift
- guac_keyboard.release(0xFFE2); // Right shift
- }
-
- // Release ctrl if implicitly released
- if (guac_keyboard.modifiers.ctrl && state.ctrl === false) {
- guac_keyboard.release(0xFFE3); // Left ctrl
- guac_keyboard.release(0xFFE4); // Right ctrl
- }
-
- // Release meta if implicitly released
- if (guac_keyboard.modifiers.meta && state.meta === false) {
- guac_keyboard.release(0xFFE7); // Left meta
- guac_keyboard.release(0xFFE8); // Right meta
- }
-
- // Release hyper if implicitly released
- if (guac_keyboard.modifiers.hyper && state.hyper === false) {
- guac_keyboard.release(0xFFEB); // Left hyper
- guac_keyboard.release(0xFFEC); // Right hyper
- }
-
- // Update state
- guac_keyboard.modifiers = state;
-
- };
-
- /**
- * Constructs a string representing all currently pressed modifiers.
- *
- * @return {String} The resulting string.
- */
- var modifier_state_to_str = function modifier_state_to_str() {
- let masks = []
- if (guac_keyboard.modifiers.alt) masks.push("alt-mask");
- if (guac_keyboard.modifiers.ctrl) masks.push("control-mask");
- if (guac_keyboard.modifiers.meta) masks.push("meta-mask");
- if (guac_keyboard.modifiers.shift) masks.push("shift-mask");
- if (guac_keyboard.modifiers.hyper) masks.push("hyper-mask");
- return masks.join('+')
- }
-
- /**
- * Reads through the event log, removing events from the head of the log
- * when the corresponding true key presses are known (or as known as they
- * can be).
- *
- * @private
- * @return {Boolean} Whether the default action of the latest event should
- * be prevented.
- */
- function interpret_events() {
-
- // Do not prevent default if no event could be interpreted
- var handled_event = interpret_event();
- if (!handled_event)
- return false;
-
- // Interpret as much as possible
- var last_event;
- do {
- last_event = handled_event;
- handled_event = interpret_event();
- } while (handled_event !== null);
-
- return last_event.defaultPrevented;
-
- }
-
- /**
- * Releases Ctrl+Alt, if both are currently pressed and the given keysym
- * looks like a key that may require AltGr.
- *
- * @private
- * @param {Number} keysym The key that was just pressed.
- */
- var release_simulated_altgr = function release_simulated_altgr(keysym) {
-
- // Both Ctrl+Alt must be pressed if simulated AltGr is in use
- if (!guac_keyboard.modifiers.ctrl || !guac_keyboard.modifiers.alt)
- return;
-
- // Assume [A-Z] never require AltGr
- if (keysym >= 0x0041 && keysym <= 0x005A)
- return;
-
- // Assume [a-z] never require AltGr
- if (keysym >= 0x0061 && keysym <= 0x007A)
- return;
-
- // Release Ctrl+Alt if the keysym is printable
- if (keysym <= 0xFF || (keysym & 0xFF000000) === 0x01000000) {
- guac_keyboard.release(0xFFE3); // Left ctrl
- guac_keyboard.release(0xFFE4); // Right ctrl
- guac_keyboard.release(0xFFE9); // Left alt
- guac_keyboard.release(0xFFEA); // Right alt
- }
-
- };
-
- /**
- * Reads through the event log, interpreting the first event, if possible,
- * and returning that event. If no events can be interpreted, due to a
- * total lack of events or the need for more events, null is returned. Any
- * interpreted events are automatically removed from the log.
- *
- * @private
- * @return {KeyEvent}
- * The first key event in the log, if it can be interpreted, or null
- * otherwise.
- */
- var interpret_event = function interpret_event() {
-
- // Peek at first event in log
- var first = eventLog[0];
- if (!first)
- return null;
-
- // Keydown event
- if (first instanceof KeydownEvent) {
-
- var keysym = null;
- var accepted_events = [];
-
- // If event itself is reliable, no need to wait for other events
- if (first.reliable) {
- keysym = first.keysym;
- accepted_events = eventLog.splice(0, 1);
- }
-
- // If keydown is immediately followed by a keypress, use the indicated character
- else if (eventLog[1] instanceof KeypressEvent) {
- keysym = eventLog[1].keysym;
- accepted_events = eventLog.splice(0, 2);
- }
-
- // If keydown is immediately followed by anything else, then no
- // keypress can possibly occur to clarify this event, and we must
- // handle it now
- else if (eventLog[1]) {
- keysym = first.keysym;
- accepted_events = eventLog.splice(0, 1);
- }
-
- // Fire a key press if valid events were found
- if (accepted_events.length > 0) {
-
- if (keysym) {
-
- // Fire event
- release_simulated_altgr(keysym);
- var defaultPrevented = !guac_keyboard.press(keysym);
- recentKeysym[first.keyCode] = keysym;
-
- // If a key is pressed while meta is held down, the keyup will
- // never be sent in Chrome, so send it now. (bug #108404)
- if (guac_keyboard.modifiers.meta && keysym !== 0xFFE7 && keysym !== 0xFFE8)
- guac_keyboard.release(keysym);
-
- // Record whether default was prevented
- for (var i=0; i
- */
-
-// Set this to override the automatic detection in websocketServerConnect()
-var ws_server;
-var ws_port;
-// Override with your own STUN servers if you want
-var rtc_configuration = {iceServers: [{urls: "stun:stun.l.google.com:19302"},
- /* TODO: do not keep these static and in clear text in production,
- * and instead use one of the mechanisms discussed in
- * https://groups.google.com/forum/#!topic/discuss-webrtc/nn8b6UboqRA
- */
- {'urls': 'turn:turn.homeneural.net:3478?transport=udp',
- 'credential': '1qaz2wsx',
- 'username': 'test'
- }],
- /* Uncomment the following line to ensure the turn server is used
- * while testing. This should be kept commented out in production,
- * as non-relay ice candidates should be preferred
- */
- // iceTransportPolicy: "relay",
- };
-
-var sessions = {}
-
-/* https://stackoverflow.com/questions/105034/create-guid-uuid-in-javascript */
-function getOurId() {
- return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
- var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
- return v.toString(16);
- });
-}
-
-function Uint8ToString(u8a){
- var CHUNK_SZ = 0x8000;
- var c = [];
- for (var i=0; i < u8a.length; i+=CHUNK_SZ) {
- c.push(String.fromCharCode.apply(null, u8a.subarray(i, i+CHUNK_SZ)));
- }
- return c.join("");
-}
-
-function Session(our_id, peer_id, closed_callback) {
- this.id = null;
- this.peer_connection = null;
- this.ws_conn = null;
- this.peer_id = peer_id;
- this.our_id = our_id;
- this.closed_callback = closed_callback;
- this.data_channel = null;
- this.input = null;
-
- this.getVideoElement = function() {
- return document.getElementById("stream-" + this.our_id);
- };
-
- this.resetState = function() {
- if (this.peer_connection) {
- this.peer_connection.close();
- this.peer_connection = null;
- }
- var videoElement = this.getVideoElement();
- if (videoElement) {
- videoElement.pause();
- videoElement.src = "";
- }
-
- var session_div = document.getElementById("session-" + this.our_id);
- if (session_div) {
- session_div.parentNode.removeChild(session_div);
- }
- if (this.ws_conn) {
- this.ws_conn.close();
- this.ws_conn = null;
- }
-
- this.input && this.input.detach();
- this.data_channel = null;
- };
-
- this.handleIncomingError = function(error) {
- this.resetState();
- this.closed_callback(this.our_id);
- };
-
- this.setStatus = function(text) {
- console.log(text);
- var span = document.getElementById("status-" + this.our_id);
- // Don't set the status if it already contains an error
- if (!span.classList.contains('error'))
- span.textContent = text;
- };
-
- this.setError = function(text) {
- console.error(text);
- var span = document.getElementById("status-" + this.our_id);
- span.textContent = text;
- span.classList.add('error');
- this.resetState();
- this.closed_callback(this.our_id);
- };
-
- // Local description was set, send it to peer
- this.onLocalDescription = function(desc) {
- console.log("Got local description: " + JSON.stringify(desc), this);
- var thiz = this;
- this.peer_connection.setLocalDescription(desc).then(() => {
- this.setStatus("Sending SDP answer");
- var sdp = {
- 'type': 'peer',
- 'sessionId': this.id,
- 'sdp': this.peer_connection.localDescription.toJSON()
- };
- this.ws_conn.send(JSON.stringify(sdp));
- }).catch(function(e) {
- thiz.setError(e);
- });
- };
-
- this.onRemoteDescriptionSet = function() {
- this.setStatus("Remote SDP set");
- this.setStatus("Got SDP offer");
- this.peer_connection.createAnswer()
- .then(this.onLocalDescription.bind(this)).catch(this.setError);
- }
-
- // SDP offer received from peer, set remote description and create an answer
- this.onIncomingSDP = function(sdp) {
- var thiz = this;
- this.peer_connection.setRemoteDescription(sdp)
- .then(this.onRemoteDescriptionSet.bind(this))
- .catch(function(e) {
- thiz.setError(e)
- });
- };
-
- // ICE candidate received from peer, add it to the peer connection
- this.onIncomingICE = function(ice) {
- var candidate = new RTCIceCandidate(ice);
- var thiz = this;
- this.peer_connection.addIceCandidate(candidate).catch(function(e) {
- thiz.setError(e)
- });
- };
-
- this.onServerMessage = function(event) {
- console.log("Received " + event.data);
- try {
- msg = JSON.parse(event.data);
- } catch (e) {
- if (e instanceof SyntaxError) {
- this.handleIncomingError("Error parsing incoming JSON: " + event.data);
- } else {
- this.handleIncomingError("Unknown error parsing response: " + event.data);
- }
- return;
- }
-
- if (msg.type == "registered") {
- this.setStatus("Registered with server");
- this.connectPeer();
- } else if (msg.type == "sessionStarted") {
- this.setStatus("Registered with server");
- this.id = msg.sessionId;
- } else if (msg.type == "error") {
- this.handleIncomingError(msg.details);
- } else if (msg.type == "endSession") {
- this.resetState();
- this.closed_callback(this.our_id);
- } else if (msg.type == "peer") {
- // Incoming peer message signals the beginning of a call
- if (!this.peer_connection)
- this.createCall(msg);
-
- if (msg.sdp != null) {
- this.onIncomingSDP(msg.sdp);
- } else if (msg.ice != null) {
- this.onIncomingICE(msg.ice);
- } else {
- this.handleIncomingError("Unknown incoming JSON: " + msg);
- }
- }
- };
-
- this.streamIsPlaying = function(e) {
- this.setStatus("Streaming");
- };
-
- this.onServerClose = function(event) {
- this.resetState();
- this.closed_callback(this.our_id);
- };
-
- this.onServerError = function(event) {
- this.handleIncomingError('Server error');
- };
-
- this.websocketServerConnect = function() {
- // Clear errors in the status span
- var span = document.getElementById("status-" + this.our_id);
- span.classList.remove('error');
- span.textContent = '';
- console.log("Our ID:", this.our_id);
- var ws_port = ws_port || '8443';
- if (window.location.protocol.startsWith ("file")) {
- var ws_server = ws_server || "127.0.0.1";
- } else if (window.location.protocol.startsWith ("http")) {
- var ws_server = ws_server || window.location.hostname;
- } else {
- throw new Error ("Don't know how to connect to the signalling server with uri" + window.location);
- }
- var ws_url = 'ws://' + ws_server + ':' + ws_port
- this.setStatus("Connecting to server " + ws_url);
- this.ws_conn = new WebSocket(ws_url);
- /* When connected, immediately register with the server */
- this.ws_conn.addEventListener('open', (event) => {
- this.setStatus("Connecting to the peer");
- this.connectPeer();
- });
- this.ws_conn.addEventListener('error', this.onServerError.bind(this));
- this.ws_conn.addEventListener('message', this.onServerMessage.bind(this));
- this.ws_conn.addEventListener('close', this.onServerClose.bind(this));
- };
-
- this.connectPeer = function() {
- this.setStatus("Connecting " + this.peer_id);
-
- this.ws_conn.send(JSON.stringify({
- "type": "startSession",
- "peerId": this.peer_id
- }));
- };
-
- this.onRemoteStreamAdded = function(event) {
- var videoTracks = event.stream.getVideoTracks();
- var audioTracks = event.stream.getAudioTracks();
-
- console.log(videoTracks);
-
- if (videoTracks.length > 0) {
- console.log('Incoming stream: ' + videoTracks.length + ' video tracks and ' + audioTracks.length + ' audio tracks');
- this.getVideoElement().srcObject = event.stream;
- this.getVideoElement().play();
- } else {
- this.handleIncomingError('Stream with unknown tracks added, resetting');
- }
- };
-
- this.createCall = function(msg) {
- console.log('Creating RTCPeerConnection');
-
- this.peer_connection = new RTCPeerConnection(rtc_configuration);
- this.peer_connection.onaddstream = this.onRemoteStreamAdded.bind(this);
-
- this.peer_connection.ondatachannel = (event) => {
- console.log(`Data channel created: ${event.channel.label}`);
- this.data_channel = event.channel;
-
- video_element = this.getVideoElement();
- if (video_element) {
- this.input = new Input(video_element, (data) => {
- if (this.data_channel) {
- console.log(`Navigation data: ${data}`);
- this.data_channel.send(JSON.stringify(data));
- }
- });
- }
-
- this.data_channel.onopen = (event) => {
- console.log("Receive channel opened, attaching input");
- this.input.attach();
- }
- this.data_channel.onclose = (event) => {
- console.info("Receive channel closed");
- this.input && this.input.detach();
- this.data_channel = null;
- }
- this.data_channel.onerror = (event) => {
- this.input && this.input.detach();
- console.warn("Error on receive channel", event.data);
- this.data_channel = null;
- }
-
- let buffer = [];
- this.data_channel.onmessage = (event) => {
- if (typeof event.data === 'string' || event.data instanceof String) {
- if (event.data == 'BEGIN_IMAGE')
- buffer = [];
- else if (event.data == 'END_IMAGE') {
- var decoder = new TextDecoder("ascii");
- var array_buffer = new Uint8Array(buffer);
- var str = decoder.decode(array_buffer);
- let img = document.getElementById("image");
- img.src = 'data:image/png;base64, ' + str;
- }
- } else {
- var i, len = buffer.length
- var view = new DataView(event.data);
- for (i = 0; i < view.byteLength; i++) {
- buffer[len + i] = view.getUint8(i);
- }
- }
- }
- }
-
- this.peer_connection.onicecandidate = (event) => {
- if (event.candidate == null) {
- console.log("ICE Candidate was null, done");
- return;
- }
- this.ws_conn.send(JSON.stringify({
- "type": "peer",
- "sessionId": this.id,
- "ice": event.candidate.toJSON()
- }));
- };
-
- this.setStatus("Created peer connection for call, waiting for SDP");
- };
-
- document.getElementById("stream-" + this.our_id).addEventListener("playing", this.streamIsPlaying.bind(this), false);
-
- this.websocketServerConnect();
-}
-
-function startSession() {
- var peer_id = document.getElementById("camera-id").value;
-
- if (peer_id === "") {
- return;
- }
-
- sessions[peer_id] = new Session(peer_id);
-}
-
-function session_closed(peer_id) {
- sessions[peer_id] = null;
-}
-
-function addPeer(peer_id, meta) {
- console.log("Meta: ", JSON.stringify(meta));
-
- var nav_ul = document.getElementById("camera-list");
-
- meta = meta ? meta : {"display-name": peer_id};
- let display_html = `${meta["display-name"] ? meta["display-name"] : peer_id}
`;
- for (const key in meta) {
- if (key != "display-name") {
- display_html += `
- ${key}: ${meta[key]}
`;
- }
- }
- display_html += "
"
-
- var li_str = '';
-
- nav_ul.insertAdjacentHTML('beforeend', li_str);
- var li = document.getElementById("peer-" + peer_id);
- li.onclick = function(e) {
- var sessions_div = document.getElementById('sessions');
- var our_id = getOurId();
- var session_div_str = '