├── LICENSE
├── Readme.md
├── easyrtc.html
├── firebase.html
├── firebase2.html
├── firebase2_physics.html
├── js
├── controls
│ └── FPControls.js
├── networks
│ ├── EasyRTCClient.js
│ ├── FirebaseClient.js
│ ├── FirebaseSignalingServer.js
│ ├── PeerJSClient.js
│ ├── RemoteSync.js
│ ├── WebRTCClient.js
│ └── WebSocketClient.js
└── physics
│ └── Physics.js
├── peerjs.html
└── peerjs_mmd.html
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Takahiro
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Readme.md:
--------------------------------------------------------------------------------
1 | # ThreeNetwork
2 |
3 | ThreeNetwork is real-time network library for Three.js. ThreeNetwork synchronizes Three.js objects with remote. It supports [PeerJS](http://peerjs.com/), [EasyRTC](https://easyrtc.com/), and [Firebase](https://firebase.google.com/)
4 |
5 | 
6 | 
7 | 
8 |
9 | ## Demo
10 |
11 | - [PeerJS](http://takahirox.github.io/ThreeNetworkDemo/peerjs.html)
12 | - [PeerJS with Skinning edit](http://takahirox.github.io/ThreeNetworkDemo/peerjs_mmd.html)
13 | - EasyRTC
14 | - [Firebase](https://takahirox.github.io/ThreeNetworkDemo/firebase.html)
15 | - [Firebase+WebRTC](https://takahirox.github.io/ThreeNetworkDemo/firebase2.html)
16 | - [Firebase+WebRTC+Physics](https://takahirox.github.io/ThreeNetworkDemo/firebase2_physics.html)
17 |
18 | ## Features
19 |
20 | - easy to setup and use
21 | - multi-user with room system
22 | - low latency with WebRTC
23 | - efficient data transfer
24 | - media streaming(audio, video) support
25 |
26 | ## Sample code
27 |
28 | In your code, import `RemoteSync`
29 |
30 | - js/networks/RemoteSync.js
31 |
32 | and `NetworkClient` (and `SignalingServer`) depending on your platform.
33 |
34 | - js/networks/FirebaseSignalingServer.js
35 | - js/networks/WebRTCClient.js
36 | - js/networks/PeerJSClient.js
37 | - js/networks/EasyRTCClient.js
38 | - js/networks/FirebaseClient.js
39 |
40 | ```javascript
41 |
42 |
43 |
44 |
45 |
46 |
47 | var remoteSync, localId;
48 |
49 | remoteSync = new THREE.RemoteSync(
50 | new THREE.WebRTCClient(
51 | new THREE.FirebaseSignalingServer( {
52 | authType: 'anonymous',
53 | apiKey: 'your-api',
54 | authDomain: 'your-project.firebaseapp.com',
55 | databaseURL: 'https://your-project.firebaseio.com'
56 | } )
57 | )
58 | );
59 |
60 | // when connects signaling server
61 | remoteSync.addEventListener( 'open', function ( id ) {
62 | localId = id;
63 | var localMesh = new THREE.Mesh(...);
64 | remoteSync.addLocalObject( localMesh, { type: 'mesh' } );
65 | scene.add( localMesh );
66 | var sharedMesh = new THREE.Mesh(...);
67 | // local and remote peers must set the same unique shared id
68 | // to a object shared between them
69 | remoteSync.addSharedObject( sharedMesh, 'unique-shared-id' );
70 | scene.add( sharedMesh );
71 | } );
72 |
73 | // when remote adds an object
74 | remoteSync.addEventListener( 'add', function ( remotePeerId, objectId, info ) {
75 | var remoteMesh;
76 | // make an object fitting to info sent from a remote peer
77 | switch( info.type ) {
78 | case 'mesh':
79 | remoteMesh = new THREE.Mesh(...);
80 | break;
81 | default:
82 | return;
83 | }
84 | scene.add( remoteMesh );
85 | remoteSync.addRemoteObject( remotePeerId, objectId, remoteMesh );
86 | } );
87 |
88 | // when remote removes an object
89 | remoteSync.addEventListener( 'remove', function ( remotePeerId, objectId, object ) {
90 | if ( object.parent !== null ) object.parent.remove( object );
91 | } );
92 |
93 | // Joins a room
94 | function connect( roomId ) {
95 | remoteSync.connect( roomId );
96 | }
97 |
98 | // sync and render
99 | function render() {
100 | requestAnimationFrame( render );
101 | remoteSync.sync();
102 | renderer.render( scene, camera );
103 | }
104 | ```
105 |
106 | ## Concept
107 |
108 | ThreeNetwork regards Three.js objects as three type objects.
109 |
110 | - Local object
111 | - Remote object
112 | - Shared object
113 |
114 | Local object is a object registered to `RemoteSync` with `.addLocalObject()`. Local object status will be sent to remote peers. Local object addition will be notified to remote peers' `add` event listener.
115 |
116 | Remote object is a object registered to `RemoteSync` with `.addRemoteObject()`. Remote object status will reflect the status of a corresponding remote peer's local object. ThreeNetwork requires local peer to add a Remote object when it's notified remote peer's Local object addition by a remote peer via `add` event listener. Notification comes with info of remote peer's Local object, local peer needs to create an appropriate object by seeing info and register is as a Remote object.
117 |
118 | Shared object is a object registered to `RemoteSync` with `.addSharedObject()`. Shared object status will be sent to remote peers and also reflect the status of a corresponding remote peer's shared object. Shared object will be bind with remote peers objects assigned the same shared-id.
119 |
120 | ThreeNetwork doesn't care objects which aren't registered to `RemoteSync`.
121 |
122 | ## Media streaming
123 |
124 | ThreeNetwork supports media streaming (audio, video), but it just transfers streaming. You need to setup in your user code if you wanna make the use it, for example if you wanna play remote peer's video streaming on local. Refer to some documents for the setup.
125 | - https://developer.mozilla.org/en/docs/Web/API/Navigator/getUserMedia
126 | - https://www.html5rocks.com/en/tutorials/getusermedia/intro/
127 |
128 | To transfer local streaming to remote, get local media stream with `navigator.*GetUserMedia()` and then pass it to `NetworkClient` as `.stream`.
129 |
130 | ```javascript
131 | navigator.getUserMedia = navigator.getUserMedia ||
132 | navigator.webkitGetUserMedia ||
133 | navigator.mozGetUserMedia ||
134 | navigator.msGetUserMedia;
135 |
136 | navigator.getUserMedia( { audio: true },
137 | function ( stream ) {
138 | remoteSync = new THREE.RemoteSync(
139 | new THREE.WebRTCClient(
140 | new THREE.FirebaseSignalingServer( { ... } ),
141 | { stream: stream }
142 | )
143 | );
144 | }
145 | );
146 | ```
147 |
148 | To receive remote peer's stream, set `remote_stream` event listener.
149 |
150 | ```javascript
151 | remoteSync.addEventListener( 'remote_stream', functioin ( remoteStream ) {
152 | // setup audio context or something with remote stream here
153 | } );
154 | ```
155 |
156 | ## Setup with servers
157 |
158 | ### PeerJS + PeerServer Cloud service
159 |
160 | The easiest way is to use PeerServer Cloud service of PeerJS.
161 |
162 | 1. Go to [PeerServer Cloud service](http://peerjs.com/peerserver)
163 | 2. Get API key
164 | 3. Pass the API key to `PeerJSClient`.
165 |
166 | ```javascript
167 |
168 |
169 |
170 |
171 | remoteSync = new THREE.RemoteSync(
172 | new THREE.PeerJSClient( {
173 | key: 'your-api'
174 | } )
175 | );
176 | ```
177 |
178 | Note that PeerServer Cloud service has limitation.
179 |
180 | - Up to 50 concurrent connections
181 | - No room system, a peer can't know other remote peers connected to the server
182 |
183 | Then you need to pass a remote peer's id you wanna connect to `.connect()`. (So, maybe the remote peer needs to share its id with you beforehand.)
184 |
185 | ```javascript
186 | remoteSync.connect( 'remote-peer-id' );
187 | ```
188 | If you wanna avoid these limitation, you need to run your own PeerServer.
189 |
190 | ### PeerJS + Your own PeerServer
191 |
192 | 1. Go to [peerjs-server GitHub](https://github.com/peers/peerjs-server)
193 | 2. Follow the instruction and run your own server
194 | 3. Set `allowDiscovery: true` of `PeerJSClient`, and pass `host`, `port`, `path` to it.
195 |
196 | ```javascript
197 |
198 |
199 |
200 |
201 | remoteSync = new THREE.RemoteSync(
202 | new THREE.PeerJSClient( {
203 | allowDiscovery: true,
204 | host: 'hostname',
205 | port: portnum,
206 | path: path
207 | } )
208 | );
209 | ```
210 |
211 | `PeerJSClient` acts as there's one room in the server then you don't need to pass id to `.connect()`.
212 |
213 | ```javascript
214 | remoteSync.connect( '' );
215 | ```
216 |
217 | ### Firebase
218 |
219 | Using Firebase is another easiest way. You can sync object via Realtime Database of Firebase. This isn't WebRTC approach then you can't transfer media streaming and latency would be higher than WebRTC. But perhaps you can sync with more many peers with good performance.
220 |
221 | 1. Go to [Firebase console](https://console.firebase.google.com/)
222 | 2. Open project
223 | 3. Setup Authentication and Realtime Database security rule
224 | 4. Pass Authentication type, your apikey, authDomain, databaseURL to `FirebaseClient`
225 |
226 | ```javascript
227 |
228 |
229 |
230 |
231 | remoteSync = new THREE.RemoteSync(
232 | new THREE.FirebaseClient( {
233 | authType: 'none', // currently only 'none' or 'anonymous'
234 | apiKey: 'your-apikey',
235 | authDomain: 'your-project-id.firebaseapp.com',
236 | databaseURL: 'https://your-project-id.firebaseio.com'
237 | } )
238 | );
239 | ```
240 |
241 | `FirebaseClient` supports room system, then pass roomId to `.connect()` to join.
242 |
243 | ```javascript
244 | remoteSync.connect( 'roomId' );
245 | ```
246 |
247 | ### Firebase + WebRTC
248 |
249 | You can also use Firebase as signaling server and connect remote peers with WebRTC.
250 |
251 | 1. Setup Firebase project (See above)
252 | 2. Pass Authentication type, your apikey, authDomain, databaseURL to `FirebaseSignalingServer`
253 | 3. Pass FirebaseSignalingServer instance to `WebRTCClient`
254 |
255 | ```javascript
256 |
257 |
258 |
259 |
260 |
261 | remoteSync = new THREE.RemoteSync(
262 | new THREE.WebRTCClient(
263 | new THREE.FirebaseSignalingServer( {
264 | authType: 'none', // currently only 'none' or 'anonymous'
265 | apiKey: 'your-apikey',
266 | authDomain: 'your-project-id.firebaseapp.com',
267 | databaseURL: 'https://your-project-id.firebaseio.com'
268 | } )
269 | )
270 | );
271 | ```
272 |
273 | `FirebaseSignalingServer`+`WebRTCClient` supports room system, then pass roomId to `.connect()` to join.
274 |
275 | ```javascript
276 | remoteSync.connect( 'roomId' );
277 | ```
278 |
279 | ### EasyRTC
280 |
281 | T.B.D.
282 |
283 | ## API
284 |
285 | `RemoteSync`
286 | - `addLocalObject( object, info, recursive )`: Registers a Local object. Local object's status will be sent to remote by invoking `.sync()`. Local object addition with `info` will be notified to remote peers' `add` event listener. If recursive is true, recursively registers object's children.
287 | - `addRemoteObject( remotePeerId, objectUuid, object )`: Registers a Remote object. Remote object's status reflect to a corresponding remote peer's local object. This method is assumed to be called in `add` event listener callback function. If a correnponding object's children in remote are recursively registered, registers recursively its children here too. In that case, assumes the same object tree structure between local and remote.
288 | - `addSharedObject( object, sharedId, recursive )`: Registers a Shared object. Shared object's status will be sent to remote by invokind `.sync()` and aldo reglect corresponding remote peers' Shared object. Shared object will bind with remote peers' shared object assigned the same shared id. If recursive is true, recursively registers object's children.
289 | - `removeLocalObject( object )`: Removes a Local object from `RemoteSync`. If it's children are recursively registered, also removes them. In that case, assumes object tree structure doesn't change since it's registered.
290 | - `removeSharedObject( sharedId )`: Unbinds Shared object. If it's children are recursively registered, also removes them. In that case, assumes object tree structure doesn't change since it's registered.
291 | - `sync( force, onlyLocal )`: Broadcasts registered Local and Shared objects' status to remote peers. The status only of the objects which are updated since last `.sync()` for the efficient data transfer. If `force` is true, the status of all objects will be sent even if they aren't updated. If `onlyLocal` is true, the status only of Local objects will be sent.
292 | - `connect( id )`: Connects a room or a remote peer (depending on platform)
293 | - `sendUserData( remotePeerId, data )`, `broadcastUserData( data )`: Sends/Broadcasts user-data to remote peer(s). These methods invoking will be notified to Remote peers' `receive_user_data`.
294 | - `addEventListener`: Adds event listener. Requires three arguments ( `type`, `object`, `function` ) for `update`, two arguments ( `type`, `function` ) for others.
295 | - `open` ( peerId ): When connected with server.
296 | - `close` ( peerId ): When disconnected from server.
297 | - `error` ( errorMessage ): When error occurs.
298 | - `connect` ( remotePeerId, fromPeer ): When connected with a remote peer. `fromPeer` is a flag indicating if a remote peer sends connection request.
299 | - `disconnect` ( remotePeerId ): When disconnected from a remote peer.
300 | - `add` ( remotePeerId, objectUuid, info ): When a remote peer adds its local object. `.addRemoteObject()` is assumed to be called in this listener callback function.
301 | - `remove` ( remotePeerId, objectUuid, remoteObject ): When a remote peer removes its local object. An object automatically be removed from `RemoteSync`.
302 | - `update`: When a shared or remote object is updated by remote peer's `.sync()`.
303 | - `receive` (data): When receives data from a remote peer.
304 | - `remote_stream` (stream): When receives media streaming from a remote peer.
305 | - `receive_user_data` (data): When received user-data from a remote peer.
306 |
--------------------------------------------------------------------------------
/easyrtc.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | three.js webrtc
5 |
6 |
7 |
25 |
26 |
27 |
28 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
435 |
436 |
437 |
438 |
439 |
446 |
447 |
448 |
449 |
450 |
451 |
--------------------------------------------------------------------------------
/firebase.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | three.js webrtc
5 |
6 |
7 |
25 |
26 |
27 |
28 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
438 |
439 |
440 |
441 |
442 |
449 |
450 |
451 |
452 |
453 |
454 |
--------------------------------------------------------------------------------
/firebase2.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | three.js webrtc
5 |
6 |
7 |
25 |
26 |
27 |
28 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
491 |
492 |
493 |
494 |
495 |
502 |
503 |
504 |
505 |
506 |
507 |
--------------------------------------------------------------------------------
/firebase2_physics.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | three.js webrtc
5 |
6 |
7 |
25 |
26 |
27 |
28 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
606 |
607 |
608 |
609 |
610 |
617 |
618 |
619 |
620 |
621 |
622 |
--------------------------------------------------------------------------------
/js/controls/FPControls.js:
--------------------------------------------------------------------------------
1 | ( function () {
2 |
3 | var KEY_SPEED = 0.015;
4 | var KEY_ANIMATION_DURATION = 80;
5 |
6 | var MOUSE_SPEED_X = 0.5;
7 | var MOUSE_SPEED_Y = 0.3;
8 |
9 | THREE.FPControls = function ( object, domElement ) {
10 |
11 | this.object = object;
12 |
13 | this.width = domElement.width;
14 | this.height = domElement.height;
15 |
16 | this.phi = 0;
17 | this.theta = 0;
18 |
19 | this.targetAngle = null;
20 | this.angleAnimation = null;
21 |
22 | this.position = new THREE.Vector3().copy( object.position );
23 | this.orientation = new THREE.Quaternion();
24 |
25 | this.rotateStart = new THREE.Vector2();
26 | this.rotateEnd = new THREE.Vector2();
27 | this.rotateDelta = new THREE.Vector2();
28 | this.isDragging = false;
29 |
30 | this.orientationOut_ = new Float32Array( 4 );
31 |
32 | domElement.addEventListener( 'wheel', this.onMouseWheel.bind( this ) );
33 | domElement.addEventListener( 'mousemove', this.onMouseMove.bind( this ) );
34 | domElement.addEventListener( 'mousedown', this.onMouseDown.bind( this ) );
35 | domElement.addEventListener( 'mouseup', this.onMouseUp.bind( this ) );
36 |
37 | };
38 |
39 | Object.assign( THREE.FPControls.prototype, {
40 |
41 | onMouseWheel: function ( e ) {
42 |
43 | e.preventDefault();
44 | e.stopPropagation();
45 |
46 | this.position.x += Math.sin( this.theta ) * KEY_SPEED * e.deltaY;
47 | this.position.y += Math.sin( -this.phi ) * KEY_SPEED * e.deltaY;
48 | this.position.z += Math.cos( this.theta ) * KEY_SPEED * e.deltaY;
49 |
50 | },
51 |
52 | onMouseUp: function ( e ) {
53 |
54 | this.isDragging = false;
55 |
56 | },
57 |
58 | onMouseDown: function ( e ) {
59 |
60 | this.rotateStart.set( e.clientX, e.clientY );
61 | this.isDragging = true;
62 |
63 | },
64 |
65 | onMouseMove: function ( e ) {
66 |
67 | if ( ! this.isDragging && ! this.isPointerLocked() ) return;
68 |
69 | if ( this.isPointerLocked() ) {
70 |
71 | var movementX = e.movementX || e.mozMovementX || 0;
72 | var movementY = e.movementY || e.mozMovementY || 0;
73 | this.rotateEnd.set( this.rotateStart.x - movementX, this.rotateStart.y - movementY );
74 |
75 | } else {
76 |
77 | this.rotateEnd.set( e.clientX, e.clientY );
78 |
79 | }
80 |
81 | this.rotateDelta.subVectors( this.rotateEnd, this.rotateStart );
82 | this.rotateStart.copy( this.rotateEnd );
83 |
84 | this.phi -= 2 * Math.PI * this.rotateDelta.y / this.height * MOUSE_SPEED_Y;
85 | this.theta -= 2 * Math.PI * this.rotateDelta.x / this.width * MOUSE_SPEED_X;
86 |
87 | this.phi = this.clamp( this.phi, -Math.PI / 2, Math.PI / 2 );
88 |
89 | },
90 |
91 | animateTheta: function ( targetAngle ) {
92 |
93 | this.animateKeyTransitions( 'theta', targetAngle );
94 |
95 | },
96 |
97 | animatePhi: function ( targetAngle ) {
98 |
99 | targetAngle = this.clamp( targetAngle, -Math.PI / 2, Math.PI / 2 );
100 | this.animateKeyTransitions( 'phi', targetAngle );
101 |
102 | },
103 |
104 | animateKeyTransitions: function ( angleName, targetAngle ) {
105 |
106 | if ( this.angleAnimation ) cancelAnimationFrame( this.angleAnimation );
107 |
108 | var startAngle = this[ angleName ];
109 |
110 | var startTime = new Date();
111 |
112 | this.angleAnimation = requestAnimationFrame( function animate() {
113 |
114 | var elapsed = new Date() - startTime;
115 |
116 | if ( elapsed >= KEY_ANIMATION_DURATION ) {
117 |
118 | this[ angleName ] = targetAngle;
119 | cancelAnimationFrame( this.angleAnimation );
120 |
121 | return;
122 |
123 | }
124 |
125 | this.angleAnimation = requestAnimationFrame( animate.bind( this ) )
126 |
127 | var percent = elapsed / KEY_ANIMATION_DURATION;
128 | this[ angleName ] = startAngle + ( targetAngle - startAngle ) * percent;
129 |
130 | }.bind( this ) );
131 |
132 | },
133 |
134 | isPointerLocked: function () {
135 |
136 | var el = document.pointerLockElement || document.mozPointerLockElement ||
137 | document.webkitPointerLockElement;
138 |
139 | return el !== undefined;
140 |
141 | },
142 |
143 | clamp: function ( value, min, max ) {
144 |
145 | return Math.min( Math.max( min, value ), max );
146 |
147 | },
148 |
149 | setFromEulerYXZ: function( quaternion, x, y, z ) {
150 |
151 | var c1 = Math.cos( x / 2 );
152 | var c2 = Math.cos( y / 2 );
153 | var c3 = Math.cos( z / 2 );
154 | var s1 = Math.sin( x / 2 );
155 | var s2 = Math.sin( y / 2 );
156 | var s3 = Math.sin( z / 2 );
157 |
158 | quaternion.x = s1 * c2 * c3 + c1 * s2 * s3;
159 | quaternion.y = c1 * s2 * c3 - s1 * c2 * s3;
160 | quaternion.z = c1 * c2 * s3 - s1 * s2 * c3;
161 | quaternion.w = c1 * c2 * c3 + s1 * s2 * s3;
162 |
163 | },
164 |
165 | update: function () {
166 |
167 | this.object.position.copy( this.position );
168 |
169 | this.setFromEulerYXZ( this.orientation, this.phi, this.theta, 0 );
170 | this.object.quaternion.copy( this.orientation );
171 |
172 | }
173 |
174 | } );
175 |
176 | } )();
177 |
--------------------------------------------------------------------------------
/js/networks/EasyRTCClient.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @author Takahiro https://github.com/takahirox
3 | *
4 | * TODO
5 | * support media streaming
6 | * optimize?
7 | */
8 |
9 | ( function () {
10 |
11 | /**
12 | * EasyRTCClient constructor.
13 | * EasyRTCClient is a EasyRTC based NetworkClient concrete class.
14 | * @param {object} params - instantiate parameters (optional)
15 | */
16 | THREE.EasyRTCClient = function ( params ) {
17 |
18 | if ( window.easyrtc === undefined ) {
19 |
20 | throw new Error( 'THREE.EasyRTCClient: Import EasyRTC from https://github.com/priologic/easyrtc.' );
21 |
22 | }
23 |
24 | if ( params === undefined ) params = {};
25 |
26 | THREE.NetworkClient.call( this, params );
27 |
28 | this.appName = params.appName !== undefined ? params.appName : 'easyrtc.three';
29 |
30 | this.connecting = false;
31 |
32 | this.init();
33 |
34 | };
35 |
36 | THREE.EasyRTCClient.prototype = Object.create( THREE.NetworkClient.prototype );
37 | THREE.EasyRTCClient.prototype.constructor = THREE.EasyRTCClient;
38 |
39 | Object.assign( THREE.EasyRTCClient.prototype, {
40 |
41 | /**
42 | * Initializes EasyRTC.
43 | */
44 | init: function () {
45 |
46 | var self = this;
47 |
48 | // received data from a remote peer
49 | easyrtc.setPeerListener(
50 |
51 | function ( who, type, content, targeting ) {
52 |
53 | self.invokeReceiveListeners( content );
54 |
55 | }
56 |
57 | );
58 |
59 | // peer joined the room
60 | easyrtc.setRoomOccupantListener(
61 |
62 | function ( name, occupants, primary ) {
63 |
64 | if ( name !== self.roomId ) return;
65 |
66 | // It seems like occupants includes all peers list in the room.
67 | // Then makes new connections with peers which aren't in self.connections,
68 | // and disconnects from peers which are in self.connections but not in occupants.
69 |
70 | var table = {};
71 |
72 | for ( var i = 0, il = self.connections.length; i < il; i ++ ) {
73 |
74 | table[ self.connections[ i ].peer ] = false;
75 |
76 | }
77 |
78 | var keys = Object.keys( occupants );
79 |
80 | for ( var i = 0, il = keys.length; i < il; i ++ ) {
81 |
82 | var key = keys[ i ];
83 |
84 | if ( table[ key ] === undefined ) {
85 |
86 | // a peer newly joined the room
87 |
88 | self.connected( occupants[ key ].easyrtcid, ! self.connecting );
89 |
90 | } else {
91 |
92 | table[ key ] = true;
93 |
94 | }
95 |
96 | }
97 |
98 | var keys = Object.keys( table );
99 |
100 | for ( var i = 0, il = keys.length; i < il; i ++ ) {
101 |
102 | var key = keys[ i ];
103 |
104 | if ( table[ key ] === false ) {
105 |
106 | // a peer left the room
107 |
108 | self.disconnected( key );
109 |
110 | }
111 |
112 | }
113 |
114 | self.connecting = false;
115 |
116 | }
117 |
118 | );
119 |
120 | // connects server
121 | easyrtc.connect( this.appName,
122 |
123 | function ( id ) {
124 |
125 | self.invokeOpenListeners( id );
126 |
127 | },
128 |
129 | function ( code, message ) {
130 |
131 | self.invokeErrorListeners( code + ': ' + message );
132 |
133 | }
134 |
135 | );
136 |
137 | },
138 |
139 | /**
140 | * Adds connection.
141 | * @param {string} id - remote peer id
142 | * @param {boolean} fromRemote - if remote peer starts connection process
143 | */
144 | connected: function ( id, fromRemote ) {
145 |
146 | if ( this.addConnection( id, { peer: id } ) ) {
147 |
148 | this.invokeConnectListeners( id, fromRemote );
149 |
150 | }
151 |
152 | },
153 |
154 | /**
155 | * Removes connection.
156 | * @param {string} id - remote peer id
157 | */
158 | disconnected: function ( id ) {
159 |
160 | if ( this.removeConnection( id ) ) {
161 |
162 | this.invokeDisconnectListeners( id );
163 |
164 | }
165 |
166 | },
167 |
168 | // public concrete method
169 |
170 | connect: function ( id ) {
171 |
172 | this.roomId = id;
173 |
174 | this.connecting = true;
175 |
176 | easyrtc.joinRoom( id, null,
177 |
178 | function ( roomName ) {},
179 |
180 | function ( code, text, roomName ) {
181 |
182 | self.invokeErrorListeners( roomName + ' ' + code + ': ' + text );
183 |
184 | }
185 |
186 | );
187 |
188 |
189 | },
190 |
191 | send: function ( destId, data ) {
192 |
193 | easyrtc.sendPeerMessage( destId, MESSAGE_TYPE, data );
194 |
195 | },
196 |
197 | broadcast: function ( data ) {
198 |
199 | for ( var i = 0, il = this.connections.length; i < il; i ++ ) {
200 |
201 | this.send( this.connections[ i ].peer, data );
202 |
203 | }
204 |
205 | }
206 |
207 | } );
208 |
209 | var MESSAGE_TYPE = 'message';
210 |
211 | } )();
212 |
--------------------------------------------------------------------------------
/js/networks/FirebaseClient.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @author Takahiro https://github.com/takahirox
3 | */
4 |
5 | ( function () {
6 |
7 | /**
8 | * FirebaseClient constructor.
9 | * FirebaseClient transfers data via Realtime database.
10 | * Note that this isn't WebRTC, so you can't transfer media streaming.
11 | * @param {object} params - parameters for instantiate and Firebase configuration (optional)
12 | */
13 | THREE.FirebaseClient = function ( params ) {
14 |
15 | if ( window.firebase === undefined ) {
16 |
17 | throw new Error( 'THREE.FirebaseClient: Import firebase from https://www.gstatic.com/firebasejs/x.x.x/firebase.js' );
18 |
19 | }
20 |
21 | if ( params === undefined ) params = {};
22 |
23 | THREE.NetworkClient.call( this, params );
24 |
25 | // Refer to Frebase document for them
26 | this.apiKey = params.apiKey !== undefined ? params.apiKey : '';
27 | this.authDomain = params.authDomain !== undefined ? params.authDomain : '';
28 | this.databaseURL = params.databaseURL !== undefined ? params.databaseURL : '';
29 |
30 | this.authType = params.authType !== undefined ? params.authType : 'anonymous';
31 |
32 | this.init();
33 | this.auth();
34 |
35 | };
36 |
37 | THREE.FirebaseClient.prototype = Object.create( THREE.NetworkClient.prototype );
38 | THREE.FirebaseClient.prototype.constructor = THREE.FirebaseClient;
39 |
40 | Object.assign( THREE.FirebaseClient.prototype, {
41 |
42 | /**
43 | * Initializes Firebase.
44 | * Note: share this code with FirebaseSignalingServer?
45 | */
46 | init: function () {
47 |
48 | firebase.initializeApp( {
49 | apiKey: this.apiKey,
50 | authDomain: this.authDomain,
51 | databaseURL: this.databaseURL
52 | } );
53 |
54 | },
55 |
56 | /**
57 | * Authorizes Firebase, depending on authorize type.
58 | * Note: share this code with FirebaseSignalingServer?
59 | */
60 | auth: function () {
61 |
62 | switch ( this.authType ) {
63 |
64 | case 'none':
65 | this.authNone();
66 | break;
67 |
68 | case 'anonymous':
69 | this.authAnonymous();
70 | break;
71 |
72 | default:
73 | console.log( 'THREE.FilebaseClient.auth: Unkown authType ' + this.authType );
74 | break;
75 |
76 | }
77 |
78 | },
79 |
80 | /**
81 | * Doesn't authorize.
82 | * Note: share this code with FirebaseSignalingServer?
83 | */
84 | authNone: function () {
85 |
86 | var self = this;
87 |
88 | // makes an unique 16-char id by myself.
89 | var id = THREE.Math.generateUUID().replace( /-/g, '' ).toLowerCase().slice( 0, 16 );
90 |
91 | // asynchronously invokes open listeners for the compatibility with other auth types.
92 | requestAnimationFrame( function () {
93 |
94 | self.id = id;
95 | self.invokeOpenListeners( id );
96 |
97 | } );
98 |
99 | },
100 |
101 | /**
102 | * Authorizes as anonymous.
103 | * Note: share this code with FirebaseSignalingServer?
104 | */
105 | authAnonymous: function () {
106 |
107 | var self = this;
108 |
109 | firebase.auth().signInAnonymously().catch( function ( error ) {
110 |
111 | console.log( 'THREE.FirebaseClient.authAnonymous: ' + error );
112 |
113 | self.invokeErrorListeners( error );
114 |
115 | } );
116 |
117 | firebase.auth().onAuthStateChanged( function ( user ) {
118 |
119 | if ( user === null ) {
120 |
121 | // disconnected from server
122 |
123 | self.invokeCloseListeners( self.id );
124 |
125 | } else {
126 |
127 | // authorized
128 |
129 | self.id = user.uid;
130 | self.invokeOpenListeners( self.id );
131 |
132 | }
133 |
134 | } );
135 |
136 | },
137 |
138 | /**
139 | * Adds connection.
140 | * @param {string} id - remote peer id
141 | * @param {boolean} fromRemote - if remote peer started connection process
142 | */
143 | connected: function ( id, fromRemote ) {
144 |
145 | var self = this;
146 |
147 | if ( ! this.addConnection( id, { peer: id } ) ) return;
148 |
149 | firebase.database().ref( this.roomId + '/' + id + '/data' ).on( 'value', function ( data ) {
150 |
151 | if ( data.val() === null || data.val() === '' ) return;
152 |
153 | self.invokeReceiveListeners( data.val() );
154 |
155 | } );
156 |
157 | this.invokeConnectListeners( id, fromRemote );
158 |
159 | },
160 |
161 | /**
162 | * Removes connection.
163 | * @param {string} id - remote peer id
164 | */
165 | removed: function ( id ) {
166 |
167 | if ( this.removeConnection( id ) ) this.invokeDisconnectListeners( id );
168 |
169 | },
170 |
171 | /**
172 | * Gets server timestamp.
173 | * Note: share this code with FirebaseSignalingServer?
174 | * @param {function} callback
175 | */
176 | getTimestamp: function ( callback ) {
177 |
178 | var ref = firebase.database().ref( 'tmp' + '/' + this.id );
179 |
180 | ref.set( firebase.database.ServerValue.TIMESTAMP );
181 |
182 | ref.once( 'value', function ( data ) {
183 |
184 | var timestamp = data.val();
185 |
186 | ref.remove();
187 |
188 | callback( timestamp );
189 |
190 | } );
191 |
192 | ref.onDisconnect().remove();
193 |
194 | },
195 |
196 | // public concrete method
197 |
198 | connect: function ( roomId ) {
199 |
200 | var self = this;
201 |
202 | this.roomId = roomId;
203 |
204 | // TODO: check if this timestamp logic can race.
205 | this.getTimestamp( function ( timestamp ) {
206 |
207 | var ref = firebase.database().ref( self.roomId + '/' + self.id );
208 |
209 | ref.set( { timestamp: timestamp, data: '' } );
210 |
211 | ref.onDisconnect().remove();
212 |
213 | firebase.database().ref( self.roomId ).on( 'child_added', function ( data ) {
214 |
215 | var remoteTimestamp = data.val().timestamp;
216 |
217 | var fromRemote = timestamp <= remoteTimestamp;
218 |
219 | self.connected( data.key, fromRemote );
220 |
221 | } );
222 |
223 | firebase.database().ref( self.roomId ).on( 'child_removed', function ( data ) {
224 |
225 | self.removed( data.key );
226 |
227 | } );
228 |
229 | } );
230 |
231 | },
232 |
233 | // TODO: enables data transfer to a specific peer, not broadcast?
234 | send: function ( id, data ) {
235 |
236 | this.broadcast( data );
237 |
238 | },
239 |
240 | broadcast: function ( data ) {
241 |
242 | firebase.database().ref( this.roomId + '/' + this.id + '/data' ).set( data );
243 |
244 | }
245 |
246 | } );
247 |
248 | } )();
249 |
--------------------------------------------------------------------------------
/js/networks/FirebaseSignalingServer.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @author Takahiro https://github.com/takahirox
3 | *
4 | * TODO:
5 | * support all authorize types.
6 | */
7 |
8 | ( function () {
9 |
10 | /**
11 | * FirebaseSignalingServer constructor.
12 | * FirebaseSignalingServer uses Firebase as a signaling server.
13 | * @param {object} params - parameters for instantiate and Firebase configuration (optional)
14 | */
15 | THREE.FirebaseSignalingServer = function ( params ) {
16 |
17 | if ( window.firebase === undefined ) {
18 |
19 | throw new Error( 'THREE.FirebaseSignalingServer: Import firebase from https://www.gstatic.com/firebasejs/x.x.x/firebase.js' );
20 |
21 | }
22 |
23 | if ( params === undefined ) params = {};
24 |
25 | THREE.SignalingServer.call( this );
26 |
27 | // Refer to Frebase document for them
28 | this.apiKey = params.apiKey !== undefined ? params.apiKey : '';
29 | this.authDomain = params.authDomain !== undefined ? params.authDomain : '';
30 | this.databaseURL = params.databaseURL !== undefined ? params.databaseURL : '';
31 |
32 | this.authType = params.authType !== undefined ? params.authType : 'anonymous';
33 |
34 | this.init();
35 | this.auth();
36 |
37 | };
38 |
39 | THREE.FirebaseSignalingServer.prototype = Object.create( THREE.SignalingServer.prototype );
40 | THREE.FirebaseSignalingServer.prototype.constructor = THREE.FirebaseSignalingServer;
41 |
42 | Object.assign( THREE.FirebaseSignalingServer.prototype, {
43 |
44 | /**
45 | * Initializes Firebase.
46 | */
47 | init: function () {
48 |
49 | firebase.initializeApp( {
50 | apiKey: this.apiKey,
51 | authDomain: this.authDomain,
52 | databaseURL: this.databaseURL
53 | } );
54 |
55 | },
56 |
57 | /**
58 | * Authorizes Firebase, depending on authorize type.
59 | */
60 | auth: function () {
61 |
62 | switch ( this.authType ) {
63 |
64 | case 'none':
65 | this.authNone();
66 | break;
67 |
68 | case 'anonymous':
69 | this.authAnonymous();
70 | break;
71 |
72 | default:
73 | console.log( 'THREE.FirebaseSignalingServer.auth: Unknown authType ' + this.authType );
74 | break;
75 |
76 | }
77 |
78 | },
79 |
80 | /**
81 | * Doesn't authorize.
82 | */
83 | authNone: function () {
84 |
85 | var self = this;
86 |
87 | // makes an unique 16-char id by myself.
88 | var id = THREE.Math.generateUUID().replace( /-/g, '' ).toLowerCase().slice( 0, 16 );
89 |
90 | // asynchronously invokes open listeners for the compatibility with other auth types.
91 | requestAnimationFrame( function () {
92 |
93 | self.id = id;
94 | self.invokeOpenListeners( id );
95 |
96 | } );
97 |
98 | },
99 |
100 | /**
101 | * Authorizes as anonymous.
102 | */
103 | authAnonymous: function () {
104 |
105 | var self = this;
106 |
107 | firebase.auth().signInAnonymously().catch( function ( error ) {
108 |
109 | console.log( 'THREE.FirebaseSignalingServer.authAnonymous: ' + error );
110 |
111 | self.invokeErrorListeners( error );
112 |
113 | } );
114 |
115 | firebase.auth().onAuthStateChanged( function ( user ) {
116 |
117 | if ( user === null ) {
118 |
119 | // disconnected from server
120 |
121 | self.invokeCloseListeners( self.id );
122 |
123 | } else {
124 |
125 | // authorized
126 |
127 | self.id = user.uid;
128 | self.invokeOpenListeners( self.id );
129 |
130 | }
131 |
132 | } );
133 |
134 | },
135 |
136 | /**
137 | * Gets server timestamp.
138 | * @param {function} callback
139 | */
140 | getTimestamp: function ( callback ) {
141 |
142 | var ref = firebase.database().ref( 'tmp' + '/' + this.id );
143 |
144 | ref.set( firebase.database.ServerValue.TIMESTAMP );
145 |
146 | ref.once( 'value', function ( data ) {
147 |
148 | var timestamp = data.val();
149 |
150 | ref.remove();
151 |
152 | callback( timestamp );
153 |
154 | } );
155 |
156 | ref.onDisconnect().remove();
157 |
158 | },
159 |
160 | // public concrete method
161 |
162 | connect: function ( roomId ) {
163 |
164 | var self = this;
165 |
166 | this.roomId = roomId;
167 |
168 | // TODO: check if this timestamp logic can race.
169 | this.getTimestamp( function( timestamp ) {
170 |
171 | var ref = firebase.database().ref( self.roomId + '/' + self.id );
172 |
173 | ref.set( { timestamp: timestamp, signal: '' } );
174 |
175 | ref.onDisconnect().remove();
176 |
177 | var doneTable = {}; // remote peer id -> true or undefined, indicates if already done.
178 |
179 | firebase.database().ref( self.roomId ).on( 'child_added', function ( data ) {
180 |
181 | var id = data.key;
182 |
183 | if ( id === self.id || doneTable[ id ] === true ) return;
184 |
185 | doneTable[ id ] = true;
186 |
187 | var remoteTimestamp = data.val().timestamp;
188 |
189 | // received signal
190 | firebase.database().ref( self.roomId + '/' + id + '/signal' ).on( 'value', function ( data ) {
191 |
192 | if ( data.val() === null || data.val() === '' ) return;
193 |
194 | self.invokeReceiveListeners( data.val() );
195 |
196 | } );
197 |
198 | self.invokeRemoteJoinListeners( id, timestamp, remoteTimestamp );
199 |
200 | } );
201 |
202 | firebase.database().ref( roomId ).on( 'child_removed', function ( data ) {
203 |
204 | delete doneTable[ data.key ];
205 |
206 | } );
207 |
208 | } );
209 |
210 | },
211 |
212 | // TODO: we should enable .send() to send signal to a peer, not only broadcast?
213 | send: function ( data ) {
214 |
215 | firebase.database().ref( this.roomId + '/' + this.id + '/signal' ).set( data );
216 |
217 | }
218 |
219 | } );
220 |
221 | } )();
222 |
--------------------------------------------------------------------------------
/js/networks/PeerJSClient.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @author Takahiro https://github.com/takahirox
3 | */
4 |
5 | ( function () {
6 |
7 | /**
8 | * PeerJSClient constructor.
9 | * PeerJSClient is a PeerJS based NetworkClient concrete class.
10 | * PeerJSClient acts depending on PeerJS server allow_discovery configuration.
11 | * If allow_discovery is true, it acts as there is only one room in the PeerJS server.
12 | * Otherwise, it acts as there is no room system in the server and connects a
13 | * specified remote peer, exchanges the connected peers list, and then connects
14 | * other listed peers (I call this snoop here).
15 | * @param {object} params - instantiate parameters (optional)
16 | */
17 | THREE.PeerJSClient = function ( params ) {
18 |
19 | if ( window.Peer === undefined ) {
20 |
21 | throw new Error( 'THREE.PeerJSClient: Import PeerJS from https://github.com/peers/peerjs.' );
22 |
23 | }
24 |
25 | if ( params === undefined ) params = {};
26 |
27 | THREE.NetworkClient.call( this, params );
28 |
29 | // Refer to PeerJS document for them.
30 | this.key = params.key !== undefined ? params.key : '';
31 | this.debugLevel = params.debugLevel !== undefined ? params.debugLevel : 0;
32 | this.host = params.host !== undefined ? params.host : '';
33 | this.port = params.port !== undefined ? params.port : null;
34 | this.path = params.path !== undefined ? params.path : '';
35 |
36 | // Set true if PeerJS server allow_discovery is true.
37 | this.allowDiscovery = params.allowDiscovery !== undefined ? params.allowDiscovery : false;
38 |
39 | this.peer = this.createPeer();
40 |
41 | };
42 |
43 | THREE.PeerJSClient.prototype = Object.create( THREE.NetworkClient.prototype );
44 | THREE.PeerJSClient.prototype.constructor = THREE.PeerJSClient;
45 |
46 | Object.assign( THREE.PeerJSClient.prototype, {
47 |
48 | // private
49 |
50 | /**
51 | * Creates Peer instance.
52 | * @returns {Peer}
53 | */
54 | createPeer: function () {
55 |
56 | var self = this;
57 |
58 | var param = { debug: this.debugLevel };
59 |
60 | if ( this.key !== '' ) param.key = this.key;
61 | if ( this.host !== '' ) param.host = this.host;
62 | if ( this.port !== null ) param.port = this.port;
63 | if ( this.path !== '' ) param.path = this.path;
64 |
65 | var peer = this.id !== '' ? new Peer( this.id, param ) : new Peer( param );
66 |
67 | // connected with PeerJS server
68 | peer.on( 'open', function ( id ) {
69 |
70 | self.invokeOpenListeners( id );
71 |
72 | } );
73 |
74 | // disconnected from PeerJS server
75 | peer.on( 'close', function ( id ) {
76 |
77 | self.invokeCloseListeners( id );
78 |
79 | } );
80 |
81 | // connected with a remote peer
82 | peer.on( 'connection', function ( connection ) {
83 |
84 | self.connected( connection, true );
85 |
86 | } );
87 |
88 | // received a call(streaming) from a remote peer
89 | peer.on( 'call', function ( call ) {
90 |
91 | call.answer( self.stream );
92 |
93 | call.on( 'stream', function ( remoteStream ) {
94 |
95 | self.invokeRemoteStreamListeners( remoteStream );
96 |
97 | } );
98 |
99 | } );
100 |
101 | // error occurred with PeerJS server
102 | peer.on( 'error', function ( error ) {
103 |
104 | self.invokeErrorListeners( error );
105 |
106 | } );
107 |
108 | return peer;
109 |
110 | },
111 |
112 | /**
113 | * Sets up and adds connection.
114 | * @param {object} connection
115 | * @param {boolean} fromRemote - if a remote peer sends connection request
116 | */
117 | connected: function ( connection, fromRemote ) {
118 |
119 | var self = this;
120 |
121 | var id = connection.peer;
122 |
123 | if ( ! this.addConnection( id, connection ) ) return;
124 |
125 | connection.on( 'open', function() {
126 |
127 | // received data from a remote peer
128 | connection.on( 'data', function( data ) {
129 |
130 | if ( data.type === SNOOPLIST_TYPE ) {
131 |
132 | self.snoop( data.list );
133 |
134 | } else {
135 |
136 | self.invokeReceiveListeners( data );
137 |
138 | }
139 |
140 | } );
141 |
142 | // disconnected from a remote peer
143 | connection.on( 'close', function () {
144 |
145 | if ( self.removeConnection( id ) ) {
146 |
147 | self.invokeDisconnectListeners( id );
148 |
149 | }
150 |
151 | } );
152 |
153 | // error occurred with a remote peer
154 | connection.on( 'error', function ( error ) {
155 |
156 | self.invokeErrorListeners( error );
157 |
158 | } );
159 |
160 | self.invokeConnectListeners( id, fromRemote );
161 |
162 | if ( ! self.allowDiscovery ) self.sendPeersList( id );
163 |
164 | if ( self.stream !== null && ! fromRemote ) self.call( id );
165 |
166 | } );
167 |
168 | },
169 |
170 | /**
171 | * Sends connected peers list to a remote peer for snoop.
172 | * @param {string} id - remote peer id
173 | */
174 | sendPeersList: function ( id ) {
175 |
176 | var component = SNOOPLIST_COMPONENT;
177 | var list = component.list;
178 | list.length = 0;
179 |
180 | for ( var i = 0, il = this.connections.length; i < il; i ++ ) {
181 |
182 | var connection = this.connections[ i ];
183 |
184 | if ( connection.peer === this.id || connection.peer === id ) continue;
185 |
186 | list.push( connection.peer );
187 |
188 | }
189 |
190 | if ( list.length > 0 ) this.send( id, component );
191 |
192 | },
193 |
194 | /**
195 | * Starts snoop.
196 | * @param {Array} peers - peers list sent from a remote peer
197 | */
198 | snoop: function ( peers ) {
199 |
200 | for ( var i = 0, il = peers.length; i < il; i ++ ) {
201 |
202 | var id = peers[ i ];
203 |
204 | if ( id === this.id || this.hasConnection( id ) ) continue;
205 |
206 | this.connect( id );
207 |
208 | }
209 |
210 | },
211 |
212 | /**
213 | * Starts call(streaming).
214 | * @param {string} id - remote peer id
215 | */
216 | call: function ( id ) {
217 |
218 | var call = this.peer.call( id, this.stream );
219 |
220 | call.on( 'stream', function ( remoteStream ) {
221 |
222 | self.onRemoteStream( remoteStream );
223 |
224 | } );
225 |
226 | },
227 |
228 | // public concrete method
229 |
230 | connect: function ( destPeerId ) {
231 |
232 | if ( this.allowDiscovery ) {
233 |
234 | // ignores destPeerId because of only one room in PeerJS Server
235 |
236 | var self = this;
237 |
238 | // get peers list from the server and connects them
239 |
240 | this.peer.listAllPeers( function ( list ) {
241 |
242 | for ( var i = 0, il = list.length; i < il; i ++ ) {
243 |
244 | var id = list[ i ];
245 |
246 | if ( ! self.hasConnection( id ) ) {
247 |
248 | self.connected( self.peer.connect( id ), false );
249 |
250 | }
251 |
252 | }
253 |
254 | } );
255 |
256 | } else {
257 |
258 | // connects a specified remote peer
259 |
260 | this.connected( this.peer.connect( destPeerId ), false );
261 |
262 | }
263 |
264 | },
265 |
266 | send: function ( id, data ) {
267 |
268 | var connection = this.connectionTable[ id ];
269 |
270 | if ( connection === undefined ) return;
271 |
272 | connection.send( data );
273 |
274 | },
275 |
276 | broadcast: function ( data ) {
277 |
278 | for ( var i = 0, il = this.connections.length; i < il; i ++ ) {
279 |
280 | this.send( this.connections[ i ].peer, data );
281 |
282 | }
283 |
284 | }
285 |
286 | } );
287 |
288 | // component for snoop
289 |
290 | var SNOOPLIST_TYPE = 'snoop';
291 |
292 | var SNOOPLIST_COMPONENT = {
293 | type: SNOOPLIST_TYPE,
294 | list: []
295 | };
296 |
297 | } )();
298 |
--------------------------------------------------------------------------------
/js/networks/RemoteSync.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @author Takahiro https://github.com/takahirox
3 | *
4 | * TODO
5 | * support interpolation
6 | * support packet loss recover for UDP
7 | * proper error handling
8 | * optimize data transfer payload
9 | * support material sync
10 | */
11 |
12 | ( function () {
13 |
14 | /**
15 | * RemoteSync constructor.
16 | * RemoteSync synchronizes registered Object3D matrix and
17 | * morphTargetInfluences with remote users.
18 | * @param {THREE.NetworkClient} client - NetworkClient handling data transfer
19 | */
20 | THREE.RemoteSync = function ( client ) {
21 |
22 | this.client = client;
23 | this.id = '';
24 |
25 | // for local object
26 |
27 | this.localObjects = [];
28 | this.localObjectTable = {}; // object.uuid -> object
29 | this.localObjectInfos = {}; // object.uuid -> info
30 |
31 | // for remote object
32 |
33 | this.remoteObjectTable = {}; // remote peer id -> remote object.uuid -> object
34 | this.remoteObjectInfos = {}; // remote peer id -> remote object.uuid -> info
35 |
36 | // for shared object
37 |
38 | this.sharedObjects = [];
39 | this.sharedObjectTable = {}; // shared id -> object
40 | this.sharedObjectRecursives = {}; // shared id -> boolean (true or undefined)
41 |
42 | // for local and shared object
43 |
44 | this.transferComponents = {}; // object.uuid -> component
45 |
46 | // event listeners
47 |
48 | this.onOpens = [];
49 | this.onCloses = [];
50 | this.onErrors = [];
51 | this.onConnects = [];
52 | this.onDisconnects = [];
53 | this.onReceives = [];
54 | this.onAdds = [];
55 | this.onRemoves = [];
56 | this.onRemoteStreams = [];
57 | this.onReceiveUserDatas = [];
58 | this.onUpdates = {}; // object.uuid -> update callback function
59 |
60 | // experiment, master
61 |
62 | this.master = false;
63 | this.masterPeer = this.id;
64 |
65 | this.onMasters = [];
66 | this.onSlaves = [];
67 | this.onMasterNotifications = [];
68 |
69 | //
70 |
71 | this.initClientEventListener();
72 |
73 | // if client is already opened
74 |
75 | if ( this.client.id !== '' ) {
76 |
77 | var self = this;
78 |
79 | requestAnimationFrame(
80 |
81 | function () {
82 |
83 | self.id = self.client.id;
84 | self.invokeOpenListeners( self.id );
85 | self.beMaster();
86 |
87 | }
88 |
89 | );
90 |
91 | }
92 |
93 | };
94 |
95 | Object.assign( THREE.RemoteSync.prototype, {
96 |
97 | // public
98 |
99 | /**
100 | * Adds EventListener. Callback function will be invoked when
101 | * 'open': a connection is established with a signaling server
102 | * 'close': a connection is disconnected from a signaling server
103 | * 'error': network related error occurs
104 | * 'connect': a connection is established with a remote peer
105 | * 'disconnect': a connection is disconnected from a remote peer
106 | * 'receive': receives remote data sent from a remote peer
107 | * 'add': receives an remote object info registered by .addLocalObject()
108 | * 'remove': receives an remote object removed by .removeLocalObject()
109 | * 'update: shared or remote object is updated by remote's .sync()
110 | * 'remote_stream': receives a remote media stream
111 | * 'receive_user_data': receives user-data from remote sent by
112 | * .sendUserData() or .broadUserData()
113 | * 'master': being a master peer from a slave peer (experiment)
114 | * 'slave': being a slave peer from a master (experiment)
115 | * 'master_notification': being notified a master peer (experiment)
116 | *
117 | * Arguments for callback functions are
118 | * 'open': {string} local peer id
119 | * 'close': {string} local peer id
120 | * 'error': {string} error message
121 | * 'connect': {string} remote peer id
122 | * {boolean} if a remote peer sends connection request
123 | * 'disconnect': {string} remote peer id
124 | * 'receive': {object} component object sent from remote peer
125 | * 'add': {string} remote peer id
126 | * {string} remote object uuid
127 | * {anything} user-specified data
128 | * 'remove': {string} remote peer id
129 | * {string} remote object uuid
130 | * {string} removed object, registered as remote object
131 | * 'remote_stream': {MediaStream} remote media stream
132 | * 'receive_user_data': {anything} user-data sent from remote
133 | * 'master_notification': {string} master peer id
134 | *
135 | * .addEventListener() requires three arguments for 'update',
136 | * two arguments for others.
137 | *
138 | * TODO: implement .removeEventListener()
139 | *
140 | * @param {string} type - event type
141 | * @param {THREE.Object3D} object - for 'update'
142 | * @param {function} func - callback function
143 | */
144 | addEventListener: function ( type, arg1, arg2 ) {
145 |
146 | var func, object;
147 |
148 | if ( type === 'update' ) {
149 |
150 | object = arg1;
151 | func = arg2;
152 |
153 | } else {
154 |
155 | func = arg1;
156 |
157 | }
158 |
159 | switch ( type ) {
160 |
161 | case 'open':
162 | this.onOpens.push( func );
163 | break;
164 |
165 | case 'close':
166 | this.onCloses.push( func );
167 | break;
168 |
169 | case 'error':
170 | this.onErrors.push( func )
171 | break;
172 |
173 | case 'connect':
174 | this.onConnects.push( func )
175 | break;
176 |
177 | case 'disconnect':
178 | this.onDisconnects.push( func );
179 | break;
180 |
181 | case 'receive':
182 | this.onReceives.push( func );
183 | break;
184 |
185 | case 'add':
186 | this.onAdds.push( func );
187 | break;
188 |
189 | case 'remove':
190 | this.onRemoves.push( func );
191 | break;
192 |
193 | case 'update':
194 | // TODO: check if object is registered as shared or remote object here?
195 | this.onUpdates[ object.uuid ] = func; // overrides without any warning so far.
196 | // even if listener is registered.
197 | break;
198 |
199 | case 'remote_stream':
200 | this.onRemoteStreams.push( func );
201 | break;
202 |
203 | case 'receive_user_data':
204 | this.onReceiveUserDatas.push( func );
205 | break;
206 |
207 | case 'master':
208 | this.onMasters.push( func );
209 | break;
210 |
211 | case 'slave':
212 | this.onSlaves.push( func );
213 | break;
214 |
215 | case 'master_notification':
216 | this.onMasterNotifications.push( func );
217 | break;
218 |
219 | default:
220 | console.log( 'THREE.RemoteSync.addEventListener: Unknown type ' + type );
221 | break;
222 |
223 | }
224 |
225 | },
226 |
227 | /**
228 | * Joins the room or connects a remote peer, depending on NetworkClient instance.
229 | * @param {string} id - destination peer id or room id.
230 | */
231 | connect: function ( id ) {
232 |
233 | this.client.connect( id );
234 |
235 | },
236 |
237 | /**
238 | * Registers a local object. Local object's matrix and
239 | * morphTargetInfluences will be sent to remote by invoking .sync().
240 | * @param {THREE.Object3D} object
241 | * @param {anything} info - user-specified info representing an object, passed to remote 'add' event listener
242 | * @param {boolean} recursive - recursively registers children if true
243 | */
244 | addLocalObject: function ( object, info, recursive ) {
245 |
246 | if ( this.localObjectTable[ object.uuid ] !== undefined ) {
247 |
248 | console.warn( 'THREE.RemoteSync.addLocalObject: This object has already been registered.' );
249 | return;
250 |
251 | }
252 |
253 | if ( info === undefined ) info = {};
254 |
255 | this.localObjectTable[ object.uuid ] = object;
256 | this.localObjects.push( object );
257 | this.localObjectInfos[ object.uuid ] = { userInfo: info };
258 |
259 | this.transferComponents[ object.uuid ] = createTransferComponent( object );
260 |
261 | if ( recursive === true ) {
262 |
263 | var self = this;
264 |
265 | // TODO: move out this function not to make function for
266 | // every .addLocalObject() call.
267 | // TODO: optimize. maybe can replace with recursive .addLocalObject() call?
268 | //
269 | // Same TODOs for .addRemoteObject(), .addSharedObject(), remoteLocalObject(),
270 | // .remoteRemoteObject(), and .remoteSharedObject().
271 | function traverse( parent ) {
272 |
273 | var array = [];
274 |
275 | for ( var i = 0, il = parent.children.length; i < il; i ++ ) {
276 |
277 | var child = parent.children[ i ];
278 |
279 | if ( self.localObjectTable[ child.uuid ] !== undefined ) continue;
280 |
281 | self.localObjectTable[ child.uuid ] = child;
282 | self.localObjects.push( child );
283 | // child: true indicates this object is registered as child of another object
284 | self.localObjectInfos[ child.uuid ] = { child: true };
285 |
286 | self.transferComponents[ child.uuid ] = createTransferComponent( child );
287 |
288 | var param = {};
289 | param.id = child.uuid;
290 | param.children = traverse( child );
291 |
292 | array[ i ] = param;
293 |
294 | }
295 |
296 | return array;
297 |
298 | }
299 |
300 | this.localObjectInfos[ object.uuid ].recursive = true;
301 | this.localObjectInfos[ object.uuid ].children = traverse( object );
302 |
303 | }
304 |
305 | if ( this.client.connectionNum() > 0 ) this.broadcastAddObjectRequest( object );
306 |
307 | },
308 |
309 | /**
310 | * Removes a registered local object. If an object's children
311 | * are recursively resistered, also removes them.
312 | * @param {THREE.Object3D} object
313 | */
314 | removeLocalObject: function ( object ) {
315 |
316 | if ( this.localObjectTable[ object.uuid ] === undefined ) {
317 |
318 | console.warn( 'THREE.RemoteSync.removeLocalObject: object not found' );
319 | return;
320 |
321 | }
322 |
323 | var info = this.localObjectInfos[ object.uuid ];
324 |
325 | delete this.localObjectTable[ object.uuid ];
326 | delete this.localObjectInfos[ object.uuid ];
327 | delete this.transferComponents[ object.uuid ];
328 |
329 | removeObjectFromArray( this.localObjects, object );
330 |
331 | if ( info.recursive === true ) {
332 |
333 | var self = this;
334 |
335 | // assumes object's tree structure doesn't change since
336 | // it's registered.
337 | function traverse( parent ) {
338 |
339 | for ( var i = 0, il = parent.children.length; i < il; i ++ ) {
340 |
341 | var child = parent.children[ i ];
342 |
343 | if ( self.localObjectTable[ child.uuid ] === undefined ) continue;
344 |
345 | delete self.localObjectTable[ child.uuid ];
346 | delete self.localObjectInfos[ child.uuid ];
347 | delete self.transferComponents[ child.uuid ];
348 |
349 | removeObjectFromArray( self.localObjects, child );
350 |
351 | traverse( child );
352 |
353 | }
354 |
355 | }
356 |
357 | traverse( object );
358 |
359 | }
360 |
361 | if ( this.client.connectionNum() > 0 ) this.broadcastRemoveObjectRequest( object );
362 |
363 | },
364 |
365 | /**
366 | * Registers an object whose matrix and morphTargetInfluences will be updated by
367 | * a remote object. Registered object will be automatically removed from RemoteSync
368 | * if corresponging object is removed from RemoteSync in remote. If corresponding
369 | * object's children is recursively registered, recursively registeres children here, too.
370 | * @param {string} destId - remote peer id
371 | * @param {string} objectId - remote object uuid
372 | * @param {THREE.Object3D} object
373 | */
374 | addRemoteObject: function ( destId, objectId, object ) {
375 |
376 | var objects = this.remoteObjectTable[ destId ];
377 |
378 | if ( objects === undefined ) {
379 |
380 | console.warn( 'THREE.RemoteSync.addRemoteObject: has not received any add object request from ' + destId + ' peer.' );
381 | return;
382 |
383 | }
384 |
385 | var infos = this.remoteObjectInfos[ destId ];
386 | var info = infos[ objectId ];
387 |
388 | if ( info === undefined ) {
389 |
390 | console.warn( 'THREE.RemoteSync.addRemoteObject: has not received ' + objectId + ' object addition request from ' + destId + ' peer.' );
391 | return;
392 |
393 | }
394 |
395 | if ( objects[ objectId ] !== undefined ) {
396 |
397 | console.warn( 'THREE.RemoteSync.addRemoteObject: object for ' + objectId + ' object of ' + destId + ' peer has been already registered.' );
398 | return;
399 |
400 | }
401 |
402 | objects[ objectId ] = object;
403 |
404 | // assumes corresponding remote's local object and this object has the same
405 | // tree structure including the order of children.
406 | if ( info.recursive === true ) {
407 |
408 | function traverse( obj, param ) {
409 |
410 | var children1 = obj.children;
411 | var children2 = param.children;
412 |
413 | for ( var i = 0, il = Math.min( children1.length, children2.length ); i < il; i ++ ) {
414 |
415 | var child1 = children1[ i ];
416 | var child2 = children2[ i ];
417 |
418 | if ( objects[ child2.id ] !== undefined ) continue;
419 |
420 | objects[ child2.id ] = child1;
421 | infos[ child2.id ] = { child: true };
422 |
423 | traverse( child1, child2 );
424 |
425 | }
426 |
427 | }
428 |
429 | traverse( object, info );
430 |
431 | }
432 |
433 | },
434 |
435 | /**
436 | * Registers a shared object. Shared object's matrix and
437 | * morphTargetInfluences will be sent from/to remote.
438 | * Shared object is associated with user-defined shared id.
439 | * It synchronizes with a remote object which has the same
440 | * shared id.
441 | * @param {THREE.Object3D} object
442 | * @param {string} id - shared id.
443 | * @param {boolean} recursive - recursively adds children if true
444 | */
445 | addSharedObject: function ( object, id, recursive ) {
446 |
447 | if ( this.sharedObjectTable[ id ] !== undefined ) {
448 |
449 | console.warn( 'THREE.RemoteSystem.addSharedObject: Shared id ' + id + ' is already used.' );
450 | return;
451 |
452 | }
453 |
454 | this.sharedObjectTable[ id ] = object;
455 | this.sharedObjects.push( object );
456 |
457 | var component = createTransferComponent( object );
458 | component.sid = id; // shared id, special property for shared object
459 | this.transferComponents[ object.uuid ] = component;
460 |
461 | if ( recursive === true ) {
462 |
463 | this.sharedObjectRecursives[ id ] = true;
464 |
465 | var self = this;
466 |
467 | function traverse( parentId, parent ) {
468 |
469 | var children = parent.children;
470 |
471 | for ( var i = 0, il = children.length; i < il; i ++ ) {
472 |
473 | var child = children[ i ];
474 | // can conflict with other user-specified id?
475 | var id = parentId + '__' + i;
476 |
477 | if ( self.sharedObjectTable[ id ] !== undefined ) continue;
478 |
479 | self.sharedObjectTable[ id ] = child;
480 | self.sharedObjects.push( child );
481 |
482 | var component = createTransferComponent( child );
483 | component.sid = id;
484 | self.transferComponents[ child.uuid ] = component;
485 |
486 | traverse( id, child );
487 |
488 | }
489 |
490 | }
491 |
492 | traverse( id, object );
493 |
494 | }
495 |
496 | },
497 |
498 | /**
499 | * Removes a shared object. If object's children are recursively
500 | * registered, also removes them.
501 | * @param {string} id - shared id
502 | */
503 | removeSharedObject: function ( id ) {
504 |
505 | if ( this.sharedObjectTable[ id ] === undefined ) {
506 |
507 | console.warn( 'THREE.RemoteSync.removeSharedObject: no found shared id ' + id );
508 | return;
509 |
510 | }
511 |
512 | var object = this.sharedObjectTable[ id ];
513 |
514 | delete this.sharedObjectTable[ id ];
515 |
516 | removeObjectFromArray( this.sharedObjects, object );
517 |
518 | if ( this.sharedObjectRecursives[ id ] === true ) {
519 |
520 | delete this.sharedObjectRecursives[ id ];
521 |
522 | var self = this;
523 |
524 | // assumes object tree structure doesn't change since
525 | // it's registered as shared object.
526 | function traverse( parentId, parent ) {
527 |
528 | var children = parent.children;
529 |
530 | for ( var i = 0, il = children.length; i < il; i ++ ) {
531 |
532 | var id = parentId + '__' + i;
533 |
534 | if ( self.sharedObjectTable[ id ] === undefined ) continue;
535 |
536 | var child = self.sharedObjectTable[ id ];
537 |
538 | delete self.sharedObjectTable[ id ];
539 |
540 | removeObjectFromArray( self.sharedObjects, child );
541 |
542 | traverse( id, child );
543 |
544 | }
545 |
546 | }
547 |
548 | traverse( id, object );
549 |
550 | }
551 |
552 | },
553 |
554 | /**
555 | * Sends registered local and shared objects' matrix and morphTargetInfluence
556 | * to remote. Only the properties which are updated since last .sync() invoking
557 | * will be sent.
558 | * @param {boolean} force - force to send the properties even if they aren't updated
559 | * @param {boolean} onlyLocal - send only local objects properties
560 | */
561 | sync: function ( force, onlyLocal ) {
562 |
563 | var component = TRANSFER_COMPONENT;
564 | component.id = this.id;
565 | component.did = null;
566 | component.type = TRANSFER_TYPE_SYNC;
567 |
568 | var list = component.list;
569 | list.length = 0;
570 |
571 | for ( var i = 0; i < 2; i ++ ) {
572 |
573 | // i === 0 local, i === 1 shared
574 |
575 | if ( i === 1 && onlyLocal === true ) continue;
576 |
577 | var array = i === 0 ? this.localObjects : this.sharedObjects;
578 |
579 | for ( var j = 0, jl = array.length; j < jl; j ++ ) {
580 |
581 | var object = array[ j ];
582 |
583 | if ( force === true || this.checkUpdate( object ) ) {
584 |
585 | list.push( this.serialize( object ) );
586 |
587 | }
588 |
589 | }
590 |
591 | }
592 |
593 | if ( list.length > 0 ) this.client.broadcast( component );
594 |
595 | },
596 |
597 | /**
598 | * Sends user-data to a peer.
599 | * @param {string} destId - a remote peer id
600 | * @param {anything} data - user-data
601 | */
602 | sendUserData: function ( destId, data ) {
603 |
604 | var component = buildUserDataComponent( this.id, data );
605 | component.did = destId;
606 | this.client.send( destId, component );
607 |
608 | },
609 |
610 | /**
611 | * Broadcasts user-data.
612 | * @param {anything} data - user-data
613 | */
614 | broadcastUserData: function ( data ) {
615 |
616 | var component = buildUserDataComponent( this.id, data );
617 | component.did = null;
618 | this.client.broadcast( component );
619 |
620 | },
621 |
622 | // private
623 |
624 | /**
625 | * Sets up event listeners for client.
626 | */
627 | initClientEventListener: function () {
628 |
629 | var self = this;
630 |
631 | this.client.addEventListener( 'open',
632 |
633 | function ( id ) {
634 |
635 | self.id = id;
636 | self.invokeOpenListeners( id );
637 | self.beMaster();
638 |
639 | }
640 |
641 | );
642 |
643 | this.client.addEventListener( 'close',
644 |
645 | function ( id ) {
646 |
647 | self.invokeCloseListeners( id );
648 |
649 | }
650 |
651 | );
652 |
653 | this.client.addEventListener( 'error',
654 |
655 | function ( error ) {
656 |
657 | self.invokeErrorListeners( error );
658 |
659 | }
660 |
661 | );
662 |
663 | this.client.addEventListener( 'connect',
664 |
665 | function ( id, fromRemote ) {
666 |
667 | self.invokeConnectListeners( id, fromRemote );
668 |
669 | if ( ! fromRemote && self.isMaster() ) self.beSlave();
670 |
671 | if ( self.isMaster() ) self.sendMasterPeerId( id );
672 |
673 | // send already registered local objects info
674 | // to newly connected remote
675 | self.sendAddObjectsRequest( id );
676 | self.sync( true, ! fromRemote );
677 |
678 | }
679 |
680 | );
681 |
682 | this.client.addEventListener( 'disconnect',
683 |
684 | function ( id ) {
685 |
686 | // removes objects registered as remote object
687 | // of disconnected peer
688 |
689 | var objects = self.remoteObjectTable[ id ];
690 |
691 | if ( objects === undefined ) return;
692 |
693 | var keys = Object.keys( objects );
694 |
695 | for ( var i = 0, il = keys.length; i < il; i ++ ) {
696 |
697 | self.removeRemoteObject( id, keys[ i ] );
698 |
699 | }
700 |
701 | delete self.remoteObjectTable[ id ];
702 |
703 | self.invokeDisconnectListeners( id );
704 |
705 | if ( self.masterPeer === id ) self.electNewMaster();
706 |
707 | }
708 |
709 | );
710 |
711 | // TODO: returns ack to ensure the data transfer?
712 | this.client.addEventListener( 'receive',
713 |
714 | function ( component ) {
715 |
716 | // if this data is not for me then ignore.
717 | if ( component.did !== undefined &&
718 | component.did !== null &&
719 | self.id !== component.did ) return;
720 |
721 | switch ( component.type ) {
722 |
723 | case TRANSFER_TYPE_SYNC:
724 | self.handleSyncRequest( component );
725 | break;
726 |
727 | case TRANSFER_TYPE_ADD:
728 | self.handleAddRequest( component );
729 | break;
730 |
731 | case TRANSFER_TYPE_REMOVE:
732 | self.handleRemoveRequest( component );
733 | break;
734 |
735 | case TRANSFER_TYPE_USER_DATA:
736 | self.invokeReceiveUserDataListeners( component );
737 | break;
738 |
739 | case TRANSFER_TYPE_MASTER_NOTIFY:
740 | self.handleMasterNotification( component );
741 | break;
742 |
743 | default:
744 | console.log( 'THREE.RemoteSync: Unknown type ' + component.type );
745 | break;
746 |
747 | }
748 |
749 | self.invokeReceiveListeners( component );
750 |
751 | }
752 |
753 | );
754 |
755 | this.client.addEventListener( 'remote_stream',
756 |
757 | function ( stream ) {
758 |
759 | self.invokeRemoteStreamListeners( stream );
760 |
761 | }
762 |
763 | );
764 |
765 | },
766 |
767 | /**
768 | * Handles sync request sent from remote.
769 | * @param {object} component - transfer component sent from remote
770 | */
771 | handleSyncRequest: function ( component ) {
772 |
773 | var destId = component.id;
774 | var list = component.list;
775 |
776 | var objects = this.remoteObjectTable[ destId ];
777 |
778 | for ( var i = 0, il = list.length; i < il; i ++ ) {
779 |
780 | var objectId = list[ i ].id; // remote object uuid
781 | var sharedId = list[ i ].sid; // shared object id
782 |
783 | var object;
784 |
785 | if ( sharedId !== undefined ) {
786 |
787 | // shared object
788 |
789 | object = this.sharedObjectTable[ sharedId ];
790 |
791 | } else {
792 |
793 | if ( objects === undefined ) continue;
794 |
795 | object = objects[ objectId ];
796 |
797 | }
798 |
799 | if ( object === undefined ) continue;
800 |
801 | this.deserialize( object, list[ i ] );
802 |
803 | // to update transfer component
804 | if ( sharedId !== undefined ) this.serialize( object );
805 |
806 | }
807 |
808 | },
809 |
810 | /**
811 | * Handles add request sent from remote.
812 | * @param {object} component - transfer component sent from remote
813 | */
814 | handleAddRequest: function ( component ) {
815 |
816 | var destId = component.id;
817 | var list = component.list;
818 |
819 | if ( this.remoteObjectTable[ destId ] === undefined ) {
820 |
821 | this.remoteObjectTable[ destId ] = {};
822 | this.remoteObjectInfos[ destId ] = {};
823 |
824 | }
825 |
826 | var objects = this.remoteObjectTable[ destId ];
827 | var infos = this.remoteObjectInfos[ destId ];
828 |
829 | for ( var i = 0, il = list.length; i < il; i ++ ) {
830 |
831 | var objectId = list[ i ].id;
832 | var info = list[ i ].info;
833 |
834 | if ( objects[ objectId ] !== undefined ) continue;
835 |
836 | infos[ objectId ] = info;
837 |
838 | this.invokeAddListeners( destId, objectId, info.userInfo );
839 |
840 | }
841 |
842 | },
843 |
844 | /**
845 | * Handles remove request sent from remote.
846 | * @param {object} component - transfer component sent from remote
847 | */
848 | handleRemoveRequest: function ( component ) {
849 |
850 | var destId = component.id;
851 | var list = component.list;
852 |
853 | var objects = this.remoteObjectTable[ destId ];
854 |
855 | if ( objects === undefined ) return;
856 |
857 | for ( var i = 0, il = list.length; i < il; i ++ ) {
858 |
859 | var objectId = list[ i ].id;
860 |
861 | var object = objests[ objectId ];
862 |
863 | if ( object === undefined ) continue;
864 |
865 | this.removeRemoveObject( destId, objectId, object );
866 |
867 | }
868 |
869 | },
870 |
871 | /**
872 | * Removes an object registered as a remote object.
873 | * Invokes 'remove' event listener. If corresponding object's children
874 | * in remote are recursively registers, also removes them.
875 | * @param {string} destId - remote peer id
876 | * @param {string} objectId - remote object uuid
877 | */
878 | removeRemoteObject: function ( destId, objectId ) {
879 |
880 | if ( this.remoteObjectTable[ destId ] === undefined ) return;
881 |
882 | var objects = this.remoteObjectTable[ destId ];
883 | var infos = this.remoteObjectInfos[ destId ];
884 |
885 | if ( objects[ objectId ] === undefined ) return;
886 |
887 | var object = objects[ objectId ];
888 | var info = infos[ objectId ];
889 |
890 | delete objects[ objectId ];
891 | delete infos[ objectId ];
892 |
893 | if ( info.recursive === true ) {
894 |
895 | // assumes remote's local object and this object has the
896 | // same tree structure including the order of children.
897 | function traverse( obj, param ) {
898 |
899 | var children1 = obj.children;
900 | var children2 = param.children;
901 |
902 | for ( var i = 0, il = Math.min( children1.length, children2.length ); i < il; i ++ ) {
903 |
904 | var child1 = children1[ i ];
905 | var child2 = children2[ i ];
906 |
907 | delete objects[ child2.id ];
908 | traverse( child1, child2 );
909 |
910 | }
911 |
912 | }
913 |
914 | traverse( object, info );
915 |
916 | }
917 |
918 | this.invokeRemoteListeners( destId, objectId, object );
919 |
920 | },
921 |
922 | /**
923 | * Checks if object properties are updated since the last .sync() invoking.
924 | * Sees number as Float32 because 1. I want to ignore very minor change
925 | * 2. it seems like number will be handled as Float32 on some platforms.
926 | * @param {THREE.Object3D} object
927 | * @returns {boolean}
928 | */
929 | checkUpdate: function ( object ) {
930 |
931 | var component = this.transferComponents[ object.uuid ];
932 |
933 | var array = component.matrix;
934 | var array2 = object.matrix.elements;
935 |
936 | for ( var i = 0, il = array.length; i < il; i ++ ) {
937 |
938 | if ( ensureFloat32( array[ i ] ) !== ensureFloat32( array2[ i ] ) ) return true;
939 |
940 | }
941 |
942 | if ( object.morphTargetInfluences !== undefined ) {
943 |
944 | var array = component.morphTargetInfluences;
945 | var array2 = object.morphTargetInfluences;
946 |
947 | for ( var i = 0, il = array.length; i < il; i ++ ) {
948 |
949 | if ( ensureFloat32( array[ i ] ) !== ensureFloat32( array2[ i ] ) ) return true;
950 |
951 | }
952 |
953 | }
954 |
955 | return false;
956 |
957 | },
958 |
959 | /**
960 | * Serializes object. Ensures number as Float32 because it seems like
961 | * number is handled as Float32.
962 | * @param {THREE.Object3D} object
963 | * @returns {object} transfer component object made from object
964 | */
965 | serialize: function ( object ) {
966 |
967 | var component = this.transferComponents[ object.uuid ];
968 |
969 | var array = component.matrix;
970 | var array2 = object.matrix.elements;
971 |
972 | for ( var i = 0, il = array.length; i < il; i ++ ) {
973 |
974 | array[ i ] = ensureFloat32( array2[ i ] );
975 |
976 | }
977 |
978 | if ( object.morphTargetInfluences !== undefined ) {
979 |
980 | var array = component.morphTargetInfluences;
981 | var array2 = object.morphTargetInfluences;
982 |
983 | for ( var i = 0, il = array.length; i < il; i ++ ) {
984 |
985 | array[ i ] = ensureFloat32( array2[ i ] );
986 |
987 | }
988 |
989 | }
990 |
991 | return component;
992 |
993 | },
994 |
995 | /**
996 | * Desrializes transfer component.
997 | * @param {THREE.Object3D} object - object will be updated with component
998 | * @param {object} component
999 | */
1000 | deserialize: function ( object, component ) {
1001 |
1002 | var transferComponent = this.transferComponents[ object.uuid ];
1003 |
1004 | object.matrix.fromArray( component.matrix );
1005 | object.matrix.decompose( object.position, object.quaternion, object.scale );
1006 |
1007 | if ( object.morphTargetInfluences !== undefined && component.morphTargetInfluences.length > 0 ) {
1008 |
1009 | var array = component.morphTargetInfluences;
1010 | var array2 = object.morphTargetInfluences;
1011 |
1012 | for ( var i = 0, il = array.length; i < il; i ++ ) {
1013 |
1014 | array2[ i ] = array[ i ];
1015 |
1016 | }
1017 |
1018 | }
1019 |
1020 | if ( this.onUpdates[ object.uuid ] !== undefined ) {
1021 |
1022 | this.onUpdates[ object.uuid ]();
1023 |
1024 | }
1025 |
1026 | },
1027 |
1028 | /**
1029 | * Broadcasts object addition request.
1030 | * @param {THREE.Object3D} object
1031 | */
1032 | broadcastAddObjectRequest: function ( object ) {
1033 |
1034 | this.client.broadcast( buildObjectAdditionComponent( this.id, object, this.localObjectInfos[ object.uuid ] ) );
1035 |
1036 | },
1037 |
1038 | /**
1039 | * Sends already registered local objects addition request.
1040 | * @param {string} destId - remote peer id
1041 | */
1042 | sendAddObjectsRequest: function ( destId ) {
1043 |
1044 | var component = buildObjectsAdditionComponent( this.id, this.localObjects, this.localObjectInfos );
1045 | component.did = destId;
1046 |
1047 | this.client.send( destId, component );
1048 |
1049 | },
1050 |
1051 | /**
1052 | * Broadcasts object removal request.
1053 | * TODO: enables multiple objects remove request?
1054 | * @param {THREE.Object3D} object
1055 | */
1056 | broadcastRemoveObjectRequest: function ( object ) {
1057 |
1058 | this.client.broadcast( buildObjectRemovalComponent( this.id, object ) );
1059 |
1060 | },
1061 |
1062 | // invoke event listeners. refer to .addEventListener() comment for arguments.
1063 |
1064 | invokeOpenListeners: function ( id ) {
1065 |
1066 | for ( var i = 0, il = this.onOpens.length; i < il; i ++ ) {
1067 |
1068 | this.onOpens[ i ]( id );
1069 |
1070 | }
1071 |
1072 | },
1073 |
1074 | invokeCloseListeners: function ( id ) {
1075 |
1076 | for ( var i = 0, il = this.onCloses.length; i < il; i ++ ) {
1077 |
1078 | this.onCloses[ i ]( id );
1079 |
1080 | }
1081 |
1082 | },
1083 |
1084 | invokeErrorListeners: function ( error ) {
1085 |
1086 | for ( var i = 0, il = this.onErrors.length; i < il; i ++ ) {
1087 |
1088 | this.onErrors[ i ]( error );
1089 |
1090 | }
1091 |
1092 | },
1093 |
1094 | invokeConnectListeners: function ( id, fromRemote ) {
1095 |
1096 | for ( var i = 0, il = this.onConnects.length; i < il; i ++ ) {
1097 |
1098 | this.onConnects[ i ]( id, fromRemote );
1099 |
1100 | }
1101 |
1102 | },
1103 |
1104 | invokeDisconnectListeners: function ( id ) {
1105 |
1106 | for ( var i = 0, il = this.onDisconnects.length; i < il; i ++ ) {
1107 |
1108 | this.onDisconnects[ i ]( id );
1109 |
1110 | }
1111 |
1112 | },
1113 |
1114 | invokeAddListeners: function ( destId, objectId, info ) {
1115 |
1116 | for ( var i = 0, il = this.onAdds.length; i < il; i ++ ) {
1117 |
1118 | this.onAdds[ i ]( destId, objectId, info );
1119 |
1120 | }
1121 |
1122 | },
1123 |
1124 | invokeRemoteListeners: function ( destId, objectId, object ) {
1125 |
1126 | for ( var i = 0, il = this.onRemoves.length; i < il; i ++ ) {
1127 |
1128 | this.onRemoves[ i ]( destId, objectId, object );
1129 |
1130 | }
1131 |
1132 | },
1133 |
1134 | invokeReceiveListeners: function ( component ) {
1135 |
1136 | for ( var i = 0, il = this.onReceives.length; i < il; i ++ ) {
1137 |
1138 | this.onReceives[ i ]( component );
1139 |
1140 | }
1141 |
1142 | },
1143 |
1144 | invokeRemoteStreamListeners: function ( stream ) {
1145 |
1146 | for ( var i = 0, il = this.onRemoteStreams.length; i < il; i ++ ) {
1147 |
1148 | this.onRemoteStreams[ i ]( stream );
1149 |
1150 | }
1151 |
1152 | },
1153 |
1154 | invokeReceiveUserDataListeners: function ( component ) {
1155 |
1156 | for ( var i = 0, il = this.onReceiveUserDatas.length; i < il; i ++ ) {
1157 |
1158 | this.onReceiveUserDatas[ i ]( component.list[ 0 ] );
1159 |
1160 | }
1161 |
1162 | },
1163 |
1164 | // experiment, master
1165 |
1166 | /**
1167 | * Note: RemoteSync automaticallly chooses one peer as a master peer in the room.
1168 | * Application will be notified when local peer becomes a master/non-master(slave)
1169 | * via 'master'/'slave' event listener.
1170 | * Application can uses this master functionality if it wants only one peer
1171 | * in the room to do something.
1172 | * This feature is very experiment now. The current logic isn't robust.
1173 | */
1174 |
1175 | // private
1176 |
1177 | /**
1178 | * Returns the flag indicating if I'm a master peer.
1179 | * @return {boolean}
1180 | */
1181 | isMaster: function () {
1182 |
1183 | return this.master;
1184 |
1185 | },
1186 |
1187 | /**
1188 | * Lets myself be a master peer.
1189 | */
1190 | beMaster: function () {
1191 |
1192 | if ( ! this.isMaster() ) {
1193 |
1194 | this.master = true;
1195 | this.masterPeer = this.id;
1196 | this.notifyBeingMaster();
1197 | this.invokeMasterListeners();
1198 |
1199 | }
1200 |
1201 | },
1202 |
1203 | /**
1204 | * Lets myself be a slave peer.
1205 | */
1206 | beSlave: function () {
1207 |
1208 | if ( this.isMaster() ) {
1209 |
1210 | this.master = false;
1211 | this.invokeSlaveListeners();
1212 |
1213 | }
1214 |
1215 | },
1216 |
1217 | /**
1218 | * Notifies that I become a master peer to other peers.
1219 | */
1220 | notifyBeingMaster: function () {
1221 |
1222 | var component = buildMasterNotificationComponent( this.id, this.masterPeer );
1223 | component.did = null;
1224 | this.client.broadcast( component );
1225 |
1226 | },
1227 |
1228 | /**
1229 | * Sends a master peer's id to a remote peer.
1230 | * @params {string} destId - remote peer id
1231 | */
1232 | sendMasterPeerId: function ( destId ) {
1233 |
1234 | var component = buildMasterNotificationComponent( this.id, this.masterPeer );
1235 | component.did = destId;
1236 | this.client.send( destId, component );
1237 |
1238 | },
1239 |
1240 | /**
1241 | * Handles master notification sent from a remote peer.
1242 | * @params {object} component - data sent from a remote peer
1243 | */
1244 | handleMasterNotification: function ( component ) {
1245 |
1246 | var newMasterPeer = component.list[ 0 ];
1247 |
1248 | if ( newMasterPeer === this.id && ! this.isMaster() ) {
1249 |
1250 | this.beMaster();
1251 |
1252 | } else if ( newMasterPeer !== this.id && this.isMaster() ) {
1253 |
1254 | this.beSlave();
1255 |
1256 | }
1257 |
1258 | if ( newMasterPeer !== this.masterPeer ) {
1259 |
1260 | this.masterPeer = newMasterPeer;
1261 |
1262 | this.invokeMasterNotificationListeners( newMasterPeer );
1263 |
1264 | }
1265 |
1266 | },
1267 |
1268 | /**
1269 | * Elects a new master peer.
1270 | * Assumes this's called when a master peer leaves the room.
1271 | */
1272 | electNewMaster: function () {
1273 |
1274 | // TODO: I don't wanna touch connections out of NetworkClient.
1275 | var connections = this.client.connections;
1276 |
1277 | // Chooses the first peer ordered by peer id as a master peer so far.
1278 | // This logic is very temporal.
1279 |
1280 | var array = [ this.id ];
1281 |
1282 | for ( var i = 0, il = connections.length; i < il; i ++ ) {
1283 |
1284 | array.push( connections[ i ].peer );
1285 |
1286 | }
1287 |
1288 | if ( array.sort()[ 0 ] === this.id ) {
1289 |
1290 | this.beMaster();
1291 |
1292 | }
1293 |
1294 | },
1295 |
1296 | invokeMasterListeners: function () {
1297 |
1298 | for ( var i = 0, il = this.onMasters.length; i < il; i ++ ) {
1299 |
1300 | this.onMasters[ i ]();
1301 |
1302 | }
1303 |
1304 | },
1305 |
1306 | invokeSlaveListeners: function () {
1307 |
1308 | for ( var i = 0, il = this.onSlaves.length; i < il; i ++ ) {
1309 |
1310 | this.onSlaves[ i ]();
1311 |
1312 | }
1313 |
1314 | },
1315 |
1316 | invokeMasterNotificationListeners: function ( masterPeer ) {
1317 |
1318 | for ( var i = 0, il = this.onMasterNotifications.length; i < il; i ++ ) {
1319 |
1320 | this.onMasterNotifications[ i ]( masterPeer );
1321 |
1322 | }
1323 |
1324 | }
1325 |
1326 | } );
1327 |
1328 | // transfer component
1329 |
1330 | var TRANSFER_TYPE_SYNC = 0;
1331 | var TRANSFER_TYPE_ADD = 1;
1332 | var TRANSFER_TYPE_REMOVE = 2;
1333 | var TRANSFER_TYPE_USER_DATA = 3;
1334 | var TRANSFER_TYPE_MASTER_NOTIFY = 4;
1335 |
1336 | var TRANSFER_COMPONENT = {
1337 | id: null, // source id
1338 | did: null, // destination id, null for broadcast
1339 | type: -1,
1340 | list: []
1341 | };
1342 |
1343 | var float32Value = new Float32Array( 1 );
1344 |
1345 | function ensureFloat32( value ) {
1346 |
1347 | float32Value[ 0 ] = value;
1348 | return float32Value[ 0 ];
1349 |
1350 | }
1351 |
1352 | function removeObjectFromArray( array, object ) {
1353 |
1354 | // TODO: optimize
1355 |
1356 | var readIndex = 0;
1357 | var writeIndex = 0;
1358 |
1359 | for ( var i = 0, il = array.length; i < il; i ++ ) {
1360 |
1361 | if ( array[ i ] === object ) {
1362 |
1363 | array[ writeIndex ] = array[ readIndex ];
1364 | writeIndex ++;
1365 |
1366 | }
1367 |
1368 | readIndex ++;
1369 |
1370 | }
1371 |
1372 | array.length = writeIndex;
1373 |
1374 | }
1375 |
1376 | /**
1377 | * Creates a new transfer component for an local or shared object.
1378 | * @param {THREE.Object3D} object
1379 | * @returns {object} transfer component
1380 | */
1381 | function createTransferComponent( object ) {
1382 |
1383 | var matrix = [];
1384 | var morphTargetInfluences = [];
1385 |
1386 | for ( var i = 0, il = object.matrix.elements.length; i < il; i ++ ) {
1387 |
1388 | matrix[ i ] = ensureFloat32( object.matrix.elements[ i ] );
1389 |
1390 | }
1391 |
1392 | if ( object.morphTargetInfluences !== undefined ) {
1393 |
1394 | for ( var i = 0, il = object.morphTargetInfluences.length; i < il; i ++ ) {
1395 |
1396 | morphTargetInfluences[ i ] = ensureFloat32( object.morphTargetInfluences[ i ] );
1397 |
1398 | }
1399 |
1400 | }
1401 |
1402 | return {
1403 | id: object.uuid,
1404 | matrix: matrix,
1405 | morphTargetInfluences: morphTargetInfluences
1406 | };
1407 |
1408 | }
1409 |
1410 | /**
1411 | * Builds transfer component for add objects request.
1412 | * TODO: move into RemoteSync?
1413 | * @param {string} sourceId - local peer id
1414 | * @param {Array} objects - Array of THREE.Object3D
1415 | * @param {object} infoTable
1416 | * @returns {object} transfer component
1417 | */
1418 | function buildObjectsAdditionComponent( sourceId, objects, infoTable ) {
1419 |
1420 | var component = TRANSFER_COMPONENT;
1421 | component.id = sourceId;
1422 | component.did = null;
1423 | component.type = TRANSFER_TYPE_ADD;
1424 |
1425 | var list = component.list;
1426 | list.length = 0;
1427 |
1428 | for ( var i = 0, il = objects.length; i < il; i ++ ) {
1429 |
1430 | var object = objects[ i ];
1431 | var info = infoTable[ object.uuid ];
1432 |
1433 | // not sends this object because it'll be included in
1434 | // parent addition request.
1435 | if ( info.child === true ) continue;
1436 |
1437 | list.push( { id: object.uuid, info: info } );
1438 |
1439 | }
1440 |
1441 | return component;
1442 |
1443 | }
1444 |
1445 | /**
1446 | * Builds transfer component for add an object request.
1447 | * @param {string} sourceId - local peer id
1448 | * @param {THREE.Object3D} object
1449 | * @param {object} infoTable
1450 | * @returns {object} transfer component
1451 | */
1452 | function buildObjectAdditionComponent( sourceId, object, info ) {
1453 |
1454 | var component = TRANSFER_COMPONENT;
1455 | component.id = sourceId;
1456 | component.did = null;
1457 | component.type = TRANSFER_TYPE_ADD;
1458 |
1459 | var list = component.list;
1460 | list.length = 0;
1461 |
1462 | list.push( { id: object.uuid, info: info } );
1463 |
1464 | return component;
1465 |
1466 | }
1467 |
1468 | /**
1469 | * Builds transfer component for remove object request.
1470 | * @param {string} sourceId - local peer id
1471 | * @param {THREE.Object3D} object
1472 | * @returns {object} transfer component
1473 | */
1474 | function buildObjectRemovalComponent( sourceId, object ) {
1475 |
1476 | var component = TRANSFER_COMPONENT;
1477 | component.id = sourceId;
1478 | component.did = null;
1479 | component.type = TRANSFER_TYPE_REMOVE;
1480 |
1481 | var list = component.list;
1482 | list.length = 0;
1483 |
1484 | list.push( { id: object.uuid } );
1485 |
1486 | return component;
1487 |
1488 | }
1489 |
1490 | /**
1491 | * Builds transfer component for user-data transfer request.
1492 | * @param {string} sourceId - local peer id
1493 | * @param {anything} data
1494 | * @returns {object} transfer component
1495 | */
1496 | function buildUserDataComponent( sourceId, data ) {
1497 |
1498 | var component = TRANSFER_COMPONENT;
1499 | component.id = sourceId;
1500 | component.type = TRANSFER_TYPE_USER_DATA;
1501 |
1502 | var list = component.list;
1503 | list.length = 0;
1504 | list.push( data );
1505 |
1506 | return component;
1507 |
1508 | }
1509 |
1510 | /**
1511 | * Builds transfer component for master peer notification.
1512 | * @param {string} sourceId - local peer id
1513 | * @param {string} masterPeer - master peer's id
1514 | * @returns {object} transfer component
1515 | */
1516 | function buildMasterNotificationComponent( sourceId, masterPeer ) {
1517 |
1518 | var component = TRANSFER_COMPONENT;
1519 | component.id = sourceId;
1520 | component.type = TRANSFER_TYPE_MASTER_NOTIFY;
1521 |
1522 | var list = component.list;
1523 | list.length = 0;
1524 | list.push( masterPeer );
1525 |
1526 | return component;
1527 |
1528 | }
1529 |
1530 | } )();
1531 |
1532 | ( function () {
1533 |
1534 | /**
1535 | * NetworkClient constructor.
1536 | * NetworkClient handles network connection and data transfer.
1537 | * NetworkClient is an abstract class.
1538 | * Set local media streaming to params.stream if you wanna send it to remote.
1539 | * Concrete class is assumed WebRTC/Firebase/WebSocket based client.
1540 | * @param {object} params - instanciate parameters (optional)
1541 | */
1542 | THREE.NetworkClient = function ( params ) {
1543 |
1544 | if ( params === undefined ) params = {};
1545 |
1546 | this.id = params.id !== undefined ? params.id : '';
1547 | this.stream = params.stream !== undefined ? params.stream : null;
1548 |
1549 | this.roomId = '';
1550 |
1551 | // connections
1552 | // connection is a object which has a remote peer id in .peer property.
1553 | // a connection per a connected remote peer.
1554 |
1555 | this.connections = [];
1556 | this.connectionTable = {}; // remote peer id -> connection
1557 |
1558 | // event listeners
1559 |
1560 | this.onOpens = [];
1561 | this.onCloses = [];
1562 | this.onErrors = [];
1563 | this.onConnects = [];
1564 | this.onDisconnects = [];
1565 | this.onReceives = [];
1566 | this.onRemoteStreams = [];
1567 |
1568 | if ( params.onOpen !== undefined ) this.addEventListener( 'open', params.onOpen );
1569 | if ( params.onClose !== undefined ) this.addEventListener( 'close', params.onClose );
1570 | if ( params.onError !== undefined ) this.addEventListener( 'error', params.onError );
1571 | if ( params.onConnect !== undefined ) this.addEventListener( 'connect', params.onConnect );
1572 | if ( params.onDisconnect !== undefined ) this.addEventListener( 'disconnect', params.onDisconnect );
1573 | if ( params.onReceive !== undefined ) this.addEventListener( 'receive', params.onReceive );
1574 | if ( params.onRemoteStream !== undefined ) this.addEventListener( 'remote_stream', params.onRemoteStream );
1575 |
1576 | };
1577 |
1578 | Object.assign( THREE.NetworkClient.prototype, {
1579 |
1580 | // public
1581 |
1582 | /**
1583 | * Adds EventListener. Callback function will be invoked when
1584 | * 'open': a connection is established with a signaling server
1585 | * 'close': a connection is disconnected from a signaling server
1586 | * 'error': network related error occurs
1587 | * 'connect': a connection is established with a remote peer
1588 | * 'disconnect': a connection is disconnected from a remote peer
1589 | * 'receive': receives remote data sent from a remote peer
1590 | * 'remote_stream': receives a remote media stream
1591 | *
1592 | * Arguments for callback functions are
1593 | * 'open': {string} local peer id
1594 | * 'close': {string} local peer id
1595 | * 'error': {string} error message
1596 | * 'connect': {string} remote peer id
1597 | * {boolean} if a remote peer sends connection request
1598 | * 'disconnect': {string} remote peer id
1599 | * 'receive': {object} component object sent from remote peer
1600 | * 'remote_stream': {MediaStream} remote media stream
1601 | *
1602 | * @param {string} type - event type
1603 | * @param {function} func - callback function
1604 | */
1605 | addEventListener: function ( type, func ) {
1606 |
1607 | switch ( type ) {
1608 |
1609 | case 'open':
1610 | this.onOpens.push( func );
1611 | break;
1612 |
1613 | case 'close':
1614 | this.onCloses.push( func );
1615 | break;
1616 |
1617 | case 'error':
1618 | this.onErrors.push( func )
1619 | break;
1620 |
1621 | case 'connect':
1622 | this.onConnects.push( func )
1623 | break;
1624 |
1625 | case 'disconnect':
1626 | this.onDisconnects.push( func );
1627 | break;
1628 |
1629 | case 'receive':
1630 | this.onReceives.push( func );
1631 | break;
1632 |
1633 | case 'remote_stream':
1634 | this.onRemoteStreams.push( func );
1635 | break;
1636 |
1637 | default:
1638 | console.log( 'THREE.NetworkClient.addEventListener: Unknown type ' + type );
1639 | break;
1640 |
1641 | }
1642 |
1643 | },
1644 |
1645 | /**
1646 | * Joins a room or connects a remote peer, depending on class.
1647 | * A child class must override this method.
1648 | * @param {string} id - room id or remote peer id, depending on class.
1649 | */
1650 | connect: function ( id ) {},
1651 |
1652 | /**
1653 | * Sends data to a remote peer.
1654 | * @param {string} id - remote peer id
1655 | * @param {anything} data
1656 | */
1657 | send: function ( id, data ) {},
1658 |
1659 | /**
1660 | * Broadcasts data to all connected peers.
1661 | * @param {anything} data
1662 | */
1663 | broadcast: function ( data ) {},
1664 |
1665 | /**
1666 | * Checks if having a connection with a remote peer.
1667 | * @param {string} id - remote peer id
1668 | * @returns {boolean}
1669 | */
1670 | hasConnection: function ( id ) {
1671 |
1672 | return this.connectionTable[ id ] !== undefined;
1673 |
1674 | },
1675 |
1676 | /**
1677 | * Returns the number of connections.
1678 | */
1679 | connectionNum: function () {
1680 |
1681 | return this.connections.length;
1682 |
1683 | },
1684 |
1685 | // private (protected)
1686 |
1687 | /**
1688 | * Adds an connection object.
1689 | * @param {string} id - remote peer id
1690 | * @param {object} connection - an object which has remote peer id as .peer property
1691 | * @returns {boolean} if succeeded
1692 | */
1693 | addConnection: function ( id, connection ) {
1694 |
1695 | if ( id === this.id || this.connectionTable[ id ] !== undefined ) return false;
1696 |
1697 | this.connections.push( connection );
1698 | this.connectionTable[ id ] = connection;
1699 |
1700 | return true;
1701 |
1702 | },
1703 |
1704 | /**
1705 | * Removes an connection object.
1706 | * @param {string} id - remote peer id
1707 | * @returns {boolean} if succeeded
1708 | */
1709 | removeConnection: function ( id ) {
1710 |
1711 | if ( id === this.id || this.connectionTable[ id ] === undefined ) return false;
1712 |
1713 | delete this.connectionTable[ id ];
1714 |
1715 | // TODO: optimize
1716 | var readIndex = 0;
1717 | var writeIndex = 0;
1718 |
1719 | for ( var i = 0, il = this.connections.length; i < il; i ++ ) {
1720 |
1721 | if ( this.connections[ readIndex ].peer !== id ) {
1722 |
1723 | this.connections[ writeIndex ] = this.connections[ readIndex ];
1724 | writeIndex++;
1725 |
1726 | }
1727 |
1728 | readIndex++;
1729 |
1730 | }
1731 |
1732 | this.connections.length = writeIndex;
1733 |
1734 | return true;
1735 |
1736 | },
1737 |
1738 | // event listeners, refer to .addEventListeners() comment for the arguments.
1739 |
1740 | invokeOpenListeners: function ( id ) {
1741 |
1742 | this.id = id;
1743 |
1744 | for ( var i = 0, il = this.onOpens.length; i < il; i ++ ) {
1745 |
1746 | this.onOpens[ i ]( id );
1747 |
1748 | }
1749 |
1750 | },
1751 |
1752 | invokeCloseListeners: function ( id ) {
1753 |
1754 | for ( var i = 0, il = this.onCloses.length; i < il; i ++ ) {
1755 |
1756 | this.onCloses[ i ]( id );
1757 |
1758 | }
1759 |
1760 | },
1761 |
1762 | invokeErrorListeners: function ( error ) {
1763 |
1764 | for ( var i = 0, il = this.onErrors.length; i < il; i ++ ) {
1765 |
1766 | this.onErrors[ i ]( error );
1767 |
1768 | }
1769 |
1770 | },
1771 |
1772 | invokeConnectListeners: function ( id, fromRemote ) {
1773 |
1774 | for ( var i = 0, il = this.onConnects.length; i < il; i ++ ) {
1775 |
1776 | this.onConnects[ i ]( id, fromRemote );
1777 |
1778 | }
1779 |
1780 | },
1781 |
1782 | invokeDisconnectListeners: function ( id ) {
1783 |
1784 | for ( var i = 0, il = this.onDisconnects.length; i < il; i ++ ) {
1785 |
1786 | this.onDisconnects[ i ]( id );
1787 |
1788 | }
1789 |
1790 | },
1791 |
1792 | invokeReceiveListeners: function ( data ) {
1793 |
1794 | for ( var i = 0, il = this.onReceives.length; i < il; i ++ ) {
1795 |
1796 | this.onReceives[ i ]( data );
1797 |
1798 | }
1799 |
1800 | },
1801 |
1802 | invokeRemoteStreamListeners: function ( stream ) {
1803 |
1804 | for ( var i = 0, il = this.onRemoteStreams.length; i < il; i ++ ) {
1805 |
1806 | this.onRemoteStreams[ i ]( stream );
1807 |
1808 | }
1809 |
1810 | }
1811 |
1812 | } );
1813 |
1814 | } )();
1815 |
1816 | ( function () {
1817 |
1818 | /**
1819 | * Abstract signaling server class used for WebRTC connection establishment.
1820 | */
1821 | THREE.SignalingServer = function () {
1822 |
1823 | this.id = ''; // local peer id, assigned when local peer connects the server
1824 | this.roomId = '';
1825 |
1826 | // event listeners
1827 |
1828 | this.onOpens = [];
1829 | this.onCloses = [];
1830 | this.onErrors = [];
1831 | this.onRemoteJoins = [];
1832 | this.onReceives = [];
1833 |
1834 | };
1835 |
1836 | Object.assign( THREE.SignalingServer.prototype, {
1837 |
1838 | /**
1839 | * Adds EventListener. Callback function will be invoked when
1840 | * 'open': a connection is established with a signaling server
1841 | * 'close': a connection is disconnected from a signaling server
1842 | * 'error': error occurs
1843 | * 'receive': receives signal from a remote peer via server
1844 | * 'remote_join': aware of a remote peer joins the room
1845 | *
1846 | * Arguments for callback functions are
1847 | * 'open': {string} local peer id
1848 | * 'close': {string} local peer id
1849 | * 'error': {string} error message
1850 | * 'receive': {object} signal sent from a remote peer
1851 | * 'remote_join': {string} remote peer id
1852 | * {number} timestamp when local peer joined the room
1853 | * {number} timestamp when remote peer joined the room
1854 | *
1855 | * @param {string} type - event type
1856 | * @param {function} func - callback function
1857 | */
1858 | addEventListener: function ( type, func ) {
1859 |
1860 | switch ( type ) {
1861 |
1862 | case 'open':
1863 | this.onOpens.push( func );
1864 | break;
1865 |
1866 | case 'close':
1867 | this.onCloses.push( func );
1868 | break;
1869 |
1870 | case 'error':
1871 | this.onErrors.push( func );
1872 | break;
1873 |
1874 | case 'receive':
1875 | this.onReceives.push( func );
1876 | break;
1877 |
1878 | case 'remote_join':
1879 | this.onRemoteJoins.push( func );
1880 | break;
1881 |
1882 | default:
1883 | console.log( 'THREE.SignalingServer.addEventListener: Unknown type ' + type );
1884 | break;
1885 |
1886 | }
1887 |
1888 | },
1889 |
1890 | // invoke event listeners. refer to .addEventListener() comment for arguments.
1891 |
1892 | invokeOpenListeners: function ( id ) {
1893 |
1894 | for ( var i = 0, il = this.onOpens.length; i < il; i ++ ) {
1895 |
1896 | this.onOpens[ i ]( id );
1897 |
1898 | }
1899 |
1900 | },
1901 |
1902 | invokeCloseListeners: function ( id ) {
1903 |
1904 | for ( var i = 0, il = this.onCloses.length; i < il; i ++ ) {
1905 |
1906 | this.onCloses[ i ]( id );
1907 |
1908 | }
1909 |
1910 | },
1911 |
1912 | invokeErrorListeners: function ( error ) {
1913 |
1914 | for ( var i = 0, il = this.onErrors.length; i < il; i ++ ) {
1915 |
1916 | this.onErrors[ i ]( error );
1917 |
1918 | }
1919 |
1920 | },
1921 |
1922 | invokeRemoteJoinListeners: function ( id, localTimestamp, remoteTimestamp ) {
1923 |
1924 | for ( var i = 0, il = this.onRemoteJoins.length; i < il; i ++ ) {
1925 |
1926 | this.onRemoteJoins[ i ]( id, localTimestamp, remoteTimestamp );
1927 |
1928 | }
1929 |
1930 | },
1931 |
1932 | invokeReceiveListeners: function ( signal ) {
1933 |
1934 | for ( var i = 0, il = this.onReceives.length; i < il; i ++ ) {
1935 |
1936 | this.onReceives[ i ]( signal );
1937 |
1938 | }
1939 |
1940 | },
1941 |
1942 | // public abstract method
1943 |
1944 | /**
1945 | * Joins a room.
1946 | * @param {string} roomId
1947 | */
1948 | connect: function ( roomId ) {},
1949 |
1950 | /**
1951 | * Sends signal.
1952 | * TODO: here assumes signal is broadcasted but we should
1953 | * enable it to send signal to a peer?
1954 | * @param {object} signal
1955 | */
1956 | send: function ( signal ) {}
1957 |
1958 | } );
1959 |
1960 | } )();
1961 |
--------------------------------------------------------------------------------
/js/networks/WebRTCClient.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @author Takahiro https://github.com/takahirox
3 | */
4 |
5 | ( function () {
6 |
7 | /**
8 | * WebRTCClient constructor.
9 | * General WebRTC Client, establishes a connection via Signaling server.
10 | * @param {THREE.SignalingServer} server
11 | * @param {object} params - parameters for instantiate (optional)
12 | */
13 | THREE.WebRTCClient = function ( server, params ) {
14 |
15 | if ( params === undefined ) params = {};
16 |
17 | THREE.NetworkClient.call( this, params );
18 |
19 | this.server = server;
20 |
21 | this.init();
22 |
23 | };
24 |
25 | THREE.WebRTCClient.prototype = Object.create( THREE.NetworkClient.prototype );
26 | THREE.WebRTCClient.prototype.constructor = THREE.WebRTCClient;
27 |
28 | Object.assign( THREE.WebRTCClient.prototype, {
29 |
30 | /**
31 | * Initializes signaling server event listener.
32 | */
33 | init: function () {
34 |
35 | var self = this;
36 |
37 | // connected with server
38 | this.server.addEventListener( 'open',
39 |
40 | function ( id ) {
41 |
42 | self.invokeOpenListeners( id );
43 |
44 | }
45 |
46 | );
47 |
48 | // disconnected from server
49 | this.server.addEventListener( 'close',
50 |
51 | function ( id ) {
52 |
53 | self.invokeCloseListeners( id );
54 |
55 | }
56 |
57 | );
58 |
59 | // error occurred with server
60 | this.server.addEventListener( 'error',
61 |
62 | function ( error ) {
63 |
64 | self.invokeErrorListeners( error );
65 |
66 | }
67 |
68 | );
69 |
70 | // aware of a remote peer join the room
71 | this.server.addEventListener( 'remote_join', function ( id, localTimestamp, remoteTimestamp ) {
72 |
73 | if ( id === self.id || self.hasConnection( id ) ) return;
74 |
75 | // TODO: need a workaround for localTimestamp === remoteTimestamp
76 | var connectFromMe = localTimestamp > remoteTimestamp;
77 |
78 | var peer = new WebRTCPeer( self.id, id, self.server, self.stream );
79 |
80 | // received signal from a remote peer via server
81 | self.server.addEventListener( 'receive',
82 |
83 | function ( signal ) {
84 |
85 | peer.handleSignal( signal );
86 |
87 | }
88 |
89 | );
90 |
91 | // connected with a remote peer
92 | peer.addEventListener( 'open', function ( id ) {
93 |
94 | if ( self.addConnection( id, peer ) ) {
95 |
96 | self.invokeConnectListeners( id, ! connectFromMe );
97 |
98 | }
99 |
100 | // TODO: remove server 'receive' listener here.
101 | // if .addConnection() fails here?
102 |
103 | } );
104 |
105 | // disconnected from a remote peer
106 | peer.addEventListener( 'close', function ( id ) {
107 |
108 | if ( self.removeConnection( id ) ) {
109 |
110 | // TODO: remove server 'receive' listener here.
111 |
112 | self.invokeDisconnectListeners( id );
113 |
114 | }
115 |
116 | } );
117 |
118 | // error occurred with a remote peer
119 | peer.addEventListener( 'error', function ( error ) {
120 |
121 | self.invokeErrorListeners( error );
122 |
123 | } );
124 |
125 | // received data from a remote peer
126 | peer.addEventListener( 'receive', function ( data ) {
127 |
128 | self.invokeReceiveListeners( data );
129 |
130 | } );
131 |
132 | // received remote media streaming
133 | peer.addEventListener( 'receive_stream', function ( stream ) {
134 |
135 | self.invokeRemoteStreamListeners( stream );
136 |
137 | } );
138 |
139 | if ( connectFromMe ) peer.offer();
140 |
141 | } );
142 |
143 | // for the compatibility with other NetworkClient classes.
144 | // if already connected with signaling server, asynchronously invokes open listeners.
145 | if ( this.server.id !== '' ) {
146 |
147 | requestAnimationFrame(
148 |
149 | function () {
150 |
151 | self.invokeOpenListeners( self.server.id );
152 |
153 | }
154 |
155 | );
156 |
157 | }
158 |
159 | },
160 |
161 | // public concrete method
162 |
163 | connect: function ( roomId ) {
164 |
165 | var self = this;
166 |
167 | this.roomId = roomId;
168 |
169 | this.server.connect( roomId );
170 |
171 | },
172 |
173 | send: function ( id, data ) {
174 |
175 | var connection = this.connectionTable[ id ];
176 |
177 | if ( connection === undefined ) return;
178 |
179 | connection.send( data );
180 |
181 | },
182 |
183 | broadcast: function ( data ) {
184 |
185 | for ( var i = 0, il = this.connections.length; i < il; i ++ ) {
186 |
187 | this.send( this.connections[ i ].peer, data );
188 |
189 | }
190 |
191 | }
192 |
193 | } );
194 |
195 | // ice servers for RTCPeerConnection.
196 |
197 | var ICE_SERVERS = [
198 | { urls: 'stun:stun.l.google.com:19302' },
199 | { urls: 'stun:stun1.l.google.com:19302' },
200 | { urls: 'stun:stun2.l.google.com:19302' },
201 | { urls: 'stun:stun3.l.google.com:19302' },
202 | { urls: 'stun:stun4.l.google.com:19302' }
203 | ];
204 |
205 | /**
206 | * WebRTCPeer constructor.
207 | * WebRTCPeer handles WebRTC connection and data transfer with RTCPeerConnection.
208 | * Refer to RTCPeerConnection document for the message handling detail.
209 | * @param {string} id - local peer id
210 | * @param {string} peer - remote peer id
211 | * @param {SignalingServer} server
212 | * @param {MediaStream} stream - sends media stream to remote peer if it's provided (optional)
213 | */
214 | var WebRTCPeer = function ( id, peer, server, stream ) {
215 |
216 | this.id = id;
217 | this.peer = peer;
218 | this.server = server;
219 | this.pc = this.createPeerConnection( stream );
220 | this.channel = null;
221 |
222 | this.open = false;
223 |
224 | // event listeners
225 |
226 | this.onOpens = [];
227 | this.onCloses = [];
228 | this.onErrors = [];
229 | this.onReceives = [];
230 | this.onReceiveStreams = [];
231 |
232 | };
233 |
234 | Object.assign( WebRTCPeer.prototype, {
235 |
236 | /**
237 | * Adds EventListener. Callback function will be invoked when
238 | * 'open': a connection is established with a remote peer
239 | * 'close': a connection is disconnected from a remote peer
240 | * 'error': error occurs
241 | * 'receive': receives data from a remote peer
242 | * 'remote_stream': receives a remote media stream
243 | *
244 | * Arguments for callback functions are
245 | * 'open': {string} local peer id
246 | * 'close': {string} local peer id
247 | * 'error': {string} error message
248 | * 'receive': {anything} signal sent from a remote peer
249 | * 'remote_stream': {MediaStream} remote media stream
250 | *
251 | * @param {string} type - event type
252 | * @param {function} func - callback function
253 | */
254 | addEventListener: function ( type, func ) {
255 |
256 | switch ( type ) {
257 |
258 | case 'open':
259 | this.onOpens.push( func );
260 | break;
261 |
262 | case 'close':
263 | this.onCloses.push( func );
264 | break;
265 |
266 | case 'error':
267 | this.onErrors.push( func );
268 | break;
269 |
270 | case 'receive':
271 | this.onReceives.push( func );
272 | break;
273 |
274 | case 'receive_stream':
275 | this.onReceiveStreams.push( func );
276 | break;
277 |
278 | default:
279 | console.log( 'WebRTCPeer.addEventListener: Unknown type ' + type );
280 | break;
281 |
282 | }
283 |
284 | },
285 |
286 | /**
287 | * Creates peer connection.
288 | * @param {MediaStream} stream - sends media stream to remote if it's provided (optional)
289 | * @returns {RTCPeerConnection}
290 | */
291 | createPeerConnection: function ( stream ) {
292 |
293 | var self = this;
294 |
295 | var RTCPeerConnection = window.RTCPeerConnection ||
296 | window.webkitRTCPeerConnection ||
297 | window.mozRTCPeerConnection ||
298 | window.msRTCPeerConnection;
299 |
300 | if ( RTCPeerConnection === undefined ) {
301 |
302 | throw new Error( 'WebRTCPeer.createPeerConnection: This browser does not seem to support WebRTC.' );
303 |
304 | }
305 |
306 | var pc = new RTCPeerConnection( { 'iceServers': ICE_SERVERS } );
307 |
308 | if ( stream !== null && stream !== undefined ) pc.addStream( stream );
309 |
310 | pc.onicecandidate = function ( event ) {
311 |
312 | if ( event.candidate ) {
313 |
314 | var params = {
315 | id: self.id,
316 | peer: self.peer,
317 | type: 'candidate',
318 | sdpMLineIndex: event.candidate.sdpMLineIndex,
319 | candidate: event.candidate.candidate
320 | };
321 |
322 | self.server.send( params );
323 |
324 | }
325 |
326 | };
327 |
328 | pc.onaddstream = function ( event ) {
329 |
330 | self.invokeReceiveStreamListeners( event.stream );
331 |
332 | };
333 |
334 | // Note: seems like channel.onclose hander is unreliable on some platforms,
335 | // so also try to detect disconnection here.
336 | pc.oniceconnectionstatechange = function() {
337 |
338 | if( self.open && pc.iceConnectionState == 'disconnected' ) {
339 |
340 | self.open = false;
341 |
342 | self.invokeCloseListeners( self.peer );
343 |
344 | }
345 |
346 | };
347 |
348 | return pc;
349 |
350 | },
351 |
352 | /**
353 | * Handles offer request.
354 | * @param {object} message - message sent from a remote peer
355 | */
356 | handleOffer: function ( message ) {
357 |
358 | var self = this;
359 |
360 | this.pc.ondatachannel = function ( event ) {
361 |
362 | self.channel = event.channel;
363 | self.setupChannelListener();
364 |
365 | };
366 |
367 | this.setRemoteDescription( message );
368 |
369 | this.pc.createAnswer(
370 |
371 | function ( sdp ) {
372 |
373 | self.handleSessionDescription( sdp );
374 |
375 | },
376 |
377 | function ( error ) {
378 |
379 | console.log( 'WebRTCPeer.handleOffer: ' + error );
380 | self.invokeErrorListeners( error );
381 |
382 | }
383 |
384 | );
385 |
386 | },
387 |
388 | /**
389 | * Handles answer response.
390 | * @param {object} message - message sent from a remote peer
391 | */
392 | handleAnswer: function ( message ) {
393 |
394 | this.setRemoteDescription( message );
395 |
396 | },
397 |
398 | /**
399 | * Handles candidate sent from a remote peer.
400 | * @param {object} message - message sent from a remote peer
401 | */
402 | handleCandidate: function ( message ) {
403 |
404 | var self = this;
405 |
406 | var RTCIceCandidate = window.RTCIceCandidate ||
407 | window.webkitRTCIceCandidate ||
408 | window.mozRTCIceCandidate;
409 |
410 | this.pc.addIceCandidate(
411 |
412 | new RTCIceCandidate( message ),
413 |
414 | function () {},
415 |
416 | function ( error ) {
417 |
418 | console.log( 'WebRTCPeer.handleCandidate: ' + error );
419 | self.invokeErrorListeners( error );
420 |
421 | }
422 |
423 | );
424 |
425 | },
426 |
427 | /**
428 | * Handles SessionDescription.
429 | * @param {RTCSessionDescription} sdp
430 | */
431 | handleSessionDescription: function ( sdp ) {
432 |
433 | var self = this;
434 |
435 | this.pc.setLocalDescription( sdp,
436 |
437 | function () {},
438 |
439 | function ( error ) {
440 |
441 | console.log( 'WebRTCPeer.handleSessionDescription: ' + error );
442 | self.invokeErrorListeners( error );
443 |
444 | }
445 |
446 | );
447 |
448 | this.server.send( {
449 | id: this.id,
450 | peer: this.peer,
451 | type: sdp.type,
452 | sdp: sdp.sdp
453 | } );
454 |
455 | },
456 |
457 | /**
458 | * Sets remote description.
459 | * @param {object} message - message sent from a remote peer
460 | */
461 | setRemoteDescription: function ( message ) {
462 |
463 | var self = this;
464 |
465 | var RTCSessionDescription = window.RTCSessionDescription ||
466 | window.webkitRTCSessionDescription ||
467 | window.mozRTCSessionDescription ||
468 | window.msRTCSessionDescription;
469 |
470 | this.pc.setRemoteDescription(
471 |
472 | new RTCSessionDescription( message ),
473 |
474 | function () {},
475 |
476 | function ( error ) {
477 |
478 | console.log( 'WebRTCPeer.setRemoteDescription: ' + error );
479 | self.invokeErrorListeners( error );
480 |
481 | }
482 |
483 | );
484 |
485 | },
486 |
487 | /**
488 | * Sets up channel listeners.
489 | */
490 | setupChannelListener: function () {
491 |
492 | var self = this;
493 |
494 | // received data from a remote peer
495 | this.channel.onmessage = function ( event ) {
496 |
497 | self.invokeReceiveListeners( JSON.parse( event.data ) );
498 |
499 | };
500 |
501 | // connected with a remote peer
502 | this.channel.onopen = function ( event ) {
503 |
504 | self.open = true;
505 |
506 | self.invokeOpenListeners( self.peer );
507 |
508 | };
509 |
510 | // disconnected from a remote peer
511 | this.channel.onclose = function ( event ) {
512 |
513 | if ( ! self.open ) return;
514 |
515 | self.open = false;
516 |
517 | self.invokeCloseListeners( self.peer );
518 |
519 | };
520 |
521 | // error occurred with a remote peer
522 | this.channel.onerror = function( error ) {
523 |
524 | self.invokeErrorListeners( error );
525 |
526 | };
527 |
528 | },
529 |
530 | // event listeners, refer to .addEventListeners() comment for the arguments.
531 |
532 | invokeOpenListeners: function ( id ) {
533 |
534 | for ( var i = 0, il = this.onOpens.length; i < il; i ++ ) {
535 |
536 | this.onOpens[ i ]( id );
537 |
538 | }
539 |
540 | },
541 |
542 | invokeCloseListeners: function ( id ) {
543 |
544 | for ( var i = 0, il = this.onCloses.length; i < il; i ++ ) {
545 |
546 | this.onCloses[ i ]( id );
547 |
548 | }
549 |
550 | },
551 |
552 | invokeErrorListeners: function ( error ) {
553 |
554 | for ( var i = 0, il = this.onErrors.length; i < il; i ++ ) {
555 |
556 | this.onErrors[ i ]( error );
557 |
558 | }
559 |
560 | },
561 |
562 | invokeReceiveListeners: function ( message ) {
563 |
564 | for ( var i = 0, il = this.onReceives.length; i < il; i ++ ) {
565 |
566 | this.onReceives[ i ]( message );
567 |
568 | }
569 |
570 | },
571 |
572 | invokeReceiveStreamListeners: function ( stream ) {
573 |
574 | for ( var i = 0, il = this.onReceiveStreams.length; i < il; i ++ ) {
575 |
576 | this.onReceiveStreams[ i ]( stream );
577 |
578 | }
579 |
580 | },
581 |
582 | // public
583 |
584 | /**
585 | * Sends connection request (offer) to a remote peer.
586 | */
587 | offer: function () {
588 |
589 | var self = this;
590 |
591 | this.channel = this.pc.createDataChannel( 'mychannel', { reliable: false } );
592 |
593 | this.setupChannelListener();
594 |
595 | this.pc.createOffer(
596 |
597 | function ( sdp ) {
598 |
599 | self.handleSessionDescription( sdp );
600 |
601 | },
602 |
603 | function ( error ) {
604 |
605 | console.log( error );
606 | self.onError( error );
607 |
608 | }
609 |
610 | );
611 |
612 | },
613 |
614 | /**
615 | * Sends data to a remote peer.
616 | * @param {anything} data
617 | */
618 | send: function ( data ) {
619 |
620 | // TODO: throw error?
621 | if ( this.channel === null || this.channel.readyState !== 'open' ) return;
622 |
623 | this.channel.send( JSON.stringify( data ) );
624 |
625 | },
626 |
627 | /**
628 | * Handles signal sent from a remote peer via server.
629 | * @param {object} signal - must have .peer as destination peer id and .id as source peer id
630 | */
631 | handleSignal: function ( signal ) {
632 |
633 | // ignores signal if it isn't for me
634 | if ( this.id !== signal.peer || this.peer !== signal.id ) return;
635 |
636 | switch ( signal.type ) {
637 |
638 | case 'offer':
639 | this.handleOffer( signal );
640 | break;
641 |
642 | case 'answer':
643 | this.handleAnswer( signal );
644 | break;
645 |
646 | case 'candidate':
647 | this.handleCandidate( signal );
648 | break;
649 |
650 | default:
651 | console.log( 'WebRTCPeer: Unknown signal type ' + signal.type );
652 | break;
653 |
654 | }
655 |
656 | }
657 |
658 | } );
659 |
660 | } )();
661 |
--------------------------------------------------------------------------------
/js/networks/WebSocketClient.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @author Takahiro https://github.com/takahirox
3 | *
4 | * TODO
5 | * implement
6 | */
7 |
8 | ( function () {
9 |
10 | /**
11 | * WebSocketClient constructor.
12 | * General WebSocket based NetworkClient.
13 | * @param params - parameters for instantiate.
14 | */
15 | THREE.WebSocketClient = function ( params ) {
16 |
17 | THREE.NetworkClient.call( this, params );
18 |
19 | };
20 |
21 | THREE.WebSocketClient.prototype = Object.create( THREE.NetworkClient.prototype );
22 | THREE.WebSocketClient.prototype.constructor = THREE.WebSocketClient;
23 |
24 | Object.assign( THREE.WebSocketClient.prototype, {
25 |
26 | } );
27 |
28 | } )();
29 |
--------------------------------------------------------------------------------
/js/physics/Physics.js:
--------------------------------------------------------------------------------
1 | ( function () {
2 |
3 | THREE.Physics = function ( params ) {
4 |
5 | if ( window.Ammo === undefined ) {
6 |
7 | throw new Error( 'import ammo.js' );
8 |
9 | }
10 |
11 | if ( params === undefined ) params = {};
12 |
13 | this.unitStep = ( params.unitStep !== undefined ) ? params.unitStep : 1 / 60;
14 | this.maxStepNum = ( params.maxStepNum !== undefined ) ? params.maxStepNum : 3;
15 |
16 | this.objects = [];
17 | this.bodies = [];
18 | this.bodyTable = {}; // object.uuid -> rigid body
19 |
20 | this.world = this.createWorld();
21 |
22 | }
23 |
24 | Object.assign( THREE.Physics.prototype, {
25 |
26 | // private
27 |
28 | createWorld: function () {
29 |
30 | var config = new Ammo.btDefaultCollisionConfiguration();
31 | var dispatcher = new Ammo.btCollisionDispatcher( config );
32 | var cache = new Ammo.btDbvtBroadphase();
33 | var solver = new Ammo.btSequentialImpulseConstraintSolver();
34 | var world = new Ammo.btDiscreteDynamicsWorld( dispatcher, cache, solver, config );
35 | world.setGravity( new Ammo.btVector3( 0, -9.8 * 10, 0 ) );
36 | return world;
37 |
38 | },
39 |
40 | createRigidBody: function ( object, params ) {
41 |
42 | if ( params === undefined ) params = {};
43 |
44 | var type = params.type !== undefined ? params.type : 'static';
45 | var shapeType = params.shapeType !== undefined ? params.shapeType : 'sphere';
46 | var weight = ( type === 'static' || params.weight === undefined ) ? 0 : params.weight;
47 | var width = params.width !== undefined ? params.width : 0.1;
48 | var height = params.height !== undefined ? params.height : 0.1;
49 | var depth = params.depth !== undefined ? params.depth : 0.1;
50 | var friction = params.friction !== undefined ? params.friction: 0.5;
51 | var restitution = params.restitution !== undefined ? params.restitution: 0.5;
52 | var groupIndex = params.groupIndex !== undefined ? params.groupIndex : 0;
53 | var groupTarget = params.groupTarget !== undefined ? params.groupTarget : 1;
54 |
55 | var shape;
56 |
57 | switch ( shapeType ) {
58 |
59 | case 'sphere':
60 | shape = new Ammo.btSphereShape( width );
61 | break;
62 |
63 | case 'box':
64 | var v = allocVector3();
65 | v.setValue( width, height, depth );
66 | shape = new Ammo.btBoxShape( v );
67 | freeVector3( v );
68 | break;
69 |
70 | case 'capsule':
71 | shape = new Ammo.btCapsuleShape( width, height );
72 | break;
73 |
74 | default:
75 | throw 'unknown shape type ' + shapeType;
76 |
77 | }
78 |
79 | var localInertia = allocVector3();
80 | localInertia.setValue( 0, 0, 0 );
81 |
82 | if( weight !== 0 ) {
83 |
84 | shape.calculateLocalInertia( weight, localInertia );
85 |
86 | }
87 |
88 | var threePosition = allocThreeVector3();
89 | var threeQuaternion = allocThreeQuaternion();
90 |
91 | object.getWorldPosition( threePosition );
92 | object.getWorldQuaternion( threeQuaternion );
93 |
94 | var position = allocVector3();
95 | var quaternion = allocQuaternion();
96 |
97 | position.setValue(
98 | threePosition.x,
99 | threePosition.y,
100 | threePosition.z
101 | );
102 |
103 | quaternion.setValue(
104 | threeQuaternion.x,
105 | threeQuaternion.y,
106 | threeQuaternion.z,
107 | threeQuaternion.w
108 | );
109 |
110 | var form = allocTransform();
111 | form.setIdentity();
112 | form.setRotation( quaternion );
113 | form.setOrigin( position );
114 |
115 | var state = new Ammo.btDefaultMotionState( form );
116 |
117 | var info = new Ammo.btRigidBodyConstructionInfo( weight, state, shape, localInertia );
118 | info.set_m_friction( friction );
119 | info.set_m_restitution( restitution );
120 |
121 | var body = new Ammo.btRigidBody( info );
122 |
123 | if ( type === 'static' ) {
124 |
125 | body.setCollisionFlags( body.getCollisionFlags() | 2 );
126 | //body.setActivationState( 4 );
127 |
128 | }
129 |
130 | if ( params.positionDamping !== undefined &&
131 | params.rotationDamping !== undefined )
132 | body.setDamping( params.positionDamping, params.rotationDamping );
133 |
134 | body.setSleepingThresholds( 0, 0 );
135 |
136 | this.world.addRigidBody( body, 1 << groupIndex, groupTarget );
137 |
138 | body.type = type;
139 |
140 | freeVector3( localInertia );
141 | freeTransform( form );
142 | freeVector3( position );
143 | freeQuaternion( quaternion );
144 | freeThreeVector3( threePosition );
145 | freeThreeQuaternion( threeQuaternion );
146 |
147 | return body;
148 |
149 | },
150 |
151 | removeElementFromArray: function ( array, object ) {
152 |
153 | var readIndex = 0;
154 | var writeIndex = 0;
155 |
156 | for ( var i = 0, il = array.length; i < il; i ++ ) {
157 |
158 | if ( array[ readIndex ] !== object ) {
159 |
160 | array[ writeIndex ] = array[ readIndex ];
161 | writeIndex ++;
162 |
163 | }
164 |
165 | readIndex ++;
166 |
167 | }
168 |
169 | array.length = writeIndex;
170 |
171 | },
172 |
173 | stepSimulation: function ( delta ) {
174 |
175 | var unitStep = this.unitStep;
176 | var stepTime = delta;
177 | var maxStepNum = ( ( delta / unitStep ) | 0 ) + 1;
178 |
179 | if ( stepTime < unitStep ) {
180 |
181 | stepTime = unitStep;
182 | maxStepNum = 1;
183 |
184 | }
185 |
186 | if ( maxStepNum > this.maxStepNum ) {
187 |
188 | maxStepNum = this.maxStepNum;
189 |
190 | }
191 |
192 | this.world.stepSimulation( stepTime, maxStepNum, unitStep );
193 |
194 | },
195 |
196 | transferFromObjects: function () {
197 |
198 | for ( var i = 0, il = this.objects.length; i < il; i ++ ) {
199 |
200 | var object = this.objects[ i ];
201 | var body = this.bodyTable[ object.uuid ];
202 |
203 | if ( body.type === 'dynamic' ) continue;
204 |
205 | var form = allocTransform();
206 | form.setIdentity();
207 |
208 | var quaternion = allocQuaternion();
209 |
210 | var threePosition = allocThreeVector3();
211 | var threeQuaternion = allocThreeQuaternion();
212 |
213 | object.getWorldPosition( threePosition );
214 | object.getWorldQuaternion( threeQuaternion );
215 |
216 | form.getOrigin().setValue(
217 | threePosition.x,
218 | threePosition.y,
219 | threePosition.z
220 | );
221 |
222 | quaternion.setValue(
223 | threeQuaternion.x,
224 | threeQuaternion.y,
225 | threeQuaternion.z,
226 | threeQuaternion.w
227 | );
228 |
229 | form.setRotation( quaternion );
230 |
231 | body.setCenterOfMassTransform( form );
232 | body.getMotionState().setWorldTransform( form );
233 |
234 | freeTransform( form );
235 | freeQuaternion( quaternion );
236 | freeThreeVector3( threePosition );
237 | freeThreeQuaternion( threeQuaternion );
238 |
239 | }
240 |
241 | },
242 |
243 | transferToObjects: function () {
244 |
245 | for ( var i = 0, il = this.objects.length; i < il; i ++ ) {
246 |
247 | var object = this.objects[ i ];
248 | var body = this.bodyTable[ object.uuid ];
249 |
250 | if ( body.type === 'static' ) continue;
251 |
252 | var form = allocTransform();
253 | var quaternion = allocQuaternion();
254 |
255 | body.getMotionState().getWorldTransform( form );
256 |
257 | var origin = form.getOrigin();
258 | form.getBasis().getRotation( quaternion );
259 |
260 | var threePosition = allocThreeVector3();
261 | var threeQuaternion = allocThreeQuaternion();
262 |
263 | threePosition.set(
264 | origin.x(),
265 | origin.y(),
266 | origin.z()
267 | );
268 |
269 | threeQuaternion.set(
270 | quaternion.x(),
271 | quaternion.y(),
272 | quaternion.z(),
273 | quaternion.w()
274 | );
275 |
276 | if ( object.parent !== null ) {
277 |
278 | // TODO: transform position and quaternion to object local world.
279 |
280 | }
281 |
282 | object.position.copy( threePosition );
283 | object.quaternion.copy( threeQuaternion );
284 |
285 | freeTransform( form );
286 | freeQuaternion( quaternion );
287 |
288 | }
289 |
290 | },
291 |
292 | // public
293 |
294 | add: function ( object, params ) {
295 |
296 | if ( params === undefined ) params = {};
297 |
298 | this.objects.push( object );
299 |
300 | var body = this.createRigidBody( object, params );
301 |
302 | this.bodies.push( body );
303 | this.bodyTable[ object.uuid ] = body;
304 |
305 | },
306 |
307 | remove: function ( object ) {
308 |
309 | var body = this.bodyTable[ object.uuid ];
310 |
311 | delete this.bodyTable[ object.uuid ];
312 |
313 | this.removeElementFromArray( this.objects, object );
314 | this.removeElementFromArray( this.bodies, body );
315 |
316 | },
317 |
318 | simulate: function ( delta ) {
319 |
320 | this.transferFromObjects();
321 | this.stepSimulation( delta );
322 | this.transferToObjects();
323 |
324 | }
325 |
326 | } );
327 |
328 | var vector3s = [];
329 | var quaternions = [];
330 | var transforms = [];
331 |
332 | var threeVector3s = [];
333 | var threeQuaternions = [];
334 | var threeMatrix4s = [];
335 |
336 | function allocVector3() {
337 |
338 | return vector3s.length > 0 ? vector3s.pop() : new Ammo.btVector3();
339 |
340 | }
341 |
342 | function freeVector3( v ) {
343 |
344 | vector3s.push( v );
345 |
346 | }
347 |
348 | function allocQuaternion() {
349 |
350 | return quaternions.length > 0 ? quaternions.pop() : new Ammo.btQuaternion();
351 |
352 | }
353 |
354 | function freeQuaternion( q ) {
355 |
356 | quaternions.push( q );
357 |
358 | }
359 |
360 | function allocTransform() {
361 |
362 | return transforms.length > 0 ? transforms.pop() : new Ammo.btTransform();
363 |
364 | }
365 |
366 | function freeTransform( t ) {
367 |
368 | transforms.push( t );
369 |
370 | }
371 |
372 | function allocThreeVector3() {
373 |
374 | return threeVector3s.length > 0 ? threeVector3s.pop() : new THREE.Vector3();
375 |
376 | }
377 |
378 | function freeThreeVector3( v ) {
379 |
380 | threeVector3s.push( v );
381 |
382 | }
383 |
384 | function allocThreeQuaternion() {
385 |
386 | return threeQuaternions.length > 0 ? threeQuaternions.pop() : new THREE.Quaternion();
387 |
388 | }
389 |
390 | function freeThreeQuaternion( q ) {
391 |
392 | threeQuaternions.push( q );
393 |
394 | }
395 |
396 | function allocThreeMatrix4() {
397 |
398 | return threeMatrix4s.length > 0 ? threeMatrix4s.pop() : new THREE.Matrix4();
399 |
400 | }
401 |
402 | function freeThreeMatrix4( m ) {
403 |
404 | threeMatrix4s.push( m );
405 |
406 | }
407 |
408 | } )();
409 |
--------------------------------------------------------------------------------
/peerjs.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | three.js webrtc
5 |
6 |
7 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
432 |
433 |
434 |
435 |
436 |
443 |
444 |
445 |
446 |
447 |
448 |
--------------------------------------------------------------------------------
/peerjs_mmd.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | three.js webrtc
5 |
6 |
7 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
462 |
463 |
464 |
465 |
466 |
473 |
474 |
475 |
476 |
477 |
478 |
--------------------------------------------------------------------------------