diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index 29e38b5a..00000000 --- a/.eslintignore +++ /dev/null @@ -1 +0,0 @@ -examples/a-saturday-night diff --git a/.eslintrc.json b/.eslintrc.json deleted file mode 100644 index 230cb1ef..00000000 --- a/.eslintrc.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "env": { - "browser": true, - "commonjs": true, - "es6": true, - "node": true - }, - "extends": "eslint:recommended", - "rules": { - "no-unused-vars": ["error", { "args": "none" }] - } -} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index 6c550257..00000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,89 +0,0 @@ -[slack]: https://aframevr.slack.com/join/shared_invite/zt-f6rne3ly-ekVaBU~Xu~fsZHXr56jacQ -[issues]: https://github.com/networked-aframe/networked-aframe/issues - -Interested in contributing? As an open source project, we'd appreciate any help -and contributions! On top of the Networked-Aframe framework itself, you can also -contribute to this related component: - -- [buffered-interpolation](https://github.com/InfiniteLee/buffered-interpolation) - -# Join the Community on Slack - -1. [Invite yourself][slack] to the A-Frame Slack channel. -2. [Join the A-Frame slack](https://aframevr.slack.com) -3. Join the #networked-aframe channel - -# Get Help or Ask a Question - -If you're not sure how to do something with Networked-Aframe, please post a question -(and any code you've tried so far) to [Github Issues][issues]. - -# File an Issue - -1. Search the [issue tracker][issues] for similar issues. -2. Specify the version of Networked-Aframe in which the bug occurred. -3. Specify information about your browser and system (e.g., "Firefox Nightly on OS X") -4. Describe the problem in detail (i.e., what happened and what you expected would happen). -5. If possible, provide a small test case with [Glitch](http://glitch.com), a link to your application, and/or a screenshot. You can fork/remix this [Glitch project](https://glitch.com/edit/#!/naf-project). - -# Contribute Code to Networked-Aframe - -[README]: https://github.com/networked-aframe/networked-aframe/blob/master/README.md -[naf]: https://github.com/networked-aframe/networked-aframe -[aframe]: https://github.com/aframevr/aframe/ -[pr]: https://www.digitalocean.com/community/tutorials/how-to-create-a-pull-request-on-github -[ssh]: https://help.github.com/articles/generating-a-new-ssh-key-and-adding-it-to-the-ssh-agent/ -[tests]: https://github.com/aframevr/aframe/tree/master/tests#a-frame-unit-tests -[tutorial]: https://github.com/networked-aframe/networked-aframe/blob/master/docs/getting-started-local.md - -Here's how to submit a pull request (PR): - -1. Have a [GitHub account](https://github.com/join) with [SSH keys][ssh] set up. -2. [Fork](https://github.com/networked-aframe/networked-aframe/fork) the repository on GitHub. -3. Clone your fork of the repository locally (i.e., `git clone git@github.com:yourusername/networked-aframe`). -4. Run `npm install` to get dependencies and `npm run dev` to serve the test examples. [Follow this tutorial on how to get started][tutorial] -5. Create a branch to work in (i.e., `git checkout -b mybranch`). -6. Make changes to your fork of the repository, commit them, and push them (i.e., `git add -A . && git commit -m 'My Changes (fixes #1234)' && git push origin mybranch`). -7. If necessary, write [unit tests](tests/unit/) and run with `npm run test`. -8. Make sure your code follows codestyle of the rest of the project. -9. If your commit changes the external API of Networked-Aframe update the [documentation][README] -9. [Submit a pull request][pr] to the master branch. If you head to the [Networked-Aframe repository][naf] after running `git push` from earlier, you should see a pop up to submit a pull request. -10. [Address review comments](http://stackoverflow.com/questions/9790448/how-to-update-a-pull-request) if any. - -As per usual with open source, you would agree to license your contributions -under the [MIT License](LICENSE). - -# Share your Work - -1. Create something awesome with networked-aframe -2. Publish your work to Github (and GitHub pages) so everyone can learn from your work. -3. Share it on [Slack][slack] on the #projects channel. -4. Let A-Frame know about it so they can feature it on their blog: [A Week of A-Frame](https://aframe.io/blog/). For this just tweet your project with a small video to @aframevr -4. For bonus points, write and publish a case study to explain how you built it. - -# Help Your Fellow A-Framers - -## On Slack - -1. [Invite yourself][slack] to the A-Frame Slack channel. -2. Join the #networked-aframe channel -3. Help answer questions that people might have and welcome new people. - -## On GitHub - -1. Help respond to [newly-filed GitHub issues][issues] - -# Curate and Make Efforts Known - -Every week, the A-Frame team rounds up all the cool stuff happening with A-Frame on their -[blog](https://aframe.io/blog). If you tweeted about it and added a comment on Slack, -there is a good chance for it to be included in the next blog post. - -Also feel free to message us on #networked-aframe channel on [Slack][slack]. - -# Spread the Word - -If you want to hold an event and talk about WebVR, A-Frame and/or Networked-Aframe, check out [the -presentation kit](https://github.com/aframevr/aframe-presentation-kit). - -Thanks so much for contributing and helping grow WebXR! diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 16d9ca01..00000000 --- a/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2017 Hayden Lee - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/README.md b/README.md deleted file mode 100644 index 7e06061a..00000000 --- a/README.md +++ /dev/null @@ -1,521 +0,0 @@ -[slack]: https://aframevr.slack.com/join/shared_invite/zt-f6rne3ly-ekVaBU~Xu~fsZHXr56jacQ - - - - -Networked-Aframe -======= - -NPM version -NPM downloads - -**Multi-user VR on the Web** - -A framework for writing multi-user VR apps in HTML and JS. - -Built on top of [A-Frame](https://aframe.io/). - -
- Features - — - Getting Started - — - Examples - — - Documentation - — - Contact -
- -
- - -Features --------- -* Support for WebRTC and/or WebSocket connections. -* Voice chat. Audio streaming to let your users talk in-app (WebRTC only). -* Video chat. See video streams in-app. -* Bandwidth sensitive. Only send network updates when things change. -* Cross-platform. Works on all modern Desktop and Mobile browsers. Oculus Rift, Oculus Quest, HTC Vive and Google Cardboard. -* Extendable. Sync any A-Frame component, including your own, without changing the component code at all. - - -Release notes -------------- - -You can read [the release notes](https://github.com/networked-aframe/networked-aframe/blob/master/docs/RELEASE_NOTES.md) to know what changed in the latest releases. - - -Getting Started ---------------- - -[![Remix on Glitch](https://cdn.glitch.com/2703baf2-b643-4da7-ab91-7ee2a2d00b5b%2Fremix-button.svg)](https://glitch.com/edit/#!/remix/naf-project) - -Follow [the NAF Getting Started tutorial](https://github.com/networked-aframe/networked-aframe/blob/master/docs/getting-started-local.md) to build your own example from scratch, including setting up a local server. - -To run the examples on your own PC: - - ```sh -git clone https://github.com/networked-aframe/networked-aframe.git # Clone the repository. -cd networked-aframe -npm install # Install dependencies. -npm run dev # Start the local development server. -``` -With the server running, browse the examples at `http://localhost:8080`. Open another browser tab and point it to the same URL to see the other client. - -For info on how to host your experience on the internet, see the [NAF Hosting Guide](https://github.com/networked-aframe/networked-aframe/blob/master/docs/hosting-networked-aframe-on-a-server.md). - - -Basic Example -------------- -```html - - - My Networked-Aframe Scene - - - - - - - - - - - - - - - -``` - -More Examples -------------- - -Open in two tabs if nobody else is online, or [remix the code examples yourself](https://glitch.com/edit/#!/remix/naf-examples). - -* [Basic](https://naf-examples.glitch.me/basic.html) -* [Basic with 4 clients](https://naf-examples.glitch.me/basic-4.html) -* [Dance Club](https://naf-examples.glitch.me/a-saturday-night/index.html) -* [Google Blocks](https://naf-examples.glitch.me/google-blocks.html) -* [Tracked Controllers](https://naf-examples.glitch.me/tracked-controllers.html) -* [Positional Audio](https://naf-examples.glitch.me/basic-audio.html) -* [Nametags](https://glitch.com/edit/#!/naf-nametags) (not updated to latest version) -* [Dynamic Room Name](https://glitch.com/edit/#!/naf-dynamic-room) (not updated to latest version) -* [Form to set room and username](https://glitch.com/edit/#!/naf-form-example) (not updated to latest version) -* [More...](https://naf-examples.glitch.me) - -Made something awesome with Networked-Aframe? [Let us know](https://github.com/networked-aframe/networked-aframe/issues) and we'll include it here. - - -Documentation -------------- - -### Overview - -Networked-Aframe works by syncing entities and their components to connected users. To connect to a room you need to add the [`networked-scene`](#scene-component) component to the `a-scene` element. For an entity to be synced, add the `networked` component to it. By default the `position` and `rotation` components are synced, but if you want to sync other components or child components you need to define a [schema](#syncing-custom-components). For more advanced control over the network messages see the sections on [Broadcasting Custom Messages](#sending-custom-messages) and [Options](#options). - - -### Scene component - -Required on the A-Frame `` component. - -```html - - ... - -``` - -| Property | Description | Default Value | -| -------- | ----------- | ------------- | -| serverURL | Choose where the WebSocket / signalling server is located. | / | -| app | Unique app name. Spaces are not allowed. | default | -| room | Unique room name. Can be multiple per app. Spaces are not allowed. There can be multiple rooms per app and clients can only connect to clients in the same app & room. | default | -| connectOnLoad | Connect to the server as soon as the webpage loads. | true | -| onConnect | Function to be called when client has successfully connected to the server. | onConnect | -| adapter | The network service that you wish to use, see [adapters](#adapters). | wseasyrtc | -| audio | Turn on / off microphone audio streaming for your app. Only works if the chosen adapter supports it. | false | -| video | Turn on / off video streaming for your app. Only works if the chosen adapter supports it. | false | -| debug | Turn on / off Networked-Aframe debug logs. | false | - -### Connecting - -By default, `networked-scene` will connect to your server automatically. To prevent this and instead have control over when to connect, set `connectOnLoad` to false in `networked-scene`. When you are ready to connect emit the `connect` event on the `a-scene` element. - -```javascript -AFRAME.scenes[0].emit('connect'); -``` - -### Disconnecting - -To disconnect simply remove the `networked-scene` component from the `a-scene` element. - -```javascript -AFRAME.scenes[0].removeAttribute('networked-scene'); -``` - -Completely removing `a-scene` from your page will also handle cleanly disconnecting. - - -### Creating Networked Entities - -```html - - - - - - - - - - - -``` - -Create an instance of a template to be synced across clients. The position and rotation will be synced by default. The [`buffered-interpolation`](https://github.com/InfiniteLee/buffered-interpolation) is added to allow for less network updates while keeping smooth motion. - -Templates must only have one root element. When `attachTemplateToLocal` is set to true, the attributes on this element will be copied to the local entity and the children will be appended to the local entity. Remotely instantiated entities will be a copy of the root element of the template with the `networked` component added to it. - -#### Example `attachTemplateToLocal=true` - -```html - - - - - - - - - - - - -``` -#### Example `attachTemplateToLocal=false` - -```html - - - - - - - - - -``` - -| Parameter | Description | Default -| -------- | ------------ | -------------- -| template | A css selector to a template tag stored in `` | '' -| attachTemplateToLocal | Does not attach the template for the local user when set to false. This is useful when there is different behavior locally and remotely. | true -| persistent | On remote creator (not owner) disconnect, attempts to take ownership of persistent entities rather than delete them | false - - -### Deleting Networked Entities - -Currently only the creator of a network entity can delete it. To delete, simply delete the element from the HTML using regular DOM APIs and Networked-Aframe will handle the syncing automatically. - - -### Syncing Custom Components - -By default, the `position` and `rotation` components on the root entity are synced. - -To sync other components and components of child entities you need to define a schema per template. Here's how to define and add a schema: - -```javascript -NAF.schemas.add({ - template: '#avatar-template', - components: [ - 'position', - 'rotation', - 'scale', - { - selector: '.hairs', - component: 'show-child' - }, - { - selector: '.head', - component: 'material', - property: 'color' - }, - ] -}); -``` - -Components of the root entity can be defined with the name of the component. Components of child entities can be defined with an object with both the `selector` field, which uses a standard CSS selector to be used by `document.querySelector`, and the `component` field which specifies the name of the component. To only sync one property of a multi-property component, add the `property` field with the name of the property. - -Once you've defined the schema then add it to the list of schemas by calling `NAF.schemas.add(YOUR_SCHEMA)`. - -Component data is retrieved by the A-Frame Component `data` property. During the network tick each component's data is checked against its previous synced value; if the data object has changed at all it will be synced across the network. - - -### Syncing nested templates - eg. hands - -To sync nested templates setup your HTML nodes like so: - -```HTML - - - - - -``` - -In this example the head/camera, left and right hands will spawn their own templates which will be networked independently of the root player. Note: this parent-child relationship only works between one level, ie. a child entity's direct parent must have the `networked` component. - -### Sending Custom Messages - -```javascript -NAF.connection.subscribeToDataChannel(dataType, callback) -NAF.connection.unsubscribeToDataChannel(dataType) - -NAF.connection.broadcastData(dataType, data) -NAF.connection.broadcastDataGuaranteed(dataType, data) - -NAF.connection.sendData(clientId, dataType, data) -NAF.connection.sendDataGuaranteed(clientId, dataType, data) -``` - -Subscribe and unsubscribe callbacks to network messages specified by `dataType`. Broadcast data to all clients in your room with the `broadcastData` functions. To send only to a specific client, use the `sendData` functions instead. - -| Parameter | Description -| -------- | ----------- -| clientId | ClientId to send this data to -| dataType | String to identify a network message. `u` is a reserved data type, don't use it pls -| callback | Function to be called when message of type `dataType` is received. Parameters: `function(senderId, dataType, data, targetId)` -| data | Object to be sent to all other clients - - -### Transfer Entity Ownership - -The owner of an entity is responsible for syncing its component data. When a user wants to modify another user's entity they must first take ownership of that entity. The [ownership transfer example](./examples/ownership-transfer.html) and the [toggle-ownership component](./examples/js/toggle-ownership.component.js) show how to take ownership of an entity and update it. - -```javascript -NAF.utils.takeOwnership(entityEl) -``` - -Take ownership of an entity. - -```javascript -NAF.utils.isMine(entityEl) -``` - -Check if you own the specified entity. - - -### Events - -Events are fired when certain things happen in NAF. To subscribe to these events follow this pattern: - -```javascript -document.body.addEventListener('clientConnected', function (evt) { - console.error('clientConnected event. clientId =', evt.detail.clientId); -}); -``` -Events need to be subscribed after the `document.body` element has been created. This could be achieved by waiting for the `document.body` `onLoad` method, or by using NAF's `onConnect` function. Use the [NAF Events Demo](https://github.com/networked-aframe/networked-aframe/blob/master/examples/basic-events.html) as an example. - -List of events: - -| Event | Description | Values | -| -------- | ----------- | ------------- | -| clientConnected | Fired when another client connects to you | `evt.detail.clientId` - ClientId of connecting client | -| clientDisconnected | Fired when another client disconnects from you | `evt.detail.clientId` - ClientId of disconnecting client | -| entityCreated | Fired when a networked entity is created | `evt.detail.el` - new entity | -| entityRemoved | Fired when a networked entity is deleted | `evt.detail.networkId` - networkId of deleted entity | - -The following events are fired on the `networked` component. See the [toggle-ownership component](./examples/js/toggle-ownership.component.js) for examples. - -List of ownership transfer events: - -| Event | Description | Values | -| -------- | ----------- | ------------- | -| ownership-gained | Fired when a networked entity's ownership is taken | `evt.detail.el` - the entity whose ownership was gained | -| | | `evt.detail.oldOwner` - the clientId of the previous owner | -| ownership-lost | Fired when a networked entity's ownership is lost | `evt.detail.el` - the entity whose ownership was lost | -| | | `evt.detail.newOwner` - the clientId of the new owner | -| ownership-changed | Fired when a networked entity's ownership is changed | `evt.detail.el` - the entity whose ownership was lost | -| | | `evt.detail.oldOwner` - the clientId of the previous owner | -| | | `evt.detail.newOwner` - the clientId of the new owner | - -### Adapters - -NAF can be used with multiple network libraries and services. An adapter is a class which adds support for a library to NAF. If you're just hacking on a small project or proof of concept you'll probably be fine with the default configuration and you can skip this section. Considerations you should make when evaluating different adapters are: - -- How many concurrent users do you need to support in one room? -- Do you want to host your own server? Or would a "serverless" solution like Firebase do the job? -- Do you need audio (microphone) streaming? -- Do you need custom server-side logic? -- Do you want a WebSocket (client-server) network architecture or WebRTC (peer-to-peer)? - -By default the `wseasyrtc` adapter is used, which does not support audio and uses a TCP connection. This is not ideal for production deployments however due to inherent connection issues with WebRTC we've set it as the default. To support audio via WebRTC be sure the server is using https and change the adapter to `easyrtc` (this uses UDP). - -If you're interested in contributing to NAF a great opportunity is to add support for more adapters and send a pull request. - -List of the supported adapters: - -| Adapter | Description | Supports Audio/Video | WebSockets or WebRTC | How to start | -| -------- | ----------- | ------------- | ----------- | ---------- | -| wseasyrtc | DEFAULT - Uses the [open-easyrtc](https://github.com/open-easyrtc/open-easyrtc) library | No | WebSockets | `npm run dev` | -| easyrtc | Uses the [open-easyrtc](https://github.com/open-easyrtc/open-easyrtc) library | Audio and Video (camera and screen share) | WebRTC | `npm run dev` | -| janus | Uses the [Janus WebRTC server](https://github.com/meetecho/janus-gateway) and [janus-plugin-sfu](https://github.com/mozilla/janus-plugin-sfu) | Audio and Video (camera OR screen share) | WebRTC | See [naf-janus-adapter](https://github.com/networked-aframe/naf-janus-adapter/tree/3.0.x) | -| socketio | SocketIO implementation without external library (work in progress, currently no maintainer) | No | WebSockets | `npm run dev-socketio` | -| webrtc | Native WebRTC implementation without external library (work in progress, currently no maintainer) | Audio | WebRTC | `npm run dev-socketio` | -| Firebase | [Firebase](https://firebase.google.com/) for WebRTC signalling (currently no maintainer) | No | WebRTC | See [naf-firebase-adapter](https://github.com/networked-aframe/naf-firebase-adapter) | -| uWS | Implementation of [uWebSockets](https://github.com/uNetworking/uWebSockets) (currently no maintainer) | No | WebSockets | See [naf-uws-adapter](https://github.com/networked-aframe/naf-uws-adapter) | - -WebRTC in the table means that component updates is using WebRTC Datachannels -(UDP) instead of the WebSocket (TCP). You still have a WebSocket for the signaling -part. - -See also the document [NAF adapters comparison](https://github.com/networked-aframe/networked-aframe/wiki/NAF-adapters-comparison). - -### Audio - -After adding `audio: true` to the `networked-scene` component (and using an adapter that supports it) you will not hear any audio by default. Though the audio will be streaming, it will not be audible until an entity with a `networked-audio-source` is created. The audio from the owner of this entity will be emitted in 3D space from that entity's position. The `networked-audio-source` component must be added to an entity (or a child of an entity) with the `networked` component. - -To quickly get started, try the [Glitch NAF Audio Example](https://glitch.com/edit/#!/networked-aframe-audio?path=public/index.html) (not updated to latest version). - -To mute/unmute the microphone, you can use the following API (easyrtc and janus adapters): - -```javascript -NAF.connection.adapter.enableMicrophone(enabled) -``` - -where `enabled` is `true` or `false`. - -### Video - -After adding `video: true` (not needed for the janus adapter) to the `networked-scene` component (and using an adapter that supports it) you will not see any video by default. Though the video will be streaming, it will not be visible until an entity using a mesh (`` for example) with a `networked-video-source` is created. The video from the owner of this entity will be visible in 3D space from that entity's position. The `networked-video-source` component must be added to an `` child entity of an entity with the `networked` component. - -This currently applies only to the easyrtc and janus adapters that supports the `getMediaStream(clientId, type="video")` API. - -See the [Video Streaming](https://github.com/networked-aframe/networked-aframe/blob/master/examples/basic-video.html) example -that shows the user camera without audio. - -To disable/reenable the camera, you can use the following API (easyrtc adapter only): - -```javascript -NAF.connection.adapter.enableCamera(enabled) -``` - -where `enabled` is `true` or `false`. - -With the easyrtc adapter, you can add an additional video track like a screen -share with the `addLocalMediaStream` and `removeLocalMediaStream` API: - -```javascript -navigator.mediaDevices.getDisplayMedia().then((stream) => { - NAF.connection.adapter.addLocalMediaStream(stream, "screen"); -}); -``` - -```javascript -NAF.connection.adapter.removeLocalMediaStream("screen"); -``` - -See the [Multi Streams](https://github.com/networked-aframe/networked-aframe/blob/master/examples/basic-multi-streams.html) example -that uses a second plane with `networked-video-source="streamName: screen"` to -show the screen share to the other participants. -Be sure to look at the comments at the end of the html file of this example for known issues. - -### Misc - -```javascript -NAF.connection.isConnected() -``` - -Returns true if a connection has been established to the signalling server. - -```javascript -NAF.connection.getConnectedClients() -``` - -Returns the list of currently connected clients. - - -### Options - -```javascript -NAF.options.updateRate -``` - -Frequency the network component `sync` function is called, per second. 10-20 is normal for most Social VR applications. Default is `15`. - -```javascript -NAF.options.useLerp -``` - -By default when an entity is created the [`aframe-lerp-component`](https://github.com/haydenjameslee/aframe-lerp-component) is attached to smooth out position and rotation network updates. Set this to false if you don't want the lerp component to be attached on creation. - -Stay in Touch -------------- - -- Join the [A-Frame Slack][slack] and add the #networked-aframe channel -- Follow changes on [GitHub](https://github.com/networked-aframe/networked-aframe/subscription) -- Let us know if you've made something with Networked-Aframe. We'd love to see it! - - -Help and More Information ------------------------------- - -* [Getting started tutorial](https://github.com/networked-aframe/networked-aframe/blob/master/docs/getting-started-local.md) -* [Edit live example on glitch.com](https://glitch.com/~naf-project) -* [Live demo site](https://naf-examples.glitch.me) -* [Networked-Aframe Adapters](https://github.com/networked-aframe) -* [A-Frame](https://aframe.io/) -* [WebXR](https://immersiveweb.dev) -* [Open EasyRTC WebRTC library](https://github.com/open-easyrtc/open-easyrtc) -* [Hayden Lee, NAF creator and previous maintainer](https://twitter.com/haydenlee37) -* [Vincent Fretin](https://twitter.com/vincentfretin) is handling new contributions and releases since the version 0.8.0 -* Bugs and requests can be filed on [GitHub Issues](https://github.com/networked-aframe/networked-aframe/issues) - - -Folder Structure ----------------- - - * `/ (root)` - * Licenses and package information - * `/dist/` - * Packaged source code for deployment - * `/server/` - * Server code - * `/examples/` - * Example experiences - * `/src/` - * Client source code - * `/tests/` - * Unit tests - - -Roadmap -------- - -* More examples! -* [Add your suggestions](https://github.com/networked-aframe/networked-aframe/issues) - -Interested in contributing? [Open an issue](https://github.com/networked-aframe/networked-aframe/issues) or send a pull request. - - -License -------- - -This program is free software and is distributed under an [MIT License](LICENSE). diff --git a/dist/networked-aframe.js b/dist/networked-aframe.js index 9fcafb4d..940fb350 100644 --- a/dist/networked-aframe.js +++ b/dist/networked-aframe.js @@ -214,7 +214,7 @@ eval("\n\nfunction _classCallCheck(instance, Constructor) { if (!(instance insta /***/ (function(module, exports, __webpack_require__) { "use strict"; -eval("\n\nfunction _typeof(obj) { \"@babel/helpers - typeof\"; if (typeof Symbol === \"function\" && typeof Symbol.iterator === \"symbol\") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === \"function\" && obj.constructor === Symbol && obj !== Symbol.prototype ? \"symbol\" : typeof obj; }; } return _typeof(obj); }\n\nfunction _slicedToArray(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _unsupportedIterableToArray(arr, i) || _nonIterableRest(); }\n\nfunction _nonIterableRest() { throw new TypeError(\"Invalid attempt to destructure non-iterable instance.\\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.\"); }\n\nfunction _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === \"string\") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === \"Object\" && o.constructor) n = o.constructor.name; if (n === \"Map\" || n === \"Set\") return Array.from(o); if (n === \"Arguments\" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); }\n\nfunction _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; }\n\nfunction _iterableToArrayLimit(arr, i) { if (typeof Symbol === \"undefined\" || !(Symbol.iterator in Object(arr))) return; var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i[\"return\"] != null) _i[\"return\"](); } finally { if (_d) throw _e; } } return _arr; }\n\nfunction _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; }\n\nfunction _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError(\"Cannot call a class as a function\"); } }\n\nfunction _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if (\"value\" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }\n\nfunction _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }\n\nfunction _inherits(subClass, superClass) { if (typeof superClass !== \"function\" && superClass !== null) { throw new TypeError(\"Super expression must either be null or a function\"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); if (superClass) _setPrototypeOf(subClass, superClass); }\n\nfunction _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); }\n\nfunction _createSuper(Derived) { var hasNativeReflectConstruct = _isNativeReflectConstruct(); return function _createSuperInternal() { var Super = _getPrototypeOf(Derived), result; if (hasNativeReflectConstruct) { var NewTarget = _getPrototypeOf(this).constructor; result = Reflect.construct(Super, arguments, NewTarget); } else { result = Super.apply(this, arguments); } return _possibleConstructorReturn(this, result); }; }\n\nfunction _possibleConstructorReturn(self, call) { if (call && (_typeof(call) === \"object\" || typeof call === \"function\")) { return call; } return _assertThisInitialized(self); }\n\nfunction _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError(\"this hasn't been initialised - super() hasn't been called\"); } return self; }\n\nfunction _isNativeReflectConstruct() { if (typeof Reflect === \"undefined\" || !Reflect.construct) return false; if (Reflect.construct.sham) return false; if (typeof Proxy === \"function\") return true; try { Date.prototype.toString.call(Reflect.construct(Date, [], function () {})); return true; } catch (e) { return false; } }\n\nfunction _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); }\n\n/* global NAF */\nvar NoOpAdapter = __webpack_require__(/*! ./NoOpAdapter */ \"./src/adapters/NoOpAdapter.js\");\n\nvar EasyRtcAdapter = /*#__PURE__*/function (_NoOpAdapter) {\n _inherits(EasyRtcAdapter, _NoOpAdapter);\n\n var _super = _createSuper(EasyRtcAdapter);\n\n function EasyRtcAdapter(easyrtc) {\n var _this;\n\n _classCallCheck(this, EasyRtcAdapter);\n\n _this = _super.call(this);\n _this.easyrtc = easyrtc || window.easyrtc;\n _this.app = \"default\";\n _this.room = \"default\";\n _this.mediaStreams = {};\n _this.remoteClients = {};\n _this.pendingMediaRequests = new Map();\n _this.serverTimeRequests = 0;\n _this.timeOffsets = [];\n _this.avgTimeOffset = 0;\n\n _this.easyrtc.setPeerOpenListener(function (clientId) {\n var clientConnection = _this.easyrtc.getPeerConnectionByUserId(clientId);\n\n _this.remoteClients[clientId] = clientConnection;\n });\n\n _this.easyrtc.setPeerClosedListener(function (clientId) {\n delete _this.remoteClients[clientId];\n });\n\n return _this;\n }\n\n _createClass(EasyRtcAdapter, [{\n key: \"setServerUrl\",\n value: function setServerUrl(url) {\n this.easyrtc.setSocketUrl(url);\n }\n }, {\n key: \"setApp\",\n value: function setApp(appName) {\n this.app = appName;\n }\n }, {\n key: \"setRoom\",\n value: function setRoom(roomName) {\n this.room = roomName;\n this.easyrtc.joinRoom(roomName, null);\n } // options: { datachannel: bool, audio: bool, video: bool }\n\n }, {\n key: \"setWebRtcOptions\",\n value: function setWebRtcOptions(options) {\n // this.easyrtc.enableDebug(true);\n this.easyrtc.enableDataChannels(options.datachannel);\n this.easyrtc.enableVideo(options.video);\n this.easyrtc.enableAudio(options.audio); // TODO receive(audio|video) options ?\n\n this.easyrtc.enableVideoReceive(true);\n this.easyrtc.enableAudioReceive(true);\n }\n }, {\n key: \"setServerConnectListeners\",\n value: function setServerConnectListeners(successListener, failureListener) {\n this.connectSuccess = successListener;\n this.connectFailure = failureListener;\n }\n }, {\n key: \"setRoomOccupantListener\",\n value: function setRoomOccupantListener(occupantListener) {\n this.easyrtc.setRoomOccupantListener(function (roomName, occupants, primary) {\n occupantListener(occupants);\n });\n }\n }, {\n key: \"setDataChannelListeners\",\n value: function setDataChannelListeners(openListener, closedListener, messageListener) {\n this.easyrtc.setDataChannelOpenListener(openListener);\n this.easyrtc.setDataChannelCloseListener(closedListener);\n this.easyrtc.setPeerListener(messageListener);\n }\n }, {\n key: \"updateTimeOffset\",\n value: function updateTimeOffset() {\n var _this2 = this;\n\n var clientSentTime = Date.now() + this.avgTimeOffset;\n return fetch(document.location.href, {\n method: \"HEAD\",\n cache: \"no-cache\"\n }).then(function (res) {\n var precision = 1000;\n var serverReceivedTime = new Date(res.headers.get(\"Date\")).getTime() + precision / 2;\n var clientReceivedTime = Date.now();\n var serverTime = serverReceivedTime + (clientReceivedTime - clientSentTime) / 2;\n var timeOffset = serverTime - clientReceivedTime;\n _this2.serverTimeRequests++;\n\n if (_this2.serverTimeRequests <= 10) {\n _this2.timeOffsets.push(timeOffset);\n } else {\n _this2.timeOffsets[_this2.serverTimeRequests % 10] = timeOffset;\n }\n\n _this2.avgTimeOffset = _this2.timeOffsets.reduce(function (acc, offset) {\n return acc += offset;\n }, 0) / _this2.timeOffsets.length;\n\n if (_this2.serverTimeRequests > 10) {\n setTimeout(function () {\n return _this2.updateTimeOffset();\n }, 5 * 60 * 1000); // Sync clock every 5 minutes.\n } else {\n _this2.updateTimeOffset();\n }\n });\n }\n }, {\n key: \"connect\",\n value: function connect() {\n var _this3 = this;\n\n Promise.all([this.updateTimeOffset(), new Promise(function (resolve, reject) {\n _this3._connect(resolve, reject);\n })]).then(function (_ref) {\n var _ref2 = _slicedToArray(_ref, 2),\n _ = _ref2[0],\n clientId = _ref2[1];\n\n _this3._myRoomJoinTime = _this3._getRoomJoinTime(clientId);\n\n _this3.connectSuccess(clientId);\n })[\"catch\"](this.connectFailure);\n }\n }, {\n key: \"shouldStartConnectionTo\",\n value: function shouldStartConnectionTo(client) {\n return this._myRoomJoinTime <= client.roomJoinTime;\n }\n }, {\n key: \"startStreamConnection\",\n value: function startStreamConnection(clientId) {\n this.easyrtc.call(clientId, function (caller, media) {\n if (media === \"datachannel\") {\n NAF.log.write(\"Successfully started datachannel to \", caller);\n }\n }, function (errorCode, errorText) {\n NAF.log.error(errorCode, errorText);\n }, function (wasAccepted) {// console.log(\"was accepted=\" + wasAccepted);\n });\n }\n }, {\n key: \"closeStreamConnection\",\n value: function closeStreamConnection(clientId) {\n this.easyrtc.hangup(clientId);\n }\n }, {\n key: \"sendData\",\n value: function sendData(clientId, dataType, data) {\n // send via webrtc otherwise fallback to websockets\n this.easyrtc.sendData(clientId, dataType, data);\n }\n }, {\n key: \"sendDataGuaranteed\",\n value: function sendDataGuaranteed(clientId, dataType, data) {\n this.easyrtc.sendDataWS(clientId, dataType, data);\n }\n }, {\n key: \"broadcastData\",\n value: function broadcastData(dataType, data) {\n var roomOccupants = this.easyrtc.getRoomOccupantsAsMap(this.room); // Iterate over the keys of the easyrtc room occupants map.\n // getRoomOccupantsAsArray uses Object.keys which allocates memory.\n\n for (var roomOccupant in roomOccupants) {\n if (roomOccupants[roomOccupant] && roomOccupant !== this.easyrtc.myEasyrtcid) {\n // send via webrtc otherwise fallback to websockets\n this.easyrtc.sendData(roomOccupant, dataType, data);\n }\n }\n }\n }, {\n key: \"broadcastDataGuaranteed\",\n value: function broadcastDataGuaranteed(dataType, data) {\n var destination = {\n targetRoom: this.room\n };\n this.easyrtc.sendDataWS(destination, dataType, data);\n }\n }, {\n key: \"getConnectStatus\",\n value: function getConnectStatus(clientId) {\n var status = this.easyrtc.getConnectStatus(clientId);\n\n if (status == this.easyrtc.IS_CONNECTED) {\n return NAF.adapters.IS_CONNECTED;\n } else if (status == this.easyrtc.NOT_CONNECTED) {\n return NAF.adapters.NOT_CONNECTED;\n } else {\n return NAF.adapters.CONNECTING;\n }\n }\n }, {\n key: \"getMediaStream\",\n value: function getMediaStream(clientId) {\n var streamName = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : \"audio\";\n\n if (this.mediaStreams[clientId] && this.mediaStreams[clientId][streamName]) {\n NAF.log.write(\"Already had \".concat(streamName, \" for \").concat(clientId));\n return Promise.resolve(this.mediaStreams[clientId][streamName]);\n } else {\n NAF.log.write(\"Waiting on \".concat(streamName, \" for \").concat(clientId)); // Create initial pendingMediaRequests with audio|video alias\n\n if (!this.pendingMediaRequests.has(clientId)) {\n var _pendingMediaRequests = {};\n var audioPromise = new Promise(function (resolve, reject) {\n _pendingMediaRequests.audio = {\n resolve: resolve,\n reject: reject\n };\n })[\"catch\"](function (e) {\n return NAF.log.warn(\"\".concat(clientId, \" getMediaStream Audio Error\"), e);\n });\n _pendingMediaRequests.audio.promise = audioPromise;\n var videoPromise = new Promise(function (resolve, reject) {\n _pendingMediaRequests.video = {\n resolve: resolve,\n reject: reject\n };\n })[\"catch\"](function (e) {\n return NAF.log.warn(\"\".concat(clientId, \" getMediaStream Video Error\"), e);\n });\n _pendingMediaRequests.video.promise = videoPromise;\n this.pendingMediaRequests.set(clientId, _pendingMediaRequests);\n }\n\n var pendingMediaRequests = this.pendingMediaRequests.get(clientId); // Create initial pendingMediaRequests with streamName\n\n if (!pendingMediaRequests[streamName]) {\n var streamPromise = new Promise(function (resolve, reject) {\n pendingMediaRequests[streamName] = {\n resolve: resolve,\n reject: reject\n };\n })[\"catch\"](function (e) {\n return NAF.log.warn(\"\".concat(clientId, \" getMediaStream \\\"\").concat(streamName, \"\\\" Error\"), e);\n });\n pendingMediaRequests[streamName].promise = streamPromise;\n }\n\n return this.pendingMediaRequests.get(clientId)[streamName].promise;\n }\n }\n }, {\n key: \"setMediaStream\",\n value: function setMediaStream(clientId, stream, streamName) {\n var pendingMediaRequests = this.pendingMediaRequests.get(clientId); // return undefined if there is no entry in the Map\n\n var clientMediaStreams = this.mediaStreams[clientId] = this.mediaStreams[clientId] || {};\n\n if (streamName === 'default') {\n // Safari doesn't like it when you use a mixed media stream where one of the tracks is inactive, so we\n // split the tracks into two streams.\n // Add mediaStreams audio streamName alias\n var audioTracks = stream.getAudioTracks();\n\n if (audioTracks.length > 0) {\n var audioStream = new MediaStream();\n\n try {\n audioTracks.forEach(function (track) {\n return audioStream.addTrack(track);\n });\n clientMediaStreams.audio = audioStream;\n } catch (e) {\n NAF.log.warn(\"\".concat(clientId, \" setMediaStream \\\"audio\\\" alias Error\"), e);\n } // Resolve the promise for the user's media stream audio alias if it exists.\n\n\n if (pendingMediaRequests) pendingMediaRequests.audio.resolve(audioStream);\n } // Add mediaStreams video streamName alias\n\n\n var videoTracks = stream.getVideoTracks();\n\n if (videoTracks.length > 0) {\n var videoStream = new MediaStream();\n\n try {\n videoTracks.forEach(function (track) {\n return videoStream.addTrack(track);\n });\n clientMediaStreams.video = videoStream;\n } catch (e) {\n NAF.log.warn(\"\".concat(clientId, \" setMediaStream \\\"video\\\" alias Error\"), e);\n } // Resolve the promise for the user's media stream video alias if it exists.\n\n\n if (pendingMediaRequests) pendingMediaRequests.video.resolve(videoStream);\n }\n } else {\n clientMediaStreams[streamName] = stream; // Resolve the promise for the user's media stream by StreamName if it exists.\n\n if (pendingMediaRequests && pendingMediaRequests[streamName]) {\n pendingMediaRequests[streamName].resolve(stream);\n }\n }\n }\n }, {\n key: \"addLocalMediaStream\",\n value: function addLocalMediaStream(stream, streamName) {\n var easyrtc = this.easyrtc;\n streamName = streamName || stream.id;\n this.setMediaStream(\"local\", stream, streamName);\n easyrtc.register3rdPartyLocalMediaStream(stream, streamName); // Add local stream to existing connections\n\n Object.keys(this.remoteClients).forEach(function (clientId) {\n if (easyrtc.getConnectStatus(clientId) !== easyrtc.NOT_CONNECTED) {\n easyrtc.addStreamToCall(clientId, streamName);\n }\n });\n }\n }, {\n key: \"removeLocalMediaStream\",\n value: function removeLocalMediaStream(streamName) {\n this.easyrtc.closeLocalMediaStream(streamName);\n delete this.mediaStreams[\"local\"][streamName];\n }\n }, {\n key: \"enableMicrophone\",\n value: function enableMicrophone(enabled) {\n this.easyrtc.enableMicrophone(enabled);\n }\n }, {\n key: \"enableCamera\",\n value: function enableCamera(enabled) {\n this.easyrtc.enableCamera(enabled);\n }\n }, {\n key: \"disconnect\",\n value: function disconnect() {\n this.easyrtc.disconnect();\n }\n /**\n * Privates\n */\n\n }, {\n key: \"_connect\",\n value: function _connect(connectSuccess, connectFailure) {\n var that = this;\n this.easyrtc.setStreamAcceptor(this.setMediaStream.bind(this));\n this.easyrtc.setOnStreamClosed(function (clientId, stream, streamName) {\n delete this.mediaStreams[clientId][streamName];\n });\n\n if (that.easyrtc.audioEnabled || that.easyrtc.videoEnabled) {\n navigator.mediaDevices.getUserMedia({\n video: that.easyrtc.videoEnabled,\n audio: that.easyrtc.audioEnabled\n }).then(function (stream) {\n that.addLocalMediaStream(stream, \"default\");\n that.easyrtc.connect(that.app, connectSuccess, connectFailure);\n }, function (errorCode, errmesg) {\n NAF.log.error(errorCode, errmesg);\n });\n } else {\n that.easyrtc.connect(that.app, connectSuccess, connectFailure);\n }\n }\n }, {\n key: \"_getRoomJoinTime\",\n value: function _getRoomJoinTime(clientId) {\n var myRoomId = NAF.room;\n var joinTime = this.easyrtc.getRoomOccupantsAsMap(myRoomId)[clientId].roomJoinTime;\n return joinTime;\n }\n }, {\n key: \"getServerTime\",\n value: function getServerTime() {\n return Date.now() + this.avgTimeOffset;\n }\n }]);\n\n return EasyRtcAdapter;\n}(NoOpAdapter);\n\nmodule.exports = EasyRtcAdapter;\n\n//# sourceURL=webpack:///./src/adapters/EasyRtcAdapter.js?"); +eval("\n\nfunction _typeof(obj) { \"@babel/helpers - typeof\"; if (typeof Symbol === \"function\" && typeof Symbol.iterator === \"symbol\") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === \"function\" && obj.constructor === Symbol && obj !== Symbol.prototype ? \"symbol\" : typeof obj; }; } return _typeof(obj); }\n\nfunction _slicedToArray(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _unsupportedIterableToArray(arr, i) || _nonIterableRest(); }\n\nfunction _nonIterableRest() { throw new TypeError(\"Invalid attempt to destructure non-iterable instance.\\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.\"); }\n\nfunction _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === \"string\") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === \"Object\" && o.constructor) n = o.constructor.name; if (n === \"Map\" || n === \"Set\") return Array.from(o); if (n === \"Arguments\" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); }\n\nfunction _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; }\n\nfunction _iterableToArrayLimit(arr, i) { if (typeof Symbol === \"undefined\" || !(Symbol.iterator in Object(arr))) return; var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i[\"return\"] != null) _i[\"return\"](); } finally { if (_d) throw _e; } } return _arr; }\n\nfunction _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; }\n\nfunction _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError(\"Cannot call a class as a function\"); } }\n\nfunction _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if (\"value\" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }\n\nfunction _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }\n\nfunction _inherits(subClass, superClass) { if (typeof superClass !== \"function\" && superClass !== null) { throw new TypeError(\"Super expression must either be null or a function\"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); if (superClass) _setPrototypeOf(subClass, superClass); }\n\nfunction _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); }\n\nfunction _createSuper(Derived) { var hasNativeReflectConstruct = _isNativeReflectConstruct(); return function _createSuperInternal() { var Super = _getPrototypeOf(Derived), result; if (hasNativeReflectConstruct) { var NewTarget = _getPrototypeOf(this).constructor; result = Reflect.construct(Super, arguments, NewTarget); } else { result = Super.apply(this, arguments); } return _possibleConstructorReturn(this, result); }; }\n\nfunction _possibleConstructorReturn(self, call) { if (call && (_typeof(call) === \"object\" || typeof call === \"function\")) { return call; } return _assertThisInitialized(self); }\n\nfunction _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError(\"this hasn't been initialised - super() hasn't been called\"); } return self; }\n\nfunction _isNativeReflectConstruct() { if (typeof Reflect === \"undefined\" || !Reflect.construct) return false; if (Reflect.construct.sham) return false; if (typeof Proxy === \"function\") return true; try { Date.prototype.toString.call(Reflect.construct(Date, [], function () {})); return true; } catch (e) { return false; } }\n\nfunction _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); }\n\n/* global NAF */\nvar NoOpAdapter = __webpack_require__(/*! ./NoOpAdapter */ \"./src/adapters/NoOpAdapter.js\");\n\nvar EasyRtcAdapter = /*#__PURE__*/function (_NoOpAdapter) {\n _inherits(EasyRtcAdapter, _NoOpAdapter);\n\n var _super = _createSuper(EasyRtcAdapter);\n\n function EasyRtcAdapter(easyrtc) {\n var _this;\n\n _classCallCheck(this, EasyRtcAdapter);\n\n _this = _super.call(this);\n _this.easyrtc = easyrtc || window.easyrtc;\n _this.app = \"default\";\n _this.room = \"default\";\n _this.mediaStreams = {};\n _this.remoteClients = {};\n _this.pendingMediaRequests = new Map();\n _this.serverTimeRequests = 0;\n _this.timeOffsets = [];\n _this.avgTimeOffset = 0;\n\n _this.easyrtc.setPeerOpenListener(function (clientId) {\n var clientConnection = _this.easyrtc.getPeerConnectionByUserId(clientId);\n\n _this.remoteClients[clientId] = clientConnection;\n });\n\n _this.easyrtc.setPeerClosedListener(function (clientId) {\n delete _this.remoteClients[clientId];\n });\n\n return _this;\n }\n\n _createClass(EasyRtcAdapter, [{\n key: \"setServerUrl\",\n value: function setServerUrl(url) {\n this.easyrtc.setSocketUrl(url);\n }\n }, {\n key: \"setApp\",\n value: function setApp(appName) {\n this.app = appName;\n }\n }, {\n key: \"setRoom\",\n value: function setRoom(roomName) {\n this.room = roomName;\n this.easyrtc.joinRoom(roomName, null);\n } // options: { datachannel: bool, audio: bool, video: bool }\n\n }, {\n key: \"setWebRtcOptions\",\n value: function setWebRtcOptions(options) {\n // this.easyrtc.enableDebug(true);\n this.easyrtc.enableDataChannels(options.datachannel);\n this.easyrtc.enableVideo(options.video);\n this.easyrtc.enableAudio(options.audio); // TODO receive(audio|video) options ?\n\n this.easyrtc.enableVideoReceive(true);\n this.easyrtc.enableAudioReceive(true);\n }\n }, {\n key: \"setServerConnectListeners\",\n value: function setServerConnectListeners(successListener, failureListener) {\n this.connectSuccess = successListener;\n this.connectFailure = failureListener;\n }\n }, {\n key: \"setRoomOccupantListener\",\n value: function setRoomOccupantListener(occupantListener) {\n this.easyrtc.setRoomOccupantListener(function (roomName, occupants, primary) {\n occupantListener(occupants);\n });\n }\n }, {\n key: \"setDataChannelListeners\",\n value: function setDataChannelListeners(openListener, closedListener, messageListener) {\n this.easyrtc.setDataChannelOpenListener(openListener);\n this.easyrtc.setDataChannelCloseListener(closedListener);\n this.easyrtc.setPeerListener(messageListener);\n }\n }, {\n key: \"updateTimeOffset\",\n value: function updateTimeOffset() {\n var _this2 = this;\n\n var clientSentTime = Date.now() + this.avgTimeOffset;\n return fetch(document.location.href, {\n method: \"HEAD\",\n cache: \"no-cache\"\n }).then(function (res) {\n var precision = 1000;\n var serverReceivedTime = new Date(res.headers.get(\"Date\")).getTime() + precision / 2;\n var clientReceivedTime = Date.now();\n var serverTime = serverReceivedTime + (clientReceivedTime - clientSentTime) / 2;\n var timeOffset = serverTime - clientReceivedTime;\n _this2.serverTimeRequests++;\n\n if (_this2.serverTimeRequests <= 10) {\n _this2.timeOffsets.push(timeOffset);\n } else {\n _this2.timeOffsets[_this2.serverTimeRequests % 10] = timeOffset;\n }\n\n _this2.avgTimeOffset = _this2.timeOffsets.reduce(function (acc, offset) {\n return acc += offset;\n }, 0) / _this2.timeOffsets.length;\n\n if (_this2.serverTimeRequests > 10) {\n setTimeout(function () {\n return _this2.updateTimeOffset();\n }, 5 * 60 * 1000); // Sync clock every 5 minutes.\n } else {\n _this2.updateTimeOffset();\n }\n });\n }\n }, {\n key: \"connect\",\n value: function connect() {\n var _this3 = this;\n\n Promise.all([this.updateTimeOffset(), new Promise(function (resolve, reject) {\n _this3._connect(resolve, reject);\n })]).then(function (_ref) {\n var _ref2 = _slicedToArray(_ref, 2),\n _ = _ref2[0],\n clientId = _ref2[1];\n\n _this3._myRoomJoinTime = _this3._getRoomJoinTime(clientId);\n\n _this3.connectSuccess(clientId);\n })[\"catch\"](this.connectFailure);\n }\n }, {\n key: \"shouldStartConnectionTo\",\n value: function shouldStartConnectionTo(client) {\n return this._myRoomJoinTime <= client.roomJoinTime;\n }\n }, {\n key: \"startStreamConnection\",\n value: function startStreamConnection(clientId) {\n this.easyrtc.call(clientId, function (caller, media) {\n if (media === \"datachannel\") {\n NAF.log.write(\"Successfully started datachannel to \", caller);\n }\n }, function (errorCode, errorText) {\n NAF.log.error(errorCode, errorText);\n }, function (wasAccepted) {// console.log(\"was accepted=\" + wasAccepted);\n });\n }\n }, {\n key: \"closeStreamConnection\",\n value: function closeStreamConnection(clientId) {\n this.easyrtc.hangup(clientId);\n }\n }, {\n key: \"sendData\",\n value: function sendData(clientId, dataType, data) {\n // send via webrtc otherwise fallback to websockets\n this.easyrtc.sendData(clientId, dataType, data);\n }\n }, {\n key: \"sendDataGuaranteed\",\n value: function sendDataGuaranteed(clientId, dataType, data) {\n this.easyrtc.sendDataWS(clientId, dataType, data);\n }\n }, {\n key: \"broadcastData\",\n value: function broadcastData(dataType, data) {\n var roomOccupants = this.easyrtc.getRoomOccupantsAsMap(this.room); // Iterate over the keys of the easyrtc room occupants map.\n // getRoomOccupantsAsArray uses Object.keys which allocates memory.\n\n for (var roomOccupant in roomOccupants) {\n if (roomOccupants[roomOccupant] && roomOccupant !== this.easyrtc.myEasyrtcid) {\n // send via webrtc otherwise fallback to websockets\n this.easyrtc.sendData(roomOccupant, dataType, data);\n }\n }\n }\n }, {\n key: \"broadcastDataGuaranteed\",\n value: function broadcastDataGuaranteed(dataType, data) {\n var destination = {\n targetRoom: this.room\n };\n this.easyrtc.sendDataWS(destination, dataType, data);\n }\n }, {\n key: \"getConnectStatus\",\n value: function getConnectStatus(clientId) {\n var status = this.easyrtc.getConnectStatus(clientId);\n\n if (status == this.easyrtc.IS_CONNECTED) {\n return NAF.adapters.IS_CONNECTED;\n } else if (status == this.easyrtc.NOT_CONNECTED) {\n return NAF.adapters.NOT_CONNECTED;\n } else {\n return NAF.adapters.CONNECTING;\n }\n }\n }, {\n key: \"getMediaStream\",\n value: function getMediaStream(clientId) {\n var streamName = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : \"audio\";\n\n if (this.mediaStreams[clientId] && this.mediaStreams[clientId][streamName]) {\n NAF.log.write(\"Already had \".concat(streamName, \" for \").concat(clientId));\n return Promise.resolve(this.mediaStreams[clientId][streamName]);\n } else {\n NAF.log.write(\"Waiting on \".concat(streamName, \" for \").concat(clientId)); // Create initial pendingMediaRequests with audio|video alias\n\n if (!this.pendingMediaRequests.has(clientId)) {\n var _pendingMediaRequests = {};\n var audioPromise = new Promise(function (resolve, reject) {\n _pendingMediaRequests.audio = {\n resolve: resolve,\n reject: reject\n };\n })[\"catch\"](function (e) {\n return NAF.log.warn(\"\".concat(clientId, \" getMediaStream Audio Error\"), e);\n });\n _pendingMediaRequests.audio.promise = audioPromise;\n var videoPromise = new Promise(function (resolve, reject) {\n _pendingMediaRequests.video = {\n resolve: resolve,\n reject: reject\n };\n })[\"catch\"](function (e) {\n return NAF.log.warn(\"\".concat(clientId, \" getMediaStream Video Error\"), e);\n });\n _pendingMediaRequests.video.promise = videoPromise;\n this.pendingMediaRequests.set(clientId, _pendingMediaRequests);\n }\n\n var pendingMediaRequests = this.pendingMediaRequests.get(clientId); // Create initial pendingMediaRequests with streamName\n\n if (!pendingMediaRequests[streamName]) {\n var streamPromise = new Promise(function (resolve, reject) {\n pendingMediaRequests[streamName] = {\n resolve: resolve,\n reject: reject\n };\n })[\"catch\"](function (e) {\n return NAF.log.warn(\"\".concat(clientId, \" getMediaStream \\\"\").concat(streamName, \"\\\" Error\"), e);\n });\n pendingMediaRequests[streamName].promise = streamPromise;\n }\n\n return this.pendingMediaRequests.get(clientId)[streamName].promise;\n }\n }\n }, {\n key: \"setMediaStream\",\n value: function setMediaStream(clientId, stream, streamName) {\n var pendingMediaRequests = this.pendingMediaRequests.get(clientId); // return undefined if there is no entry in the Map\n\n var clientMediaStreams = this.mediaStreams[clientId] = this.mediaStreams[clientId] || {};\n\n if (streamName === 'default') {\n // Safari doesn't like it when you use a mixed media stream where one of the tracks is inactive, so we\n // split the tracks into two streams.\n // Add mediaStreams audio streamName alias\n var audioTracks = stream.getAudioTracks();\n\n if (audioTracks.length > 0) {\n var audioStream = new MediaStream();\n\n try {\n audioTracks.forEach(function (track) {\n return audioStream.addTrack(track);\n });\n clientMediaStreams.audio = audioStream;\n } catch (e) {\n NAF.log.warn(\"\".concat(clientId, \" setMediaStream \\\"audio\\\" alias Error\"), e);\n } // Resolve the promise for the user's media stream audio alias if it exists.\n\n\n if (pendingMediaRequests) pendingMediaRequests.audio.resolve(audioStream);\n } // Add mediaStreams video streamName alias\n\n\n var videoTracks = stream.getVideoTracks();\n\n if (videoTracks.length > 0) {\n var videoStream = new MediaStream();\n\n try {\n videoTracks.forEach(function (track) {\n return videoStream.addTrack(track);\n });\n clientMediaStreams.video = videoStream;\n } catch (e) {\n NAF.log.warn(\"\".concat(clientId, \" setMediaStream \\\"video\\\" alias Error\"), e);\n } // Resolve the promise for the user's media stream video alias if it exists.\n\n\n if (pendingMediaRequests) pendingMediaRequests.video.resolve(videoStream);\n }\n } else {\n clientMediaStreams[streamName] = stream; // Resolve the promise for the user's media stream by StreamName if it exists.\n\n if (pendingMediaRequests && pendingMediaRequests[streamName]) {\n pendingMediaRequests[streamName].resolve(stream);\n }\n }\n }\n }, {\n key: \"addLocalMediaStream\",\n value: function addLocalMediaStream(stream, streamName) {\n var easyrtc = this.easyrtc;\n streamName = streamName || stream.id;\n this.setMediaStream(\"local\", stream, streamName);\n easyrtc.register3rdPartyLocalMediaStream(stream, streamName); // Add local stream to existing connections\n\n Object.keys(this.remoteClients).forEach(function (clientId) {\n if (easyrtc.getConnectStatus(clientId) !== easyrtc.NOT_CONNECTED) {\n easyrtc.addStreamToCall(clientId, streamName);\n }\n });\n }\n }, {\n key: \"removeLocalMediaStream\",\n value: function removeLocalMediaStream(streamName) {\n this.easyrtc.closeLocalMediaStream(streamName);\n delete this.mediaStreams[\"local\"][streamName];\n }\n }, {\n key: \"enableMicrophone\",\n value: function enableMicrophone(enabled) {\n this.easyrtc.enableMicrophone(enabled);\n }\n }, {\n key: \"enableCamera\",\n value: function enableCamera(enabled) {\n this.easyrtc.enableCamera(enabled);\n }\n }, {\n key: \"disconnect\",\n value: function disconnect() {\n this.easyrtc.disconnect();\n }\n /**\r\n * Privates\r\n */\n\n }, {\n key: \"_connect\",\n value: function _connect(connectSuccess, connectFailure) {\n var that = this;\n this.easyrtc.setStreamAcceptor(this.setMediaStream.bind(this));\n this.easyrtc.setOnStreamClosed(function (clientId, stream, streamName) {\n delete this.mediaStreams[clientId][streamName];\n });\n\n if (that.easyrtc.audioEnabled || that.easyrtc.videoEnabled) {\n navigator.mediaDevices.getUserMedia({\n video: that.easyrtc.videoEnabled,\n audio: that.easyrtc.audioEnabled\n }).then(function (stream) {\n that.addLocalMediaStream(stream, \"default\");\n that.easyrtc.connect(that.app, connectSuccess, connectFailure);\n }, function (errorCode, errmesg) {\n NAF.log.error(errorCode, errmesg);\n });\n } else {\n that.easyrtc.connect(that.app, connectSuccess, connectFailure);\n }\n }\n }, {\n key: \"_getRoomJoinTime\",\n value: function _getRoomJoinTime(clientId) {\n var myRoomId = NAF.room;\n var joinTime = this.easyrtc.getRoomOccupantsAsMap(myRoomId)[clientId].roomJoinTime;\n return joinTime;\n }\n }, {\n key: \"getServerTime\",\n value: function getServerTime() {\n return Date.now() + this.avgTimeOffset;\n }\n }]);\n\n return EasyRtcAdapter;\n}(NoOpAdapter);\n\nmodule.exports = EasyRtcAdapter;\n\n//# sourceURL=webpack:///./src/adapters/EasyRtcAdapter.js?"); /***/ }), @@ -250,7 +250,7 @@ eval("\n\nfunction _typeof(obj) { \"@babel/helpers - typeof\"; if (typeof Symbol /***/ (function(module, exports, __webpack_require__) { "use strict"; -eval("\n\nfunction _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError(\"Cannot call a class as a function\"); } }\n\nfunction _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if (\"value\" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }\n\nfunction _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }\n\n/* global NAF, io */\n\n/**\n * SocketIO Adapter (socketio)\n * networked-scene: serverURL needs to be ws://localhost:8080 when running locally\n */\nvar SocketioAdapter = /*#__PURE__*/function () {\n function SocketioAdapter() {\n _classCallCheck(this, SocketioAdapter);\n\n if (io === undefined) console.warn('It looks like socket.io has not been loaded before SocketioAdapter. Please do that.');\n this.app = \"default\";\n this.room = \"default\";\n this.occupantListener = null;\n this.myRoomJoinTime = null;\n this.myId = null;\n this.occupants = {}; // id -> joinTimestamp\n\n this.connectedClients = [];\n this.serverTimeRequests = 0;\n this.timeOffsets = [];\n this.avgTimeOffset = 0;\n }\n\n _createClass(SocketioAdapter, [{\n key: \"setServerUrl\",\n value: function setServerUrl(wsUrl) {\n this.wsUrl = wsUrl;\n }\n }, {\n key: \"setApp\",\n value: function setApp(appName) {\n this.app = appName;\n }\n }, {\n key: \"setRoom\",\n value: function setRoom(roomName) {\n this.room = roomName;\n }\n }, {\n key: \"setWebRtcOptions\",\n value: function setWebRtcOptions(options) {// No WebRTC support\n }\n }, {\n key: \"setServerConnectListeners\",\n value: function setServerConnectListeners(successListener, failureListener) {\n this.connectSuccess = successListener;\n this.connectFailure = failureListener;\n }\n }, {\n key: \"setRoomOccupantListener\",\n value: function setRoomOccupantListener(occupantListener) {\n this.occupantListener = occupantListener;\n }\n }, {\n key: \"setDataChannelListeners\",\n value: function setDataChannelListeners(openListener, closedListener, messageListener) {\n this.openListener = openListener;\n this.closedListener = closedListener;\n this.messageListener = messageListener;\n }\n }, {\n key: \"connect\",\n value: function connect() {\n var self = this;\n this.updateTimeOffset().then(function () {\n if (!self.wsUrl || self.wsUrl === \"/\") {\n if (location.protocol === \"https:\") {\n self.wsUrl = \"wss://\" + location.host;\n } else {\n self.wsUrl = \"ws://\" + location.host;\n }\n }\n\n NAF.log.write(\"Attempting to connect to socket.io\");\n var socket = self.socket = io(self.wsUrl);\n socket.on(\"connect\", function () {\n NAF.log.write(\"User connected\", socket.id);\n self.myId = socket.id;\n self.joinRoom();\n });\n socket.on(\"connectSuccess\", function (data) {\n var joinedTime = data.joinedTime;\n self.myRoomJoinTime = joinedTime;\n NAF.log.write(\"Successfully joined room\", self.room, \"at server time\", joinedTime);\n self.connectSuccess(self.myId);\n });\n socket.on(\"error\", function (err) {\n console.error(\"Socket connection failure\", err);\n self.connectFailure();\n });\n socket.on(\"occupantsChanged\", function (data) {\n var occupants = data.occupants;\n NAF.log.write('occupants changed', data);\n self.receivedOccupants(occupants);\n });\n\n function receiveData(packet) {\n var from = packet.from;\n var type = packet.type;\n var data = packet.data;\n self.messageListener(from, type, data);\n }\n\n socket.on(\"send\", receiveData);\n socket.on(\"broadcast\", receiveData);\n });\n }\n }, {\n key: \"joinRoom\",\n value: function joinRoom() {\n NAF.log.write(\"Joining room\", this.room);\n this.socket.emit(\"joinRoom\", {\n room: this.room\n });\n }\n }, {\n key: \"receivedOccupants\",\n value: function receivedOccupants(occupants) {\n delete occupants[this.myId];\n this.occupants = occupants;\n this.occupantListener(occupants);\n }\n }, {\n key: \"shouldStartConnectionTo\",\n value: function shouldStartConnectionTo(client) {\n return true;\n }\n }, {\n key: \"startStreamConnection\",\n value: function startStreamConnection(remoteId) {\n this.connectedClients.push(remoteId);\n this.openListener(remoteId);\n }\n }, {\n key: \"closeStreamConnection\",\n value: function closeStreamConnection(clientId) {\n this.connectedClients = this.connectedClients.filter(function (c) {\n return c != clientId;\n });\n this.closedListener(clientId);\n }\n }, {\n key: \"getConnectStatus\",\n value: function getConnectStatus(clientId) {\n var connected = this.connectedClients.indexOf(clientId) != -1;\n\n if (connected) {\n return NAF.adapters.IS_CONNECTED;\n } else {\n return NAF.adapters.NOT_CONNECTED;\n }\n }\n }, {\n key: \"sendData\",\n value: function sendData(to, type, data) {\n this.sendDataGuaranteed(to, type, data);\n }\n }, {\n key: \"sendDataGuaranteed\",\n value: function sendDataGuaranteed(to, type, data) {\n var packet = {\n from: this.myId,\n to: to,\n type: type,\n data: data,\n sending: true\n };\n\n if (this.socket) {\n this.socket.emit(\"send\", packet);\n } else {\n NAF.log.warn('SocketIO socket not created yet');\n }\n }\n }, {\n key: \"broadcastData\",\n value: function broadcastData(type, data) {\n this.broadcastDataGuaranteed(type, data);\n }\n }, {\n key: \"broadcastDataGuaranteed\",\n value: function broadcastDataGuaranteed(type, data) {\n var packet = {\n from: this.myId,\n type: type,\n data: data,\n broadcasting: true\n };\n\n if (this.socket) {\n this.socket.emit(\"broadcast\", packet);\n } else {\n NAF.log.warn('SocketIO socket not created yet');\n }\n }\n }, {\n key: \"getMediaStream\",\n value: function getMediaStream(clientId) {// Do not support WebRTC\n }\n }, {\n key: \"updateTimeOffset\",\n value: function updateTimeOffset() {\n var _this = this;\n\n var clientSentTime = Date.now() + this.avgTimeOffset;\n return fetch(document.location.href, {\n method: \"HEAD\",\n cache: \"no-cache\"\n }).then(function (res) {\n var precision = 1000;\n var serverReceivedTime = new Date(res.headers.get(\"Date\")).getTime() + precision / 2;\n var clientReceivedTime = Date.now();\n var serverTime = serverReceivedTime + (clientReceivedTime - clientSentTime) / 2;\n var timeOffset = serverTime - clientReceivedTime;\n _this.serverTimeRequests++;\n\n if (_this.serverTimeRequests <= 10) {\n _this.timeOffsets.push(timeOffset);\n } else {\n _this.timeOffsets[_this.serverTimeRequests % 10] = timeOffset;\n }\n\n _this.avgTimeOffset = _this.timeOffsets.reduce(function (acc, offset) {\n return acc += offset;\n }, 0) / _this.timeOffsets.length;\n\n if (_this.serverTimeRequests > 10) {\n setTimeout(function () {\n return _this.updateTimeOffset();\n }, 5 * 60 * 1000); // Sync clock every 5 minutes.\n } else {\n _this.updateTimeOffset();\n }\n });\n }\n }, {\n key: \"getServerTime\",\n value: function getServerTime() {\n return new Date().getTime() + this.avgTimeOffset;\n }\n }, {\n key: \"disconnect\",\n value: function disconnect() {\n this.socket.disconnect();\n }\n }]);\n\n return SocketioAdapter;\n}(); // NAF.adapters.register(\"socketio\", SocketioAdapter);\n\n\nmodule.exports = SocketioAdapter;\n\n//# sourceURL=webpack:///./src/adapters/naf-socketio-adapter.js?"); +eval("\n\nfunction _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError(\"Cannot call a class as a function\"); } }\n\nfunction _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if (\"value\" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }\n\nfunction _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }\n\n/* global NAF, io */\n\n/**\r\n * SocketIO Adapter (socketio)\r\n * networked-scene: serverURL needs to be ws://localhost:8080 when running locally\r\n */\nvar SocketioAdapter = /*#__PURE__*/function () {\n function SocketioAdapter() {\n _classCallCheck(this, SocketioAdapter);\n\n if (io === undefined) console.warn('It looks like socket.io has not been loaded before SocketioAdapter. Please do that.');\n this.app = \"default\";\n this.room = \"default\";\n this.occupantListener = null;\n this.myRoomJoinTime = null;\n this.myId = null;\n this.occupants = {}; // id -> joinTimestamp\n\n this.connectedClients = [];\n this.serverTimeRequests = 0;\n this.timeOffsets = [];\n this.avgTimeOffset = 0;\n }\n\n _createClass(SocketioAdapter, [{\n key: \"setServerUrl\",\n value: function setServerUrl(wsUrl) {\n this.wsUrl = wsUrl;\n }\n }, {\n key: \"setApp\",\n value: function setApp(appName) {\n this.app = appName;\n }\n }, {\n key: \"setRoom\",\n value: function setRoom(roomName) {\n this.room = roomName;\n }\n }, {\n key: \"setWebRtcOptions\",\n value: function setWebRtcOptions(options) {// No WebRTC support\n }\n }, {\n key: \"setServerConnectListeners\",\n value: function setServerConnectListeners(successListener, failureListener) {\n this.connectSuccess = successListener;\n this.connectFailure = failureListener;\n }\n }, {\n key: \"setRoomOccupantListener\",\n value: function setRoomOccupantListener(occupantListener) {\n this.occupantListener = occupantListener;\n }\n }, {\n key: \"setDataChannelListeners\",\n value: function setDataChannelListeners(openListener, closedListener, messageListener) {\n this.openListener = openListener;\n this.closedListener = closedListener;\n this.messageListener = messageListener;\n }\n }, {\n key: \"connect\",\n value: function connect() {\n var self = this;\n this.updateTimeOffset().then(function () {\n if (!self.wsUrl || self.wsUrl === \"/\") {\n if (location.protocol === \"https:\") {\n self.wsUrl = \"wss://\" + location.host;\n } else {\n self.wsUrl = \"ws://\" + location.host;\n }\n }\n\n NAF.log.write(\"Attempting to connect to socket.io\");\n var socket = self.socket = io(self.wsUrl);\n socket.on(\"connect\", function () {\n NAF.log.write(\"User connected\", socket.id);\n self.myId = socket.id;\n self.joinRoom();\n });\n socket.on(\"connectSuccess\", function (data) {\n var joinedTime = data.joinedTime;\n self.myRoomJoinTime = joinedTime;\n NAF.log.write(\"Successfully joined room\", self.room, \"at server time\", joinedTime);\n self.connectSuccess(self.myId);\n });\n socket.on(\"error\", function (err) {\n console.error(\"Socket connection failure\", err);\n self.connectFailure();\n });\n socket.on(\"occupantsChanged\", function (data) {\n var occupants = data.occupants;\n NAF.log.write('occupants changed', data);\n self.receivedOccupants(occupants);\n });\n\n function receiveData(packet) {\n var from = packet.from;\n var type = packet.type;\n var data = packet.data;\n self.messageListener(from, type, data);\n }\n\n socket.on(\"send\", receiveData);\n socket.on(\"broadcast\", receiveData);\n });\n }\n }, {\n key: \"joinRoom\",\n value: function joinRoom() {\n NAF.log.write(\"Joining room\", this.room);\n this.socket.emit(\"joinRoom\", {\n room: this.room\n });\n }\n }, {\n key: \"receivedOccupants\",\n value: function receivedOccupants(occupants) {\n delete occupants[this.myId];\n this.occupants = occupants;\n this.occupantListener(occupants);\n }\n }, {\n key: \"shouldStartConnectionTo\",\n value: function shouldStartConnectionTo(client) {\n return true;\n }\n }, {\n key: \"startStreamConnection\",\n value: function startStreamConnection(remoteId) {\n this.connectedClients.push(remoteId);\n this.openListener(remoteId);\n }\n }, {\n key: \"closeStreamConnection\",\n value: function closeStreamConnection(clientId) {\n this.connectedClients = this.connectedClients.filter(function (c) {\n return c != clientId;\n });\n this.closedListener(clientId);\n }\n }, {\n key: \"getConnectStatus\",\n value: function getConnectStatus(clientId) {\n var connected = this.connectedClients.indexOf(clientId) != -1;\n\n if (connected) {\n return NAF.adapters.IS_CONNECTED;\n } else {\n return NAF.adapters.NOT_CONNECTED;\n }\n }\n }, {\n key: \"sendData\",\n value: function sendData(to, type, data) {\n this.sendDataGuaranteed(to, type, data);\n }\n }, {\n key: \"sendDataGuaranteed\",\n value: function sendDataGuaranteed(to, type, data) {\n var packet = {\n from: this.myId,\n to: to,\n type: type,\n data: data,\n sending: true\n };\n\n if (this.socket) {\n this.socket.emit(\"send\", packet);\n } else {\n NAF.log.warn('SocketIO socket not created yet');\n }\n }\n }, {\n key: \"broadcastData\",\n value: function broadcastData(type, data) {\n this.broadcastDataGuaranteed(type, data);\n }\n }, {\n key: \"broadcastDataGuaranteed\",\n value: function broadcastDataGuaranteed(type, data) {\n var packet = {\n from: this.myId,\n type: type,\n data: data,\n broadcasting: true\n };\n\n if (this.socket) {\n this.socket.emit(\"broadcast\", packet);\n } else {\n NAF.log.warn('SocketIO socket not created yet');\n }\n }\n }, {\n key: \"getMediaStream\",\n value: function getMediaStream(clientId) {// Do not support WebRTC\n }\n }, {\n key: \"updateTimeOffset\",\n value: function updateTimeOffset() {\n var _this = this;\n\n var clientSentTime = Date.now() + this.avgTimeOffset;\n return fetch(document.location.href, {\n method: \"HEAD\",\n cache: \"no-cache\"\n }).then(function (res) {\n var precision = 1000;\n var serverReceivedTime = new Date(res.headers.get(\"Date\")).getTime() + precision / 2;\n var clientReceivedTime = Date.now();\n var serverTime = serverReceivedTime + (clientReceivedTime - clientSentTime) / 2;\n var timeOffset = serverTime - clientReceivedTime;\n _this.serverTimeRequests++;\n\n if (_this.serverTimeRequests <= 10) {\n _this.timeOffsets.push(timeOffset);\n } else {\n _this.timeOffsets[_this.serverTimeRequests % 10] = timeOffset;\n }\n\n _this.avgTimeOffset = _this.timeOffsets.reduce(function (acc, offset) {\n return acc += offset;\n }, 0) / _this.timeOffsets.length;\n\n if (_this.serverTimeRequests > 10) {\n setTimeout(function () {\n return _this.updateTimeOffset();\n }, 5 * 60 * 1000); // Sync clock every 5 minutes.\n } else {\n _this.updateTimeOffset();\n }\n });\n }\n }, {\n key: \"getServerTime\",\n value: function getServerTime() {\n return new Date().getTime() + this.avgTimeOffset;\n }\n }, {\n key: \"disconnect\",\n value: function disconnect() {\n this.socket.disconnect();\n }\n }]);\n\n return SocketioAdapter;\n}(); // NAF.adapters.register(\"socketio\", SocketioAdapter);\n\n\nmodule.exports = SocketioAdapter;\n\n//# sourceURL=webpack:///./src/adapters/naf-socketio-adapter.js?"); /***/ }), @@ -262,7 +262,7 @@ eval("\n\nfunction _classCallCheck(instance, Constructor) { if (!(instance insta /***/ (function(module, exports, __webpack_require__) { "use strict"; -eval("\n\nfunction _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError(\"Cannot call a class as a function\"); } }\n\nfunction _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if (\"value\" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }\n\nfunction _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }\n\n/* global NAF, io */\nvar WebRtcPeer = /*#__PURE__*/function () {\n function WebRtcPeer(localId, remoteId, sendSignalFunc) {\n _classCallCheck(this, WebRtcPeer);\n\n this.localId = localId;\n this.remoteId = remoteId;\n this.sendSignalFunc = sendSignalFunc;\n this.open = false;\n this.channelLabel = \"networked-aframe-channel\";\n this.pc = this.createPeerConnection();\n this.channel = null;\n }\n\n _createClass(WebRtcPeer, [{\n key: \"setDatachannelListeners\",\n value: function setDatachannelListeners(openListener, closedListener, messageListener, trackListener) {\n this.openListener = openListener;\n this.closedListener = closedListener;\n this.messageListener = messageListener;\n this.trackListener = trackListener;\n }\n }, {\n key: \"offer\",\n value: function offer(options) {\n var self = this; // reliable: false - UDP\n\n this.setupChannel(this.pc.createDataChannel(this.channelLabel, {\n reliable: false\n })); // If there are errors with Safari implement this:\n // https://github.com/OpenVidu/openvidu/blob/master/openvidu-browser/src/OpenViduInternal/WebRtcPeer/WebRtcPeer.ts#L154\n\n if (options.sendAudio) {\n options.localAudioStream.getTracks().forEach(function (track) {\n return self.pc.addTrack(track, options.localAudioStream);\n });\n }\n\n this.pc.createOffer(function (sdp) {\n self.handleSessionDescription(sdp);\n }, function (error) {\n NAF.log.error(\"WebRtcPeer.offer: \" + error);\n }, {\n offerToReceiveAudio: true,\n offerToReceiveVideo: false\n });\n }\n }, {\n key: \"handleSignal\",\n value: function handleSignal(signal) {\n // ignores signal if it isn't for me\n if (this.localId !== signal.to || this.remoteId !== signal.from) return;\n\n switch (signal.type) {\n case \"offer\":\n this.handleOffer(signal);\n break;\n\n case \"answer\":\n this.handleAnswer(signal);\n break;\n\n case \"candidate\":\n this.handleCandidate(signal);\n break;\n\n default:\n NAF.log.error(\"WebRtcPeer.handleSignal: Unknown signal type \" + signal.type);\n break;\n }\n }\n }, {\n key: \"send\",\n value: function send(type, data) {\n if (this.channel === null || this.channel.readyState !== \"open\") {\n return;\n }\n\n this.channel.send(JSON.stringify({\n type: type,\n data: data\n }));\n }\n }, {\n key: \"getStatus\",\n value: function getStatus() {\n if (this.channel === null) return WebRtcPeer.NOT_CONNECTED;\n\n switch (this.channel.readyState) {\n case \"open\":\n return WebRtcPeer.IS_CONNECTED;\n\n case \"connecting\":\n return WebRtcPeer.CONNECTING;\n\n case \"closing\":\n case \"closed\":\n default:\n return WebRtcPeer.NOT_CONNECTED;\n }\n }\n /*\n * Privates\n */\n\n }, {\n key: \"createPeerConnection\",\n value: function createPeerConnection() {\n var self = this;\n var RTCPeerConnection = window.RTCPeerConnection || window.webkitRTCPeerConnection || window.mozRTCPeerConnection || window.msRTCPeerConnection;\n\n if (RTCPeerConnection === undefined) {\n throw new Error(\"WebRtcPeer.createPeerConnection: This browser does not seem to support WebRTC.\");\n }\n\n var pc = new RTCPeerConnection({\n iceServers: WebRtcPeer.ICE_SERVERS\n });\n\n pc.onicecandidate = function (event) {\n if (event.candidate) {\n self.sendSignalFunc({\n from: self.localId,\n to: self.remoteId,\n type: \"candidate\",\n sdpMLineIndex: event.candidate.sdpMLineIndex,\n candidate: event.candidate.candidate\n });\n }\n }; // Note: seems like channel.onclose hander is unreliable on some platforms,\n // so also tries to detect disconnection here.\n\n\n pc.oniceconnectionstatechange = function () {\n if (self.open && pc.iceConnectionState === \"disconnected\") {\n self.open = false;\n self.closedListener(self.remoteId);\n }\n };\n\n pc.ontrack = function (e) {\n self.trackListener(self.remoteId, e.streams[0]);\n };\n\n return pc;\n }\n }, {\n key: \"setupChannel\",\n value: function setupChannel(channel) {\n var self = this;\n this.channel = channel; // received data from a remote peer\n\n this.channel.onmessage = function (event) {\n var data = JSON.parse(event.data);\n self.messageListener(self.remoteId, data.type, data.data);\n }; // connected with a remote peer\n\n\n this.channel.onopen = function (_event) {\n self.open = true;\n self.openListener(self.remoteId);\n }; // disconnected with a remote peer\n\n\n this.channel.onclose = function (_event) {\n if (!self.open) return;\n self.open = false;\n self.closedListener(self.remoteId);\n }; // error occurred with a remote peer\n\n\n this.channel.onerror = function (error) {\n NAF.log.error(\"WebRtcPeer.channel.onerror: \" + error);\n };\n }\n }, {\n key: \"handleOffer\",\n value: function handleOffer(message) {\n var self = this;\n\n this.pc.ondatachannel = function (event) {\n self.setupChannel(event.channel);\n };\n\n this.setRemoteDescription(message);\n this.pc.createAnswer(function (sdp) {\n self.handleSessionDescription(sdp);\n }, function (error) {\n NAF.log.error(\"WebRtcPeer.handleOffer: \" + error);\n });\n }\n }, {\n key: \"handleAnswer\",\n value: function handleAnswer(message) {\n this.setRemoteDescription(message);\n }\n }, {\n key: \"handleCandidate\",\n value: function handleCandidate(message) {\n var RTCIceCandidate = window.RTCIceCandidate || window.webkitRTCIceCandidate || window.mozRTCIceCandidate;\n this.pc.addIceCandidate(new RTCIceCandidate(message), function () {}, function (error) {\n NAF.log.error(\"WebRtcPeer.handleCandidate: \" + error);\n });\n }\n }, {\n key: \"handleSessionDescription\",\n value: function handleSessionDescription(sdp) {\n this.pc.setLocalDescription(sdp, function () {}, function (error) {\n NAF.log.error(\"WebRtcPeer.handleSessionDescription: \" + error);\n });\n this.sendSignalFunc({\n from: this.localId,\n to: this.remoteId,\n type: sdp.type,\n sdp: sdp.sdp\n });\n }\n }, {\n key: \"setRemoteDescription\",\n value: function setRemoteDescription(message) {\n var RTCSessionDescription = window.RTCSessionDescription || window.webkitRTCSessionDescription || window.mozRTCSessionDescription || window.msRTCSessionDescription;\n this.pc.setRemoteDescription(new RTCSessionDescription(message), function () {}, function (error) {\n NAF.log.error(\"WebRtcPeer.setRemoteDescription: \" + error);\n });\n }\n }, {\n key: \"close\",\n value: function close() {\n if (this.pc) {\n this.pc.close();\n }\n }\n }]);\n\n return WebRtcPeer;\n}();\n\nWebRtcPeer.IS_CONNECTED = \"IS_CONNECTED\";\nWebRtcPeer.CONNECTING = \"CONNECTING\";\nWebRtcPeer.NOT_CONNECTED = \"NOT_CONNECTED\";\nWebRtcPeer.ICE_SERVERS = [{\n urls: \"stun:stun1.l.google.com:19302\"\n}, {\n urls: \"stun:stun2.l.google.com:19302\"\n}, {\n urls: \"stun:stun3.l.google.com:19302\"\n}, {\n urls: \"stun:stun4.l.google.com:19302\"\n}];\n/**\n * Native WebRTC Adapter (native-webrtc)\n * For use with uws-server.js\n * networked-scene: serverURL needs to be ws://localhost:8080 when running locally\n */\n\nvar WebrtcAdapter = /*#__PURE__*/function () {\n function WebrtcAdapter() {\n _classCallCheck(this, WebrtcAdapter);\n\n if (io === undefined) console.warn('It looks like socket.io has not been loaded before WebrtcAdapter. Please do that.');\n this.app = \"default\";\n this.room = \"default\";\n this.occupantListener = null;\n this.myRoomJoinTime = null;\n this.myId = null;\n this.peers = {}; // id -> WebRtcPeer\n\n this.occupants = {}; // id -> joinTimestamp\n\n this.audioStreams = {};\n this.pendingAudioRequest = {};\n this.serverTimeRequests = 0;\n this.timeOffsets = [];\n this.avgTimeOffset = 0;\n }\n\n _createClass(WebrtcAdapter, [{\n key: \"setServerUrl\",\n value: function setServerUrl(wsUrl) {\n this.wsUrl = wsUrl;\n }\n }, {\n key: \"setApp\",\n value: function setApp(appName) {\n this.app = appName;\n }\n }, {\n key: \"setRoom\",\n value: function setRoom(roomName) {\n this.room = roomName;\n }\n }, {\n key: \"setWebRtcOptions\",\n value: function setWebRtcOptions(options) {\n if (options.datachannel === false) {\n NAF.log.error(\"WebrtcAdapter.setWebRtcOptions: datachannel must be true.\");\n }\n\n if (options.audio === true) {\n this.sendAudio = true;\n }\n\n if (options.video === true) {\n NAF.log.warn(\"WebrtcAdapter does not support video yet.\");\n }\n }\n }, {\n key: \"setServerConnectListeners\",\n value: function setServerConnectListeners(successListener, failureListener) {\n this.connectSuccess = successListener;\n this.connectFailure = failureListener;\n }\n }, {\n key: \"setRoomOccupantListener\",\n value: function setRoomOccupantListener(occupantListener) {\n this.occupantListener = occupantListener;\n }\n }, {\n key: \"setDataChannelListeners\",\n value: function setDataChannelListeners(openListener, closedListener, messageListener) {\n this.openListener = openListener;\n this.closedListener = closedListener;\n this.messageListener = messageListener;\n }\n }, {\n key: \"connect\",\n value: function connect() {\n var self = this;\n this.updateTimeOffset().then(function () {\n if (!self.wsUrl || self.wsUrl === \"/\") {\n if (location.protocol === \"https:\") {\n self.wsUrl = \"wss://\" + location.host;\n } else {\n self.wsUrl = \"ws://\" + location.host;\n }\n }\n\n NAF.log.write(\"Attempting to connect to socket.io\");\n var socket = self.socket = io(self.wsUrl);\n socket.on(\"connect\", function () {\n NAF.log.write(\"User connected\", socket.id);\n self.myId = socket.id;\n self.joinRoom();\n });\n socket.on(\"connectSuccess\", function (data) {\n var joinedTime = data.joinedTime;\n self.myRoomJoinTime = joinedTime;\n NAF.log.write(\"Successfully joined room\", self.room, \"at server time\", joinedTime);\n\n if (self.sendAudio) {\n var mediaConstraints = {\n audio: true,\n video: false\n };\n navigator.mediaDevices.getUserMedia(mediaConstraints).then(function (localStream) {\n self.storeAudioStream(self.myId, localStream);\n self.connectSuccess(self.myId);\n localStream.getTracks().forEach(function (track) {\n Object.keys(self.peers).forEach(function (peerId) {\n self.peers[peerId].pc.addTrack(track, localStream);\n });\n });\n })[\"catch\"](function (e) {\n NAF.log.error(e);\n console.error(\"Microphone is disabled due to lack of permissions\");\n self.sendAudio = false;\n self.connectSuccess(self.myId);\n });\n } else {\n self.connectSuccess(self.myId);\n }\n });\n socket.on(\"error\", function (err) {\n console.error(\"Socket connection failure\", err);\n self.connectFailure();\n });\n socket.on(\"occupantsChanged\", function (data) {\n var occupants = data.occupants;\n NAF.log.write('occupants changed', data);\n self.receivedOccupants(occupants);\n });\n\n function receiveData(packet) {\n var from = packet.from;\n var type = packet.type;\n var data = packet.data;\n\n if (type === 'ice-candidate') {\n self.peers[from].handleSignal(data);\n return;\n }\n\n self.messageListener(from, type, data);\n }\n\n socket.on(\"send\", receiveData);\n socket.on(\"broadcast\", receiveData);\n });\n }\n }, {\n key: \"joinRoom\",\n value: function joinRoom() {\n NAF.log.write(\"Joining room\", this.room);\n this.socket.emit(\"joinRoom\", {\n room: this.room\n });\n }\n }, {\n key: \"receivedOccupants\",\n value: function receivedOccupants(occupants) {\n var _this = this;\n\n delete occupants[this.myId];\n this.occupants = occupants;\n var self = this;\n var localId = this.myId;\n\n var _loop = function _loop(key) {\n var remoteId = key;\n if (_this.peers[remoteId]) return \"continue\";\n var peer = new WebRtcPeer(localId, remoteId, function (data) {\n self.socket.emit('send', {\n from: localId,\n to: remoteId,\n type: 'ice-candidate',\n data: data,\n sending: true\n });\n });\n peer.setDatachannelListeners(self.openListener, self.closedListener, self.messageListener, self.trackListener.bind(self));\n self.peers[remoteId] = peer;\n };\n\n for (var key in occupants) {\n var _ret = _loop(key);\n\n if (_ret === \"continue\") continue;\n }\n\n this.occupantListener(occupants);\n }\n }, {\n key: \"shouldStartConnectionTo\",\n value: function shouldStartConnectionTo(client) {\n return (this.myRoomJoinTime || 0) <= (client || 0);\n }\n }, {\n key: \"startStreamConnection\",\n value: function startStreamConnection(remoteId) {\n var _this2 = this;\n\n NAF.log.write('starting offer process');\n\n if (this.sendAudio) {\n this.getMediaStream(this.myId).then(function (stream) {\n var options = {\n sendAudio: true,\n localAudioStream: stream\n };\n\n _this2.peers[remoteId].offer(options);\n });\n } else {\n this.peers[remoteId].offer({});\n }\n }\n }, {\n key: \"closeStreamConnection\",\n value: function closeStreamConnection(clientId) {\n NAF.log.write('closeStreamConnection', clientId, this.peers);\n this.peers[clientId].close();\n delete this.peers[clientId];\n delete this.occupants[clientId];\n this.closedListener(clientId);\n }\n }, {\n key: \"getConnectStatus\",\n value: function getConnectStatus(clientId) {\n var peer = this.peers[clientId];\n if (peer === undefined) return NAF.adapters.NOT_CONNECTED;\n\n switch (peer.getStatus()) {\n case WebRtcPeer.IS_CONNECTED:\n return NAF.adapters.IS_CONNECTED;\n\n case WebRtcPeer.CONNECTING:\n return NAF.adapters.CONNECTING;\n\n case WebRtcPeer.NOT_CONNECTED:\n default:\n return NAF.adapters.NOT_CONNECTED;\n }\n }\n }, {\n key: \"sendData\",\n value: function sendData(to, type, data) {\n this.peers[to].send(type, data);\n }\n }, {\n key: \"sendDataGuaranteed\",\n value: function sendDataGuaranteed(to, type, data) {\n var packet = {\n from: this.myId,\n to: to,\n type: type,\n data: data,\n sending: true\n };\n this.socket.emit(\"send\", packet);\n }\n }, {\n key: \"broadcastData\",\n value: function broadcastData(type, data) {\n for (var clientId in this.peers) {\n this.sendData(clientId, type, data);\n }\n }\n }, {\n key: \"broadcastDataGuaranteed\",\n value: function broadcastDataGuaranteed(type, data) {\n var packet = {\n from: this.myId,\n type: type,\n data: data,\n broadcasting: true\n };\n this.socket.emit(\"broadcast\", packet);\n }\n }, {\n key: \"storeAudioStream\",\n value: function storeAudioStream(clientId, stream) {\n this.audioStreams[clientId] = stream;\n\n if (this.pendingAudioRequest[clientId]) {\n NAF.log.write(\"Received pending audio for \" + clientId);\n this.pendingAudioRequest[clientId](stream);\n delete this.pendingAudioRequest[clientId](stream);\n }\n }\n }, {\n key: \"trackListener\",\n value: function trackListener(clientId, stream) {\n this.storeAudioStream(clientId, stream);\n }\n }, {\n key: \"getMediaStream\",\n value: function getMediaStream(clientId) {\n var self = this;\n\n if (this.audioStreams[clientId]) {\n NAF.log.write(\"Already had audio for \" + clientId);\n return Promise.resolve(this.audioStreams[clientId]);\n } else {\n NAF.log.write(\"Waiting on audio for \" + clientId);\n return new Promise(function (resolve) {\n self.pendingAudioRequest[clientId] = resolve;\n });\n }\n }\n }, {\n key: \"updateTimeOffset\",\n value: function updateTimeOffset() {\n var _this3 = this;\n\n var clientSentTime = Date.now() + this.avgTimeOffset;\n return fetch(document.location.href, {\n method: \"HEAD\",\n cache: \"no-cache\"\n }).then(function (res) {\n var precision = 1000;\n var serverReceivedTime = new Date(res.headers.get(\"Date\")).getTime() + precision / 2;\n var clientReceivedTime = Date.now();\n var serverTime = serverReceivedTime + (clientReceivedTime - clientSentTime) / 2;\n var timeOffset = serverTime - clientReceivedTime;\n _this3.serverTimeRequests++;\n\n if (_this3.serverTimeRequests <= 10) {\n _this3.timeOffsets.push(timeOffset);\n } else {\n _this3.timeOffsets[_this3.serverTimeRequests % 10] = timeOffset;\n }\n\n _this3.avgTimeOffset = _this3.timeOffsets.reduce(function (acc, offset) {\n return acc += offset;\n }, 0) / _this3.timeOffsets.length;\n\n if (_this3.serverTimeRequests > 10) {\n setTimeout(function () {\n return _this3.updateTimeOffset();\n }, 5 * 60 * 1000); // Sync clock every 5 minutes.\n } else {\n _this3.updateTimeOffset();\n }\n });\n }\n }, {\n key: \"getServerTime\",\n value: function getServerTime() {\n return new Date().getTime() + this.avgTimeOffset;\n }\n }]);\n\n return WebrtcAdapter;\n}(); // NAF.adapters.register(\"native-webrtc\", WebrtcAdapter);\n\n\nmodule.exports = WebrtcAdapter;\n\n//# sourceURL=webpack:///./src/adapters/naf-webrtc-adapter.js?"); +eval("\n\nfunction _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError(\"Cannot call a class as a function\"); } }\n\nfunction _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if (\"value\" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }\n\nfunction _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }\n\n/* global NAF, io */\nvar WebRtcPeer = /*#__PURE__*/function () {\n function WebRtcPeer(localId, remoteId, sendSignalFunc) {\n _classCallCheck(this, WebRtcPeer);\n\n this.localId = localId;\n this.remoteId = remoteId;\n this.sendSignalFunc = sendSignalFunc;\n this.open = false;\n this.channelLabel = \"networked-aframe-channel\";\n this.pc = this.createPeerConnection();\n this.channel = null;\n }\n\n _createClass(WebRtcPeer, [{\n key: \"setDatachannelListeners\",\n value: function setDatachannelListeners(openListener, closedListener, messageListener, trackListener) {\n this.openListener = openListener;\n this.closedListener = closedListener;\n this.messageListener = messageListener;\n this.trackListener = trackListener;\n }\n }, {\n key: \"offer\",\n value: function offer(options) {\n var self = this; // reliable: false - UDP\n\n this.setupChannel(this.pc.createDataChannel(this.channelLabel, {\n reliable: false\n })); // If there are errors with Safari implement this:\n // https://github.com/OpenVidu/openvidu/blob/master/openvidu-browser/src/OpenViduInternal/WebRtcPeer/WebRtcPeer.ts#L154\n\n if (options.sendAudio) {\n options.localAudioStream.getTracks().forEach(function (track) {\n return self.pc.addTrack(track, options.localAudioStream);\n });\n }\n\n this.pc.createOffer(function (sdp) {\n self.handleSessionDescription(sdp);\n }, function (error) {\n NAF.log.error(\"WebRtcPeer.offer: \" + error);\n }, {\n offerToReceiveAudio: true,\n offerToReceiveVideo: false\n });\n }\n }, {\n key: \"handleSignal\",\n value: function handleSignal(signal) {\n // ignores signal if it isn't for me\n if (this.localId !== signal.to || this.remoteId !== signal.from) return;\n\n switch (signal.type) {\n case \"offer\":\n this.handleOffer(signal);\n break;\n\n case \"answer\":\n this.handleAnswer(signal);\n break;\n\n case \"candidate\":\n this.handleCandidate(signal);\n break;\n\n default:\n NAF.log.error(\"WebRtcPeer.handleSignal: Unknown signal type \" + signal.type);\n break;\n }\n }\n }, {\n key: \"send\",\n value: function send(type, data) {\n if (this.channel === null || this.channel.readyState !== \"open\") {\n return;\n }\n\n this.channel.send(JSON.stringify({\n type: type,\n data: data\n }));\n }\n }, {\n key: \"getStatus\",\n value: function getStatus() {\n if (this.channel === null) return WebRtcPeer.NOT_CONNECTED;\n\n switch (this.channel.readyState) {\n case \"open\":\n return WebRtcPeer.IS_CONNECTED;\n\n case \"connecting\":\n return WebRtcPeer.CONNECTING;\n\n case \"closing\":\n case \"closed\":\n default:\n return WebRtcPeer.NOT_CONNECTED;\n }\n }\n /*\r\n * Privates\r\n */\n\n }, {\n key: \"createPeerConnection\",\n value: function createPeerConnection() {\n var self = this;\n var RTCPeerConnection = window.RTCPeerConnection || window.webkitRTCPeerConnection || window.mozRTCPeerConnection || window.msRTCPeerConnection;\n\n if (RTCPeerConnection === undefined) {\n throw new Error(\"WebRtcPeer.createPeerConnection: This browser does not seem to support WebRTC.\");\n }\n\n var pc = new RTCPeerConnection({\n iceServers: WebRtcPeer.ICE_SERVERS\n });\n\n pc.onicecandidate = function (event) {\n if (event.candidate) {\n self.sendSignalFunc({\n from: self.localId,\n to: self.remoteId,\n type: \"candidate\",\n sdpMLineIndex: event.candidate.sdpMLineIndex,\n candidate: event.candidate.candidate\n });\n }\n }; // Note: seems like channel.onclose hander is unreliable on some platforms,\n // so also tries to detect disconnection here.\n\n\n pc.oniceconnectionstatechange = function () {\n if (self.open && pc.iceConnectionState === \"disconnected\") {\n self.open = false;\n self.closedListener(self.remoteId);\n }\n };\n\n pc.ontrack = function (e) {\n self.trackListener(self.remoteId, e.streams[0]);\n };\n\n return pc;\n }\n }, {\n key: \"setupChannel\",\n value: function setupChannel(channel) {\n var self = this;\n this.channel = channel; // received data from a remote peer\n\n this.channel.onmessage = function (event) {\n var data = JSON.parse(event.data);\n self.messageListener(self.remoteId, data.type, data.data);\n }; // connected with a remote peer\n\n\n this.channel.onopen = function (_event) {\n self.open = true;\n self.openListener(self.remoteId);\n }; // disconnected with a remote peer\n\n\n this.channel.onclose = function (_event) {\n if (!self.open) return;\n self.open = false;\n self.closedListener(self.remoteId);\n }; // error occurred with a remote peer\n\n\n this.channel.onerror = function (error) {\n NAF.log.error(\"WebRtcPeer.channel.onerror: \" + error);\n };\n }\n }, {\n key: \"handleOffer\",\n value: function handleOffer(message) {\n var self = this;\n\n this.pc.ondatachannel = function (event) {\n self.setupChannel(event.channel);\n };\n\n this.setRemoteDescription(message);\n this.pc.createAnswer(function (sdp) {\n self.handleSessionDescription(sdp);\n }, function (error) {\n NAF.log.error(\"WebRtcPeer.handleOffer: \" + error);\n });\n }\n }, {\n key: \"handleAnswer\",\n value: function handleAnswer(message) {\n this.setRemoteDescription(message);\n }\n }, {\n key: \"handleCandidate\",\n value: function handleCandidate(message) {\n var RTCIceCandidate = window.RTCIceCandidate || window.webkitRTCIceCandidate || window.mozRTCIceCandidate;\n this.pc.addIceCandidate(new RTCIceCandidate(message), function () {}, function (error) {\n NAF.log.error(\"WebRtcPeer.handleCandidate: \" + error);\n });\n }\n }, {\n key: \"handleSessionDescription\",\n value: function handleSessionDescription(sdp) {\n this.pc.setLocalDescription(sdp, function () {}, function (error) {\n NAF.log.error(\"WebRtcPeer.handleSessionDescription: \" + error);\n });\n this.sendSignalFunc({\n from: this.localId,\n to: this.remoteId,\n type: sdp.type,\n sdp: sdp.sdp\n });\n }\n }, {\n key: \"setRemoteDescription\",\n value: function setRemoteDescription(message) {\n var RTCSessionDescription = window.RTCSessionDescription || window.webkitRTCSessionDescription || window.mozRTCSessionDescription || window.msRTCSessionDescription;\n this.pc.setRemoteDescription(new RTCSessionDescription(message), function () {}, function (error) {\n NAF.log.error(\"WebRtcPeer.setRemoteDescription: \" + error);\n });\n }\n }, {\n key: \"close\",\n value: function close() {\n if (this.pc) {\n this.pc.close();\n }\n }\n }]);\n\n return WebRtcPeer;\n}();\n\nWebRtcPeer.IS_CONNECTED = \"IS_CONNECTED\";\nWebRtcPeer.CONNECTING = \"CONNECTING\";\nWebRtcPeer.NOT_CONNECTED = \"NOT_CONNECTED\";\nWebRtcPeer.ICE_SERVERS = [{\n urls: \"stun:stun1.l.google.com:19302\"\n}, {\n urls: \"stun:stun2.l.google.com:19302\"\n}, {\n urls: \"stun:stun3.l.google.com:19302\"\n}, {\n urls: \"stun:stun4.l.google.com:19302\"\n}];\n/**\r\n * Native WebRTC Adapter (native-webrtc)\r\n * For use with uws-server.js\r\n * networked-scene: serverURL needs to be ws://localhost:8080 when running locally\r\n */\n\nvar WebrtcAdapter = /*#__PURE__*/function () {\n function WebrtcAdapter() {\n _classCallCheck(this, WebrtcAdapter);\n\n if (io === undefined) console.warn('It looks like socket.io has not been loaded before WebrtcAdapter. Please do that.');\n this.app = \"default\";\n this.room = \"default\";\n this.occupantListener = null;\n this.myRoomJoinTime = null;\n this.myId = null;\n this.peers = {}; // id -> WebRtcPeer\n\n this.occupants = {}; // id -> joinTimestamp\n\n this.audioStreams = {};\n this.pendingAudioRequest = {};\n this.serverTimeRequests = 0;\n this.timeOffsets = [];\n this.avgTimeOffset = 0;\n }\n\n _createClass(WebrtcAdapter, [{\n key: \"setServerUrl\",\n value: function setServerUrl(wsUrl) {\n this.wsUrl = wsUrl;\n }\n }, {\n key: \"setApp\",\n value: function setApp(appName) {\n this.app = appName;\n }\n }, {\n key: \"setRoom\",\n value: function setRoom(roomName) {\n this.room = roomName;\n }\n }, {\n key: \"setWebRtcOptions\",\n value: function setWebRtcOptions(options) {\n if (options.datachannel === false) {\n NAF.log.error(\"WebrtcAdapter.setWebRtcOptions: datachannel must be true.\");\n }\n\n if (options.audio === true) {\n this.sendAudio = true;\n }\n\n if (options.video === true) {\n NAF.log.warn(\"WebrtcAdapter does not support video yet.\");\n }\n }\n }, {\n key: \"setServerConnectListeners\",\n value: function setServerConnectListeners(successListener, failureListener) {\n this.connectSuccess = successListener;\n this.connectFailure = failureListener;\n }\n }, {\n key: \"setRoomOccupantListener\",\n value: function setRoomOccupantListener(occupantListener) {\n this.occupantListener = occupantListener;\n }\n }, {\n key: \"setDataChannelListeners\",\n value: function setDataChannelListeners(openListener, closedListener, messageListener) {\n this.openListener = openListener;\n this.closedListener = closedListener;\n this.messageListener = messageListener;\n }\n }, {\n key: \"connect\",\n value: function connect() {\n var self = this;\n this.updateTimeOffset().then(function () {\n if (!self.wsUrl || self.wsUrl === \"/\") {\n if (location.protocol === \"https:\") {\n self.wsUrl = \"wss://\" + location.host;\n } else {\n self.wsUrl = \"ws://\" + location.host;\n }\n }\n\n NAF.log.write(\"Attempting to connect to socket.io\");\n var socket = self.socket = io(self.wsUrl);\n socket.on(\"connect\", function () {\n NAF.log.write(\"User connected\", socket.id);\n self.myId = socket.id;\n self.joinRoom();\n });\n socket.on(\"connectSuccess\", function (data) {\n var joinedTime = data.joinedTime;\n self.myRoomJoinTime = joinedTime;\n NAF.log.write(\"Successfully joined room\", self.room, \"at server time\", joinedTime);\n\n if (self.sendAudio) {\n var mediaConstraints = {\n audio: true,\n video: false\n };\n navigator.mediaDevices.getUserMedia(mediaConstraints).then(function (localStream) {\n self.storeAudioStream(self.myId, localStream);\n self.connectSuccess(self.myId);\n localStream.getTracks().forEach(function (track) {\n Object.keys(self.peers).forEach(function (peerId) {\n self.peers[peerId].pc.addTrack(track, localStream);\n });\n });\n })[\"catch\"](function (e) {\n NAF.log.error(e);\n console.error(\"Microphone is disabled due to lack of permissions\");\n self.sendAudio = false;\n self.connectSuccess(self.myId);\n });\n } else {\n self.connectSuccess(self.myId);\n }\n });\n socket.on(\"error\", function (err) {\n console.error(\"Socket connection failure\", err);\n self.connectFailure();\n });\n socket.on(\"occupantsChanged\", function (data) {\n var occupants = data.occupants;\n NAF.log.write('occupants changed', data);\n self.receivedOccupants(occupants);\n });\n\n function receiveData(packet) {\n var from = packet.from;\n var type = packet.type;\n var data = packet.data;\n\n if (type === 'ice-candidate') {\n self.peers[from].handleSignal(data);\n return;\n }\n\n self.messageListener(from, type, data);\n }\n\n socket.on(\"send\", receiveData);\n socket.on(\"broadcast\", receiveData);\n });\n }\n }, {\n key: \"joinRoom\",\n value: function joinRoom() {\n NAF.log.write(\"Joining room\", this.room);\n this.socket.emit(\"joinRoom\", {\n room: this.room\n });\n }\n }, {\n key: \"receivedOccupants\",\n value: function receivedOccupants(occupants) {\n var _this = this;\n\n delete occupants[this.myId];\n this.occupants = occupants;\n var self = this;\n var localId = this.myId;\n\n var _loop = function _loop(key) {\n var remoteId = key;\n if (_this.peers[remoteId]) return \"continue\";\n var peer = new WebRtcPeer(localId, remoteId, function (data) {\n self.socket.emit('send', {\n from: localId,\n to: remoteId,\n type: 'ice-candidate',\n data: data,\n sending: true\n });\n });\n peer.setDatachannelListeners(self.openListener, self.closedListener, self.messageListener, self.trackListener.bind(self));\n self.peers[remoteId] = peer;\n };\n\n for (var key in occupants) {\n var _ret = _loop(key);\n\n if (_ret === \"continue\") continue;\n }\n\n this.occupantListener(occupants);\n }\n }, {\n key: \"shouldStartConnectionTo\",\n value: function shouldStartConnectionTo(client) {\n return (this.myRoomJoinTime || 0) <= (client || 0);\n }\n }, {\n key: \"startStreamConnection\",\n value: function startStreamConnection(remoteId) {\n var _this2 = this;\n\n NAF.log.write('starting offer process');\n\n if (this.sendAudio) {\n this.getMediaStream(this.myId).then(function (stream) {\n var options = {\n sendAudio: true,\n localAudioStream: stream\n };\n\n _this2.peers[remoteId].offer(options);\n });\n } else {\n this.peers[remoteId].offer({});\n }\n }\n }, {\n key: \"closeStreamConnection\",\n value: function closeStreamConnection(clientId) {\n NAF.log.write('closeStreamConnection', clientId, this.peers);\n this.peers[clientId].close();\n delete this.peers[clientId];\n delete this.occupants[clientId];\n this.closedListener(clientId);\n }\n }, {\n key: \"getConnectStatus\",\n value: function getConnectStatus(clientId) {\n var peer = this.peers[clientId];\n if (peer === undefined) return NAF.adapters.NOT_CONNECTED;\n\n switch (peer.getStatus()) {\n case WebRtcPeer.IS_CONNECTED:\n return NAF.adapters.IS_CONNECTED;\n\n case WebRtcPeer.CONNECTING:\n return NAF.adapters.CONNECTING;\n\n case WebRtcPeer.NOT_CONNECTED:\n default:\n return NAF.adapters.NOT_CONNECTED;\n }\n }\n }, {\n key: \"sendData\",\n value: function sendData(to, type, data) {\n this.peers[to].send(type, data);\n }\n }, {\n key: \"sendDataGuaranteed\",\n value: function sendDataGuaranteed(to, type, data) {\n var packet = {\n from: this.myId,\n to: to,\n type: type,\n data: data,\n sending: true\n };\n this.socket.emit(\"send\", packet);\n }\n }, {\n key: \"broadcastData\",\n value: function broadcastData(type, data) {\n for (var clientId in this.peers) {\n this.sendData(clientId, type, data);\n }\n }\n }, {\n key: \"broadcastDataGuaranteed\",\n value: function broadcastDataGuaranteed(type, data) {\n var packet = {\n from: this.myId,\n type: type,\n data: data,\n broadcasting: true\n };\n this.socket.emit(\"broadcast\", packet);\n }\n }, {\n key: \"storeAudioStream\",\n value: function storeAudioStream(clientId, stream) {\n this.audioStreams[clientId] = stream;\n\n if (this.pendingAudioRequest[clientId]) {\n NAF.log.write(\"Received pending audio for \" + clientId);\n this.pendingAudioRequest[clientId](stream);\n delete this.pendingAudioRequest[clientId](stream);\n }\n }\n }, {\n key: \"trackListener\",\n value: function trackListener(clientId, stream) {\n this.storeAudioStream(clientId, stream);\n }\n }, {\n key: \"getMediaStream\",\n value: function getMediaStream(clientId) {\n var self = this;\n\n if (this.audioStreams[clientId]) {\n NAF.log.write(\"Already had audio for \" + clientId);\n return Promise.resolve(this.audioStreams[clientId]);\n } else {\n NAF.log.write(\"Waiting on audio for \" + clientId);\n return new Promise(function (resolve) {\n self.pendingAudioRequest[clientId] = resolve;\n });\n }\n }\n }, {\n key: \"updateTimeOffset\",\n value: function updateTimeOffset() {\n var _this3 = this;\n\n var clientSentTime = Date.now() + this.avgTimeOffset;\n return fetch(document.location.href, {\n method: \"HEAD\",\n cache: \"no-cache\"\n }).then(function (res) {\n var precision = 1000;\n var serverReceivedTime = new Date(res.headers.get(\"Date\")).getTime() + precision / 2;\n var clientReceivedTime = Date.now();\n var serverTime = serverReceivedTime + (clientReceivedTime - clientSentTime) / 2;\n var timeOffset = serverTime - clientReceivedTime;\n _this3.serverTimeRequests++;\n\n if (_this3.serverTimeRequests <= 10) {\n _this3.timeOffsets.push(timeOffset);\n } else {\n _this3.timeOffsets[_this3.serverTimeRequests % 10] = timeOffset;\n }\n\n _this3.avgTimeOffset = _this3.timeOffsets.reduce(function (acc, offset) {\n return acc += offset;\n }, 0) / _this3.timeOffsets.length;\n\n if (_this3.serverTimeRequests > 10) {\n setTimeout(function () {\n return _this3.updateTimeOffset();\n }, 5 * 60 * 1000); // Sync clock every 5 minutes.\n } else {\n _this3.updateTimeOffset();\n }\n });\n }\n }, {\n key: \"getServerTime\",\n value: function getServerTime() {\n return new Date().getTime() + this.avgTimeOffset;\n }\n }]);\n\n return WebrtcAdapter;\n}(); // NAF.adapters.register(\"native-webrtc\", WebrtcAdapter);\n\n\nmodule.exports = WebrtcAdapter;\n\n//# sourceURL=webpack:///./src/adapters/naf-webrtc-adapter.js?"); /***/ }), @@ -286,7 +286,7 @@ eval("\n\n/* global AFRAME, NAF, THREE */\nvar naf = __webpack_require__(/*! ../ /***/ (function(module, exports, __webpack_require__) { "use strict"; -eval("\n\n/* global AFRAME, NAF */\nAFRAME.registerComponent('networked-scene', {\n schema: {\n serverURL: {\n \"default\": '/'\n },\n app: {\n \"default\": 'default'\n },\n room: {\n \"default\": 'default'\n },\n connectOnLoad: {\n \"default\": true\n },\n onConnect: {\n \"default\": 'onConnect'\n },\n adapter: {\n \"default\": 'wseasyrtc'\n },\n // See https://github.com/networked-aframe/networked-aframe#adapters for list of adapters\n audio: {\n \"default\": false\n },\n // Only if adapter supports audio\n video: {\n \"default\": false\n },\n // Only if adapter supports video\n debug: {\n \"default\": false\n }\n },\n init: function init() {\n var el = this.el;\n this.connect = this.connect.bind(this);\n el.addEventListener('connect', this.connect);\n\n if (this.data.connectOnLoad) {\n el.emit('connect', null, false);\n }\n },\n\n /**\n * Connect to signalling server and begin connecting to other clients\n */\n connect: function connect() {\n NAF.log.setDebug(this.data.debug);\n NAF.log.write('Networked-Aframe Connecting...');\n this.checkDeprecatedProperties();\n this.setupNetworkAdapter();\n\n if (this.hasOnConnectFunction()) {\n this.callOnConnect();\n }\n\n return NAF.connection.connect(this.data.serverURL, this.data.app, this.data.room, this.data.audio, this.data.video);\n },\n checkDeprecatedProperties: function checkDeprecatedProperties() {// No current\n },\n setupNetworkAdapter: function setupNetworkAdapter() {\n var adapterName = this.data.adapter;\n var adapter = NAF.adapters.make(adapterName);\n NAF.connection.setNetworkAdapter(adapter);\n this.el.emit('adapter-ready', adapter, false);\n },\n hasOnConnectFunction: function hasOnConnectFunction() {\n return this.data.onConnect != '' && window[this.data.onConnect];\n },\n callOnConnect: function callOnConnect() {\n NAF.connection.onConnect(window[this.data.onConnect]);\n },\n remove: function remove() {\n NAF.log.write('networked-scene disconnected');\n this.el.removeEventListener('connect', this.connect);\n NAF.connection.disconnect();\n }\n});\n\n//# sourceURL=webpack:///./src/components/networked-scene.js?"); +eval("\n\n/* global AFRAME, NAF */\nAFRAME.registerComponent('networked-scene', {\n schema: {\n serverURL: {\n \"default\": '/'\n },\n app: {\n \"default\": 'default'\n },\n room: {\n \"default\": 'default'\n },\n connectOnLoad: {\n \"default\": true\n },\n onConnect: {\n \"default\": 'onConnect'\n },\n adapter: {\n \"default\": 'wseasyrtc'\n },\n // See https://github.com/networked-aframe/networked-aframe#adapters for list of adapters\n audio: {\n \"default\": false\n },\n // Only if adapter supports audio\n video: {\n \"default\": false\n },\n // Only if adapter supports video\n debug: {\n \"default\": false\n }\n },\n init: function init() {\n var el = this.el;\n this.connect = this.connect.bind(this);\n el.addEventListener('connect', this.connect);\n\n if (this.data.connectOnLoad) {\n el.emit('connect', null, false);\n }\n },\n\n /**\r\n * Connect to signalling server and begin connecting to other clients\r\n */\n connect: function connect() {\n NAF.log.setDebug(this.data.debug);\n NAF.log.write('Networked-Aframe Connecting...');\n this.checkDeprecatedProperties();\n this.setupNetworkAdapter();\n\n if (this.hasOnConnectFunction()) {\n this.callOnConnect();\n }\n\n return NAF.connection.connect(this.data.serverURL, this.data.app, this.data.room, this.data.audio, this.data.video);\n },\n checkDeprecatedProperties: function checkDeprecatedProperties() {// No current\n },\n setupNetworkAdapter: function setupNetworkAdapter() {\n var adapterName = this.data.adapter;\n var adapter = NAF.adapters.make(adapterName);\n NAF.connection.setNetworkAdapter(adapter);\n this.el.emit('adapter-ready', adapter, false);\n },\n hasOnConnectFunction: function hasOnConnectFunction() {\n return this.data.onConnect != '' && window[this.data.onConnect];\n },\n callOnConnect: function callOnConnect() {\n NAF.connection.onConnect(window[this.data.onConnect]);\n },\n remove: function remove() {\n NAF.log.write('networked-scene disconnected');\n this.el.removeEventListener('connect', this.connect);\n NAF.connection.disconnect();\n }\n});\n\n//# sourceURL=webpack:///./src/components/networked-scene.js?"); /***/ }), @@ -298,7 +298,7 @@ eval("\n\n/* global AFRAME, NAF */\nAFRAME.registerComponent('networked-scene', /***/ (function(module, exports, __webpack_require__) { "use strict"; -eval("\n\n/* global AFRAME, NAF, THREE */\nvar naf = __webpack_require__(/*! ../NafIndex */ \"./src/NafIndex.js\");\n\nAFRAME.registerComponent('networked-video-source', {\n schema: {\n streamName: {\n \"default\": 'video'\n }\n },\n dependencies: ['material'],\n init: function init() {\n var _this = this;\n\n this.videoTexture = null;\n this.video = null;\n this.stream = null;\n this._setMediaStream = this._setMediaStream.bind(this);\n NAF.utils.getNetworkedEntity(this.el).then(function (networkedEl) {\n var ownerId = networkedEl.components.networked.data.owner;\n\n if (ownerId) {\n NAF.connection.adapter.getMediaStream(ownerId, _this.data.streamName).then(_this._setMediaStream)[\"catch\"](function (e) {\n return naf.log.error(\"Error getting media stream for \".concat(ownerId), e);\n });\n } else {// Correctly configured local entity, perhaps do something here for enabling debug audio loopback\n }\n });\n },\n _setMediaStream: function _setMediaStream(newStream) {\n if (!this.video) {\n this.setupVideo();\n }\n\n if (newStream != this.stream) {\n if (this.stream) {\n this._clearMediaStream();\n }\n\n if (newStream) {\n this.video.srcObject = newStream;\n var playResult = this.video.play();\n\n if (playResult instanceof Promise) {\n playResult[\"catch\"](function (e) {\n return naf.log.error(\"Error play video stream\", e);\n });\n }\n\n if (this.videoTexture) {\n this.videoTexture.dispose();\n }\n\n this.videoTexture = new THREE.VideoTexture(this.video);\n var mesh = this.el.getObject3D('mesh');\n mesh.material.map = this.videoTexture;\n mesh.material.needsUpdate = true;\n }\n\n this.stream = newStream;\n }\n },\n _clearMediaStream: function _clearMediaStream() {\n this.stream = null;\n\n if (this.videoTexture) {\n if (this.videoTexture.image instanceof HTMLVideoElement) {\n // Note: this.videoTexture.image === this.video\n var video = this.videoTexture.image;\n video.pause();\n video.srcObject = null;\n video.load();\n }\n\n this.videoTexture.dispose();\n this.videoTexture = null;\n }\n },\n remove: function remove() {\n this._clearMediaStream();\n },\n setupVideo: function setupVideo() {\n if (!this.video) {\n var video = document.createElement('video');\n video.setAttribute('autoplay', true);\n video.setAttribute('playsinline', true);\n video.setAttribute('muted', true);\n this.video = video;\n }\n }\n});\n\n//# sourceURL=webpack:///./src/components/networked-video-source.js?"); +eval("\n\n/* global AFRAME, NAF, THREE */\nvar naf = __webpack_require__(/*! ../NafIndex */ \"./src/NafIndex.js\");\n\nAFRAME.registerComponent('networked-video-source', {\n schema: {\n streamName: {\n \"default\": 'video'\n },\n useGreenScreen: {\n \"default\": false\n },\n ThresholdMin: {\n \"default\": 0.106\n },\n ThresholdMax: {\n \"default\": 0.13\n },\n red: {\n \"default\": 48\n },\n green: {\n \"default\": 146\n },\n blue: {\n \"default\": 89\n }\n },\n dependencies: ['material'],\n update: function update() {\n var _this = this;\n\n this.videoTexture = null;\n this.video = null;\n this.stream = null;\n this._setMediaStream = this._setMediaStream.bind(this);\n NAF.utils.getNetworkedEntity(this.el).then(function (networkedEl) {\n var ownerId = networkedEl.components.networked.data.owner;\n\n if (ownerId) {\n NAF.connection.adapter.getMediaStream(ownerId, _this.data.streamName).then(_this._setMediaStream)[\"catch\"](function (e) {\n return naf.log.error(\"Error getting media stream for \".concat(ownerId), e);\n });\n } else {// Correctly configured local entity, perhaps do something here for enabling debug audio loopback\n }\n });\n },\n _setMediaStream: function _setMediaStream(newStream) {\n if (!this.video) {\n this.setupVideo();\n }\n\n if (newStream != this.stream) {\n if (this.stream) {\n this._clearMediaStream();\n }\n\n if (newStream) {\n this.video.srcObject = newStream;\n var playResult = this.video.play();\n\n if (playResult instanceof Promise) {\n playResult[\"catch\"](function (e) {\n return naf.log.error(\"Error play video stream\", e);\n });\n }\n\n if (this.videoTexture) {\n this.videoTexture.dispose();\n }\n\n this.videoTexture = new THREE.VideoTexture(this.video);\n var mesh = this.el.getObject3D('mesh'); // replace green with transparent pixels\n\n if (this.data.useGreenScreen) {\n this.uniforms = {};\n this.uniforms.uMap = {\n type: 't',\n value: this.videoTexture\n };\n this.uniforms.ThresholdMin = {\n type: 'float',\n value: this.data.ThresholdMin\n };\n this.uniforms.ThresholdMax = {\n type: 'float',\n value: this.data.ThresholdMax\n };\n this.uniforms.red = {\n type: 'float',\n value: this.data.red\n };\n this.uniforms.green = {\n type: 'float',\n value: this.data.green\n };\n this.uniforms.blue = {\n type: 'float',\n value: this.data.blue\n };\n this.uniforms = THREE.UniformsUtils.merge([this.uniforms, THREE.UniformsLib['lights']]);\n this.materialIncoming = new THREE.ShaderMaterial({\n uniforms: this.uniforms\n });\n this.materialIncoming.vertexShader = \"\\n varying vec2 vUv;\\n\\n void main() {\\n vec4 worldPosition = modelViewMatrix * vec4( position, 1.0 );\\n vec3 vWorldPosition = worldPosition.xyz;\\n vUv = uv;\\n gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );\\n }\\n \";\n this.materialIncoming.fragmentShader = \"\\n varying vec2 vUv;\\n uniform sampler2D uMap;\\n uniform float ThresholdMin;\\n uniform float ThresholdMax;\\n uniform float red;\\n uniform float green;\\n uniform float blue;\\n \\n mat4 RGBtoYUV = mat4(0.257, 0.439, -0.148, 0.0,\\n 0.504, -0.368, -0.291, 0.0,\\n 0.098, -0.071, 0.439, 0.0,\\n 0.0625, 0.500, 0.500, 1.0 );\\n \\n void main() {\\n vec2 uv = vUv;\\n vec4 tex1 = texture2D(uMap, uv * 1.0);\\n\\n //color to be removed\\n vec4 chromaKey = vec4(red / 255.0, green/255.0, blue/255.0, 1);\\n \\n //convert from RGB to YCvCr/YUV\\n vec4 keyYUV = RGBtoYUV * chromaKey;\\n \\n vec4 yuv = RGBtoYUV * tex1;\\n \\n float cc;\\n \\n float tmp = sqrt(pow(keyYUV.g - yuv.g, 2.0) + pow(keyYUV.b - yuv.b, 2.0));\\n if (tmp < ThresholdMin)\\n cc = 0.0;\\n else if (tmp < ThresholdMax)\\n cc = (tmp - ThresholdMin)/(ThresholdMax - ThresholdMin);\\n else\\n cc= 1.0;\\n \\n gl_FragColor = max(tex1 - (1.0 - cc) * chromaKey, 0.0);\\n \\n // if (tex1.g - tex1.r > greenThreshold)\\n // discard; \\n // else\\n // gl_FragColor = fragColor;\\n }\\n \";\n this.materialIncoming.transparent = true;\n this.materialIncoming.side = THREE.DoubleSide;\n mesh.material = this.materialIncoming;\n } else {\n mesh.material.map = this.videoTexture;\n }\n\n mesh.material.needsUpdate = true;\n }\n\n this.stream = newStream;\n }\n },\n _clearMediaStream: function _clearMediaStream() {\n this.stream = null;\n\n if (this.videoTexture) {\n if (this.videoTexture.image instanceof HTMLVideoElement) {\n // Note: this.videoTexture.image === this.video\n var video = this.videoTexture.image;\n video.pause();\n video.srcObject = null;\n video.load();\n }\n\n this.videoTexture.dispose();\n this.videoTexture = null;\n }\n },\n remove: function remove() {\n this._clearMediaStream();\n },\n setupVideo: function setupVideo() {\n if (!this.video) {\n var video = document.createElement('video');\n video.setAttribute('autoplay', true);\n video.setAttribute('playsinline', true);\n video.setAttribute('muted', true);\n this.video = video;\n }\n }\n});\n\n//# sourceURL=webpack:///./src/components/networked-video-source.js?"); /***/ }), @@ -346,7 +346,7 @@ eval("\n\nvar options = {\n debug: false,\n updateRate: 15,\n // How often ne /***/ (function(module, exports, __webpack_require__) { "use strict"; -eval("\n\n/* global NAF */\nmodule.exports.whenEntityLoaded = function (entity, callback) {\n if (entity.hasLoaded) {\n callback();\n }\n\n entity.addEventListener('loaded', function () {\n callback();\n });\n};\n\nmodule.exports.createHtmlNodeFromString = function (str) {\n var div = document.createElement('div');\n div.innerHTML = str;\n var child = div.firstChild;\n return child;\n};\n\nmodule.exports.getCreator = function (el) {\n var components = el.components;\n\n if (components['networked']) {\n return components['networked'].data.creator;\n }\n\n return null;\n};\n\nmodule.exports.getNetworkOwner = function (el) {\n var components = el.components;\n\n if (components['networked']) {\n return components['networked'].data.owner;\n }\n\n return null;\n};\n\nmodule.exports.getNetworkId = function (el) {\n var components = el.components;\n\n if (components['networked']) {\n return components['networked'].data.networkId;\n }\n\n return null;\n};\n\nmodule.exports.now = function () {\n return Date.now();\n};\n\nmodule.exports.createNetworkId = function () {\n return Math.random().toString(36).substring(2, 9);\n};\n/**\n * Find the closest ancestor (including the passed in entity) that has a `networked` component\n * @param {ANode} entity - Entity to begin the search on\n * @returns {Promise} An promise that resolves to an entity with a `networked` component\n */\n\n\nfunction getNetworkedEntity(entity) {\n return new Promise(function (resolve, reject) {\n var curEntity = entity;\n\n while (curEntity && curEntity.components && !curEntity.components.networked) {\n curEntity = curEntity.parentNode;\n }\n\n if (!curEntity || !curEntity.components || !curEntity.components.networked) {\n return reject(\"Entity does not have and is not a child of an entity with the [networked] component \");\n }\n\n if (curEntity.hasLoaded) {\n resolve(curEntity);\n } else {\n curEntity.addEventListener(\"instantiated\", function () {\n resolve(curEntity);\n }, {\n once: true\n });\n }\n });\n}\n\nmodule.exports.getNetworkedEntity = getNetworkedEntity;\n\nmodule.exports.takeOwnership = function (entity) {\n var curEntity = entity;\n\n while (curEntity && curEntity.components && !curEntity.components.networked) {\n curEntity = curEntity.parentNode;\n }\n\n if (!curEntity || !curEntity.components || !curEntity.components.networked) {\n throw new Error(\"Entity does not have and is not a child of an entity with the [networked] component \");\n }\n\n return curEntity.components.networked.takeOwnership();\n};\n\nmodule.exports.isMine = function (entity) {\n var curEntity = entity;\n\n while (curEntity && curEntity.components && !curEntity.components.networked) {\n curEntity = curEntity.parentNode;\n }\n\n if (!curEntity || !curEntity.components || !curEntity.components.networked) {\n throw new Error(\"Entity does not have and is not a child of an entity with the [networked] component \");\n } // When remote networked entities are initially created, there's a frame delay before they are completely instantiated.\n // On that frame, data is undefined so we can't check the owner. In this instance we assume that the user is not the owner of the entity.\n\n\n if (!curEntity.components.networked.data) {\n return false;\n }\n\n return curEntity.components.networked.data.owner === NAF.clientId;\n};\n\nmodule.exports.almostEqualVec3 = function (u, v, epsilon) {\n return Math.abs(u.x - v.x) < epsilon && Math.abs(u.y - v.y) < epsilon && Math.abs(u.z - v.z) < epsilon;\n};\n\n//# sourceURL=webpack:///./src/utils.js?"); +eval("\n\n/* global NAF */\nmodule.exports.whenEntityLoaded = function (entity, callback) {\n if (entity.hasLoaded) {\n callback();\n }\n\n entity.addEventListener('loaded', function () {\n callback();\n });\n};\n\nmodule.exports.createHtmlNodeFromString = function (str) {\n var div = document.createElement('div');\n div.innerHTML = str;\n var child = div.firstChild;\n return child;\n};\n\nmodule.exports.getCreator = function (el) {\n var components = el.components;\n\n if (components['networked']) {\n return components['networked'].data.creator;\n }\n\n return null;\n};\n\nmodule.exports.getNetworkOwner = function (el) {\n var components = el.components;\n\n if (components['networked']) {\n return components['networked'].data.owner;\n }\n\n return null;\n};\n\nmodule.exports.getNetworkId = function (el) {\n var components = el.components;\n\n if (components['networked']) {\n return components['networked'].data.networkId;\n }\n\n return null;\n};\n\nmodule.exports.now = function () {\n return Date.now();\n};\n\nmodule.exports.createNetworkId = function () {\n return Math.random().toString(36).substring(2, 9);\n};\n/**\r\n * Find the closest ancestor (including the passed in entity) that has a `networked` component\r\n * @param {ANode} entity - Entity to begin the search on\r\n * @returns {Promise} An promise that resolves to an entity with a `networked` component\r\n */\n\n\nfunction getNetworkedEntity(entity) {\n return new Promise(function (resolve, reject) {\n var curEntity = entity;\n\n while (curEntity && curEntity.components && !curEntity.components.networked) {\n curEntity = curEntity.parentNode;\n }\n\n if (!curEntity || !curEntity.components || !curEntity.components.networked) {\n return reject(\"Entity does not have and is not a child of an entity with the [networked] component \");\n }\n\n if (curEntity.hasLoaded) {\n resolve(curEntity);\n } else {\n curEntity.addEventListener(\"instantiated\", function () {\n resolve(curEntity);\n }, {\n once: true\n });\n }\n });\n}\n\nmodule.exports.getNetworkedEntity = getNetworkedEntity;\n\nmodule.exports.takeOwnership = function (entity) {\n var curEntity = entity;\n\n while (curEntity && curEntity.components && !curEntity.components.networked) {\n curEntity = curEntity.parentNode;\n }\n\n if (!curEntity || !curEntity.components || !curEntity.components.networked) {\n throw new Error(\"Entity does not have and is not a child of an entity with the [networked] component \");\n }\n\n return curEntity.components.networked.takeOwnership();\n};\n\nmodule.exports.isMine = function (entity) {\n var curEntity = entity;\n\n while (curEntity && curEntity.components && !curEntity.components.networked) {\n curEntity = curEntity.parentNode;\n }\n\n if (!curEntity || !curEntity.components || !curEntity.components.networked) {\n throw new Error(\"Entity does not have and is not a child of an entity with the [networked] component \");\n } // When remote networked entities are initially created, there's a frame delay before they are completely instantiated.\n // On that frame, data is undefined so we can't check the owner. In this instance we assume that the user is not the owner of the entity.\n\n\n if (!curEntity.components.networked.data) {\n return false;\n }\n\n return curEntity.components.networked.data.owner === NAF.clientId;\n};\n\nmodule.exports.almostEqualVec3 = function (u, v, epsilon) {\n return Math.abs(u.x - v.x) < epsilon && Math.abs(u.y - v.y) < epsilon && Math.abs(u.z - v.z) < epsilon;\n};\n\n//# sourceURL=webpack:///./src/utils.js?"); /***/ }) diff --git a/dist/networked-aframe.min.js b/dist/networked-aframe.min.js index d07ef7a1..d7489429 100644 --- a/dist/networked-aframe.min.js +++ b/dist/networked-aframe.min.js @@ -1 +1 @@ -!function(e){var t={};function n(i){if(t[i])return t[i].exports;var o=t[i]={i:i,l:!1,exports:{}};return e[i].call(o.exports,o,o.exports,n),o.l=!0,o.exports}n.m=e,n.c=t,n.d=function(e,t,i){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:i})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var i=Object.create(null);if(n.r(i),Object.defineProperty(i,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)n.d(i,o,function(t){return e[t]}.bind(null,o));return i},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=2)}([function(e,t,n){"use strict";var i=n(3),o=n(4),r=n(5),a=n(6),s=n(7),c=n(9),u=n(10),l={app:"",room:"",clientId:""};l.options=i,l.utils=o,l.log=new r,l.schemas=new a,l.version="0.8.3",l.adapters=new u;var d=new s,h=new c(d);l.connection=h,l.entities=d,e.exports=window.NAF=l},function(e,t,n){"use strict";function i(e){return(i="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}function o(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function r(e,t){for(var n=0;n is defined."));if(!this.validateTemplate(e,t))return;this.templateCache[e.template]=document.importNode(t.content,!0)}else NAF.log.error("Schema not valid: ",e),NAF.log.error("See https://github.com/networked-aframe/networked-aframe#syncing-custom-components")}},{key:"getCachedTemplate",value:function(e){return this.templateIsCached(e)||(this.templateExistsInScene(e)?this.add(this.createDefaultSchema(e)):NAF.log.error("Template el for ".concat(e," is not in the scene, add the template to and register with NAF.schemas.add."))),this.templateCache[e].firstElementChild.cloneNode(!0)}},{key:"templateIsCached",value:function(e){return!!this.templateCache[e]}},{key:"getComponents",value:function(e){var t=["position","rotation"];return this.hasTemplate(e)&&(t=this.schemaDict[e].components),t}},{key:"hasTemplate",value:function(e){return!!this.schemaDict[e]}},{key:"templateExistsInScene",value:function(e){var t=document.querySelector(e);return t&&this.isTemplateTag(t)}},{key:"validateSchema",value:function(e){return!(!e.template||!e.components)}},{key:"validateTemplate",value:function(e,t){return this.isTemplateTag(t)?!!this.templateHasOneOrZeroChildren(t)||(NAF.log.error("Template for ".concat(e.template," has more than one child. Templates must have one direct child element, no more. Template found:"),t),!1):(NAF.log.error("Template for ".concat(e.template," is not a