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 + + + + + +
+

GstWebRTC API

+
+
+
+ none + +
+
+
+
+
+
+
+ +
+
+
+

Remote Streams

+
    +
    +
    + + + diff --git a/net/webrtc/gstwebrtc-api/package.json b/net/webrtc/gstwebrtc-api/package.json new file mode 100644 index 00000000..260b9d32 --- /dev/null +++ b/net/webrtc/gstwebrtc-api/package.json @@ -0,0 +1,59 @@ +{ + "name": "gstwebrtc-api", + "version": "1.0.0", + "description": "Javascript API to integrate GStreamer WebRTC streams (webrtcsrc/webrtcsink) in a web browser", + "keywords": [ + "webrtc", + "multimedia", + "realtime", + "gstreamer", + "audio", + "video" + ], + "homepage": "https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/tree/main/net/webrtc/gstwebrtc-api", + "bugs": { + "url": "https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/issues" + }, + "license": "MPL-2.0", + "author": { + "name": "Loïc Le Page", + "email": "llepage@igalia.com", + "url": "https://www.igalia.com/" + }, + "repository": { + "type": "git", + "url": "https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs.git", + "directory": "net/webrtc/gstwebrtc-api" + }, + "browser": "dist/gstwebrtc-api-${npm_package_version}.min.js", + "files": [ + "dist/", + "docs/", + "src/", + "third-party/", + "index.html", + "webpack.config.js" + ], + "devDependencies": { + "eslint": "8.37.0", + "html-webpack-plugin": "5.5.0", + "jsdoc": "4.0.2", + "rimraf": "4.4.1", + "terser-webpack-plugin": "5.3.7", + "webpack": "5.77.0", + "webpack-cli": "5.0.1", + "webpack-dev-server": "4.13.1" + }, + "dependencies": { + "webrtc-adapter": "8.2.2" + }, + "scripts": { + "check": "eslint src", + "format": "eslint --fix --fix-type layout src", + "build": "rimraf dist && webpack", + "docs": "rimraf docs && jsdoc src/*.js -d docs/ -p package.json -R README.md", + "make": "npm run check && npm run build && npm run docs", + "prepack": "npm run make", + "start": "webpack serve" + } +} diff --git a/net/webrtc/gstwebrtc-api/src/com-channel.js b/net/webrtc/gstwebrtc-api/src/com-channel.js new file mode 100644 index 00000000..92ac999b --- /dev/null +++ b/net/webrtc/gstwebrtc-api/src/com-channel.js @@ -0,0 +1,334 @@ +/* + * 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 ConsumerSession from "./consumer-session"; +import ProducerSession from "./producer-session"; + +const SignallingServerMessageType = Object.freeze({ + welcome: "welcome", + peerStatusChanged: "peerStatusChanged", + list: "list", + sessionStarted: "sessionStarted", + peer: "peer", + startSession: "startSession", + endSession: "endSession", + error: "error" +}); + +function normalizeProducer(producer, excludedId) { + if (!producer || (typeof (producer) !== "object")) { + return null; + } + + const normalizedProducer = { + id: "", + meta: {} + }; + + if (producer.id && (typeof (producer.id) === "string")) { + normalizedProducer.id = producer.id; + } else if (producer.peerId && (typeof (producer.peerId) === "string")) { + normalizedProducer.id = producer.peerId; + } else { + return null; + } + + if (normalizedProducer.id === excludedId) { + return null; + } + + if (producer.meta && (typeof (producer.meta) === "object")) { + normalizedProducer.meta = producer.meta; + } + + Object.freeze(normalizedProducer.meta); + return Object.freeze(normalizedProducer); +} + +export default class ComChannel extends EventTarget { + constructor(url, meta, webrtcConfig) { + super(); + + this._meta = meta; + this._webrtcConfig = webrtcConfig; + this._ws = new WebSocket(url); + this._ready = false; + this._channelId = ""; + this._producerSession = null; + this._consumerSessions = {}; + + this._ws.onerror = (event) => { + this.dispatchEvent(new ErrorEvent("error", { + message: event.message || "WebSocket error", + error: event.error || new Error( + this._ready ? "transportation error" : "cannot connect to signaling server") + })); + this.close(); + }; + + this._ws.onclose = () => { + this._ready = false; + this._channelId = ""; + this._ws = null; + + this.closeAllConsumerSessions(); + + if (this._producerSession) { + this._producerSession.close(); + this._producerSession = null; + } + + this.dispatchEvent(new Event("closed")); + }; + + this._ws.onmessage = (event) => { + try { + const msg = JSON.parse(event.data); + if (msg && (typeof (msg) === "object")) { + switch (msg.type) { + + case SignallingServerMessageType.welcome: + this._channelId = msg.peerId; + try { + this._ws.send(JSON.stringify({ + type: "setPeerStatus", + roles: ["listener"], + meta: meta + })); + } catch (ex) { + this.dispatchEvent(new ErrorEvent("error", { + message: "cannot initialize connection to signaling server", + error: ex + })); + this.close(); + } + break; + + case SignallingServerMessageType.peerStatusChanged: + if (msg.peerId === this._channelId) { + if (!this._ready && msg.roles.includes("listener")) { + this._ready = true; + this.dispatchEvent(new Event("ready")); + this.send({ type: "list" }); + } + + if (this._producerSession && msg.roles.includes("producer")) { + this._producerSession.onProducerRegistered(); + } + } else { + const normalizedProducer = normalizeProducer(msg, this._channelId); + if (normalizedProducer) { + if (msg.roles.includes("producer")) { + this.dispatchEvent(new CustomEvent("producerAdded", { detail: normalizedProducer })); + } else { + this.dispatchEvent(new CustomEvent("producerRemoved", { detail: normalizedProducer })); + } + } + } + break; + + case SignallingServerMessageType.list: + for (const producer of msg.producers) { + const normalizedProducer = normalizeProducer(producer, this._channelId); + if (normalizedProducer) { + this.dispatchEvent(new CustomEvent("producerAdded", { detail: normalizedProducer })); + } + } + break; + + case SignallingServerMessageType.sessionStarted: + { + const session = this.getConsumerSession(msg.peerId); + if (session) { + delete this._consumerSessions[msg.peerId]; + + session.onSessionStarted(msg.peerId, msg.sessionId); + if (session.sessionId && !(session.sessionId in this._consumerSessions)) { + this._consumerSessions[session.sessionId] = session; + } else { + session.close(); + } + } + } + break; + + case SignallingServerMessageType.peer: + { + const session = this.getConsumerSession(msg.sessionId); + if (session) { + session.onSessionPeerMessage(msg); + } else if (this._producerSession) { + this._producerSession.onSessionPeerMessage(msg); + } + } + break; + + case SignallingServerMessageType.startSession: + if (this._producerSession) { + this._producerSession.onStartSessionMessage(msg); + } + break; + + case SignallingServerMessageType.endSession: + { + const session = this.getConsumerSession(msg.sessionId); + if (session) { + session.close(); + } else if (this._producerSession) { + this._producerSession.onEndSessionMessage(msg); + } + } + break; + + case SignallingServerMessageType.error: + this.dispatchEvent(new ErrorEvent("error", { + message: "error received from signaling server", + error: new Error(msg.details) + })); + break; + + default: + throw new Error(`unknown message type: "${msg.type}"`); + } + } + } catch (ex) { + this.dispatchEvent(new ErrorEvent("error", { + message: "cannot parse incoming message from signaling server", + error: ex + })); + } + }; + } + + get meta() { + return this._meta; + } + + get webrtcConfig() { + return this._webrtcConfig; + } + + get ready() { + return this._ready; + } + + get channelId() { + return this._channelId; + } + + get producerSession() { + return this._producerSession; + } + + createProducerSession(stream) { + if (!this._ready || !(stream instanceof MediaStream)) { + return null; + } + + if (this._producerSession) { + if (this._producerSession.stream === stream) { + return this._producerSession; + } else { + return null; + } + } + + const session = new ProducerSession(this, stream); + this._producerSession = session; + + session.addEventListener("closed", () => { + if (this._producerSession === session) { + this._producerSession = null; + } + }); + + return session; + } + + createConsumerSession(producerId) { + if (!this._ready || !producerId || (typeof (producerId) !== "string")) { + return null; + } + + if (producerId in this._consumerSessions) { + return this._consumerSessions[producerId]; + } + + for (const session of Object.values(this._consumerSessions)) { + if (session.peerId === producerId) { + return session; + } + } + + const session = new ConsumerSession(producerId, this); + this._consumerSessions[producerId] = session; + + session.addEventListener("closed", (event) => { + let sessionId = event.target.sessionId; + if (!sessionId) { + sessionId = event.target.peerId; + } + + if ((sessionId in this._consumerSessions) && (this._consumerSessions[sessionId] === session)) { + delete this._consumerSessions[sessionId]; + } + }); + + return session; + } + + getConsumerSession(sessionId) { + if (sessionId in this._consumerSessions) { + return this._consumerSessions[sessionId]; + } else { + return null; + } + } + + closeAllConsumerSessions() { + for (const session of Object.values(this._consumerSessions)) { + session.close(); + } + + this._consumerSessions = {}; + } + + send(data) { + if (this._ready && data && (typeof (data) === "object")) { + try { + this._ws.send(JSON.stringify(data)); + return true; + } catch (ex) { + this.dispatchEvent(new ErrorEvent("error", { + message: "cannot send message to signaling server", + error: ex + })); + } + } + + return false; + } + + close() { + if (this._ws) { + this._ready = false; + this._channelId = ""; + this._ws.close(); + + this.closeAllConsumerSessions(); + + if (this._producerSession) { + this._producerSession.close(); + this._producerSession = null; + } + } + } +} diff --git a/net/webrtc/gstwebrtc-api/src/config.js b/net/webrtc/gstwebrtc-api/src/config.js new file mode 100644 index 00000000..5335ea45 --- /dev/null +++ b/net/webrtc/gstwebrtc-api/src/config.js @@ -0,0 +1,53 @@ +/* + * 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/. + */ + +/** + * GStreamer WebRTC configuration. + *

    You can override default values by defining configuration before receiving the DOMContentLoaded event.
    + * Once the DOMContentLoaded event triggered, changing configuration will have no effect.

    + *

    For example: + *

    + *     const signalingProtocol = window.location.protocol.startsWith("https") ? "wss" : "ws";
    + *     window.gstWebRTCConfig = {
    + *         meta: { name: `WebClient-${Date.now()}` },
    + *         signalingServerUrl: `${signalingProtocol}://${window.location.host}/webrtc`
    + *     };
    + * 

    + * @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.

    + * @function + * @memberof gstWebRTCAPI + * @instance + * @param {string} producerId - The unique identifier of the remote producer to connect to. + * @returns {gstWebRTCAPI.ConsumerSession} The WebRTC session between the selected remote producer and this local + * consumer, or null in case of error. To start connecting and receiving the remote streams, you still need to call + * {@link gstWebRTCAPI.ConsumerSession#connect} after adding on the returned session all the event listeners you may + * need. + */ +function createConsumerSession(producerId) { + if (apiState.channel) { + return apiState.channel.createConsumerSession(producerId); + } else { + return null; + } +} + +/** + * The GStreamer WebRTC Javascript API. + * @namespace gstWebRTCAPI + */ +const gstWebRTCAPI = Object.freeze({ + SessionState: SessionState, + registerConnectionListener: registerConnectionListener, + unregisterConnectionListener: unregisterConnectionListener, + unregisterAllConnectionListeners: unregisterAllConnectionListeners, + createProducerSession: createProducerSession, + getAvailableProducers: getAvailableProducers, + registerProducersListener: registerProducersListener, + unregisterProducersListener: unregisterProducersListener, + unregisterAllProducersListeners: unregisterAllProducersListeners, + createConsumerSession: createConsumerSession +}); + +function triggerConnected(clientId) { + for (const listener of apiState.connectionListeners) { + try { + listener.connected(clientId); + } catch (ex) { + console.error("a listener callback should not throw any exception", ex); + } + } +} + +function triggerDisconnected() { + for (const listener of apiState.connectionListeners) { + try { + listener.disconnected(); + } catch (ex) { + console.error("a listener callback should not throw any exception", ex); + } + } +} + +function triggerProducerAdded(producer) { + if (producer.id in apiState.producers) { + return; + } + + apiState.producers[producer.id] = producer; + for (const listener of apiState.producersListeners) { + try { + listener.producerAdded(producer); + } catch (ex) { + console.error("a listener callback should not throw any exception", ex); + } + } +} + +function triggerProducerRemoved(producerId) { + if (producerId in apiState.producers) { + const producer = apiState.producers[producerId]; + delete apiState.producers[producerId]; + + for (const listener of apiState.producersListeners) { + try { + listener.producerRemoved(producer); + } catch (ex) { + console.error("a listener callback should not throw any exception", ex); + } + } + } +} + +function connectChannel() { + if (apiState.channel) { + const oldChannel = apiState.channel; + apiState.channel = null; + oldChannel.close(); + for (const key in apiState.producers) { + triggerProducerRemoved(key); + } + apiState.producers = {}; + triggerDisconnected(); + } + + apiState.channel = new ComChannel( + apiState.config.signalingServerUrl, + apiState.config.meta, + apiState.config.webrtcConfig); + + apiState.channel.addEventListener("error", (event) => { + if (event.target === apiState.channel) { + console.error(event.message, event.error); + } + }); + + apiState.channel.addEventListener("closed", (event) => { + if (event.target === apiState.channel) { + apiState.channel = null; + for (const key in apiState.producers) { + triggerProducerRemoved(key); + } + apiState.producers = {}; + triggerDisconnected(); + + if (apiState.config.reconnectionTimeout > 0) { + window.setTimeout(connectChannel, apiState.config.reconnectionTimeout); + } + } + }); + + apiState.channel.addEventListener("ready", (event) => { + if (event.target === apiState.channel) { + triggerConnected(apiState.channel.channelId); + } + }); + + apiState.channel.addEventListener("producerAdded", (event) => { + if (event.target === apiState.channel) { + triggerProducerAdded(event.detail); + } + }); + + apiState.channel.addEventListener("producerRemoved", (event) => { + if (event.target === apiState.channel) { + triggerProducerRemoved(event.detail.id); + } + }); +} + +function start(userConfig) { + if (apiState.config) { + throw new Error("GstWebRTC API is already started"); + } + + const config = Object.assign({}, defaultConfig); + if (userConfig && (typeof (userConfig) === "object")) { + Object.assign(config, userConfig); + } + + if (typeof (config.meta) !== "object") { + config.meta = null; + } + + apiState.config = config; + connectChannel(); +} + +export { gstWebRTCAPI, start }; diff --git a/net/webrtc/gstwebrtc-api/src/index.js b/net/webrtc/gstwebrtc-api/src/index.js new file mode 100644 index 00000000..880b587c --- /dev/null +++ b/net/webrtc/gstwebrtc-api/src/index.js @@ -0,0 +1,57 @@ +/* + * 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 "webrtc-adapter"; +import { gstWebRTCAPI, start } from "./gstwebrtc-api"; + +/** + * @external MediaStream + * @see https://developer.mozilla.org/en-US/docs/Web/API/MediaStream + */ +/** + * @external RTCPeerConnection + * @see https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection + */ +/** + * @external RTCDataChannel + * @see https://developer.mozilla.org/en-US/docs/Web/API/RTCDataChannel + */ +/** + * @external EventTarget + * @see https://developer.mozilla.org/en-US/docs/Web/API/EventTarget + */ +/** + * @external Event + * @see https://developer.mozilla.org/en-US/docs/Web/API/Event + */ +/** + * @external ErrorEvent + * @see https://developer.mozilla.org/en-US/docs/Web/API/ErrorEvent + */ +/** + * @external CustomEvent + * @see https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent + */ +/** + * @external Error + * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error + */ +/** + * @external HTMLVideoElement + * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLVideoElement + */ + +if (!window.gstWebRTCAPI) { + window.gstWebRTCAPI = gstWebRTCAPI; + window.addEventListener("DOMContentLoaded", () => { + start(window.gstWebRTCConfig); + }); +} diff --git a/net/webrtc/gstwebrtc-api/src/keysyms.js b/net/webrtc/gstwebrtc-api/src/keysyms.js new file mode 100644 index 00000000..5eeefc82 --- /dev/null +++ b/net/webrtc/gstwebrtc-api/src/keysyms.js @@ -0,0 +1,2021 @@ +/* + * 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/. + */ + +const keysymsToStrings = Object.freeze({ + 0x0020: "space", + 0x0021: "exclam", + 0x0022: "quotedbl", + 0x0023: "numbersign", + 0x0024: "dollar", + 0x0025: "percent", + 0x0026: "ampersand", + 0x0027: "apostrophe", + 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", + 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", + 0x00D1: "Ntilde", + 0x00D2: "Ograve", + 0x00D3: "Oacute", + 0x00D4: "Ocircumflex", + 0x00D5: "Otilde", + 0x00D6: "Odiaeresis", + 0x00D7: "multiply", + 0x00D8: "Ooblique", + 0x00D9: "Ugrave", + 0x00DA: "Uacute", + 0x00DB: "Ucircumflex", + 0x00DC: "Udiaeresis", + 0x00DD: "Yacute", + 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: "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: "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", + 0x047E: "overline", + 0x04A1: "kana_fullstop", + 0x04A2: "kana_openingbracket", + 0x04A3: "kana_closingbracket", + 0x04A4: "kana_comma", + 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", + 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", + 0x04C2: "kana_TSU", + 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", + 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", + 0x05AC: "Arabic_comma", + 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", + 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", + 0x06A1: "Serbian_dje", + 0x06A2: "Macedonia_gje", + 0x06A3: "Cyrillic_io", + 0x06A4: "Ukranian_je", + 0x06A5: "Macedonia_dse", + 0x06A6: "Ukranian_i", + 0x06A7: "Ukranian_yi", + 0x06A8: "Serbian_je", + 0x06A9: "Serbian_lje", + 0x06AA: "Serbian_nje", + 0x06AB: "Serbian_tshe", + 0x06AC: "Macedonia_kje", + 0x06AD: "Ukrainian_ghe_with_upturn", + 0x06AE: "Byelorussian_shortu", + 0x06AF: "Serbian_dze", + 0x06B0: "numerosign", + 0x06B1: "Serbian_DJE", + 0x06B2: "Macedonia_GJE", + 0x06B3: "Cyrillic_IO", + 0x06B4: "Ukranian_JE", + 0x06B5: "Macedonia_DSE", + 0x06B6: "Ukranian_I", + 0x06B7: "Ukranian_YI", + 0x06B8: "Serbian_JE", + 0x06B9: "Serbian_LJE", + 0x06BA: "Serbian_NJE", + 0x06BB: "Serbian_TSHE", + 0x06BC: "Macedonia_KJE", + 0x06BD: "Ukrainian_GHE_WITH_UPTURN", + 0x06BE: "Byelorussian_SHORTU", + 0x06BF: "Cyrillic_DZHE", + 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_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_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_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", + 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_beth", + 0x0CE2: "hebrew_gimmel", + 0x0CE3: "hebrew_daleth", + 0x0CE4: "hebrew_he", + 0x0CE5: "hebrew_waw", + 0x0CE6: "hebrew_zayin", + 0x0CE7: "hebrew_chet", + 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_samekh", + 0x0CF2: "hebrew_ayin", + 0x0CF3: "hebrew_finalpe", + 0x0CF4: "hebrew_pe", + 0x0CF5: "hebrew_finalzadi", + 0x0CF6: "hebrew_zadi", + 0x0CF7: "hebrew_kuf", + 0x0CF8: "hebrew_resh", + 0x0CF9: "hebrew_shin", + 0x0CFA: "hebrew_taf", + 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", + 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", + 0x13BC: "OE", + 0x13BD: "oe", + 0x13BE: "Ydiaeresis", + 0x20AC: "EuroSign", + 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", + 0xFE01: "ISO_Lock", + 0xFE02: "ISO_Level2_Latch", + 0xFE03: "ISO_Level3_Shift", + 0xFE04: "ISO_Level3_Latch", + 0xFE05: "ISO_Level3_Lock", + 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", + 0xFE11: "ISO_Level5_Shift", + 0xFE12: "ISO_Level5_Latch", + 0xFE13: "ISO_Level5_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", + 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", + 0xFE65: "dead_abovereversedcomma", + 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", + 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", + 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", + 0xFE90: "dead_lowline", + 0xFE91: "dead_aboveverticalline", + 0xFE92: "dead_belowverticalline", + 0xFE93: "dead_longsolidusoverlay", + 0xFEA0: "ch", + 0xFEA1: "Ch", + 0xFEA2: "CH", + 0xFEA3: "c_h", + 0xFEA4: "C_h", + 0xFEA5: "C_H", + 0xFED0: "First_Virtual_Screen", + 0xFED1: "Prev_Virtual_Screen", + 0xFED2: "Next_Virtual_Screen", + 0xFED4: "Last_Virtual_Screen", + 0xFED5: "Terminate_Server", + 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", + 0xFEF9: "Pointer_EnableKeys", + 0xFEFA: "Pointer_Accelerate", + 0xFEFB: "Pointer_DfltBtnNext", + 0xFEFC: "Pointer_DfltBtnPrev", + 0xFEFD: "Pointer_Drag5", + 0xFF08: "BackSpace", + 0xFF09: "Tab", + 0xFF0A: "Linefeed", + 0xFF0B: "Clear", + 0xFF0D: "Return", + 0xFF13: "Pause", + 0xFF14: "Scroll_Lock", + 0xFF15: "Sys_Req", + 0xFF1B: "Escape", + 0xFF20: "Multi_key", + 0xFF21: "Kanji", + 0xFF22: "Muhenkan", + 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", + 0xFF31: "Hangul", + 0xFF32: "Hangul_Start", + 0xFF33: "Hangul_End", + 0xFF34: "Hangul_Hanja", + 0xFF35: "Hangul_Jamo", + 0xFF36: "Hangul_Romaja", + 0xFF37: "Codeinput", + 0xFF38: "Hangul_Jeonja", + 0xFF39: "Hangul_Banja", + 0xFF3A: "Hangul_PreHanja", + 0xFF3B: "Hangul_PostHanja", + 0xFF3C: "SingleCandidate", + 0xFF3D: "MultipleCandidate", + 0xFF3E: "PreviousCandidate", + 0xFF3F: "Hangul_Special", + 0xFF50: "Home", + 0xFF51: "Left", + 0xFF52: "Up", + 0xFF53: "Right", + 0xFF54: "Down", + 0xFF55: "Page_Up", + 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", + 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_Page_Up", + 0xFF9B: "KP_Page_Down", + 0xFF9C: "KP_End", + 0xFF9D: "KP_Begin", + 0xFF9E: "KP_Insert", + 0xFF9F: "KP_Delete", + 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", + 0xFFBD: "KP_Equal", + 0xFFBE: "F1", + 0xFFBF: "F2", + 0xFFC0: "F3", + 0xFFC1: "F4", + 0xFFC2: "F5", + 0xFFC3: "F6", + 0xFFC4: "F7", + 0xFFC5: "F8", + 0xFFC6: "F9", + 0xFFC7: "F10", + 0xFFC8: "L1", + 0xFFC9: "L2", + 0xFFCA: "L3", + 0xFFCB: "L4", + 0xFFCC: "L5", + 0xFFCD: "L6", + 0xFFCE: "L7", + 0xFFCF: "L8", + 0xFFD0: "L9", + 0xFFD1: "L10", + 0xFFD2: "R1", + 0xFFD3: "R2", + 0xFFD4: "R3", + 0xFFD5: "R4", + 0xFFD6: "R5", + 0xFFD7: "R6", + 0xFFD8: "R7", + 0xFFD9: "R8", + 0xFFDA: "R9", + 0xFFDB: "R10", + 0xFFDC: "R11", + 0xFFDD: "R12", + 0xFFDE: "R13", + 0xFFDF: "R14", + 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", + 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", + 0xFFFF: "Delete", + 0xFFFFFF: "VoidSymbol", + 0x100012C: "Ibreve", + 0x100012D: "ibreve", + 0x1000174: "Wcircumflex", + 0x1000175: "wcircumflex", + 0x1000176: "Ycircumflex", + 0x1000177: "ycircumflex", + 0x100018F: "SCHWA", + 0x100019F: "Obarred", + 0x10001A0: "Ohorn", + 0x10001A1: "ohorn", + 0x10001AF: "Uhorn", + 0x10001B0: "uhorn", + 0x10001B5: "Zstroke", + 0x10001B6: "zstroke", + 0x10001B7: "EZH", + 0x10001D1: "Ocaron", + 0x10001D2: "ocaron", + 0x10001E6: "Gcaron", + 0x10001E7: "gcaron", + 0x1000259: "schwa", + 0x1000275: "obarred", + 0x1000292: "ezh", + 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", + 0x1000531: "Armenian_AYB", + 0x1000532: "Armenian_BEN", + 0x1000533: "Armenian_GIM", + 0x1000534: "Armenian_DA", + 0x1000535: "Armenian_YECH", + 0x1000536: "Armenian_ZA", + 0x1000537: "Armenian_E", + 0x1000538: "Armenian_AT", + 0x1000539: "Armenian_TO", + 0x100053A: "Armenian_ZHE", + 0x100053B: "Armenian_INI", + 0x100053C: "Armenian_LYUN", + 0x100053D: "Armenian_KHE", + 0x100053E: "Armenian_TSA", + 0x100053F: "Armenian_KEN", + 0x1000540: "Armenian_HO", + 0x1000541: "Armenian_DZA", + 0x1000542: "Armenian_GHAT", + 0x1000543: "Armenian_TCHE", + 0x1000544: "Armenian_MEN", + 0x1000545: "Armenian_HI", + 0x1000546: "Armenian_NU", + 0x1000547: "Armenian_SHA", + 0x1000548: "Armenian_VO", + 0x1000549: "Armenian_CHA", + 0x100054A: "Armenian_PE", + 0x100054B: "Armenian_JE", + 0x100054C: "Armenian_RA", + 0x100054D: "Armenian_SE", + 0x100054E: "Armenian_VEV", + 0x100054F: "Armenian_TYUN", + 0x1000550: "Armenian_RE", + 0x1000551: "Armenian_TSO", + 0x1000552: "Armenian_VYUN", + 0x1000553: "Armenian_PYUR", + 0x1000554: "Armenian_KE", + 0x1000555: "Armenian_O", + 0x1000556: "Armenian_FE", + 0x100055A: "Armenian_apostrophe", + 0x100055B: "Armenian_accent", + 0x100055C: "Armenian_amanak", + 0x100055D: "Armenian_but", + 0x100055E: "Armenian_paruyk", + 0x1000561: "Armenian_ayb", + 0x1000562: "Armenian_ben", + 0x1000563: "Armenian_gim", + 0x1000564: "Armenian_da", + 0x1000565: "Armenian_yech", + 0x1000566: "Armenian_za", + 0x1000567: "Armenian_e", + 0x1000568: "Armenian_at", + 0x1000569: "Armenian_to", + 0x100056A: "Armenian_zhe", + 0x100056B: "Armenian_ini", + 0x100056C: "Armenian_lyun", + 0x100056D: "Armenian_khe", + 0x100056E: "Armenian_tsa", + 0x100056F: "Armenian_ken", + 0x1000570: "Armenian_ho", + 0x1000571: "Armenian_dza", + 0x1000572: "Armenian_ghat", + 0x1000573: "Armenian_tche", + 0x1000574: "Armenian_men", + 0x1000575: "Armenian_hi", + 0x1000576: "Armenian_nu", + 0x1000577: "Armenian_sha", + 0x1000578: "Armenian_vo", + 0x1000579: "Armenian_cha", + 0x100057A: "Armenian_pe", + 0x100057B: "Armenian_je", + 0x100057C: "Armenian_ra", + 0x100057D: "Armenian_se", + 0x100057E: "Armenian_vev", + 0x100057F: "Armenian_tyun", + 0x1000580: "Armenian_re", + 0x1000581: "Armenian_tso", + 0x1000582: "Armenian_vyun", + 0x1000583: "Armenian_pyur", + 0x1000584: "Armenian_ke", + 0x1000585: "Armenian_o", + 0x1000586: "Armenian_fe", + 0x1000587: "Armenian_ligature_ew", + 0x1000589: "Armenian_full_stop", + 0x100058A: "Armenian_hyphen", + 0x1000653: "Arabic_madda_above", + 0x1000654: "Arabic_hamza_above", + 0x1000655: "Arabic_hamza_below", + 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", + 0x100066A: "Arabic_percent", + 0x1000670: "Arabic_superscript_alef", + 0x1000679: "Arabic_tteh", + 0x100067E: "Arabic_peh", + 0x1000686: "Arabic_tcheh", + 0x1000688: "Arabic_ddal", + 0x1000691: "Arabic_rreh", + 0x1000698: "Arabic_jeh", + 0x10006A4: "Arabic_veh", + 0x10006A9: "Arabic_keheh", + 0x10006AF: "Arabic_gaf", + 0x10006BA: "Arabic_noon_ghunna", + 0x10006BE: "Arabic_heh_doachashmee", + 0x10006C1: "Arabic_heh_goal", + 0x10006CC: "Arabic_farsi_yeh", + 0x10006D2: "Arabic_yeh_baree", + 0x10006D4: "Arabic_fullstop", + 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", + 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", + 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", + 0x1001E02: "Babovedot", + 0x1001E03: "babovedot", + 0x1001E0A: "Dabovedot", + 0x1001E0B: "dabovedot", + 0x1001E1E: "Fabovedot", + 0x1001E1F: "fabovedot", + 0x1001E36: "Lbelowdot", + 0x1001E37: "lbelowdot", + 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", + 0x1001E8A: "Xabovedot", + 0x1001E8B: "xabovedot", + 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", + 0x1001EF2: "Ygrave", + 0x1001EF3: "ygrave", + 0x1001EF4: "Ybelowdot", + 0x1001EF5: "ybelowdot", + 0x1001EF6: "Yhook", + 0x1001EF7: "yhook", + 0x1001EF8: "Ytilde", + 0x1001EF9: "ytilde", + 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", + 0x10020A0: "EcuSign", + 0x10020A1: "ColonSign", + 0x10020A2: "CruzeiroSign", + 0x10020A3: "FFrancSign", + 0x10020A4: "LiraSign", + 0x10020A5: "MillSign", + 0x10020A6: "NairaSign", + 0x10020A7: "PesetaSign", + 0x10020A8: "RupeeSign", + 0x10020A9: "WonSign", + 0x10020AA: "NewSheqelSign", + 0x10020AB: "DongSign", + 0x1002202: "partdifferential", + 0x1002205: "emptyset", + 0x1002208: "elementof", + 0x1002209: "notelementof", + 0x100220B: "containsas", + 0x100221A: "squareroot", + 0x100221B: "cuberoot", + 0x100221C: "fourthroot", + 0x100222C: "dintegral", + 0x100222D: "tintegral", + 0x1002235: "because", + 0x1002247: "notapproxeq", + 0x1002248: "approxeq", + 0x1002262: "notidentical", + 0x1002263: "stricteq", + 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" +}); + +export default function getKeysymString(keysym) { + return keysymsToStrings[keysym]; +} diff --git a/net/webrtc/gstwebrtc-api/src/producer-session.js b/net/webrtc/gstwebrtc-api/src/producer-session.js new file mode 100644 index 00000000..e278bdd1 --- /dev/null +++ b/net/webrtc/gstwebrtc-api/src/producer-session.js @@ -0,0 +1,279 @@ +/* + * 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"; + +/** + * @class gstWebRTCAPI.ClientSession + * @hideconstructor + * @classdesc Client session representing a link between a remote consumer and a local producer session. + * @extends {gstWebRTCAPI.WebRTCSession} + */ +class ClientSession extends WebRTCSession { + constructor(peerId, sessionId, comChannel, stream) { + super(peerId, comChannel); + this._sessionId = sessionId; + this._state = SessionState.streaming; + + const connection = new RTCPeerConnection(this._comChannel.webrtcConfig); + this._rtcPeerConnection = connection; + + for (const track of stream.getTracks()) { + connection.addTrack(track, stream); + } + + 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")); + + connection.setLocalDescription().then(() => { + if ((this._rtcPeerConnection === connection) && 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(); + } + }); + } + + onSessionPeerMessage(msg) { + if ((this._state === SessionState.closed) || !this._rtcPeerConnection) { + return; + } + + if (msg.sdp) { + this._rtcPeerConnection.setRemoteDescription(msg.sdp).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 producer's client session ${this._peerId}`); + } + } +} + +/** + * Event name: "clientConsumerAdded".
    + * Triggered when a remote consumer peer connects to a local {@link gstWebRTCAPI.ProducerSession}. + * @event gstWebRTCAPI#ClientConsumerAddedEvent + * @type {external:CustomEvent} + * @property {gstWebRTCAPI.ClientSession} detail - The WebRTC session associated with the added consumer peer. + * @see gstWebRTCAPI.ProducerSession + */ +/** + * Event name: "clientConsumerRemoved".
    + * Triggered when a remote consumer peer disconnects from a local {@link gstWebRTCAPI.ProducerSession}. + * @event gstWebRTCAPI#ClientConsumerRemovedEvent + * @type {external:CustomEvent} + * @property {gstWebRTCAPI.ClientSession} detail - The WebRTC session associated with the removed consumer peer. + * @see gstWebRTCAPI.ProducerSession + */ + +/** + * @class gstWebRTCAPI.ProducerSession + * @hideconstructor + * @classdesc Producer session managing the streaming out of a local {@link external:MediaStream}.
    + * It manages all underlying WebRTC connections to each peer client consuming the stream. + *

    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 = '

    Status: unknown

    ' - sessions_div.insertAdjacentHTML('beforeend', session_div_str); - sessions[peer_id] = new Session(our_id, peer_id, session_closed); - } -} - -function clearPeers() { - var nav_ul = document.getElementById("camera-list"); - - while (nav_ul.firstChild) { - nav_ul.removeChild(nav_ul.firstChild); - } -} - -function onServerMessage(event) { - console.log("Received " + event.data); - - try { - msg = JSON.parse(event.data); - } catch (e) { - if (e instanceof SyntaxError) { - console.error("Error parsing incoming JSON: " + event.data); - } else { - console.error("Unknown error parsing response: " + event.data); - } - return; - } - - if (msg.type == "welcome") { - console.info(`Got welcomed with ID ${msg.peer_id}`); - ws_conn.send(JSON.stringify({ - "type": "list" - })); - } else if (msg.type == "list") { - clearPeers(); - for (i = 0; i < msg.producers.length; i++) { - addPeer(msg.producers[i].id, msg.producers[i].meta); - } - } else if (msg.type == "peerStatusChanged") { - var li = document.getElementById("peer-" + msg.peerId); - if (msg.roles.includes("producer")) { - if (li == null) { - console.error('Adding peer'); - addPeer(msg.peerId, msg.meta); - } - } else if (li != null) { - li.parentNode.removeChild(li); - } - } else { - console.error("Unsupported message: ", msg); - } -}; - -function clearConnection() { - ws_conn.removeEventListener('error', onServerError); - ws_conn.removeEventListener('message', onServerMessage); - ws_conn.removeEventListener('close', onServerClose); - ws_conn = null; -} - -function onServerClose(event) { - clearConnection(); - clearPeers(); - console.log("Close"); - window.setTimeout(connect, 1000); -}; - -function onServerError(event) { - clearConnection(); - clearPeers(); - console.log("Error", event); - window.setTimeout(connect, 1000); -}; - -function connect() { - 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 - console.log("Connecting listener"); - ws_conn = new WebSocket(ws_url); - ws_conn.addEventListener('open', (event) => { - ws_conn.send(JSON.stringify({ - "type": "setPeerStatus", - "roles": ["listener"] - })); - }); - ws_conn.addEventListener('error', onServerError); - ws_conn.addEventListener('message', onServerMessage); - ws_conn.addEventListener('close', onServerClose); -} - -function setup() { - connect(); -}