Initial commit, working full screen video
This commit is contained in:
commit
6b90668501
212
.eslintrc
Normal file
212
.eslintrc
Normal file
@ -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
|
||||
}
|
||||
}
|
||||
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
*.swp
|
||||
*.swo
|
||||
node_modules/
|
||||
config.js
|
||||
coverage/
|
||||
19
.travis.yml
Normal file
19
.travis.yml
Normal file
@ -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
|
||||
36
Makefile
Normal file
36
Makefile
Normal file
@ -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/
|
||||
48
README.md
Normal file
48
README.md
Normal file
@ -0,0 +1,48 @@
|
||||
# Remote Control Server
|
||||
|
||||
[](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.
|
||||
|
||||
<img src="http://i.imgur.com/38MzUIg.png" width="400px">
|
||||
<img src="http://i.imgur.com/cn1IUK8.png" width="400px">
|
||||
<img src="http://i.imgur.com/xtpgXoG.png" width="400px">
|
||||
|
||||
# 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
|
||||
54
package.json
Normal file
54
package.json
Normal file
@ -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": "<rootDir>/node_modules/babel-jest",
|
||||
"unmockedModulePathPatterns": [
|
||||
"<rootDir>/node_modules/react",
|
||||
"<rootDir>/node_modules/react-dom",
|
||||
"<rootDir>/node_modules/react-addons-test-utils",
|
||||
"<rootDir>/node_modules/fbjs"
|
||||
],
|
||||
"modulePathIgnorePatterns": [
|
||||
"<rootDir>/node_modules/"
|
||||
]
|
||||
}
|
||||
}
|
||||
49
src/index.js
Normal file
49
src/index.js
Normal file
@ -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)));
|
||||
});
|
||||
2
src/js/browser/createObjectURL.js
Normal file
2
src/js/browser/createObjectURL.js
Normal file
@ -0,0 +1,2 @@
|
||||
'use strict';
|
||||
module.exports = object => window.URL.createObjectURL(object);
|
||||
16
src/js/browser/getUserMedia.js
Normal file
16
src/js/browser/getUserMedia.js
Normal file
@ -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;
|
||||
2
src/js/browser/navigator.js
Normal file
2
src/js/browser/navigator.js
Normal file
@ -0,0 +1,2 @@
|
||||
'use strict';
|
||||
module.exports = window.navigator;
|
||||
60
src/js/components/__tests__/app-test.js
Normal file
60
src/js/components/__tests__/app-test.js
Normal file
@ -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(<div><App /></div>);
|
||||
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'
|
||||
}]]);
|
||||
});
|
||||
|
||||
});
|
||||
42
src/js/components/app.js
Normal file
42
src/js/components/app.js
Normal file
@ -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 (
|
||||
<div className={className} key={userId}>
|
||||
<video onClick={markActive} onLoadedMetadata={play} src={url} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
return (<div className="app">
|
||||
<div className="videos">
|
||||
{videos}
|
||||
</div>
|
||||
</div>);
|
||||
}
|
||||
|
||||
module.exports = app;
|
||||
5
src/js/dispatcher/dispatcher.js
Normal file
5
src/js/dispatcher/dispatcher.js
Normal file
@ -0,0 +1,5 @@
|
||||
const Dispatcher = require('flux').Dispatcher;
|
||||
|
||||
const dispatcher = new Dispatcher();
|
||||
|
||||
module.exports = dispatcher;
|
||||
39
src/js/index.js
Normal file
39
src/js/index.js
Normal file
@ -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(<App />, 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);
|
||||
});
|
||||
});
|
||||
8
src/js/peer/Peer.js
Normal file
8
src/js/peer/Peer.js
Normal file
@ -0,0 +1,8 @@
|
||||
'use strict';
|
||||
const Peer = require('simple-peer');
|
||||
|
||||
function init(opts) {
|
||||
return Peer(opts);
|
||||
}
|
||||
|
||||
module.exports = { init };
|
||||
174
src/js/peer/__tests__/handshake-test.js
Normal file
174
src/js/peer/__tests__/handshake-test.js
Normal file
@ -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'
|
||||
}]]);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
91
src/js/peer/handshake.js
Normal file
91
src/js/peer/handshake.js
Normal file
@ -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 };
|
||||
5
src/js/socket.js
Normal file
5
src/js/socket.js
Normal file
@ -0,0 +1,5 @@
|
||||
let SocketIOClient = require('socket.io-client');
|
||||
|
||||
let socket = new SocketIOClient();
|
||||
|
||||
module.exports = socket;
|
||||
35
src/js/store/__tests__/activeStore-test.js
Normal file
35
src/js/store/__tests__/activeStore-test.js
Normal file
@ -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);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
57
src/js/store/__tests__/streamStore-test.js
Normal file
57
src/js/store/__tests__/streamStore-test.js
Normal file
@ -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);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
40
src/js/store/activeStore.js
Normal file
40
src/js/store/activeStore.js
Normal file
@ -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
|
||||
};
|
||||
44
src/js/store/streamStore.js
Normal file
44
src/js/store/streamStore.js
Normal file
@ -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
|
||||
};
|
||||
70
src/less/fonts.less
Normal file
70
src/less/fonts.less
Normal file
@ -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'; } /* '' */
|
||||
BIN
src/less/fonts/icons.eot
Normal file
BIN
src/less/fonts/icons.eot
Normal file
Binary file not shown.
27
src/less/fonts/icons.svg
Normal file
27
src/less/fonts/icons.svg
Normal file
@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg xmlns="http://www.w3.org/2000/svg">
|
||||
<metadata>Copyright (C) 2015 by original authors @ fontello.com</metadata>
|
||||
<defs>
|
||||
<font id="icons" horiz-adv-x="1000" >
|
||||
<font-face font-family="icons" font-weight="400" font-stretch="normal" units-per-em="1000" ascent="850" descent="-150" />
|
||||
<missing-glyph horiz-adv-x="1000" />
|
||||
<glyph glyph-name="down-open-big" unicode="" d="m63 570l370-356 372 356q22 26 48 0 26-22 0-48l-396-392q-22-22-48 0l-396 392q-26 26 0 48 24 24 50 0z" horiz-adv-x="866" />
|
||||
<glyph glyph-name="down-open" unicode="" d="m564 422l-234-224q-18-18-40-18t-40 18l-234 224q-16 16-16 41t16 41q38 38 78 0l196-188 196 188q40 38 78 0 16-16 16-41t-16-41z" horiz-adv-x="580" />
|
||||
<glyph glyph-name="mouse" unicode="" d="m551 130q28-80-17-157t-139-111q-94-28-175 9t-103 117l-106 384q-20 68 6 134t84 106l-96 186q-14 34 14 48 30 18 48-14l98-192q80 22 154-16t102-116z m-324 274q28 10 40 36t4 54q-10 28-35 41t-53 5q-28-10-40-36t-4-54q10-28 35-41t53-5z" horiz-adv-x="561" />
|
||||
<glyph glyph-name="keyboard" unicode="" d="m930 650q28 0 49-21t21-49l0-460q0-30-21-50t-49-20l-860 0q-28 0-49 20t-21 50l0 460q0 28 21 49t49 21l860 0z m-380-100l0-100 100 0 0 100-100 0z m150-150l-100 0 0-100 100 0 0 100z m-300 150l0-100 100 0 0 100-100 0z m150-150l-100 0 0-100 100 0 0 100z m-300 150l0-100 100 0 0 100-100 0z m150-150l-100 0 0-100 100 0 0 100z m-300 150l0-100 100 0 0 100-100 0z m150-150l-100 0 0-100 100 0 0 100z m-50-250l0 100-100 0 0-100 100 0z m550 0l0 100-500 0 0-100 500 0z m150 0l0 100-100 0 0-100 100 0z m-150 150l100 0 0 100-100 0 0-100z m150 150l0 100-200 0 0-100 200 0z" horiz-adv-x="1000" />
|
||||
<glyph glyph-name="left-open" unicode="" d="m242 626q14 16 39 16t41-16q38-36 0-80l-186-196 186-194q38-44 0-80-16-16-40-16t-40 16l-226 236q-16 16-16 38 0 24 16 40 206 214 226 236z" horiz-adv-x="341" />
|
||||
<glyph glyph-name="right-open" unicode="" d="m98 626l226-236q16-16 16-40 0-22-16-38l-226-236q-16-16-40-16t-40 16q-36 36 0 80l186 194-186 196q-36 44 0 80 16 16 41 16t39-16z" horiz-adv-x="340" />
|
||||
<glyph glyph-name="up-open" unicode="" d="m564 280q16-16 16-41t-16-41q-38-38-78 0l-196 188-196-188q-40-38-78 0-16 16-16 41t16 41l234 224q16 16 40 16t40-16z" horiz-adv-x="580" />
|
||||
<glyph glyph-name="arrows" unicode="" d="m784 111l127 128 0-336-335 0 128 130-128 127 79 79z m-431 686l-129-127 128-127-80-80-126 128-128-129 0 335 335 0z m0-637l-129-127 129-130-335 0 0 336 128-128 128 128z m558 637l0-335-127 129-128-128-79 80 127 127-128 127 335 0z" horiz-adv-x="928" />
|
||||
<glyph glyph-name="up-hand" unicode="" d="m714-43q0 15-10 25t-25 11-26-11-10-25 10-25 26-11 25 11 10 25z m72 426q0 106-93 106-15 0-32-3-9 17-29 27t-41 9-39-10q-27 30-66 30-14 0-31-6t-26-14v185q0 29-22 50t-50 22q-28 0-50-22t-21-50v-321q-11 0-27 8t-31 19-38 18-47 8q-37 0-55-25t-17-64q0-13 78-50 25-14 36-21 36-22 81-62 45-40 59-57 32-38 32-78v-18h357v18q0 40 18 93t36 108 18 100z m71 3q0-74-38-179-33-92-33-125v-161q0-29-21-50t-51-21h-357q-29 0-50 21t-21 50v161q0 6-3 12t-8 13-10 13-12 13-12 12-12 10-10 8q-41 36-72 56-11 7-34 18t-40 21-36 23-27 30-10 39q0 70 37 115t106 46q38 0 71-13v209q0 58 43 101t100 42q58 0 101-42t42-101v-94q35-2 66-21 12 2 24 2 57 0 100-34 77 1 122-47t45-127z" horiz-adv-x="857.1" />
|
||||
<glyph glyph-name="check" unicode="" d="m249 0q-34 0-56 28l-180 236q-16 24-12 52t26 46 51 14 47-28l118-154 296 474q16 24 43 30t53-8q24-16 30-43t-8-53l-350-560q-20-32-56-32z" horiz-adv-x="667" />
|
||||
<glyph glyph-name="cancel" unicode="" d="m452 194q18-18 18-43t-18-43q-18-16-43-16t-43 16l-132 152-132-152q-18-16-43-16t-43 16q-16 18-16 43t16 43l138 156-138 158q-16 18-16 43t16 43q18 16 43 16t43-16l132-152 132 152q18 16 43 16t43-16q18-18 18-43t-18-43l-138-158z" horiz-adv-x="470" />
|
||||
<glyph glyph-name="level-up" unicode="" d="m200 350l0-90-200 160 200 170 0-100 550 0q40 0 70-29t30-71l0-280-140 0 0 240-510 0z" horiz-adv-x="850" />
|
||||
<glyph glyph-name="login" unicode="" d="m800 800q42 0 71-29t29-71l0-700q0-40-29-70t-71-30l-450 0q-40 0-69 30t-29 70l0 100 98 0 0-100 450 0 0 700-450 0 0-150-98 0 0 150q0 42 29 71t69 29l450 0z m-350-670l0 120-450 0 0 150 450 0 0 120 200-194z" horiz-adv-x="900" />
|
||||
<glyph glyph-name="left-open-big" unicode="" d="m452-20q26-26 0-48-26-26-48 0l-392 394q-24 24 0 50l392 394q22 26 48 0 26-22 0-48l-358-372z" horiz-adv-x="465" />
|
||||
<glyph glyph-name="right-open-big" unicode="" d="m13-20l358 370-358 372q-26 26 0 48 26 26 48 0l392-394q24-26 0-50l-392-394q-22-26-48 0-26 22 0 48z" horiz-adv-x="465" />
|
||||
<glyph glyph-name="up-open-big" unicode="" d="m804 130l-372 358-370-358q-26-22-50 0-24 24 0 50l396 390q26 26 48 0l396-390q24-26 0-50-26-22-48 0z" horiz-adv-x="864" />
|
||||
</font>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.7 KiB |
BIN
src/less/fonts/icons.ttf
Normal file
BIN
src/less/fonts/icons.ttf
Normal file
Binary file not shown.
BIN
src/less/fonts/icons.woff
Normal file
BIN
src/less/fonts/icons.woff
Normal file
Binary file not shown.
72
src/less/main.less
Normal file
72
src/less/main.less
Normal file
@ -0,0 +1,72 @@
|
||||
@import "fonts.less";
|
||||
|
||||
@color-bg: #086788;
|
||||
@color-fg: #07A0C3;
|
||||
// @color-btn: #F0C808;
|
||||
@color-btn: #FFF1D0;
|
||||
@color-active: #F0C808;
|
||||
@icon-size: 48px;
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html, body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: @color-bg;
|
||||
color: @color-fg;
|
||||
margin: 0 0;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
|
||||
#container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.app {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
|
||||
.videos {
|
||||
position: absolute;
|
||||
height: 100px;
|
||||
bottom: 15px;
|
||||
right: 0px;
|
||||
text-align: right;
|
||||
|
||||
.video-container {
|
||||
display: inline-block;
|
||||
margin-right: 10px;
|
||||
width: 150px;
|
||||
height: 100%;
|
||||
z-index: 2;
|
||||
|
||||
video {
|
||||
pointer: cursor;
|
||||
object-fit: cover;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.video-container.active {
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
z-index: 1;
|
||||
|
||||
video {
|
||||
pointer: inherit;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
BIN
src/res/icon.png
Normal file
BIN
src/res/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.8 KiB |
114
src/server/__tests__/socket-test.js
Normal file
114
src/server/__tests__/socket-test.js
Normal file
@ -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'
|
||||
}]
|
||||
}
|
||||
]]);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
36
src/server/socket.js
Normal file
36
src/server/socket.js
Normal file
@ -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 };
|
||||
});
|
||||
}
|
||||
|
||||
};
|
||||
16
src/views/index.jade
Normal file
16
src/views/index.jade
Normal file
@ -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')
|
||||
Loading…
x
Reference in New Issue
Block a user