├── .gitignore ├── LICENSE.md ├── README.md ├── index.html ├── main.js ├── package.json ├── server ├── app.js ├── bin │ └── www ├── middlewares │ └── rtcServer.js ├── package.json ├── public │ ├── images │ │ ├── checkerboard.jpg │ │ ├── uni_hifi.jpg │ │ └── uni_lowfi.jpg │ ├── javascripts │ │ ├── helpers │ │ │ └── THREEx.WindowResize.js │ │ ├── index.js │ │ ├── rtcClient │ │ │ └── rtcClient.js │ │ ├── scene │ │ │ ├── detector.js │ │ │ ├── mainWindow.js │ │ │ ├── surfaceDisplay.js │ │ │ └── widget.js │ │ ├── webvr │ │ │ ├── webvr-polyfill-init.js │ │ │ ├── webvr-polyfill.js │ │ │ └── webvrconfig.js │ │ └── webvrThree │ │ │ ├── vrcontrol.js │ │ │ ├── vreffect.js │ │ │ └── webvr.js │ └── stylesheets │ │ └── style.css ├── routes │ └── index.js ├── views │ ├── error.jade │ ├── index.jade │ └── layout.jade └── webpack.config.js ├── src ├── common │ ├── rtcClient.js │ └── socketio.js ├── components │ ├── window.js │ └── windowContainer.js └── renderer.js └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | bundle.js 3 | npm-debug.log 4 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | CC0 1.0 Universal 2 | ================== 3 | 4 | Statement of Purpose 5 | --------------------- 6 | 7 | The laws of most jurisdictions throughout the world automatically confer exclusive Copyright and Related Rights (defined below) upon the creator and subsequent owner(s) (each and all, an "owner") of an original work of authorship and/or a database (each, a "Work"). 8 | 9 | Certain owners wish to permanently relinquish those rights to a Work for the purpose of contributing to a commons of creative, cultural and scientific works ("Commons") that the public can reliably and without fear of later claims of infringement build upon, modify, incorporate in other works, reuse and redistribute as freely as possible in any form whatsoever and for any purposes, including without limitation commercial purposes. These owners may contribute to the Commons to promote the ideal of a free culture and the further production of creative, cultural and scientific works, or to gain reputation or greater distribution for their Work in part through the use and efforts of others. 10 | 11 | For these and/or other purposes and motivations, and without any expectation of additional consideration or compensation, the person associating CC0 with a Work (the "Affirmer"), to the extent that he or she is an owner of Copyright and Related Rights in the Work, voluntarily elects to apply CC0 to the Work and publicly distribute the Work under its terms, with knowledge of his or her Copyright and Related Rights in the Work and the meaning and intended legal effect of CC0 on those rights. 12 | 13 | 1. Copyright and Related Rights. 14 | -------------------------------- 15 | A Work made available under CC0 may be protected by copyright and related or neighboring rights ("Copyright and Related Rights"). Copyright and Related Rights include, but are not limited to, the following: 16 | 17 | i. the right to reproduce, adapt, distribute, perform, display, communicate, and translate a Work; 18 | ii. moral rights retained by the original author(s) and/or performer(s); 19 | iii. publicity and privacy rights pertaining to a person's image or likeness depicted in a Work; 20 | iv. rights protecting against unfair competition in regards to a Work, subject to the limitations in paragraph 4(a), below; 21 | v. rights protecting the extraction, dissemination, use and reuse of data in a Work; 22 | vi. database rights (such as those arising under Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, and under any national implementation thereof, including any amended or successor version of such directive); and 23 | vii. other similar, equivalent or corresponding rights throughout the world based on applicable law or treaty, and any national implementations thereof. 24 | 25 | 2. Waiver. 26 | ----------- 27 | To the greatest extent permitted by, but not in contravention of, applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and unconditionally waives, abandons, and surrenders all of Affirmer's Copyright and Related Rights and associated claims and causes of action, whether now known or unknown (including existing as well as future claims and causes of action), in the Work (i) in all territories worldwide, (ii) for the maximum duration provided by applicable law or treaty (including future time extensions), (iii) in any current or future medium and for any number of copies, and (iv) for any purpose whatsoever, including without limitation commercial, advertising or promotional purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each member of the public at large and to the detriment of Affirmer's heirs and successors, fully intending that such Waiver shall not be subject to revocation, rescission, cancellation, termination, or any other legal or equitable action to disrupt the quiet enjoyment of the Work by the public as contemplated by Affirmer's express Statement of Purpose. 28 | 29 | 3. Public License Fallback. 30 | ---------------------------- 31 | Should any part of the Waiver for any reason be judged legally invalid or ineffective under applicable law, then the Waiver shall be preserved to the maximum extent permitted taking into account Affirmer's express Statement of Purpose. In addition, to the extent the Waiver is so judged Affirmer hereby grants to each affected person a royalty-free, non transferable, non sublicensable, non exclusive, irrevocable and unconditional license to exercise Affirmer's Copyright and Related Rights in the Work (i) in all territories worldwide, (ii) for the maximum duration provided by applicable law or treaty (including future time extensions), (iii) in any current or future medium and for any number of copies, and (iv) for any purpose whatsoever, including without limitation commercial, advertising or promotional purposes (the "License"). The License shall be deemed effective as of the date CC0 was applied by Affirmer to the Work. Should any part of the License for any reason be judged legally invalid or ineffective under applicable law, such partial invalidity or ineffectiveness shall not invalidate the remainder of the License, and in such case Affirmer hereby affirms that he or she will not (i) exercise any of his or her remaining Copyright and Related Rights in the Work or (ii) assert any associated claims and causes of action with respect to the Work, in either case contrary to Affirmer's express Statement of Purpose. 32 | 33 | 4. Limitations and Disclaimers. 34 | -------------------------------- 35 | 36 | a. No trademark or patent rights held by Affirmer are waived, abandoned, surrendered, licensed or otherwise affected by this document. 37 | b. Affirmer offers the Work as-is and makes no representations or warranties of any kind concerning the Work, express, implied, statutory or otherwise, including without limitation warranties of title, merchantability, fitness for a particular purpose, non infringement, or the absence of latent or other defects, accuracy, or the present or absence of errors, whether or not discoverable, all to the greatest extent permissible under applicable law. 38 | c. Affirmer disclaims responsibility for clearing rights of other persons that may apply to the Work or any use thereof, including without limitation any person's Copyright and Related Rights in the Work. Further, Affirmer disclaims responsibility for obtaining any necessary consents, permissions or other rights required for any use of the Work. 39 | d. Affirmer understands and acknowledges that Creative Commons is not a party to this document and has no duty or obligation with respect to this CC0 or use of the Work. 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # coder from 22nd century (c22) 2 | 3 | ## Why 4 | 5 | A demo of Micro-Situational Mixed Reality. You could check it out from [this article](http://www.hanyi.name/blog/2016/11/20/micro-situational-mixed-reality/) (Chinese only), or youtube video below (English subtitle). 6 | 7 | [![C22](http://img.youtube.com/vi/24fQwHYODeI/0.jpg)](http://www.youtube.com/watch?v=24fQwHYODeI"Coder from 22nd Century") 8 | 9 | ![Full](http://7xk84n.com1.z0.glb.clouddn.com/c22/full.jpg) 10 | 11 | Does world still need coders in 22nd century? No one knows except who may have ability on somehow mysterious prophecy. Depending on the trend of technology growth in next decades years, while it is indeed possible to deliver a blueprint which describes a programming scene that overcomes the gap from time to space, from devices to brains. 12 | 13 | ## Compatibility 14 | 15 | All OS supported for the Host. 16 | 17 | VR display (Cardboard) only work for Android now. Other platforms please refer to [WebRTC website](https://webrtc.org/native-code/) 18 | 19 | Basically the major techniques (WebRTC, WebVR) of C22 are not widely supported by mobile world (up to end of Nov, 2016), but which is highly possible to be brought to Android & IOS in 2017 according to both engineers interview from Google and Apple. 20 | 21 | There is no stable version browser supporting WebVR natively, luckily we use webvr-polyfill for early development. Or you may use latest version of Chrome without it. 22 | 23 | Besides WebVR, The incompatibility is mainly from WebRTC, which Safari totally ignore (should be released in 2017). 24 | 25 | ## Network 26 | 27 | In development we prefer using WIFI HotSpot directly than communicating through any AP. Please make sure your WIFI environment is unlimited otherwise there may be unbearable lag in mobile devices. 28 | 29 | ## Resolution 30 | 31 | It's strictly limited to mobile devices and hardly to be resolved by us. But our suggestion is just using lower display resolution in your PC. 32 | 33 | In development we switched to 1024*640 for Mac Pro, you could try higher if you have mobile screen with 1080p or plus. 34 | 35 | ## How to use 36 | 37 | Install packages for both desktop code and server code: 38 | ```[shell] 39 | npm install 40 | ``` 41 | Build bundle.js for both desktop code and server code: 42 | ```[shell] 43 | webpack 44 | ``` 45 | Under root path, run: 46 | ```[shell] 47 | npm start 48 | ``` 49 | Open chrome in your cardboard device, and access: 50 | 51 | http://host:8301 52 | 53 | Try to active some applications screen in host side and come back to VR. 54 | 55 | Enjoy and be dizzy:p 56 | 57 | ## Technologies 58 | 59 | Thus we have this repository, which has technology stack of: 60 | 61 | Electron 62 | 63 | Express 64 | 65 | WebSocket 66 | 67 | WebRTC 68 | 69 | WebVR 70 | 71 | three.js 72 | 73 | ~~Unity3D~~ 74 | 75 | ## Roadmap 76 | 77 | Native WebVR support 78 | 79 | Elastic apps showing 80 | 81 | Control switch for apps 82 | 83 | Network tuning 84 | 85 | Display tuning 86 | 87 | Native WebVR apps support 88 | 89 | Peripherals for WebVR apps (like bluetooth keyboards, earphones, daydream controllers, etc) 90 | 91 | Well supported peripherals summary 92 | 93 | Support for non-coders 94 | 95 | #### License [CC0 1.0 (Public Domain)](LICENSE.md) 96 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Coder from 22nd century 6 | 7 | 8 |
9 | 10 | We are using Node.js , 11 | Chromium , 12 | and Electron . 13 | 14 | 18 | 19 | -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | const electron = require('electron') 2 | // Module to control application life. 3 | const mainApp = electron.app 4 | // Module to create native browser window. 5 | const BrowserWindow = electron.BrowserWindow 6 | 7 | const path = require('path') 8 | const url = require('url') 9 | 10 | // Keep a global reference of the window object, if you don't, the window will 11 | // be closed automatically when the JavaScript object is garbage collected. 12 | let mainWindow 13 | 14 | function createWindow () { 15 | // Create the browser window. 16 | mainWindow = new BrowserWindow({width: 800, height: 600}) 17 | 18 | // and load the index.html of the app. 19 | mainWindow.loadURL(url.format({ 20 | pathname: path.join(__dirname, 'index.html'), 21 | protocol: 'file:', 22 | slashes: true 23 | })) 24 | 25 | // Open the DevTools. 26 | mainWindow.webContents.openDevTools() 27 | 28 | // Emitted when the window is closed. 29 | mainWindow.on('closed', function () { 30 | // Dereference the window object, usually you would store windows 31 | // in an array if your app supports multi windows, this is the time 32 | // when you should delete the corresponding element. 33 | mainWindow = null 34 | }) 35 | } 36 | 37 | // This method will be called when Electron has finished 38 | // initialization and is ready to create browser windows. 39 | // Some APIs can only be used after this event occurs. 40 | mainApp.on('ready', createWindow) 41 | 42 | // Quit when all windows are closed. 43 | mainApp.on('window-all-closed', function () { 44 | // On OS X it is common for applications and their menu bar 45 | // to stay active until the user quits explicitly with Cmd + Q 46 | if (process.platform !== 'darwin') { 47 | mainApp.quit() 48 | } 49 | }) 50 | 51 | mainApp.on('activate', function () { 52 | // On OS X it's common to re-create a window in the app when the 53 | // dock icon is clicked and there are no other windows open. 54 | if (mainWindow === null) { 55 | createWindow() 56 | } 57 | }) 58 | 59 | require('./server/app') 60 | // In this file you can include the rest of your app's specific main process 61 | // code. You can also put them in separate files and require them here. 62 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "coderFrom22ndCentury", 3 | "version": "0.1.0", 4 | "description": "", 5 | "main": "main.js", 6 | "scripts": { 7 | "start": "electron ." 8 | }, 9 | "repository": "", 10 | "keywords": [], 11 | "author": "Han Yi", 12 | "license": "CC0-1.0", 13 | "devDependencies": { 14 | "babel-core": "^6.18.2", 15 | "babel-loader": "^6.2.7", 16 | "babel-preset-es2015": "^6.18.0", 17 | "babel-preset-react": "^6.16.0", 18 | "babel-preset-stage-1": "^6.16.0", 19 | "body-parser": "~1.15.2", 20 | "cookie-parser": "~1.4.3", 21 | "debug": "~2.2.0", 22 | "electron": "^1.4.1", 23 | "express": "~4.14.0", 24 | "jade": "~1.11.0", 25 | "morgan": "~1.7.0", 26 | "react": "^15.3.2", 27 | "react-dom": "^15.3.2", 28 | "serve-favicon": "~2.3.0", 29 | "socket.io": "^1.5.1", 30 | "socket.io-client": "^1.5.1" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /server/app.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var path = require('path'); 3 | var favicon = require('serve-favicon'); 4 | var logger = require('morgan'); 5 | var cookieParser = require('cookie-parser'); 6 | var bodyParser = require('body-parser'); 7 | 8 | var index = require('./routes/index'); 9 | 10 | var app = express(); 11 | var rtcServer = require('./middlewares/rtcServer'); 12 | 13 | // view engine setup 14 | app.set('views', path.join(__dirname, 'views')); 15 | app.set('view engine', 'jade'); 16 | 17 | // uncomment after placing your favicon in /public 18 | //app.use(favicon(path.join(__dirname, 'public', 'favicon.ico'))); 19 | app.use(logger('dev')); 20 | app.use(bodyParser.json()); 21 | app.use(bodyParser.urlencoded({ extended: false })); 22 | app.use(cookieParser()); 23 | app.use(express.static(path.join(__dirname, 'public'))); 24 | 25 | app.use('/', index); 26 | 27 | // catch 404 and forward to error handler 28 | app.use(function(req, res, next) { 29 | var err = new Error('Not Found'); 30 | err.status = 404; 31 | next(err); 32 | }); 33 | 34 | // error handler 35 | app.use(function(err, req, res, next) { 36 | // set locals, only providing error in development 37 | res.locals.message = err.message; 38 | res.locals.error = req.app.get('env') === 'development' ? err : {}; 39 | 40 | // render the error page 41 | res.status(err.status || 500); 42 | res.render('error'); 43 | }); 44 | 45 | var port = process.env.PORT || 8301; 46 | 47 | var server = app.listen(port, function () { 48 | console.log('Updated : Server listening at port %d', port); 49 | }); 50 | 51 | rtcServer(server); 52 | 53 | module.exports = app; 54 | -------------------------------------------------------------------------------- /server/bin/www: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | 7 | var app = require('../app'); 8 | var debug = require('debug')('server:server'); 9 | var http = require('http'); 10 | 11 | /** 12 | * Get port from environment and store in Express. 13 | */ 14 | 15 | var port = normalizePort(process.env.PORT || '3000'); 16 | app.set('port', port); 17 | 18 | /** 19 | * Create HTTP server. 20 | */ 21 | 22 | var server = http.createServer(app); 23 | 24 | /** 25 | * Listen on provided port, on all network interfaces. 26 | */ 27 | 28 | server.listen(port); 29 | server.on('error', onError); 30 | server.on('listening', onListening); 31 | 32 | /** 33 | * Normalize a port into a number, string, or false. 34 | */ 35 | 36 | function normalizePort(val) { 37 | var port = parseInt(val, 10); 38 | 39 | if (isNaN(port)) { 40 | // named pipe 41 | return val; 42 | } 43 | 44 | if (port >= 0) { 45 | // port number 46 | return port; 47 | } 48 | 49 | return false; 50 | } 51 | 52 | /** 53 | * Event listener for HTTP server "error" event. 54 | */ 55 | 56 | function onError(error) { 57 | if (error.syscall !== 'listen') { 58 | throw error; 59 | } 60 | 61 | var bind = typeof port === 'string' 62 | ? 'Pipe ' + port 63 | : 'Port ' + port; 64 | 65 | // handle specific listen errors with friendly messages 66 | switch (error.code) { 67 | case 'EACCES': 68 | console.error(bind + ' requires elevated privileges'); 69 | process.exit(1); 70 | break; 71 | case 'EADDRINUSE': 72 | console.error(bind + ' is already in use'); 73 | process.exit(1); 74 | break; 75 | default: 76 | throw error; 77 | } 78 | } 79 | 80 | /** 81 | * Event listener for HTTP server "listening" event. 82 | */ 83 | 84 | function onListening() { 85 | var addr = server.address(); 86 | var bind = typeof addr === 'string' 87 | ? 'pipe ' + addr 88 | : 'port ' + addr.port; 89 | debug('Listening on ' + bind); 90 | } 91 | -------------------------------------------------------------------------------- /server/middlewares/rtcServer.js: -------------------------------------------------------------------------------- 1 | function socket(server) { 2 | var io = require('socket.io')(server); 3 | io.on('connection', function (socket) { 4 | socket.emit('news', {hello: 'world'}); 5 | socket.on('offer', function (data) { 6 | io.sockets.emit('emitOffer', data); 7 | }); 8 | socket.on('answer', function (data) { 9 | io.sockets.emit('answer', data); 10 | }); 11 | socket.on('connect_timeout', function (data) { 12 | socket.disconnect(); 13 | }); 14 | socket.on('offerCandidate', function (data) { 15 | io.sockets.emit('offerCandidate', data); 16 | }); 17 | socket.on('answerCandidate', function (data) { 18 | io.sockets.emit('answerCandidate', data); 19 | }); 20 | }); 21 | } 22 | module.exports = socket; 23 | -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "server", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "start": "node ./bin/www" 7 | }, 8 | "dependencies": { 9 | "body-parser": "~1.15.2", 10 | "cookie-parser": "~1.4.3", 11 | "debug": "~2.2.0", 12 | "express": "~4.14.0", 13 | "jade": "~1.11.0", 14 | "morgan": "~1.7.0", 15 | "serve-favicon": "~2.3.0" 16 | }, 17 | "devDependencies": { 18 | "socket.io-client": "^1.6.0", 19 | "three": "^0.82.1", 20 | "webvr-polyfill": "^0.9.23" 21 | } 22 | } -------------------------------------------------------------------------------- /server/public/images/checkerboard.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanystudy/coder-from-22nd-century/f209ee8d77cdc77a4d2dae4c6b1d08f80a7f91fc/server/public/images/checkerboard.jpg -------------------------------------------------------------------------------- /server/public/images/uni_hifi.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanystudy/coder-from-22nd-century/f209ee8d77cdc77a4d2dae4c6b1d08f80a7f91fc/server/public/images/uni_hifi.jpg -------------------------------------------------------------------------------- /server/public/images/uni_lowfi.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanystudy/coder-from-22nd-century/f209ee8d77cdc77a4d2dae4c6b1d08f80a7f91fc/server/public/images/uni_lowfi.jpg -------------------------------------------------------------------------------- /server/public/javascripts/helpers/THREEx.WindowResize.js: -------------------------------------------------------------------------------- 1 | // This THREEx helper makes it easy to handle window resize. 2 | // It will update renderer and camera when window is resized. 3 | // 4 | // # Usage 5 | // 6 | // **Step 1**: Start updating renderer and camera 7 | // 8 | // ```var windowResize = THREEx.WindowResize(aRenderer, aCamera)``` 9 | // 10 | // **Step 2**: Start updating renderer and camera 11 | // 12 | // ```windowResize.stop()``` 13 | // # Code 14 | 15 | // 16 | 17 | /** @namespace */ 18 | window.THREEx = window.THREEx || {}; 19 | 20 | /** 21 | * Update renderer and camera when the window is resized 22 | * 23 | * @param {Object} renderer the renderer to update 24 | * @param {Object} Camera the camera to update 25 | */ 26 | THREEx.WindowResize = function(renderer, camera){ 27 | var callback = function(){ 28 | // notify the renderer of the size change 29 | renderer.setSize( window.innerWidth, window.innerHeight ); 30 | // update the camera 31 | camera.aspect = window.innerWidth / window.innerHeight; 32 | camera.updateProjectionMatrix(); 33 | } 34 | // bind the resize event 35 | window.addEventListener('resize', callback, false); 36 | // return .stop() the function to stop watching window resize 37 | return { 38 | /** 39 | * Stop watching window resize 40 | */ 41 | stop : function(){ 42 | window.removeEventListener('resize', callback); 43 | } 44 | }; 45 | } 46 | -------------------------------------------------------------------------------- /server/public/javascripts/index.js: -------------------------------------------------------------------------------- 1 | window.THREE = require('three') 2 | require('./webvr/webvr-polyfill-init') 3 | require('./webvrThree/vreffect') 4 | require('./webvrThree/vrcontrol') 5 | require('./webvrThree/webvr') 6 | 7 | import remoteVideos from './rtcClient/rtcClient' 8 | import MainWindow from './scene/mainWindow' 9 | 10 | let mainWindow = new MainWindow(remoteVideos, window.innerWidth, window.innerHeight) 11 | let controls = new THREE.VRControls(mainWindow.camera) 12 | let effect = new THREE.VREffect(mainWindow.renderer) 13 | mainWindow.setVR(controls, effect) 14 | if (navigator.getVRDisplays) { 15 | navigator.getVRDisplays() 16 | .then( function ( displays ) { 17 | effect.setVRDisplay( displays[ 0 ] ); 18 | controls.setVRDisplay( displays[ 0 ] ); 19 | } ) 20 | .catch( function () { 21 | // no displays 22 | } ); 23 | document.body.appendChild(WEBVR.getButton(effect)); 24 | } 25 | -------------------------------------------------------------------------------- /server/public/javascripts/rtcClient/rtcClient.js: -------------------------------------------------------------------------------- 1 | var socket = require('socket.io-client')(location.origin); 2 | var pc2 = null; 3 | var webrtc = []; 4 | var remoteVideos = [ 5 | document.createElement('video'), 6 | document.createElement('video'), 7 | document.createElement('video'), 8 | document.createElement('video') 9 | ] 10 | const MAX_VIDEOS = 4 11 | var indexToBeChanged = 0 12 | 13 | function onCreateAnswerSuccess(desc) { 14 | pc2.setLocalDescription(desc); 15 | socket.emit('answer', desc); 16 | } 17 | 18 | function gotRemoteStream(e) { 19 | remoteVideos[indexToBeChanged].setAttribute('autoPlay','autoPlay') 20 | remoteVideos[indexToBeChanged].srcObject = e.stream 21 | indexToBeChanged = (indexToBeChanged + 1) % MAX_VIDEOS 22 | } 23 | 24 | socket.on('news', function (data) { 25 | console.log(data); 26 | }); 27 | 28 | socket.on('emitOffer', function (data) { 29 | let servers = null 30 | if (window.RTCPeerConnection) { 31 | pc2 = new RTCPeerConnection(servers) 32 | } 33 | else { 34 | pc2 = new webkitRTCPeerConnection(servers) 35 | } 36 | 37 | pc2.onicecandidate = function(e) { 38 | if (e.candidate) 39 | socket.emit('answerCandidate', e.candidate) 40 | }; 41 | pc2.getLocalStreams().forEach(function(stream) { 42 | pc2.removeStream(stream); 43 | }); 44 | pc2.onaddstream = gotRemoteStream; 45 | pc2.setRemoteDescription(new RTCSessionDescription(data)) 46 | pc2.createAnswer().then( 47 | onCreateAnswerSuccess, 48 | null 49 | ); 50 | }); 51 | 52 | socket.on('offerCandidate', function (data) { 53 | pc2.addIceCandidate(new RTCIceCandidate(data)); 54 | }); 55 | 56 | socket.on('cleanUp', function (data) { 57 | webrtc = webrtc.filter(function (rtc) { 58 | return rtc.connectionState() !== 'closed'; 59 | }) 60 | }); 61 | 62 | socket.on('connect_timeout', function (data) { 63 | socket.disconnect(); 64 | }); 65 | 66 | export default remoteVideos 67 | -------------------------------------------------------------------------------- /server/public/javascripts/scene/detector.js: -------------------------------------------------------------------------------- 1 | const Detector = { 2 | 3 | canvas: !! window.CanvasRenderingContext2D, 4 | webgl: ( function () { try { return !! window.WebGLRenderingContext && !! document.createElement( 'canvas' ).getContext( 'experimental-webgl' ); } catch( e ) { return false; } } )(), 5 | workers: !! window.Worker, 6 | fileapi: window.File && window.FileReader && window.FileList && window.Blob, 7 | 8 | getWebGLErrorMessage: function () { 9 | 10 | var element = document.createElement( 'div' ); 11 | element.id = 'webgl-error-message'; 12 | element.style.fontFamily = 'monospace'; 13 | element.style.fontSize = '13px'; 14 | element.style.fontWeight = 'normal'; 15 | element.style.textAlign = 'center'; 16 | element.style.background = '#fff'; 17 | element.style.color = '#000'; 18 | element.style.padding = '1.5em'; 19 | element.style.width = '400px'; 20 | element.style.margin = '5em auto 0'; 21 | 22 | if ( ! this.webgl ) { 23 | 24 | element.innerHTML = window.WebGLRenderingContext ? [ 25 | 'Your graphics card does not seem to support WebGL.
', 26 | 'Find out how to get it here.' 27 | ].join( '\n' ) : [ 28 | 'Your browser does not seem to support WebGL.
', 29 | 'Find out how to get it here.' 30 | ].join( '\n' ); 31 | 32 | } 33 | 34 | return element; 35 | 36 | }, 37 | 38 | addGetWebGLMessage: function ( parameters ) { 39 | 40 | var parent, id, element; 41 | 42 | parameters = parameters || {}; 43 | 44 | parent = parameters.parent !== undefined ? parameters.parent : document.body; 45 | id = parameters.id !== undefined ? parameters.id : 'oldie'; 46 | 47 | element = Detector.getWebGLErrorMessage(); 48 | element.id = id; 49 | 50 | parent.appendChild( element ); 51 | 52 | } 53 | 54 | } 55 | 56 | export default Detector 57 | -------------------------------------------------------------------------------- /server/public/javascripts/scene/mainWindow.js: -------------------------------------------------------------------------------- 1 | import Widget from './widget' 2 | import SurfaceDisplay from './surfaceDisplay' 3 | 4 | require('../helpers/THREEx.WindowResize') 5 | 6 | const DISPLAY_WIDTH = 960, DISPLAY_HEIGHT = 600, DISTANCE = 280, GAP = 1 7 | 8 | export default class MainWindow extends Widget { 9 | THREE = window.THREE || {}; 10 | THREEx = window.THREEx || {}; 11 | 12 | constructor(videos, width, height) { 13 | super() 14 | 15 | this.videos = videos 16 | this.windowWidth = width 17 | this.windowHeight = height 18 | 19 | this.resizeWidget(this.windowWidth, this.windowHeight) 20 | 21 | this.camera = new THREE.PerspectiveCamera( 60, DISPLAY_WIDTH/DISPLAY_HEIGHT, 0.1, 1000) 22 | this.scene = this.initScene() 23 | 24 | this.camera.position.set(0, 0, 0) 25 | this.camera.lookAt(new THREE.Vector3(0, 0, 1)) 26 | this.scene.add(this.camera) 27 | 28 | // this.scene.add(new THREE.AxisHelper(50)) 29 | 30 | THREEx.WindowResize(this.renderer, this.camera); 31 | 32 | this.displays = this.createDisplayGroup() 33 | } 34 | 35 | createDisplayGroup = () => { 36 | let displays = [] 37 | 38 | let surfaceDisplay = new SurfaceDisplay(this.videos[0], DISPLAY_WIDTH, DISPLAY_HEIGHT) 39 | surfaceDisplay.setPosition(0, -25, -DISTANCE) 40 | this.scene.add(surfaceDisplay.getMesh()) 41 | displays.push(surfaceDisplay) 42 | 43 | let surfaceDisplayLeft = new SurfaceDisplay(this.videos[2], DISPLAY_WIDTH, DISPLAY_HEIGHT) 44 | surfaceDisplayLeft.getMesh().rotateY(Math.PI / 2.6) 45 | surfaceDisplayLeft.setPosition(-DISPLAY_WIDTH/3.5, -25, -DISTANCE + 110) 46 | this.scene.add(surfaceDisplayLeft.getMesh()) 47 | displays.push(surfaceDisplayLeft) 48 | 49 | let surfaceDisplayRight = new SurfaceDisplay(this.videos[3], DISPLAY_WIDTH, DISPLAY_HEIGHT) 50 | surfaceDisplayRight.getMesh().rotateY(-Math.PI / 2.6) 51 | surfaceDisplayRight.setPosition(DISPLAY_WIDTH/3.5, -25, -DISTANCE + 110) 52 | this.scene.add(surfaceDisplayRight.getMesh()) 53 | displays.push(surfaceDisplayRight) 54 | 55 | let surfaceDisplayTop = new SurfaceDisplay(this.videos[1], DISPLAY_WIDTH, DISPLAY_HEIGHT) 56 | surfaceDisplayTop.getMesh().rotateX(Math.PI / 16) 57 | surfaceDisplayTop.setPosition(0, DISPLAY_HEIGHT/3.5, -DISTANCE + 8) 58 | this.scene.add(surfaceDisplayTop.getMesh()) 59 | displays.push(surfaceDisplayTop) 60 | 61 | 62 | return displays 63 | } 64 | 65 | initScene = () => { 66 | let scene = new THREE.Scene() 67 | 68 | var floorMaterial = new THREE.MeshBasicMaterial( { wireframe: true, side: THREE.DoubleSide } ) 69 | var floorGeometry = new THREE.PlaneGeometry(2000, 2000, 50, 50) 70 | var floor = new THREE.Mesh(floorGeometry, floorMaterial) 71 | floor.position.set(0, -DISPLAY_HEIGHT/2, 0) 72 | floor.rotation.x = Math.PI / 2 73 | // scene.add(floor) 74 | 75 | var imageLoader = new THREE.TextureLoader() 76 | imageLoader.load("/images/uni_lowfi.jpg", function(backgroundTexture) { 77 | var material = new THREE.MeshBasicMaterial({map:backgroundTexture, side: THREE.BackSide}) 78 | var skyBox = new THREE.Mesh( 79 | new THREE.SphereGeometry(1000,60,40), 80 | material 81 | ); 82 | skyBox.position.set(0, 0, 0) 83 | scene.add(skyBox) 84 | }); 85 | 86 | return scene 87 | } 88 | 89 | setVR = (controls, effect) => { 90 | this.effect = effect 91 | this.controls = controls 92 | } 93 | 94 | update = () => { 95 | this.displays.forEach(display => display.update()) 96 | this.controls.update() 97 | this.effect.render( this.scene, this.camera ) 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /server/public/javascripts/scene/surfaceDisplay.js: -------------------------------------------------------------------------------- 1 | export default class SurfaceDisplay { 2 | THREE = window.THREE || {} 3 | 4 | constructor(video, width = 576, height = 360) { 5 | this.width = width 6 | this.height = height 7 | 8 | this.video = video 9 | this.video.load() 10 | 11 | this.createContextAndTexture() 12 | 13 | this.display = this.createDisplayMesh(this.videoTexture) 14 | } 15 | 16 | createContextAndTexture = () => { 17 | this.videoTexture = new THREE.Texture( this.video ) 18 | this.videoTexture.minFilter = THREE.LinearFilter 19 | this.videoTexture.magFilter = THREE.LinearFilter 20 | } 21 | 22 | createDisplayMesh = (texture) => { 23 | texture.flipY = false 24 | const displayMaterial = new THREE.MeshBasicMaterial({ /*wireframe: true, */map: texture, overdraw: true, side: THREE.DoubleSide}) 25 | let displayGeometry = new THREE.CylinderGeometry( 1.0, 1.0, this.height/this.width/2.0, 100.0, 100.0, true, -0.25, 0.5 ); 26 | displayGeometry.rotateY(-Math.PI).rotateZ(Math.PI) 27 | displayGeometry.scale(this.height, this.height, this.height) 28 | displayGeometry.translate(0, 0, this.height) 29 | return new THREE.Mesh(displayGeometry, displayMaterial) 30 | } 31 | 32 | getMesh = () => this.display 33 | 34 | setPosition = (x, y, z) => { 35 | this.display.position.set(x, y, z) 36 | } 37 | 38 | update = () => { 39 | if (this.video.readyState === this.video.HAVE_ENOUGH_DATA) { 40 | this.videoTexture.needsUpdate = true 41 | } 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /server/public/javascripts/scene/widget.js: -------------------------------------------------------------------------------- 1 | import Detector from './detector' 2 | 3 | const SCREEN_WIDTH = window.innerWidth, SCREEN_HEIGHT = window.innerHeight; 4 | const VIEW_ANGLE = 45, ASPECT = SCREEN_WIDTH / SCREEN_HEIGHT, NEAR = 0.1, FAR = 10000; 5 | 6 | export default class Widget { 7 | THREE = window.THREE || {} 8 | constructor() { 9 | this.renderer = this.initRenderer() 10 | 11 | this.camera = new THREE.PerspectiveCamera( VIEW_ANGLE, ASPECT, NEAR, FAR) 12 | this.scene = new THREE.Scene() 13 | 14 | this.init() 15 | 16 | this.animate() 17 | } 18 | 19 | init = () => { 20 | this.camera.position.set(0,0,2) 21 | this.scene.add(this.camera) 22 | this.camera.lookAt(this.scene.position) 23 | } 24 | 25 | initRenderer = () => { 26 | let renderer = null 27 | if (Detector.webgl) 28 | renderer = new THREE.WebGLRenderer({antialias: true}) 29 | else 30 | renderer = new THREE.CanvasRenderer() 31 | 32 | renderer.setSize(SCREEN_WIDTH, SCREEN_HEIGHT) 33 | 34 | let container = document.getElementById('ThreeJS') 35 | container.appendChild(renderer.domElement) 36 | 37 | return renderer 38 | } 39 | 40 | resizeWidget = (width, height) => { 41 | this.renderer.setSize(width, height) 42 | } 43 | 44 | animate = () => { 45 | requestAnimationFrame( this.animate ) 46 | this.render() 47 | this.update() 48 | this.renderer.render(this.scene, this.camera) 49 | } 50 | 51 | render = () => { 52 | } 53 | 54 | update = () => { 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /server/public/javascripts/webvr/webvr-polyfill-init.js: -------------------------------------------------------------------------------- 1 | require('./webvrconfig'); 2 | require('./webvr-polyfill'); 3 | InitializeWebVRPolyfill(); 4 | -------------------------------------------------------------------------------- /server/public/javascripts/webvr/webvrconfig.js: -------------------------------------------------------------------------------- 1 | window.WebVRConfig = { DEFER_INITIALIZATION: true } 2 | -------------------------------------------------------------------------------- /server/public/javascripts/webvrThree/vrcontrol.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author dmarcos / https://github.com/dmarcos 3 | * @author mrdoob / http://mrdoob.com 4 | * @revisedBy hanystudy / https://github.com/hanystudy 5 | */ 6 | 7 | var THREE = window.THREE || {}; 8 | 9 | THREE.VRControls = function ( object, onError ) { 10 | 11 | var scope = this; 12 | 13 | var vrDisplay, vrDisplays; 14 | 15 | var standingMatrix = new THREE.Matrix4(); 16 | 17 | var frameData = null; 18 | 19 | if ( 'VRFrameData' in window ) { 20 | 21 | frameData = new VRFrameData(); 22 | 23 | } 24 | 25 | function gotVRDisplays( displays ) { 26 | 27 | vrDisplays = displays; 28 | 29 | if ( displays.length > 0 ) { 30 | 31 | vrDisplay = displays[ 0 ]; 32 | 33 | } else { 34 | 35 | if ( onError ) onError( 'VR input not available.' ); 36 | 37 | } 38 | 39 | } 40 | 41 | if ( navigator.getVRDisplays ) { 42 | 43 | navigator.getVRDisplays().then( gotVRDisplays ).catch ( function () { 44 | 45 | console.warn( 'THREE.VRControls: Unable to get VR Displays' ); 46 | 47 | } ); 48 | 49 | } 50 | 51 | // the Rift SDK returns the position in meters 52 | // this scale factor allows the user to define how meters 53 | // are converted to scene units. 54 | 55 | this.scale = 1; 56 | 57 | // If true will use "standing space" coordinate system where y=0 is the 58 | // floor and x=0, z=0 is the center of the room. 59 | this.standing = false; 60 | 61 | // Distance from the users eyes to the floor in meters. Used when 62 | // standing=true but the VRDisplay doesn't provide stageParameters. 63 | this.userHeight = 1.6; 64 | 65 | this.getVRDisplay = function () { 66 | 67 | return vrDisplay; 68 | 69 | }; 70 | 71 | this.setVRDisplay = function ( value ) { 72 | 73 | vrDisplay = value; 74 | 75 | }; 76 | 77 | this.getVRDisplays = function () { 78 | 79 | console.warn( 'THREE.VRControls: getVRDisplays() is being deprecated.' ); 80 | return vrDisplays; 81 | 82 | }; 83 | 84 | this.getStandingMatrix = function () { 85 | 86 | return standingMatrix; 87 | 88 | }; 89 | 90 | this.update = function () { 91 | 92 | if ( vrDisplay ) { 93 | 94 | var pose; 95 | 96 | if ( vrDisplay.getFrameData ) { 97 | 98 | vrDisplay.getFrameData( frameData ); 99 | pose = frameData.pose; 100 | 101 | } else if ( vrDisplay.getPose ) { 102 | 103 | pose = vrDisplay.getPose(); 104 | 105 | } 106 | 107 | if ( pose.orientation !== null ) { 108 | 109 | object.quaternion.fromArray( pose.orientation ); 110 | 111 | } 112 | 113 | if ( pose.position !== null ) { 114 | 115 | object.position.fromArray( pose.position ); 116 | 117 | } else { 118 | 119 | object.position.set( 0, 0, 0 ); 120 | 121 | } 122 | 123 | if ( this.standing ) { 124 | 125 | if ( vrDisplay.stageParameters ) { 126 | 127 | object.updateMatrix(); 128 | 129 | standingMatrix.fromArray( vrDisplay.stageParameters.sittingToStandingTransform ); 130 | object.applyMatrix( standingMatrix ); 131 | 132 | } else { 133 | 134 | object.position.setY( object.position.y + this.userHeight ); 135 | 136 | } 137 | 138 | } 139 | 140 | object.position.multiplyScalar( scope.scale ); 141 | 142 | } 143 | 144 | }; 145 | 146 | this.resetPose = function () { 147 | 148 | if ( vrDisplay ) { 149 | 150 | vrDisplay.resetPose(); 151 | 152 | } 153 | 154 | }; 155 | 156 | this.resetSensor = function () { 157 | 158 | console.warn( 'THREE.VRControls: .resetSensor() is now .resetPose().' ); 159 | this.resetPose(); 160 | 161 | }; 162 | 163 | this.zeroSensor = function () { 164 | 165 | console.warn( 'THREE.VRControls: .zeroSensor() is now .resetPose().' ); 166 | this.resetPose(); 167 | 168 | }; 169 | 170 | this.dispose = function () { 171 | 172 | vrDisplay = null; 173 | 174 | }; 175 | 176 | }; 177 | -------------------------------------------------------------------------------- /server/public/javascripts/webvrThree/vreffect.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author dmarcos / https://github.com/dmarcos 3 | * @author mrdoob / http://mrdoob.com 4 | * @revisedBy hanystudy / https://github.com/hanystudy 5 | * 6 | * WebVR Spec: http://mozvr.github.io/webvr-spec/webvr.html 7 | * 8 | * Firefox: http://mozvr.com/downloads/ 9 | * Chromium: https://webvr.info/get-chrome 10 | * 11 | */ 12 | 13 | var THREE = window.THREE || {}; 14 | 15 | THREE.VREffect = function( renderer, onError ) { 16 | 17 | var vrDisplay, vrDisplays; 18 | var eyeTranslationL = new THREE.Vector3(); 19 | var eyeTranslationR = new THREE.Vector3(); 20 | var renderRectL, renderRectR; 21 | 22 | var frameData = null; 23 | 24 | if ( 'VRFrameData' in window ) { 25 | 26 | frameData = new window.VRFrameData(); 27 | 28 | } 29 | 30 | function gotVRDisplays( displays ) { 31 | 32 | vrDisplays = displays; 33 | 34 | if ( displays.length > 0 ) { 35 | 36 | vrDisplay = displays[ 0 ]; 37 | 38 | } else { 39 | 40 | if ( onError ) onError( 'HMD not available' ); 41 | 42 | } 43 | 44 | } 45 | 46 | if ( navigator.getVRDisplays ) { 47 | 48 | navigator.getVRDisplays().then( gotVRDisplays ).catch( function() { 49 | 50 | console.warn( 'THREE.VREffect: Unable to get VR Displays' ); 51 | 52 | } ); 53 | 54 | } 55 | 56 | // 57 | 58 | this.isPresenting = false; 59 | this.scale = 1; 60 | 61 | var scope = this; 62 | 63 | var rendererSize = renderer.getSize(); 64 | var rendererUpdateStyle = false; 65 | var rendererPixelRatio = renderer.getPixelRatio(); 66 | 67 | this.getVRDisplay = function() { 68 | 69 | return vrDisplay; 70 | 71 | }; 72 | 73 | this.setVRDisplay = function( value ) { 74 | 75 | vrDisplay = value; 76 | 77 | }; 78 | 79 | this.getVRDisplays = function() { 80 | 81 | console.warn( 'THREE.VREffect: getVRDisplays() is being deprecated.' ); 82 | return vrDisplays; 83 | 84 | }; 85 | 86 | this.setSize = function( width, height, updateStyle ) { 87 | 88 | rendererSize = { width: width, height: height }; 89 | rendererUpdateStyle = updateStyle; 90 | 91 | if ( scope.isPresenting ) { 92 | 93 | var eyeParamsL = vrDisplay.getEyeParameters( 'left' ); 94 | renderer.setPixelRatio( 1 ); 95 | renderer.setSize( eyeParamsL.renderWidth * 2, eyeParamsL.renderHeight, false ); 96 | 97 | } else { 98 | 99 | renderer.setPixelRatio( rendererPixelRatio ); 100 | renderer.setSize( width, height, updateStyle ); 101 | 102 | } 103 | 104 | }; 105 | 106 | // VR presentation 107 | 108 | var canvas = renderer.domElement; 109 | var defaultLeftBounds = [ 0.0, 0.0, 0.5, 1.0 ]; 110 | var defaultRightBounds = [ 0.5, 0.0, 0.5, 1.0 ]; 111 | 112 | function onVRDisplayPresentChange() { 113 | 114 | var wasPresenting = scope.isPresenting; 115 | scope.isPresenting = vrDisplay !== undefined && vrDisplay.isPresenting; 116 | 117 | if ( scope.isPresenting ) { 118 | 119 | var eyeParamsL = vrDisplay.getEyeParameters( 'left' ); 120 | var eyeWidth = eyeParamsL.renderWidth; 121 | var eyeHeight = eyeParamsL.renderHeight; 122 | 123 | if ( ! wasPresenting ) { 124 | 125 | rendererPixelRatio = renderer.getPixelRatio(); 126 | rendererSize = renderer.getSize(); 127 | 128 | renderer.setPixelRatio( 1 ); 129 | renderer.setSize( eyeWidth * 2, eyeHeight, false ); 130 | 131 | } 132 | 133 | } else if ( wasPresenting ) { 134 | 135 | renderer.setPixelRatio( rendererPixelRatio ); 136 | renderer.setSize( rendererSize.width, rendererSize.height, rendererUpdateStyle ); 137 | 138 | } 139 | 140 | } 141 | 142 | window.addEventListener( 'vrdisplaypresentchange', onVRDisplayPresentChange, false ); 143 | 144 | this.setFullScreen = function( boolean ) { 145 | 146 | return new Promise( function( resolve, reject ) { 147 | 148 | if ( vrDisplay === undefined ) { 149 | 150 | reject( new Error( 'No VR hardware found.' ) ); 151 | return; 152 | 153 | } 154 | 155 | if ( scope.isPresenting === boolean ) { 156 | 157 | resolve(); 158 | return; 159 | 160 | } 161 | 162 | if ( boolean ) { 163 | 164 | resolve( vrDisplay.requestPresent( [ { source: canvas } ] ) ); 165 | 166 | } else { 167 | 168 | resolve( vrDisplay.exitPresent() ); 169 | 170 | } 171 | 172 | } ); 173 | 174 | }; 175 | 176 | this.requestPresent = function() { 177 | 178 | return this.setFullScreen( true ); 179 | 180 | }; 181 | 182 | this.exitPresent = function() { 183 | 184 | return this.setFullScreen( false ); 185 | 186 | }; 187 | 188 | this.requestAnimationFrame = function( f ) { 189 | 190 | if ( vrDisplay !== undefined ) { 191 | 192 | return vrDisplay.requestAnimationFrame( f ); 193 | 194 | } else { 195 | 196 | return window.requestAnimationFrame( f ); 197 | 198 | } 199 | 200 | }; 201 | 202 | this.cancelAnimationFrame = function( h ) { 203 | 204 | if ( vrDisplay !== undefined ) { 205 | 206 | vrDisplay.cancelAnimationFrame( h ); 207 | 208 | } else { 209 | 210 | window.cancelAnimationFrame( h ); 211 | 212 | } 213 | 214 | }; 215 | 216 | this.submitFrame = function() { 217 | 218 | if ( vrDisplay !== undefined && scope.isPresenting ) { 219 | 220 | vrDisplay.submitFrame(); 221 | 222 | } 223 | 224 | }; 225 | 226 | this.autoSubmitFrame = true; 227 | 228 | // render 229 | 230 | var cameraL = new THREE.PerspectiveCamera(); 231 | cameraL.layers.enable( 1 ); 232 | 233 | var cameraR = new THREE.PerspectiveCamera(); 234 | cameraR.layers.enable( 2 ); 235 | 236 | this.render = function( scene, camera, renderTarget, forceClear ) { 237 | 238 | if ( vrDisplay && scope.isPresenting ) { 239 | 240 | var autoUpdate = scene.autoUpdate; 241 | 242 | if ( autoUpdate ) { 243 | 244 | scene.updateMatrixWorld(); 245 | scene.autoUpdate = false; 246 | 247 | } 248 | 249 | var eyeParamsL = vrDisplay.getEyeParameters( 'left' ); 250 | var eyeParamsR = vrDisplay.getEyeParameters( 'right' ); 251 | 252 | eyeTranslationL.fromArray( eyeParamsL.offset ); 253 | eyeTranslationR.fromArray( eyeParamsR.offset ); 254 | 255 | if ( Array.isArray( scene ) ) { 256 | 257 | console.warn( 'THREE.VREffect.render() no longer supports arrays. Use object.layers instead.' ); 258 | scene = scene[ 0 ]; 259 | 260 | } 261 | 262 | // When rendering we don't care what the recommended size is, only what the actual size 263 | // of the backbuffer is. 264 | var size = renderer.getSize(); 265 | var layers = vrDisplay.getLayers(); 266 | var leftBounds; 267 | var rightBounds; 268 | 269 | if ( layers.length ) { 270 | 271 | var layer = layers[ 0 ]; 272 | 273 | leftBounds = layer.leftBounds !== null && layer.leftBounds.length === 4 ? layer.leftBounds : defaultLeftBounds; 274 | rightBounds = layer.rightBounds !== null && layer.rightBounds.length === 4 ? layer.rightBounds : defaultRightBounds; 275 | 276 | } else { 277 | 278 | leftBounds = defaultLeftBounds; 279 | rightBounds = defaultRightBounds; 280 | 281 | } 282 | 283 | renderRectL = { 284 | x: Math.round( size.width * leftBounds[ 0 ] ), 285 | y: Math.round( size.height * leftBounds[ 1 ] ), 286 | width: Math.round( size.width * leftBounds[ 2 ] ), 287 | height: Math.round( size.height * leftBounds[ 3 ] ) 288 | }; 289 | renderRectR = { 290 | x: Math.round( size.width * rightBounds[ 0 ] ), 291 | y: Math.round( size.height * rightBounds[ 1 ] ), 292 | width: Math.round( size.width * rightBounds[ 2 ] ), 293 | height: Math.round( size.height * rightBounds[ 3 ] ) 294 | }; 295 | 296 | if ( renderTarget ) { 297 | 298 | renderer.setRenderTarget( renderTarget ); 299 | renderTarget.scissorTest = true; 300 | 301 | } else { 302 | 303 | renderer.setRenderTarget( null ); 304 | renderer.setScissorTest( true ); 305 | 306 | } 307 | 308 | if ( renderer.autoClear || forceClear ) renderer.clear(); 309 | 310 | if ( camera.parent === null ) camera.updateMatrixWorld(); 311 | 312 | camera.matrixWorld.decompose( cameraL.position, cameraL.quaternion, cameraL.scale ); 313 | camera.matrixWorld.decompose( cameraR.position, cameraR.quaternion, cameraR.scale ); 314 | 315 | var scale = this.scale; 316 | cameraL.translateOnAxis( eyeTranslationL, scale ); 317 | cameraR.translateOnAxis( eyeTranslationR, scale ); 318 | 319 | if ( vrDisplay.getFrameData ) { 320 | 321 | vrDisplay.depthNear = camera.near; 322 | vrDisplay.depthFar = camera.far; 323 | 324 | vrDisplay.getFrameData( frameData ); 325 | 326 | cameraL.projectionMatrix.elements = frameData.leftProjectionMatrix; 327 | cameraR.projectionMatrix.elements = frameData.rightProjectionMatrix; 328 | 329 | } else { 330 | 331 | cameraL.projectionMatrix = fovToProjection( eyeParamsL.fieldOfView, true, camera.near, camera.far ); 332 | cameraR.projectionMatrix = fovToProjection( eyeParamsR.fieldOfView, true, camera.near, camera.far ); 333 | 334 | } 335 | 336 | // render left eye 337 | if ( renderTarget ) { 338 | 339 | renderTarget.viewport.set( renderRectL.x, renderRectL.y, renderRectL.width, renderRectL.height ); 340 | renderTarget.scissor.set( renderRectL.x, renderRectL.y, renderRectL.width, renderRectL.height ); 341 | 342 | } else { 343 | 344 | renderer.setViewport( renderRectL.x, renderRectL.y, renderRectL.width, renderRectL.height ); 345 | renderer.setScissor( renderRectL.x, renderRectL.y, renderRectL.width, renderRectL.height ); 346 | 347 | } 348 | renderer.render( scene, cameraL, renderTarget, forceClear ); 349 | 350 | // render right eye 351 | if ( renderTarget ) { 352 | 353 | renderTarget.viewport.set( renderRectR.x, renderRectR.y, renderRectR.width, renderRectR.height ); 354 | renderTarget.scissor.set( renderRectR.x, renderRectR.y, renderRectR.width, renderRectR.height ); 355 | 356 | } else { 357 | 358 | renderer.setViewport( renderRectR.x, renderRectR.y, renderRectR.width, renderRectR.height ); 359 | renderer.setScissor( renderRectR.x, renderRectR.y, renderRectR.width, renderRectR.height ); 360 | 361 | } 362 | renderer.render( scene, cameraR, renderTarget, forceClear ); 363 | 364 | if ( renderTarget ) { 365 | 366 | renderTarget.viewport.set( 0, 0, size.width, size.height ); 367 | renderTarget.scissor.set( 0, 0, size.width, size.height ); 368 | renderTarget.scissorTest = false; 369 | renderer.setRenderTarget( null ); 370 | 371 | } else { 372 | 373 | renderer.setViewport( 0, 0, size.width, size.height ); 374 | renderer.setScissorTest( false ); 375 | 376 | } 377 | 378 | if ( autoUpdate ) { 379 | 380 | scene.autoUpdate = true; 381 | 382 | } 383 | 384 | if ( scope.autoSubmitFrame ) { 385 | 386 | scope.submitFrame(); 387 | 388 | } 389 | 390 | return; 391 | 392 | } 393 | 394 | // Regular render mode if not HMD 395 | 396 | renderer.render( scene, camera, renderTarget, forceClear ); 397 | 398 | }; 399 | 400 | this.dispose = function() { 401 | 402 | window.removeEventListener( 'vrdisplaypresentchange', onVRDisplayPresentChange, false ); 403 | 404 | }; 405 | 406 | // 407 | 408 | function fovToNDCScaleOffset( fov ) { 409 | 410 | var pxscale = 2.0 / ( fov.leftTan + fov.rightTan ); 411 | var pxoffset = ( fov.leftTan - fov.rightTan ) * pxscale * 0.5; 412 | var pyscale = 2.0 / ( fov.upTan + fov.downTan ); 413 | var pyoffset = ( fov.upTan - fov.downTan ) * pyscale * 0.5; 414 | return { scale: [ pxscale, pyscale ], offset: [ pxoffset, pyoffset ] }; 415 | 416 | } 417 | 418 | function fovPortToProjection( fov, rightHanded, zNear, zFar ) { 419 | 420 | rightHanded = rightHanded === undefined ? true : rightHanded; 421 | zNear = zNear === undefined ? 0.01 : zNear; 422 | zFar = zFar === undefined ? 10000.0 : zFar; 423 | 424 | var handednessScale = rightHanded ? - 1.0 : 1.0; 425 | 426 | // start with an identity matrix 427 | var mobj = new THREE.Matrix4(); 428 | var m = mobj.elements; 429 | 430 | // and with scale/offset info for normalized device coords 431 | var scaleAndOffset = fovToNDCScaleOffset( fov ); 432 | 433 | // X result, map clip edges to [-w,+w] 434 | m[ 0 * 4 + 0 ] = scaleAndOffset.scale[ 0 ]; 435 | m[ 0 * 4 + 1 ] = 0.0; 436 | m[ 0 * 4 + 2 ] = scaleAndOffset.offset[ 0 ] * handednessScale; 437 | m[ 0 * 4 + 3 ] = 0.0; 438 | 439 | // Y result, map clip edges to [-w,+w] 440 | // Y offset is negated because this proj matrix transforms from world coords with Y=up, 441 | // but the NDC scaling has Y=down (thanks D3D?) 442 | m[ 1 * 4 + 0 ] = 0.0; 443 | m[ 1 * 4 + 1 ] = scaleAndOffset.scale[ 1 ]; 444 | m[ 1 * 4 + 2 ] = - scaleAndOffset.offset[ 1 ] * handednessScale; 445 | m[ 1 * 4 + 3 ] = 0.0; 446 | 447 | // Z result (up to the app) 448 | m[ 2 * 4 + 0 ] = 0.0; 449 | m[ 2 * 4 + 1 ] = 0.0; 450 | m[ 2 * 4 + 2 ] = zFar / ( zNear - zFar ) * - handednessScale; 451 | m[ 2 * 4 + 3 ] = ( zFar * zNear ) / ( zNear - zFar ); 452 | 453 | // W result (= Z in) 454 | m[ 3 * 4 + 0 ] = 0.0; 455 | m[ 3 * 4 + 1 ] = 0.0; 456 | m[ 3 * 4 + 2 ] = handednessScale; 457 | m[ 3 * 4 + 3 ] = 0.0; 458 | 459 | mobj.transpose(); 460 | 461 | return mobj; 462 | 463 | } 464 | 465 | function fovToProjection( fov, rightHanded, zNear, zFar ) { 466 | 467 | var DEG2RAD = Math.PI / 180.0; 468 | 469 | var fovPort = { 470 | upTan: Math.tan( fov.upDegrees * DEG2RAD ), 471 | downTan: Math.tan( fov.downDegrees * DEG2RAD ), 472 | leftTan: Math.tan( fov.leftDegrees * DEG2RAD ), 473 | rightTan: Math.tan( fov.rightDegrees * DEG2RAD ) 474 | }; 475 | 476 | return fovPortToProjection( fovPort, rightHanded, zNear, zFar ); 477 | 478 | } 479 | 480 | }; 481 | -------------------------------------------------------------------------------- /server/public/javascripts/webvrThree/webvr.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author mrdoob / http://mrdoob.com 3 | * @revisedBy hanystudy / https://github.com/hanystudy 4 | * 5 | * Based on @tojiro's vr-samples-utils.js 6 | */ 7 | 8 | window.WEBVR = { 9 | 10 | isLatestAvailable: function () { 11 | 12 | console.warn( 'WEBVR: isLatestAvailable() is being deprecated. Use .isAvailable() instead.' ); 13 | return this.isAvailable(); 14 | 15 | }, 16 | 17 | isAvailable: function () { 18 | 19 | return navigator.getVRDisplays !== undefined; 20 | 21 | }, 22 | 23 | getMessage: function () { 24 | 25 | var message; 26 | 27 | if ( navigator.getVRDisplays ) { 28 | 29 | navigator.getVRDisplays().then( function ( displays ) { 30 | 31 | if ( displays.length === 0 ) message = 'WebVR supported, but no VRDisplays found.'; 32 | 33 | } ); 34 | 35 | } else { 36 | 37 | message = 'Your browser does not support WebVR. See webvr.info for assistance.'; 38 | 39 | } 40 | 41 | if ( message !== undefined ) { 42 | 43 | var container = document.createElement( 'div' ); 44 | container.style.position = 'absolute'; 45 | container.style.left = '0'; 46 | container.style.top = '0'; 47 | container.style.right = '0'; 48 | container.style.zIndex = '999'; 49 | container.align = 'center'; 50 | 51 | var error = document.createElement( 'div' ); 52 | error.style.fontFamily = 'sans-serif'; 53 | error.style.fontSize = '16px'; 54 | error.style.fontStyle = 'normal'; 55 | error.style.lineHeight = '26px'; 56 | error.style.backgroundColor = '#fff'; 57 | error.style.color = '#000'; 58 | error.style.padding = '10px 20px'; 59 | error.style.margin = '50px'; 60 | error.style.display = 'inline-block'; 61 | error.innerHTML = message; 62 | container.appendChild( error ); 63 | 64 | return container; 65 | 66 | } 67 | 68 | }, 69 | 70 | getButton: function ( effect ) { 71 | 72 | var button = document.createElement( 'button' ); 73 | button.style.position = 'absolute'; 74 | button.style.left = 'calc(50% - 50px)'; 75 | button.style.bottom = '20px'; 76 | button.style.width = '100px'; 77 | button.style.border = '0'; 78 | button.style.padding = '8px'; 79 | button.style.cursor = 'pointer'; 80 | button.style.backgroundColor = '#000'; 81 | button.style.color = '#fff'; 82 | button.style.fontFamily = 'sans-serif'; 83 | button.style.fontSize = '13px'; 84 | button.style.fontStyle = 'normal'; 85 | button.style.textAlign = 'center'; 86 | button.style.zIndex = '999'; 87 | button.textContent = 'ENTER VR'; 88 | button.onclick = function() { 89 | 90 | effect.isPresenting ? effect.exitPresent() : effect.requestPresent(); 91 | 92 | }; 93 | 94 | window.addEventListener( 'vrdisplaypresentchange', function ( event ) { 95 | 96 | button.textContent = effect.isPresenting ? 'EXIT VR' : 'ENTER VR'; 97 | 98 | }, false ); 99 | 100 | return button; 101 | 102 | } 103 | 104 | }; 105 | -------------------------------------------------------------------------------- /server/public/stylesheets/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding: 50px; 3 | font: 14px "Lucida Grande", Helvetica, Arial, sans-serif; 4 | } 5 | 6 | a { 7 | color: #00B7FF; 8 | } 9 | 10 | #ThreeJS { 11 | position: absolute; 12 | left:0px; 13 | top:0px; 14 | } 15 | 16 | video { 17 | display: none; 18 | } 19 | -------------------------------------------------------------------------------- /server/routes/index.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var router = express.Router(); 3 | 4 | /* GET home page. */ 5 | router.get('/', function(req, res, next) { 6 | res.render('index'); 7 | }); 8 | 9 | module.exports = router; 10 | -------------------------------------------------------------------------------- /server/views/error.jade: -------------------------------------------------------------------------------- 1 | extends layout 2 | 3 | block content 4 | h1= message 5 | h2= error.status 6 | pre #{error.stack} 7 | -------------------------------------------------------------------------------- /server/views/index.jade: -------------------------------------------------------------------------------- 1 | extends layout 2 | 3 | block content 4 | div#ThreeJS 5 | video#remoteVideo(autoplay="autoplay") 6 | -------------------------------------------------------------------------------- /server/views/layout.jade: -------------------------------------------------------------------------------- 1 | doctype html 2 | html 3 | head 4 | title= title 5 | link(rel='stylesheet', href='/stylesheets/style.css') 6 | body 7 | block content 8 | script(src='/dist/bundle.js') 9 | -------------------------------------------------------------------------------- /server/webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | entry: "./public/javascripts/index.js", 3 | output: { 4 | path: __dirname, 5 | filename: "./public/dist/bundle.js" 6 | }, 7 | module: { 8 | loaders: [ 9 | { 10 | test: /\.(js|jsx)$/, 11 | loader: 'babel-loader', 12 | exclude: /node_modules/, 13 | query: { 14 | babelrc: false, 15 | presets: ['es2015', 'stage-1', 'react'] 16 | } 17 | }, 18 | ] 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /src/common/rtcClient.js: -------------------------------------------------------------------------------- 1 | import {socket} from './socketio' 2 | 3 | const offerOptions = { 4 | offerToReceiveAudio: 1, 5 | offerToReceiveVideo: 1 6 | } 7 | 8 | export const rtcClient = (function() { 9 | 10 | let webrtc = null 11 | let checkedSources = {} 12 | let checkedWebRTC = {} 13 | 14 | const init = () => { 15 | socket.on('news', function (data) {}) 16 | socket.on('answer', (data) => { 17 | if (webrtc) webrtc.setRemoteDescription(new RTCSessionDescription(data)) 18 | }); 19 | socket.on('answerCandidate', (data) => { 20 | if (webrtc) webrtc.addIceCandidate(new RTCIceCandidate(data)) 21 | }); 22 | socket.on('connect_timeout', function () { 23 | socket.disconnect() 24 | }) 25 | } 26 | 27 | const createRTCClient = (id, sourceObject) => { 28 | let pc1 = null 29 | checkedSources[id] = sourceObject 30 | if (window.RTCPeerConnection) { 31 | pc1 = new RTCPeerConnection(null) 32 | } 33 | else { 34 | pc1 = new webkitRTCPeerConnection(null) 35 | } 36 | webrtc = pc1 37 | checkedWebRTC[id] = pc1 38 | 39 | function onCreateOfferSuccess(desc) { 40 | pc1.setLocalDescription(desc) 41 | socket.emit('offer', desc) 42 | } 43 | function onCreateSessionDescriptionError(error) { } 44 | pc1.onicecandidate = function(e) { 45 | if (e.candidate) socket.emit('offerCandidate', e.candidate) 46 | } 47 | pc1.addStream(sourceObject.stream) 48 | pc1.createOffer(offerOptions).then( 49 | onCreateOfferSuccess, 50 | onCreateSessionDescriptionError 51 | ) 52 | } 53 | 54 | const removeRTCClient = (id, sourceObject) => { 55 | let pc1 = checkedWebRTC[id] 56 | checkedSources[id] = null 57 | pc1.getLocalStreams().forEach((stream) => { 58 | pc1.removeStream(stream) 59 | }) 60 | pc1.close() 61 | checkedWebRTC[id] = null 62 | } 63 | 64 | return { init, createRTCClient, removeRTCClient } 65 | })() 66 | -------------------------------------------------------------------------------- /src/common/socketio.js: -------------------------------------------------------------------------------- 1 | let io = require('socket.io-client') 2 | 3 | export let socket = io('http://localhost:8301') 4 | -------------------------------------------------------------------------------- /src/components/window.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export default class Window extends React.Component { 4 | constructor() { 5 | super() 6 | this.state = { video: null } 7 | } 8 | 9 | componentDidMount() { 10 | if (this.props.source) { 11 | let source = this.props.source 12 | const handleStream = (stream) => { 13 | this.setState({ 14 | video: , 15 | title: source.name, 16 | id: source.id, 17 | stream: stream 18 | }) 19 | } 20 | const handleError = (e) => {} 21 | navigator.webkitGetUserMedia({ 22 | audio: false, 23 | video: { 24 | mandatory: { 25 | chromeMediaSource: 'desktop', 26 | chromeMediaSourceId: source.id, 27 | minWidth: 960, 28 | maxWidth: 960, 29 | minHeight: 600, 30 | maxHeight: 600 31 | } 32 | } 33 | }, handleStream, handleError) 34 | } 35 | } 36 | 37 | windowChange = (e) => { 38 | const target = e.target 39 | const sourceObject = { 40 | checked: target.checked, 41 | stream: this.state.stream 42 | } 43 | this.props.windowChange(target.value, sourceObject) 44 | } 45 | 46 | render() { 47 | return
48 | {this.state.video} 49 |
50 | 51 | {this.state.title} 52 |
53 |
54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/components/windowContainer.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Window from './window' 3 | const {desktopCapturer} = require('electron') 4 | import {rtcClient} from '../common/rtcClient' 5 | 6 | export default class WindowContainer extends React.Component { 7 | constructor() { 8 | super() 9 | this.state = { 10 | sources: [] 11 | } 12 | } 13 | 14 | componentDidMount() { 15 | rtcClient.init() 16 | desktopCapturer.getSources({types: ['window', 'screen']}, (error, sources) => { 17 | this.setState({sources}) 18 | }) 19 | } 20 | 21 | windowList() { 22 | return this.state.sources.map((source) => { 23 | return 24 | }) 25 | } 26 | 27 | windowChange = (id, sourceObject) => { 28 | if (sourceObject.checked) { 29 | rtcClient.createRTCClient(id, sourceObject) 30 | } 31 | else { 32 | rtcClient.removeRTCClient(id, sourceObject) 33 | } 34 | } 35 | 36 | render() { 37 | return
{this.windowList()}
38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/renderer.js: -------------------------------------------------------------------------------- 1 | // This file is required by the index.html file and will 2 | // be executed in the renderer process for that window. 3 | // All of the Node.js APIs are available in this process. 4 | // In the renderer process. 5 | import React from 'react' 6 | import ReactDOM from 'react-dom' 7 | import WindowContainer from './components/windowContainer' 8 | 9 | ReactDOM.render( 10 | , 11 | document.getElementById('root') 12 | ) 13 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | entry: "./src/renderer.js", 3 | output: { 4 | path: __dirname, 5 | filename: "./public/bundle.js" 6 | }, 7 | target: 'electron-renderer', 8 | module: { 9 | loaders: [ 10 | { 11 | test: /\.(js|jsx)$/, 12 | loader: 'babel-loader', 13 | exclude: /node_modules/, 14 | query: { 15 | babelrc: false, 16 | presets: ['es2015', 'stage-1', 'react'] 17 | } 18 | }, 19 | ] 20 | } 21 | }; 22 | --------------------------------------------------------------------------------