From 6b906685012d96acdc143641c04aba8c71b3cd28 Mon Sep 17 00:00:00 2001 From: Jerko Steiner Date: Thu, 31 Mar 2016 18:03:39 -0400 Subject: [PATCH] Initial commit, working full screen video --- .babelrc | 3 + .eslintrc | 212 +++++++++++++++++++++ .gitignore | 5 + .travis.yml | 19 ++ Makefile | 36 ++++ README.md | 48 +++++ package.json | 54 ++++++ src/index.js | 49 +++++ src/js/browser/createObjectURL.js | 2 + src/js/browser/getUserMedia.js | 16 ++ src/js/browser/navigator.js | 2 + src/js/components/__tests__/app-test.js | 60 ++++++ src/js/components/app.js | 42 ++++ src/js/dispatcher/dispatcher.js | 5 + src/js/index.js | 39 ++++ src/js/peer/Peer.js | 8 + src/js/peer/__tests__/handshake-test.js | 174 +++++++++++++++++ src/js/peer/handshake.js | 91 +++++++++ src/js/socket.js | 5 + src/js/store/__tests__/activeStore-test.js | 35 ++++ src/js/store/__tests__/streamStore-test.js | 57 ++++++ src/js/store/activeStore.js | 40 ++++ src/js/store/streamStore.js | 44 +++++ src/less/fonts.less | 70 +++++++ src/less/fonts/icons.eot | Bin 0 -> 6740 bytes src/less/fonts/icons.svg | 27 +++ src/less/fonts/icons.ttf | Bin 0 -> 6584 bytes src/less/fonts/icons.woff | Bin 0 -> 4076 bytes src/less/main.less | 72 +++++++ src/res/icon.png | Bin 0 -> 7959 bytes src/server/__tests__/socket-test.js | 114 +++++++++++ src/server/socket.js | 36 ++++ src/views/index.jade | 16 ++ 33 files changed, 1381 insertions(+) create mode 100644 .babelrc create mode 100644 .eslintrc create mode 100644 .gitignore create mode 100644 .travis.yml create mode 100644 Makefile create mode 100644 README.md create mode 100644 package.json create mode 100644 src/index.js create mode 100644 src/js/browser/createObjectURL.js create mode 100644 src/js/browser/getUserMedia.js create mode 100644 src/js/browser/navigator.js create mode 100644 src/js/components/__tests__/app-test.js create mode 100644 src/js/components/app.js create mode 100644 src/js/dispatcher/dispatcher.js create mode 100644 src/js/index.js create mode 100644 src/js/peer/Peer.js create mode 100644 src/js/peer/__tests__/handshake-test.js create mode 100644 src/js/peer/handshake.js create mode 100644 src/js/socket.js create mode 100644 src/js/store/__tests__/activeStore-test.js create mode 100644 src/js/store/__tests__/streamStore-test.js create mode 100644 src/js/store/activeStore.js create mode 100644 src/js/store/streamStore.js create mode 100644 src/less/fonts.less create mode 100644 src/less/fonts/icons.eot create mode 100644 src/less/fonts/icons.svg create mode 100644 src/less/fonts/icons.ttf create mode 100644 src/less/fonts/icons.woff create mode 100644 src/less/main.less create mode 100644 src/res/icon.png create mode 100644 src/server/__tests__/socket-test.js create mode 100644 src/server/socket.js create mode 100644 src/views/index.jade diff --git a/.babelrc b/.babelrc new file mode 100644 index 0000000..86c445f --- /dev/null +++ b/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": ["es2015", "react"] +} diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..f569a0c --- /dev/null +++ b/.eslintrc @@ -0,0 +1,212 @@ +{ + "parserOptions": { + "ecmaFeatures": { + "globalReturn": true, + "modules": true, + "jsx": true + } + }, + + "plugins": [ + "react" + ], + + "esnext": true, + + "env": { + "browser": false, + "es6": true, + "amd": true, + "mocha": true, + "node": true + }, + + "globals": { + "document": false, + "escape": false, + "navigator": false, + "unescape": false, + "expect": true, + "localStorage": false, + "window": false, + "jest": true + }, + + "rules": { + "block-scoped-var": 0, + "brace-style": [2, "1tbs", { "allowSingleLine": true }], + "max-len": [2, 80, 4], + "camelcase": 0, + "comma-dangle": 0, + "comma-spacing": [2, { "before": false, "after": true }], + "comma-style": [2, "last"], + "complexity": 0, + "consistent-return": 0, + "consistent-this": 0, + "curly": 0, + "default-case": 0, + "dot-notation": 0, + "eol-last": 2, + "eqeqeq": [2, "allow-null"], + "func-names": 0, + "func-style": [0, "declaration"], + "generator-star-spacing": [2], + "guard-for-in": 0, + "handle-callback-err": [2, "^(err|error|anySpecificError)$" ], + "key-spacing": [2, { "beforeColon": false, "afterColon": true }], + "max-depth": 0, + "max-nested-callbacks": 0, + "max-params": 0, + "max-statements": 0, + "new-cap": [2, { "newIsCap": true, "capIsNew": false }], + "new-parens": 2, + "no-alert": 2, + "no-array-constructor": 2, + "no-bitwise": 0, + "no-caller": 2, + "no-catch-shadow": 0, + "no-cond-assign": 2, + "no-console": 0, + "no-constant-condition": 0, + "no-control-regex": 2, + "no-debugger": 2, + "no-delete-var": 2, + "no-div-regex": 0, + "no-dupe-args": 2, + "no-dupe-keys": 2, + "no-duplicate-case": 2, + "no-else-return": 0, + "no-empty": 0, + "no-empty-character-class": 2, + "no-labels": 2, + "no-eq-null": 0, + "no-eval": 2, + "no-ex-assign": 2, + "no-extend-native": 2, + "no-extra-bind": 2, + "no-extra-boolean-cast": 2, + "no-extra-parens": 0, + "no-extra-semi": 0, + "no-extra-strict": 0, + "no-fallthrough": 2, + "no-floating-decimal": 2, + "no-func-assign": 2, + "no-implied-eval": 2, + "no-inline-comments": 0, + "no-inner-declarations": [2, "functions"], + "no-invalid-regexp": 2, + "no-irregular-whitespace": 2, + "no-iterator": 2, + "no-label-var": 2, + "no-lone-blocks": 2, + "no-lonely-if": 0, + "no-loop-func": 0, + "no-mixed-requires": [0, false], + "no-mixed-spaces-and-tabs": [2, false], + "no-multi-spaces": 2, + "no-multi-str": 2, + "no-multiple-empty-lines": 0, + "no-native-reassign": 2, + "no-negated-in-lhs": 2, + "no-nested-ternary": 0, + "no-new": 0, + "no-new-func": 2, + "no-new-object": 2, + "no-new-require": 2, + "no-new-wrappers": 2, + "no-obj-calls": 2, + "no-octal": 2, + "no-octal-escape": 2, + "no-path-concat": 0, + "no-plusplus": 0, + "no-process-env": 0, + "no-process-exit": 0, + "no-proto": 2, + "no-redeclare": 2, + "no-regex-spaces": 2, + "no-reserved-keys": 0, + "no-restricted-modules": 0, + "no-return-assign": 2, + "no-script-url": 2, + "no-self-compare": 2, + "no-sequences": 2, + "no-shadow": 0, + "no-shadow-restricted-names": 2, + "no-spaced-func": 2, + "no-sparse-arrays": 2, + "no-sync": 0, + "no-ternary": 0, + "no-throw-literal": 2, + "no-trailing-spaces": 2, + "no-undef": 2, + "no-undef-init": 2, + "no-undefined": 0, + "no-underscore-dangle": 0, + "no-unreachable": 2, + "no-unused-expressions": 0, + "no-unused-vars": [2, { "vars": "all", "args": "none" }], + "no-use-before-define": 0, + "no-var": 0, + "no-void": 0, + "no-warning-comments": [0, { "terms": ["todo", "fixme", "xxx"], "location": "start" }], + "no-with": 2, + "one-var": 0, + "operator-assignment": [0, "always"], + "padded-blocks": 0, + "quote-props": 0, + "quotes": [2, "single", "avoid-escape"], + "radix": 2, + "semi": [2, "always"], + "semi-spacing": 0, + "sort-vars": 0, + "keyword-spacing": ["error", {"before": true, "after": true, "overrides": {}}], + "space-before-blocks": [2, "always"], + "space-before-function-paren": [2, "never"], + "space-in-brackets": 0, + "space-in-parens": [2, "never"], + "space-infix-ops": 2, + "space-unary-ops": [2, { "words": true, "nonwords": false }], + "spaced-comment": [2, "always"], + "strict": 0, + "use-isnan": 2, + "valid-jsdoc": 0, + "valid-typeof": 2, + "vars-on-top": 0, + "wrap-iife": [2, "any"], + "wrap-regex": 0, + "yoda": [2, "never"], + + // "react/display-name": 1, + "react/forbid-prop-types": 1, + "react/jsx-boolean-value": 1, + "react/jsx-closing-bracket-location": 1, + "react/jsx-curly-spacing": 1, + "react/jsx-handler-names": 1, + "react/jsx-indent-props": [1, 2], + "react/jsx-key": 1, + "react/jsx-max-props-per-line": 0, + "react/jsx-no-bind": 1, + "react/jsx-no-duplicate-props": 1, + "react/jsx-no-literals": 1, + "react/jsx-no-undef": 1, + "react/jsx-pascal-case": 0, + "jsx-quotes": 1, + "react/sort-prop-types": 1, + "react/jsx-sort-props": 1, + "react/jsx-uses-react": 1, + "react/jsx-uses-vars": 1, + "react/no-danger": 1, + "react/no-did-mount-set-state": 1, + "react/no-did-update-set-state": 1, + "react/no-direct-mutation-state": 1, + "react/no-set-state": 1, + "react/no-unknown-property": 1, + "react/prefer-es6-class": 0, + // "react/prop-types": 1, + "react/react-in-jsx-scope": 1, + "react/require-extension": 1, + "react/self-closing-comp": 1, + "react/sort-comp": 1, + "react/wrap-multilines": 1 + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..796cf43 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +*.swp +*.swo +node_modules/ +config.js +coverage/ diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..0cfb6ee --- /dev/null +++ b/.travis.yml @@ -0,0 +1,19 @@ +language: node_js +node_js: + - "5.1.0" +script: make +env: + - CXX=g++-4.8 +notifications: + email: false +addons: + apt: + sources: + - ubuntu-toolchain-r-test + packages: + - libx11-dev + - zlib1g-dev + - libpng12-dev + - libxtst-dev + - g++-4.8 + - gcc-4.8 diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..77afa57 --- /dev/null +++ b/Makefile @@ -0,0 +1,36 @@ +export PATH := node_modules/.bin:$(PATH) +SHELL=/bin/bash + +.PHONY: build +build: clean + + eslint src/ + + jest --coverage + + mkdir -p dist/js dist/less + + browserify -t babelify ./src/js/index.js -o ./dist/js/index.js + + lessc ./src/less/main.less ./dist/less/main.css + + cp ./src/index.js ./dist/index.js + cp -r ./src/server ./dist/server + cp -r ./src/less/fonts ./dist/less/fonts + cp -r ./src/views ./dist/views + cp -r ./src/res ./dist/res + +.PHONY: test +test: + + jest + +.PHONY: run +run: + + node ./src/index.js + +.PHONY: clean +clean: + + rm -rf dist/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..ea4fbe7 --- /dev/null +++ b/README.md @@ -0,0 +1,48 @@ +# Remote Control Server + +[![Build Status](https://travis-ci.org/jeremija/remote-control-server.svg?branch=master)](https://travis-ci.org/jeremija/remote-control-server) + +Remote control your PC from your web browser on your other PC or mobile device. + +Supports mouse movements, scrolling, clicking and keyboard input. + +Work in progress. + + + + + +# Install & Run + +Install from npm: + +```bash +npm install -g remote-control-server +remote-control-server +``` + +or use from git source: + +```bash +git clone https://github.com/jeremija/remote-control-server.git +cd node-mobile-remote +npm install +npm start +``` + +On your other machine or mobile device open the url: + +```bash +http://192.168.0.10:3000 +``` + +Replace `192.168.0.10` with the LAN IP address of your server. + +# Note + +This package requires [robotjs](https://www.npmjs.com/package/robotjs) so make +sure you have the required prerequisites installed for compiling that package. + +# license + +MIT diff --git a/package.json b/package.json new file mode 100644 index 0000000..70edf7f --- /dev/null +++ b/package.json @@ -0,0 +1,54 @@ +{ + "name": "remote-control-server", + "version": "1.3.2", + "description": "Remote control your PC from your web browser on your other PC or mobile device", + "repository": "https://github.com/jeremija/remote-control-server", + "main": "src/index.js", + "bin": { + "remote-control-server": "./dist/index.js" + }, + "scripts": { + "start": "node src/index.js", + "test": "jest --coverage", + "testify": "jest --watch", + "lint": "eslint ./index.js ./src/js" + }, + "author": "", + "license": "MIT", + "dependencies": { + "babel-jest": "^10.0.1", + "babel-preset-es2015": "^6.6.0", + "babel-preset-react": "^6.5.0", + "babelify": "^7.2.0", + "bluebird": "^3.3.4", + "express": "^4.13.3", + "flux": "^2.1.1", + "jade": "^1.11.0", + "react": "^0.14.8", + "react-dom": "^0.14.8", + "simple-peer": "^6.0.3", + "socket.io": "^1.3.7", + "socket.io-client": "^1.3.7", + "underscore": "^1.8.3" + }, + "devDependencies": { + "browserify-middleware": "^7.0.0", + "eslint": "^2.5.3", + "eslint-plugin-react": "^4.2.3", + "jest-cli": "^0.10.0", + "less-middleware": "^2.0.1", + "react-addons-test-utils": "^0.14.8" + }, + "jest": { + "scriptPreprocessor": "/node_modules/babel-jest", + "unmockedModulePathPatterns": [ + "/node_modules/react", + "/node_modules/react-dom", + "/node_modules/react-addons-test-utils", + "/node_modules/fbjs" + ], + "modulePathIgnorePatterns": [ + "/node_modules/" + ] + } +} diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..972a2d5 --- /dev/null +++ b/src/index.js @@ -0,0 +1,49 @@ +#!/usr/bin/env node +'use strict'; +if (!process.env.DEBUG) { + process.env.DEBUG = 'video-server:*'; +} + +const express = require('express'); +const app = express(); +const http = require('http').Server(app); +const io = require('socket.io')(http); +const path = require('path'); +const os = require('os'); + +const handleSocket = require('./server/socket.js'); + +app.set('view engine', 'jade'); +app.set('views', path.join(__dirname, 'views')); + +app.use('/res', express.static(path.join(__dirname, 'res'))); + +if (path.basename(__dirname) === 'dist') { + app.use('/js', express.static(path.join(__dirname, 'js'))); + app.use('/less', express.static(path.join(__dirname, 'less'))); +} else { + const browserify = require('browserify-middleware'); + const less = require('less-middleware'); + browserify.settings({ + transform: ['babelify'] + }); + + const tempDir = path.join(os.tmpDir(), 'node-mpv-css-cache'); + app.use('/js', browserify(path.join(__dirname, './js'))); + app.use('/less', less(path.join(__dirname, './less'), { dest: tempDir})); + app.use('/less', express.static(tempDir)); + app.use('/less/fonts', express.static( + path.join(__dirname, './less/fonts'))); +} + +app.get('/', (req, res) => res.render('index')); + +io.on('connection', socket => handleSocket(socket, io)); + +let port = process.env.PORT || 3000; +let ifaces = os.networkInterfaces(); +http.listen(port, function() { + Object.keys(ifaces).forEach(ifname => + ifaces[ifname].forEach(iface => + console.log('listening on', iface.address, 'and port', port))); +}); diff --git a/src/js/browser/createObjectURL.js b/src/js/browser/createObjectURL.js new file mode 100644 index 0000000..7c5380f --- /dev/null +++ b/src/js/browser/createObjectURL.js @@ -0,0 +1,2 @@ +'use strict'; +module.exports = object => window.URL.createObjectURL(object); diff --git a/src/js/browser/getUserMedia.js b/src/js/browser/getUserMedia.js new file mode 100644 index 0000000..abbe853 --- /dev/null +++ b/src/js/browser/getUserMedia.js @@ -0,0 +1,16 @@ +'use strict'; +const navigator = require('../browser/navigator.js'); +const Promise = require('bluebird'); + +function getUserMedia(constraints) { + if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) { + return navigator.mediaDevices.getUserMedia(constraints); + } + + return new Promise((resolve, reject) => { + const getMedia = navigator.getUserMedia || navigator.webkitGetUserMedia; + getMedia.call(navigator, constraints, resolve, reject); + }); +} + +module.exports = getUserMedia; diff --git a/src/js/browser/navigator.js b/src/js/browser/navigator.js new file mode 100644 index 0000000..685ac48 --- /dev/null +++ b/src/js/browser/navigator.js @@ -0,0 +1,2 @@ +'use strict'; +module.exports = window.navigator; diff --git a/src/js/components/__tests__/app-test.js b/src/js/components/__tests__/app-test.js new file mode 100644 index 0000000..b059486 --- /dev/null +++ b/src/js/components/__tests__/app-test.js @@ -0,0 +1,60 @@ +jest.dontMock('../app.js'); +jest.dontMock('underscore'); + +const React = require('react'); +const ReactDOM = require('react-dom'); +const TestUtils = require('react-addons-test-utils'); + +const App = require('../app.js'); +const activeStore = require('../../store/activeStore.js'); +const dispatcher = require('../../dispatcher/dispatcher.js'); +const streamStore = require('../../store/streamStore.js'); + +describe('app', () => { + + beforeEach(() => { + dispatcher.dispatch.mockClear(); + }); + + function render(active) { + streamStore.getStreams.mockReturnValue({ + user1: { stream: 1 }, + user2: { stream: 2 } + }); + let component = TestUtils.renderIntoDocument(
); + return ReactDOM.findDOMNode(component).children[0]; + } + + it('should render div.app', () => { + let node = render(); + expect(node.tagName).toBe('DIV'); + expect(node.className).toBe('app'); + }); + + it('should have rendered two videos', () => { + let node = render(); + + expect(node.querySelectorAll('video').length).toBe(2); + }); + + it('should mark .active video', () => { + activeStore.getActive.mockReturnValue('user1'); + activeStore.isActive.mockImplementation(test => test === 'user1'); + + let node = render(); + expect(node.querySelectorAll('.video-container').length).toBe(2); + expect(node.querySelectorAll('.video-container.active').length).toBe(1); + }); + + it('should dispatch mark-active on video click', () => { + let node = render(); + + TestUtils.Simulate.click(node.querySelectorAll('video')[1]); + + expect(dispatcher.dispatch.mock.calls).toEqual([[{ + type: 'mark-active', + userId: 'user2' + }]]); + }); + +}); diff --git a/src/js/components/app.js b/src/js/components/app.js new file mode 100644 index 0000000..19d3887 --- /dev/null +++ b/src/js/components/app.js @@ -0,0 +1,42 @@ +const React = require('react'); +const _ = require('underscore'); +const activeStore = require('../store/activeStore.js'); +const createObjectURL = require('../browser/createObjectURL'); +const dispatcher = require('../dispatcher/dispatcher.js'); +const streamStore = require('../store/streamStore.js'); + +function app() { + let streams = streamStore.getStreams(); + + function play(event) { + event.target.play(); + } + + let videos = _.map(streams, (stream, userId) => { + let url = createObjectURL(stream); + + function markActive() { + dispatcher.dispatch({ + type: 'mark-active', + userId + }); + } + + let className = 'video-container'; + className += activeStore.isActive(userId) ? ' active' : ''; + + return ( +
+
+ ); + }); + + return (
+
+ {videos} +
+
); +} + +module.exports = app; diff --git a/src/js/dispatcher/dispatcher.js b/src/js/dispatcher/dispatcher.js new file mode 100644 index 0000000..4a8678f --- /dev/null +++ b/src/js/dispatcher/dispatcher.js @@ -0,0 +1,5 @@ +const Dispatcher = require('flux').Dispatcher; + +const dispatcher = new Dispatcher(); + +module.exports = dispatcher; diff --git a/src/js/index.js b/src/js/index.js new file mode 100644 index 0000000..b2ab49d --- /dev/null +++ b/src/js/index.js @@ -0,0 +1,39 @@ +if (window.localStorage && !window.localStorage.debug) { + window.localStorage.debug = 'video-client:*'; +} + +const React = require('react'); +const ReactDom = require('react-dom'); + +const App = require('./components/app.js'); +const handshake = require('./peer/handshake.js'); +const debug = require('debug')('video-client:index'); +const getUserMedia = require('./browser/getUserMedia.js'); +const socket = require('./socket.js'); +const activeStore = require('./store/activeStore.js'); +const streamStore = require('./store/streamStore.js'); + +function render() { + ReactDom.render(, document.querySelector('#container')); +} + +streamStore.addListener(() => () => { + debug('streamStore - change'); + debug(streamStore.getStreams()); +}); +streamStore.addListener(render); +activeStore.addListener(render); + +render(); + +socket.once('connect', () => { + debug('socket connected'); + getUserMedia({ video: true, audio: true }) + .then(stream => { + debug('forwarding stream to handshake'); + handshake.init(socket, 'test', stream); + }) + .catch(err => { + debug('error getting media: %s %s', err.name, err.message); + }); +}); diff --git a/src/js/peer/Peer.js b/src/js/peer/Peer.js new file mode 100644 index 0000000..1ff4034 --- /dev/null +++ b/src/js/peer/Peer.js @@ -0,0 +1,8 @@ +'use strict'; +const Peer = require('simple-peer'); + +function init(opts) { + return Peer(opts); +} + +module.exports = { init }; diff --git a/src/js/peer/__tests__/handshake-test.js b/src/js/peer/__tests__/handshake-test.js new file mode 100644 index 0000000..596be02 --- /dev/null +++ b/src/js/peer/__tests__/handshake-test.js @@ -0,0 +1,174 @@ +jest.dontMock('../handshake.js'); +jest.dontMock('events'); +jest.dontMock('debug'); +jest.dontMock('underscore'); + +const dispatcher = require('../../dispatcher/dispatcher.js'); +const handshake = require('../handshake.js'); +const Peer = require('../Peer.js'); +const EventEmitter = require('events').EventEmitter; + +describe('handshake', () => { + + let socket, peers; + beforeEach(() => { + socket = new EventEmitter(); + socket.id = 'a'; + peers = []; + + Peer.init = jest.genMockFunction().mockImplementation(() => { + let peer = new EventEmitter(); + peer.destroy = jest.genMockFunction(); + peer.signal = jest.genMockFunction(); + peers.push(peer); + return peer; + }); + + dispatcher.dispatch.mockClear(); + }); + + describe('socket events', () => { + + describe('users', () => { + + it('add a peer for each new user and destroy peers for missing', () => { + handshake.init(socket, 'bla'); + + // given + let payload = { + users: [{ id: 'a'}, { id: 'b' }], + initiator: '/#a', + }; + socket.emit('users', payload); + expect(peers.length).toBe(2); + + // when + payload = { + users: [{ id: 'a'}, { id: 'c' }], + initiator: '/#c', + }; + socket.emit('users', payload); + + // then + expect(peers.length).toBe(3); + expect(peers[0].destroy.mock.calls.length).toBe(0); + expect(peers[1].destroy.mock.calls.length).toBe(1); + expect(peers[2].destroy.mock.calls.length).toBe(0); + }); + + }); + + describe('signal', () => { + let data; + beforeEach(() => { + data = {}; + handshake.init(socket, 'bla'); + socket.emit('users', { + initiator: '#/a', + users: [{ id: 'a' }] + }); + }); + + it('should forward signal to peer', () => { + socket.emit('signal', { + userId: 'a', + data + }); + + expect(peers.length).toBe(1); + expect(peers[0].signal.mock.calls.length).toBe(1); + }); + + it('does nothing if no peer', () => { + socket.emit('signal', { + userId: 'b', + data + }); + + expect(peers.length).toBe(1); + expect(peers[0].signal.mock.calls.length).toBe(0); + }); + + }); + + }); + + describe('peer events', () => { + + let peer; + beforeEach(() => { + let ready = false; + socket.once('ready', () => { ready = true; }); + + handshake.init(socket, 'bla'); + + socket.emit('users', { + users: [{ id: 'a' }], + initiator: '/#a' + }); + expect(peers.length).toBe(1); + peer = peers[0]; + + expect(ready).toBeDefined(); + }); + + describe('error', () => { + + it('destroys peer', () => { + peer.emit('error', new Error('bla')); + expect(peer.destroy.mock.calls.length).toBe(1); + }); + + }); + + describe('signal', () => { + + it('emits socket signal with user id', done => { + let signal = { bla: 'bla' }; + + socket.once('signal', payload => { + expect(payload.userId).toEqual('a'); + expect(payload.signal).toBe(signal); + done(); + }); + + peer.emit('signal', signal); + }); + + }); + + describe('stream', () => { + + it('adds a stream to streamStore', () => { + expect(dispatcher.dispatch.mock.calls.length).toBe(0); + + let stream = {}; + peer.emit('stream', stream); + + expect(dispatcher.dispatch.mock.calls.length).toBe(1); + expect(dispatcher.dispatch.mock.calls).toEqual([[{ + type: 'add-stream', + userId: 'a', + stream + }]]); + }); + + }); + + describe('close', () => { + + it('removes stream from streamStore', () => { + peer.emit('close'); + + expect(dispatcher.dispatch.mock.calls.length).toBe(1); + expect(dispatcher.dispatch.mock.calls).toEqual([[{ + type: 'remove-stream', + userId: 'a' + }]]); + }); + + }); + + }); + +}); diff --git a/src/js/peer/handshake.js b/src/js/peer/handshake.js new file mode 100644 index 0000000..a4cb18a --- /dev/null +++ b/src/js/peer/handshake.js @@ -0,0 +1,91 @@ +'use strict'; +const Peer = require('./Peer.js'); +const debug = require('debug')('video-client:peer'); +const dispatcher = require('../dispatcher/dispatcher.js'); +const _ = require('underscore'); + +function init(socket, roomName, stream) { + let peers = {}; + + function createPeer(user, initiator) { + debug('create peer: %s', user.id); + + let peer = peers[user.id] = Peer.init({ + initiator: '/#' + socket.id === initiator, + stream + }); + + peer.once('error', err => { + debug('peer: %s, error %s', user.id, err.stack); + destroyPeer(user.id); + }); + + peer.on('signal', signal => { + debug('peer: %s, signal: %o', user.id, signal); + + let payload = { userId: user.id, signal }; + socket.emit('signal', payload); + }); + + peer.once('connect', () => { + debug('peer: %s, connect', user.id); + }); + + peer.on('stream', stream => { + debug('peer: %s, stream', user.id); + dispatcher.dispatch({ + type: 'add-stream', + userId: user.id, + stream + }); + }); + + peer.once('close', () => { + debug('peer: %s, close', user.id); + dispatcher.dispatch({ + type: 'remove-stream', + userId: user.id + }); + delete peers[user.id]; + }); + } + + function destroyPeer(userId) { + debug('destroy peer: %s', userId); + let peer = peers[userId]; + if (!peer) return debug('peer: %s peer not found', userId); + peer.destroy(); + delete peers[userId]; + } + + socket.on('signal', payload => { + let peer = peers[payload.userId]; + let signal = payload.signal; + // debug('socket signal, userId: %s, signal: %o', payload.userId, signal); + + if (!peer) return debug('user: %s, no peer found', payload.userId); + peer.signal(signal); + }); + + + socket.on('users', payload => { + let { initiator, users } = payload; + debug('socket users: %o', users); + + users + .filter(user => !peers[user.id] && user.id !== '/#' + socket.id) + .forEach(user => createPeer(user, initiator)); + + let newUsersMap = _.indexBy(users, 'id'); + _.chain(peers) + .map((peer, id) => id) + .filter(id => !newUsersMap[id]) + .each(destroyPeer); + }); + + debug('socket.id: %s', socket.id); + debug('emit ready for room: %s', roomName); + socket.emit('ready', roomName); +} + +module.exports = { init }; diff --git a/src/js/socket.js b/src/js/socket.js new file mode 100644 index 0000000..2c0ac54 --- /dev/null +++ b/src/js/socket.js @@ -0,0 +1,5 @@ +let SocketIOClient = require('socket.io-client'); + +let socket = new SocketIOClient(); + +module.exports = socket; diff --git a/src/js/store/__tests__/activeStore-test.js b/src/js/store/__tests__/activeStore-test.js new file mode 100644 index 0000000..40252e5 --- /dev/null +++ b/src/js/store/__tests__/activeStore-test.js @@ -0,0 +1,35 @@ +jest.dontMock('../activeStore.js'); +jest.dontMock('debug'); + +const dispatcher = require('../../dispatcher/dispatcher.js'); +const activeStore = require('../activeStore.js'); + +describe('activeStore', () => { + + let handleAction = dispatcher.register.mock.calls[0][0]; + let onChange = jest.genMockFunction(); + + beforeEach(() => { + onChange.mockClear(); + activeStore.addListener(onChange); + }); + afterEach(() => activeStore.removeListener(onChange)); + + describe('mark-active', () => { + + it('should mark id as active', () => { + expect(activeStore.getActive()).not.toBeDefined(); + expect(activeStore.isActive('user1')).toBe(false); + expect(onChange.mock.calls.length).toBe(0); + + handleAction({ type: 'mark-active', userId: 'user1' }); + + expect(activeStore.getActive()).toBe('user1'); + expect(activeStore.isActive('user1')).toBe(true); + + expect(onChange.mock.calls.length).toBe(1); + }); + + }); + +}); diff --git a/src/js/store/__tests__/streamStore-test.js b/src/js/store/__tests__/streamStore-test.js new file mode 100644 index 0000000..75cafc1 --- /dev/null +++ b/src/js/store/__tests__/streamStore-test.js @@ -0,0 +1,57 @@ +jest.dontMock('../streamStore.js'); +jest.dontMock('debug'); + +const dispatcher = require('../../dispatcher/dispatcher.js'); +const streamStore = require('../streamStore.js'); + +describe('streamStore', () => { + + let handleAction = dispatcher.register.mock.calls[0][0]; + let onChange = jest.genMockFunction(); + + beforeEach(() => { + onChange.mockClear(); + streamStore.addListener(onChange); + }); + afterEach(() => streamStore.removeListener(onChange)); + + describe('add-stream and remove-stream', () => { + + it('should add a stream', () => { + let stream = {}; + + handleAction({ type: 'add-stream', userId: 'user1', stream }); + + expect(streamStore.getStream('user1')).toBe(stream); + expect(onChange.mock.calls.length).toEqual(1); + }); + + it('should add a stream multiple times', () => { + let stream1 = {}; + let stream2 = {}; + + handleAction({ type: 'add-stream', userId: 'user1', stream: stream1 }); + handleAction({ type: 'add-stream', userId: 'user2', stream: stream2 }); + + expect(streamStore.getStream('user1')).toBe(stream1); + expect(streamStore.getStream('user2')).toBe(stream2); + expect(streamStore.getStreams()).toEqual({ + user1: stream1, + user2: stream2 + }); + expect(onChange.mock.calls.length).toEqual(2); + }); + + it('should remove a stream', () => { + let stream = {}; + + handleAction({ type: 'add-stream', userId: 'user1', stream }); + handleAction({ type: 'remove-stream', userId: 'user1' }); + + expect(streamStore.getStream('user1')).not.toBeDefined(); + expect(onChange.mock.calls.length).toEqual(2); + }); + + }); + +}); diff --git a/src/js/store/activeStore.js b/src/js/store/activeStore.js new file mode 100644 index 0000000..50f812d --- /dev/null +++ b/src/js/store/activeStore.js @@ -0,0 +1,40 @@ +'use strict'; +const EventEmitter = require('events'); +const debug = require('debug')('video-client:activeStore'); +const dispatcher = require('../dispatcher/dispatcher.js'); + +const emitter = new EventEmitter(); +const addListener = cb => emitter.on('change', cb); +const removeListener = cb => emitter.removeListener('change', cb); + +let active; + +const handlers = { + 'mark-active': ({ userId }) => { + debug('mark-active, userId: %s', userId); + active = userId; + } +}; + +const dispatcherIndex = dispatcher.register(action => { + let handle = handlers[action.type]; + if (!handle) return; + handle(action); + emitter.emit('change'); +}); + +function getActive() { + return active; +} + +function isActive(test) { + return active === test; +} + +module.exports = { + dispatcherIndex, + addListener, + removeListener, + getActive, + isActive +}; diff --git a/src/js/store/streamStore.js b/src/js/store/streamStore.js new file mode 100644 index 0000000..4951cdf --- /dev/null +++ b/src/js/store/streamStore.js @@ -0,0 +1,44 @@ +'use strict'; +const EventEmitter = require('events'); +const debug = require('debug')('video-client:streamStore'); +const dispatcher = require('../dispatcher/dispatcher.js'); + +const emitter = new EventEmitter(); +const addListener = cb => emitter.on('change', cb); +const removeListener = cb => emitter.removeListener('change', cb); + +const streams = {}; + +const handlers = { + 'add-stream': ({ userId, stream }) => { + debug('add-stream, user: %s', userId); + streams[userId] = stream; + }, + 'remove-stream': ({ userId }) => { + debug('remove-stream, user: %s', userId); + delete streams[userId]; + } +}; + +const dispatcherIndex = dispatcher.register(action => { + let handle = handlers[action.type]; + if (!handle) return; + handle(action); + emitter.emit('change'); +}); + +function getStream(userId) { + return streams[userId]; +} + +function getStreams() { + return streams; +} + +module.exports = { + dispatcherIndex, + addListener, + removeListener, + getStream, + getStreams +}; diff --git a/src/less/fonts.less b/src/less/fonts.less new file mode 100644 index 0000000..afa9c58 --- /dev/null +++ b/src/less/fonts.less @@ -0,0 +1,70 @@ +@font-face { + font-family: 'icons'; + src: url('./fonts/icons.eot?37351711'); + src: url('./fonts/icons.eot?37351711#iefix') format('embedded-opentype'), + url('./fonts/icons.woff?37351711') format('woff'), + url('./fonts/icons.ttf?37351711') format('truetype'), + url('./fonts/icons.svg?37351711#icons') format('svg'); + font-weight: normal; + font-style: normal; +} +// Chrome hack: SVG is rendered more smooth in Windozze. 100% magic, uncomment if you need it. +// Note, that will break hinting! In other OS-es font will be not as sharp as it could be +// @media screen and (-webkit-min-device-pixel-ratio:0) { +// @font-face { +// font-family: 'icons'; +// src: url('../font/icons.svg?37351711#icons') format('svg'); +// } +// } + +[class^="icon-"]:before, [class*=" icon-"]:before { + font-family: "icons"; + font-style: normal; + font-weight: normal; + speak: none; + + display: inline-block; + text-decoration: inherit; + width: 1em; + margin-right: .2em; + text-align: center; + /* opacity: .8; */ + + /* For safety - reset parent styles, that can break glyph codes*/ + font-variant: normal; + text-transform: none; + + /* fix buttons height, for twitter bootstrap */ + line-height: 1em; + + /* Animation center compensation - margins should be symmetric */ + /* remove if not needed */ + margin-left: .2em; + + /* you can be more comfortable with increased icons size */ + /* font-size: 120%; */ + + /* Font smoothing. That was taken from TWBS */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + + /* Uncomment for 3D effect */ + /* text-shadow: 1px 1px 1px rgba(127, 127, 127, 0.3); */ +} + +.icon-down-open-big:before { content: '\e800'; } /* '' */ +.icon-down-open:before { content: '\e801'; } /* '' */ +.icon-mouse:before { content: '\e802'; } /* '' */ +.icon-keyboard:before { content: '\e803'; } /* '' */ +.icon-left-open:before { content: '\e804'; } /* '' */ +.icon-right-open:before { content: '\e805'; } /* '' */ +.icon-up-open:before { content: '\e806'; } /* '' */ +.icon-arrows:before { content: '\e807'; } /* '' */ +.icon-up-hand:before { content: '\e808'; } /* '' */ +.icon-check:before { content: '\e80b'; } /* '' */ +.icon-cancel:before { content: '\e80c'; } /* '' */ +.icon-level-up:before { content: '\e80d'; } /* '' */ +.icon-login:before { content: '\e80e'; } /* '' */ +.icon-left-open-big:before { content: '\e81d'; } /* '' */ +.icon-right-open-big:before { content: '\e81e'; } /* '' */ +.icon-up-open-big:before { content: '\e81f'; } /* '' */ diff --git a/src/less/fonts/icons.eot b/src/less/fonts/icons.eot new file mode 100644 index 0000000000000000000000000000000000000000..cd06366ba6fe2ef670c2c2422c0de05f9e69db25 GIT binary patch literal 6740 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 aHztVSPMj{Y#_(;v^>tDAr#KzP+5ZATvHd>) literal 0 HcmV?d00001 diff --git a/src/less/fonts/icons.svg b/src/less/fonts/icons.svg new file mode 100644 index 0000000..e60e02b --- /dev/null +++ b/src/less/fonts/icons.svg @@ -0,0 +1,27 @@ + + + +Copyright (C) 2015 by original authors @ fontello.com + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/less/fonts/icons.ttf b/src/less/fonts/icons.ttf new file mode 100644 index 0000000000000000000000000000000000000000..f17209bb81f1464d5b5348efef20826568651cd8 GIT binary patch literal 6584 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 a1@Dwh=XjKFH1x944d`eXsDDyLh5s*IrUnlH literal 0 HcmV?d00001 diff --git a/src/server/__tests__/socket-test.js b/src/server/__tests__/socket-test.js new file mode 100644 index 0000000..efc13bf --- /dev/null +++ b/src/server/__tests__/socket-test.js @@ -0,0 +1,114 @@ +'use strict'; +jest.dontMock('../socket.js'); +jest.dontMock('events'); +jest.dontMock('debug'); +jest.dontMock('underscore'); + +const EventEmitter = require('events').EventEmitter; +const handleSocket = require('../socket.js'); + +describe('socket', () => { + + let socket, io, rooms; + beforeEach(() => { + socket = new EventEmitter(); + socket.id = '/#socket0'; + socket.join = jest.genMockFunction(); + socket.leave = jest.genMockFunction(); + rooms = {}; + + io = {}; + io.in = io.to = jest.genMockFunction().mockImplementation(room => { + return (rooms[room] = rooms[room] || { + emit: jest.genMockFunction() + }); + }); + + io.sockets = { + adapter: { + rooms: { + room1: { + '/#socket0': true + }, + room2: { + '/#socket0': true + }, + room3: { + sockets: { + '/#socket0': true, + '/#socket1': true, + '/#socket2': true + } + } + } + } + }; + + socket.leave = jest.genMockFunction(); + socket.join = jest.genMockFunction(); + }); + + it('should be a function', () => { + expect(typeof handleSocket).toBe('function'); + }); + + describe('socket events', () => { + + beforeEach(() => handleSocket(socket, io)); + + describe('signal', () => { + + it('should broadcast signal to specific user', () => { + let signal = { type: 'signal' }; + + socket.emit('signal', { userId: 'a', signal }); + + expect(io.to.mock.calls).toEqual([[ 'a' ]]); + expect(io.to('a').emit.mock.calls).toEqual([[ + 'signal', { + userId: '/#socket0', + signal + } + ]]); + }); + + }); + + describe('ready', () => { + + it('should call socket.leave if socket.room', () => { + socket.room = 'room1'; + socket.emit('ready', 'room2'); + + expect(socket.leave.mock.calls).toEqual([[ 'room1' ]]); + expect(socket.join.mock.calls).toEqual([[ 'room2' ]]); + }); + + it('should call socket.join to room', () => { + socket.emit('ready', 'room3'); + expect(socket.join.mock.calls).toEqual([[ 'room3' ]]); + }); + + it('should emit users', () => { + socket.emit('ready', 'room3'); + + expect(io.to.mock.calls).toEqual([[ 'room3' ]]); + expect(io.to('room3').emit.mock.calls).toEqual([[ + 'users', { + initiator: '/#socket0', + users: [{ + id: '/#socket0', + }, { + id: '/#socket1', + }, { + id: '/#socket2' + }] + } + ]]); + }); + + }); + + }); + +}); diff --git a/src/server/socket.js b/src/server/socket.js new file mode 100644 index 0000000..7ae706d --- /dev/null +++ b/src/server/socket.js @@ -0,0 +1,36 @@ +'use strict'; +const debug = require('debug')('video-server:socket'); +const _ = require('underscore'); + +module.exports = function(socket, io) { + + socket.on('signal', payload => { + debug('signal: %s, payload: %o', socket.id, payload); + io.to(payload.userId).emit('signal', { + userId: socket.id, + signal: payload.signal + }); + }); + + socket.on('ready', roomName => { + debug('ready: %s, room: %s', socket.id, roomName); + if (socket.room) socket.leave(socket.room); + socket.room = roomName; + socket.join(roomName); + socket.room = roomName; + + let users = getUsers(roomName); + debug('ready: %s, room: %s, users: %o', socket.id, roomName, users); + io.to(roomName).emit('users', { + initiator: socket.id, + users + }); + }); + + function getUsers(roomName) { + return _.map(io.sockets.adapter.rooms[roomName].sockets, (_, id) => { + return { id }; + }); + } + +}; diff --git a/src/views/index.jade b/src/views/index.jade new file mode 100644 index 0000000..20effc4 --- /dev/null +++ b/src/views/index.jade @@ -0,0 +1,16 @@ +doctype html +html + head + title Remote Control + meta(name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no") + meta(name="mobile-web-app-capable" content="yes") + meta(name="apple-mobile-web-app-capable" content="yes") + link(rel="apple-touch-icon" href="res/icon.png") + link(rel="icon" sizes="256x256" href="res/icon.png") + link(rel="stylesheet" type="text/css" href="less/main.css") + + body + + div#container + + script(src='js/index.js')