diff --git a/.gitignore b/.gitignore index 23b8d3d..71b76e3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ -node_modules/ build/ +node_modules/ npm-debug.log +config.js diff --git a/README.md b/README.md index f26dbb7..c253717 100644 --- a/README.md +++ b/README.md @@ -1,47 +1,10 @@ -# Ricoh Video Streaming Sample +# Ricoh Video Streaming Samples -Video streaming API sample app. +### Two-way video streaming -## Requirements +* [Video Chat Sample](https://github.com/ricohapi/video-streaming-sample-app/tree/master/samples/towway). -* Google Chrome 49 or newer. -* Web Camera accessible from your browser. -* Enable Web Camera access in your browser setting. +### One-way video streaming -You'll also need - -* Ricoh API Client Credentials (client_id & client_secret) -* Ricoh ID (user_id & password) - -If you don't have them, please register them at [THETA Developers Website](http://contest.theta360.com/). - -## Setup - -```sh -$ git clone https://github.com/ricohapi/video-streaming-sample-app.git -$ cd video-streaming-sample-app -$ cp samples/config_template.js samples/config.js -``` - -and put your credentials into the `config.js`. - -## Build - -```sh -$ npm install -$ gulp build -``` - -## Video Streaming - -Connect the Web Camera and execute `gulp run`, then the browser will be opened. -Select the Web Camera, put your Ricoh ID & password, and submit the login button. -Let the peer user login on his own device following the instruction above, then put the peer's User ID to the Peer-ID field and submit Connect button, then streaming connection will start between you and the peer. - -```sh -$ gulp run -``` - -## THETA View - -If the peer user is using THETA, push THETA View button, then you'll see the draggable & zoomable 360° view. +* [Broadcast Sample](https://github.com/ricohapi/video-streaming-sample-app/tree/master/samples/oneway-broadcast). +* [Watch Sample](https://github.com/ricohapi/video-streaming-sample-app/tree/master/samples/oneway-watch). diff --git a/gulpfile.js b/gulpfile.js deleted file mode 100644 index 4a6c1f2..0000000 --- a/gulpfile.js +++ /dev/null @@ -1,20 +0,0 @@ -var gulp = require('gulp'), - webserver = require('gulp-webserver'), - webpack = require('webpack-stream'), - webpackConfig = require('./webpack.config.js'); - -gulp.task('run', function () { - gulp.src('') - .pipe(webserver({ - host: 'localhost', - port: 8034, - open: true, - fallback: 'samples/index.html' - })); -}); - -gulp.task('build', function() { - return gulp.src('') - .pipe(webpack(webpackConfig)) - .pipe(gulp.dest('')); -}); \ No newline at end of file diff --git a/samples/.gitignore b/samples/.gitignore deleted file mode 100644 index 1bf4259..0000000 --- a/samples/.gitignore +++ /dev/null @@ -1 +0,0 @@ -config.js diff --git a/LICENSE b/samples/LICENSE similarity index 100% rename from LICENSE rename to samples/LICENSE diff --git a/samples/UDCStrophe.js b/samples/UDCStrophe.js deleted file mode 100644 index e354e2e..0000000 --- a/samples/UDCStrophe.js +++ /dev/null @@ -1,64 +0,0 @@ -'use strict'; -/** - * Copyright (c) 2016 Ricoh Company, Ltd. All Rights Reserved. - * See LICENSE for more information - */ - -const AuthClient = require('ricohapi-auth').AuthClient; - -function _log(str) { - console.log(str); -} - -class UDCStrophe { - constructor(clientID, clientSecret) { - this._client = new AuthClient(clientID, clientSecret); - } - - _onConnect(self, status) { - if (status == Strophe.Status.CONNECTING) { - _log('Strophe is connecting.'); - } else if (status == Strophe.Status.CONNFAIL) { - _log('Strophe failed to connect.'); - self.connectReject(); - } else if (status == Strophe.Status.DISCONNECTING) { - _log('Strophe is disconnecting.'); - } else if (status == Strophe.Status.DISCONNECTED) { - _log('Strophe is disconnected.'); - } else if (status == Strophe.Status.CONNECTED) { - _log('Strophe is connected.'); - self.connection.send($pres()); - self.connectResolve(); - } else if (status == Strophe.Status.AUTHENTICATING) { - _log('Strophe is authenticating.'); - } else if (status == Strophe.Status.AUTHFAIL) { - _log('Strophe is authfail.'); - self.connectReject(); - } - } - - connect(userID, userPass) { - this._client.setResourceOwnerCreds(userID.split('+')[0], userPass); - this.id = userID; - - return new Promise((resolve, reject) => { - this._client.session(AuthClient.SCOPES.VStream) - .then(() => { - this.connection = new Strophe.Connection('https://sig.ricohapi.com/http-bind/'); - Strophe.SASLSHA1.test = () => false; - this.connectResolve = resolve; - this.connectReject = reject; - this.connection.connect(userID.replace(/@/g, '\\40') + '@sig.ricohapi.com@sig.ricohapi.com/peer', this._client.accessToken, this._onConnect.bind(this, this)); - }) - .catch(e => reject(e)); - }); - } - - disconnect() { - if (this.connection === undefined) return; - this.connection.disconnect('none'); - this.connection = undefined; - } - -}; -exports.UDCStrophe = UDCStrophe; diff --git a/samples/common/.eslintrc b/samples/common/.eslintrc new file mode 100644 index 0000000..2c397d8 --- /dev/null +++ b/samples/common/.eslintrc @@ -0,0 +1,7 @@ +{ + "extends": "airbnb", + "rules": { + "strict": 0, + "no-underscore-dangle": 0 + } +} diff --git a/samples/common/UDCConnection.js b/samples/common/UDCConnection.js new file mode 100644 index 0000000..59fedd0 --- /dev/null +++ b/samples/common/UDCConnection.js @@ -0,0 +1,146 @@ +/* eslint no-console: ["error", { allow: ["info"] }] */ +'use strict'; +/* + * Copyright (c) 2016 Ricoh Company, Ltd. All Rights Reserved. + * See LICENSE for more information + */ + +export class UDCConnection { + _isData(session) { + return !session.audio && !session.video && !session.screen && session.data; + } + + constructor(conn, connectCallback) { + const connection = conn; + + connection.socket = connection.bosh; // set here, can't set on caller + + connection.socket.onmessage = (uid, data) => { + console.info(`${data.eventName} received`); + if (data.eventName === connection.socketMessageEvent) { + connection.socket.onMessagesCallback(data.data); + } + }; + + connection.socket.onMessagesCallback = message => { + const myJid = connection.userid.replace(/@/g, '\\40'); + const toJid = message.remoteUserId.replace(/@/g, '\\40'); + if (myJid !== toJid) return; + + const senderPeer = connection.peers[message.sender]; + if (senderPeer && senderPeer.extra !== message.extra) { + senderPeer.extra = message.extra; + connection.onExtraDataUpdated({ userid: message.sender, extra: message.extra }); + } + + if (message.message.streamSyncNeeded && senderPeer) { + console.info('streamSyncNeeded NOT SUPPORTED'); + return; + } + + if (message.message === 'connectWithAllParticipants') { + console.info('connectWithAllParticipants NOT SUPPORTED'); + return; + } + + if (message.message === 'removeFromBroadcastersList') { + console.info('removeFromBroadcastersList NOT SUPPORTED'); + return; + } + + if (message.message === 'dropPeerConnection') { + console.info('dropPeerConnection NOT SUPPORTED'); + return; + } + + if (message.message.allParticipants) { + console.info('allParticipants NOT SUPPORTED'); + return; + } + + if (message.message.newParticipant) { + console.info('newParticipant NOT SUPPORTED'); + return; + } + + if (message.message.readyForOffer || message.message.addMeAsBroadcaster) { + connection.addNewBroadcaster(message.sender); + } + + if (message.message.newParticipationRequest && message.sender !== connection.userid) { + if (senderPeer) connection.deletePeer(message.sender); + + const offerAudio = connection.sdpConstraints.mandatory.OfferToReceiveAudio; + const offerVideo = connection.sdpConstraints.mandatory.OfferToReceiveVideo; + const ses = connection.session; + const msg = message.message; + const noRemoteSdp = { OfferToReceiveAudio: offerAudio, OfferToReceiveVideo: offerVideo }; + const oneLocalSdp = { OfferToReceiveAudio: !!ses.audio, OfferToReceiveVideo: !!ses.video }; + const noLocalSdp = ses.oneway ? oneLocalSdp : noRemoteSdp; + const rOneWay = !!ses.oneway || connection.direction === 'one-way'; + const noNeedRemoteStream = typeof msg.isOneWay !== 'undefined' ? msg.isOneWay : rOneWay; + + const userPref = { + extra: message.extra || {}, + localPeerSdpConstraints: msg.remotePeerSdpConstraints || noRemoteSdp, + remotePeerSdpConstraints: msg.localPeerSdpConstraints || noLocalSdp, + isOneWay: noNeedRemoteStream, + dontGetRemoteStream: noNeedRemoteStream, + isDataOnly: typeof msg.isDataOnly !== 'undefined' ? msg.isDataOnly : this._isData(ses), + dontAttachLocalStream: !!msg.dontGetRemoteStream, + connectionDescription: message, + successCallback: () => { + if (noNeedRemoteStream || rOneWay || this._isData(connection.session)) { + connection.addNewBroadcaster(message.sender, userPref); + } + }, + }; + connection.onNewParticipant(message.sender, userPref); + return; + } + + if (message.message.shiftedModerationControl) { + console.info('shiftedModerationControl NOT SUPPORTED'); + return; + } + + if (message.message.changedUUID) { + console.info('changedUUID NOT SUPPORTED'); + // no return + } + + if (message.message.userLeft) { + connection.multiPeersHandler.onUserLeft(message.sender); + if (!!message.message.autoCloseEntireSession) { + connection.leave(); + } + return; + } + + connection.multiPeersHandler.addNegotiatedMessage(message.message, message.sender); + }; + + + connection.socket.emit = (eventName, data, callback) => { + if (eventName === 'changed-uuid') return; + if (eventName === 'message') { + // data:uid, callback:data on boshclient + connection.socket.onmessage(data, JSON.parse(callback)); + return; + } + if (typeof data === 'undefined') return; + if (data.message && data.message.shiftedModerationControl) return; + + if (eventName === 'disconnect-with') { + if (connection.peers[data]) { + connection.peers[data].peer.close(); + } + return; + } + connection.socket.send(data.remoteUserId, JSON.stringify({ eventName, data })); + console.info(`${eventName} sended`); + if (callback) { callback(); } + }; + connectCallback(connection.socket); + } +} diff --git a/samples/common/app.js b/samples/common/app.js new file mode 100644 index 0000000..55b6d96 --- /dev/null +++ b/samples/common/app.js @@ -0,0 +1,179 @@ +/* global Clipboard,RTCMultiConnection,DetectRTC,m,CONFIG */ +/* eslint no-console: ["error", { allow: ["error"] }] */ +'use strict'; + +/** + * Copyright (c) 2016 Ricoh Company, Ltd. All Rights Reserved. + * See LICENSE for more information + */ + +const RicohAPI = require('./ricohapi-bosh'); +const UDCConnection = require('./UDCConnection').UDCConnection; + +const conn = new RTCMultiConnection(); +const aclient = new RicohAPI.AuthClient(CONFIG.clientId, CONFIG.clientSecret); +const bosh = new RicohAPI.BoshClient(aclient); + +if (typeof Clipboard !== 'undefined') { + const clipboard = new Clipboard('.cpbtn'); + clipboard.on('success', e => e.clearSelection()); +} + +let _instance = null; // for singleton + +// Model +export class App { + + constructor() { + if (_instance !== null) return _instance; + this._cameras = []; + this.isWatcher = false; + this.isFirefox = false; + this.isOneWay = (typeof window.__oneway !== 'undefined'); + + if (typeof m === 'undefined') return _instance; + this.username = m.prop(''); + this.userpass = m.prop(''); + this.peername = m.prop(''); + this.peerurl = m.prop(''); + this.peermoz = m.prop(''); + this.peerurlid = m.prop(''); + this.myurl = m.prop(''); + this.mymoz = m.prop(''); + this.myurlid = m.prop(''); + this.camid = m.prop(''); + this.state = m.prop('initial'); + + _instance = this; + return _instance; + } + + _connect(uname, upass) { + return new Promise((resolve, reject) => { + aclient.setResourceOwnerCreds(uname.split('+')[0], upass); + bosh.connect(uname, upass) + .then(() => { + conn.userid = uname; + conn.bosh = bosh; + conn.setCustomSocketHandler(UDCConnection); + conn.socketMessageEvent = 'ricohapi-streaming'; + conn.mediaConstraints.audio = false; + resolve(); + }) + .catch(e => { + console.error(e); + reject(); + }); + }); + } + + connect() { + this.state('connecting'); + return new Promise((resolve, reject) => { + this._connect(this.username(), this.userpass()) + .then(() => { + this.state('ready'); + resolve(); + }) + .catch(() => { + this.state('fail'); + reject(); + }); + }); + } + + disconnect() { + conn.close(); + setTimeout(() => { // delay for firefox + bosh.disconnect(); + this.myurl(''); + this.mymoz(''); + this.peerurl(''); + this.peermoz(''); + this.camid(''); + this.state('initial'); + }, 1000); + } + + _rmc3() { + this.state('calling'); + conn.session = { audio: false, video: true, oneway: this.isOneWay }; + conn.sdpConstraints.mandatory = { + OfferToReceiveAudio: false, + OfferToReceiveVideo: !this.isOneWay, + }; + conn.onstream = event => { + const elm = event.mediaElement; + elm.controls = false; + if (this.myurl() || this.mymoz() || this.isWatcher) { + if (conn.DetectRTC.browser.isFirefox) this.peermoz(elm.mozSrcObject); + else this.peerurl(elm.src); + this.peerurlid(elm.id); + this.peername(event.userid); + this.state('chatting'); + } else { + if (conn.DetectRTC.browser.isFirefox) this.mymoz(elm.mozSrcObject); + else this.myurl(elm.src); + this.myurlid(elm.id); + this.state('chatready'); + } + elm.play(); + setTimeout(() => elm.play(), 5000); + m.redraw(); + }; + } + + call() { + if (this.isOneWay) this.isWatcher = true; + this._rmc3(); + conn.join(this.peername()); + } + + open() { + conn.mediaConstraints.video = { deviceId: this.camid() }; + this._rmc3(); + conn.open(this.username()); + } + + list(cb) { + conn.DetectRTC.load(() => { + conn.DetectRTC.MediaDevices.forEach(device => { + if (device.kind.indexOf('video') === -1) return; + this.isFirefox = (device.label === 'Please invoke getUserMedia once.'); + const did = this.isFirefox ? '' : device.id; + const dlabel = this.isFirefox ? 'choose when needed' : device.label; + if (this._cameras.find(c => c.id === did)) return; + this._cameras.push({ id: did, label: dlabel }); + }); + cb(this._cameras); + }); + } + + // out of Mithril + headless(uname, upass, dom) { + this._connect(uname, upass) + .then(() => { + conn.videosContainer = dom; + conn.session = { audio: false, video: true, oneway: true }; + conn.sdpConstraints.mandatory = { + OfferToReceiveAudio: false, + OfferToReceiveVideo: false, + }; + conn.onstream = event => { + const elm = event.mediaElement; + conn.videosContainer.appendChild(elm); + elm.play(); + setTimeout(() => elm.play(), 3000); + }; + conn.open(uname); + }) + .catch(console.error); + } + + static getInstance() { + if (_instance === null) { + _instance = new App(); + } + return _instance; + } +} diff --git a/samples/common/components.js b/samples/common/components.js new file mode 100644 index 0000000..b4643af --- /dev/null +++ b/samples/common/components.js @@ -0,0 +1,299 @@ +/* global m */ +'use strict'; + +/** + * Copyright (c) 2016 Ricoh Company, Ltd. All Rights Reserved. + * See LICENSE for more information + */ + +const App = require('./app').App; +const ThetaView = require('./thetaview').ThetaView; + +const thetaview = new ThetaView(); + +const login = {}; +login.vm = (() => { + const vm = {}; + vm.init = (app, cb) => { + vm._app = app; + + vm.cameras = []; + vm.camstate = m.prop('loading'); + + vm.login = () => { + cb(false); + vm._app.connect() + .then(() => cb(true)) + .catch(() => cb(false)); + }; + + vm.updateSelect = evt => { + vm._app.camid(evt.target.value); + }; + + vm._app.list(cameras => { // todo on access error + vm.cameras = cameras; + if (vm._app.isOneWay) { + vm.camstate('oneway'); + } else { + vm.camstate(vm.cameras.length === 0 ? 'nocam' : 'ready'); + } + cb(false); + }); + }; + return vm; +})(); + +login.controller = function ctrl() { + this.app = App.getInstance(); + this.vm = login.vm; + + login.vm.init(this.app, done => { + m.redraw(); + if (done) { + m.route('/streaming/vchat'); + } + }); +}; + +login.view_cam = (vm, camstate, isFireFox) => { + const confirm = m('p', 'Please click `allow` on the top of the screen, ' + + 'so we can access your webcam for calls.'); + + if ((camstate === 'oneway') || isFireFox) { + return undefined; + } + if (camstate === 'nocam') { + return [confirm, m('p', { class: 'err' }, 'No cameras.')]; + } + if (camstate === 'fail') { + return [confirm, + m('p', { class: 'err' }, + 'Failed to access the webcam. Make sure to run this demo on an' + + 'http server and click allow when asked for permission by the browser.'), + ]; + } + if (camstate === 'loading') { + return m('span', { class: 'glyphicon glyphicon-refresh glyphicon-spin' }); + } + return m('div', { class: 'form-group' }, [ + m('label', { for: 'inputcam' }, 'WebCam:'), + m('div', { class: 'input-group' }, [ + m('span', { class: 'input-group-addon' }, + m('span', { class: 'glyphicon glyphicon-facetime-video' })), + m('select', { id: 'inputcam', class: 'form-control', onchange: vm.updateSelect }, + vm.cameras.map(cam => m('option', { value: cam.id }, cam.label))), + ]), + ]); +}; + +login.view_connect = (vm, camstate, constate) => { + if ((camstate !== 'ready') && (camstate !== 'oneway')) return undefined; + + const loginButton = constate === 'connecting' ? + m('span', { class: 'glyphicon glyphicon-refresh glyphicon-spin' }) : + m('button', { class: 'btn btn-success btn-block', onclick: vm.login }, 'Login'); + + const errmsg = constate === 'fail' ? + m('p', { class: 'err' }, 'Login failed.') : undefined; + + return [loginButton, errmsg]; +}; + +login.view = ctrl => { + const app = ctrl.app; + const vm = ctrl.vm; + + return m('div', { class: 'form-login panel panel-default' }, [ + m('div', { class: 'panel-body' }, [ + m('h2', 'Video streaming sample'), + m('div', { class: 'form-group' }, [ + m('label', { for: 'inputuser' }, 'ID:'), + m('div', { class: 'input-group' }, [ + m('span', { class: 'input-group-addon' }, + m('span', { class: 'glyphicon glyphicon-user' })), + m('input', { + type: 'text', + id: 'inputuser', + class: 'form-control', + placeholder: 'user@example.com', + required: ' ', + autofocus: ' ', + oninput: m.withAttr('value', app.username), + value: app.username(), + }), + ]), + ]), + m('div', { class: 'form-group' }, [ + m('label', { for: 'inputpass' }, 'Pass:'), + m('div', { class: 'input-group' }, [ + m('span', { class: 'input-group-addon' }, + m('span', { class: 'glyphicon glyphicon-lock' })), + m('input', { + type: 'password', + id: 'inputpass', + class: 'form-control', + placeholder: '********', + required: ' ', + oninput: m.withAttr('value', app.userpass), + value: app.userpass(), + }), + ]), + ]), + login.view_cam(vm, vm.camstate(), app.isFirefox), + login.view_connect(vm, vm.camstate(), app.state()), + ]), + ]); +}; + + +const chat = {}; +chat.vm = (() => { + const vm = {}; + vm.init = (app, cb) => { + vm._app = app; + + vm.isThetaView = m.prop(false); + + vm.call = () => vm._app.call(); + vm.open = () => vm._app.open(); + vm.copy = () => vm._app.copy(); + + vm.theta = () => vm.isThetaView(!vm.isThetaView()); + + vm.logout = () => { + vm._app.disconnect(); + cb(true); + }; + }; + return vm; +})(); + +chat.controller = function ctrl() { + this.app = App.getInstance(); + if (this.app.state() === 'initial') { + m.route('/streaming'); + return; + } + this.vm = chat.vm; + + chat.vm.init(this.app, done => { + if (done) { + m.route('/streaming'); + } + }); +}; + +chat.startPeer = elm => { + thetaview.setContainer(elm); + thetaview.start(elm.firstChild); +}; + +chat.stopPeer = elm => { + thetaview.stop(elm.firstChild); +}; + +chat.view_peer = (vm, app, constate) => (constate !== 'chatting' ? undefined : [ + m('button', { class: 'btn btn-success btn-block', onclick: vm.theta }, + 'Theta view'), + m('div', { config: vm.isThetaView() ? chat.startPeer : chat.stopPeer }, [ + m('video', { + id: app.peerurlid(), + width: '100%', + height: '100%', + src: app.peerurl(), + mozSrcObject: app.peermoz(), + autoplay: ' ', + }), + ]), +]); + +chat.view_call = (vm, constate) => { + const connectButton = constate === 'calling' ? + m('span', { class: 'glyphicon glyphicon-refresh glyphicon-spin' }) : + m('button', { class: 'btn btn-success btn-block', onclick: vm.call }, + 'Connect'); + return constate === 'chatting' ? undefined : connectButton; +}; + +chat.view_connect = (vm, app, constate) => { + if (constate === 'chatready') return undefined; + return m('p', [ + m('div', { class: 'form-group' }, [ + m('label', { for: 'inputpeer' }, 'Peer-ID:'), + m('div', { class: 'input-group' }, [ + m('span', { class: 'input-group-addon' }, + m('span', { class: 'glyphicon glyphicon-user' })), + m('input', { + type: 'text', + id: 'inputpeer', + class: 'form-control', + placeholder: 'user@example.com', + required: ' ', + autofocus: ' ', + oninput: m.withAttr('value', app.peername), + value: app.peername(), + }), + ]), + ]), + chat.view_call(vm, constate), + ]); +}; + +chat.view_my = (app, constate) => { + if ((constate === 'ready') || app.isWatcher) return undefined; + return [m('div', { class: 'text-center' }, + m('video', { + src: app.myurl(), + mozSrcObject: app.mymoz(), + width: 200, + height: 200, + autoplay: ' ', + }) + )]; +}; + +chat.view_open = (vm, app, constate) => { + if (constate !== 'ready') return undefined; + if (app.isOneWay) return undefined; + return m('button', { class: 'btn btn-success btn-block', onclick: vm.open }, 'Open'); +}; + +chat.view = ctrl => { + const app = ctrl.app; + const vm = ctrl.vm; + + return m('div', { class: 'row' }, [ + m('div', { class: 'col-md-9' }, chat.view_peer(vm, app, app.state())), + + m('div', { class: 'col-md-3' }, [ + m('div', { class: 'panel panel-default form-call' }, [ + m('p', [ + m('div', { class: 'form-group' }, [ + m('label', { for: 'inputmy' }, 'ID:'), + m('div', { class: 'input-group' }, [ + m('input', { + type: 'text', + id: 'inputmy', + class: 'form-control', + readonly: ' ', + value: app.username(), + }), + m('span', { class: 'input-group-btn' }, + m('button', { class: 'btn btn-default cpbtn', 'data-clipboard-target': '#inputmy' }, + m('i', { class: 'glyphicon glyphicon-paperclip' }) + )), + ]), + ]), + chat.view_open(vm, app, app.state()), + ]), + chat.view_connect(vm, app, app.state()), + m('button', { class: 'btn btn-danger btn-block', onclick: vm.logout }, 'Logout'), + chat.view_my(app, app.state()), + ]), + ]), + ]); +}; + +module.exports.login = login; +module.exports.chat = chat; diff --git a/samples/common/main.js b/samples/common/main.js new file mode 100644 index 0000000..340260e --- /dev/null +++ b/samples/common/main.js @@ -0,0 +1,25 @@ +/* global m,USER */ +'use strict'; + +/** + * Copyright (c) 2016 Ricoh Company, Ltd. All Rights Reserved. + * See LICENSE for more information + */ + +const App = require('./app').App; +const Compo = require('./components'); + +// window.__oneway = true; + +if (typeof m === 'undefined') { // headless + const app = App.getInstance(); + app.headless(USER.userId, USER.userPass, + document.querySelector('#wrapper')); +} else { + m.route.mode = 'hash'; + m.route(document.querySelector('#wrapper'), + '/streaming/', { + '/streaming': Compo.login, + '/streaming/vchat': Compo.chat, + }); +} diff --git a/samples/common/ricohapi-bosh.js b/samples/common/ricohapi-bosh.js new file mode 100644 index 0000000..f6fb6ee --- /dev/null +++ b/samples/common/ricohapi-bosh.js @@ -0,0 +1,104 @@ +/* global Strophe */ +/* eslint no-console: ["error", { allow: ["info"] }] */ +'use strict'; +/* + * Copyright (c) 2016 Ricoh Company, Ltd. All Rights Reserved. + * See LICENSE for more information + */ + +const EventEmitter = require('events').EventEmitter; +const AuthClient = require('ricohapi-auth').AuthClient; +// const Strophe = require('node-strophe').Strophe.Strophe; + +if (Strophe.SASLSHA1) { + Strophe.SASLSHA1.test = () => false; +} + +const MSGS = {}; +MSGS[Strophe.Status.CONNECTING] = 'The connection is currently being made.'; +MSGS[Strophe.Status.CONNFAIL] = 'The connection attempt failed.'; +MSGS[Strophe.Status.AUTHENTICATING] = 'The connection is authenticating.'; +MSGS[Strophe.Status.AUTHFAIL] = 'The authentication attempt failed.'; +MSGS[Strophe.Status.CONNECTED] = 'The connection has succeeded.'; +MSGS[Strophe.Status.DISCONNECTED] = 'The connection has been terminated.'; +MSGS[Strophe.Status.DISCONNECTING] = 'The connection is currently being terminated.'; +MSGS[Strophe.Status.ATTACHED] = 'The connection has been attached.'; +MSGS[Strophe.Status.ERROR] = 'An error has occurred.'; + +class BoshClient extends EventEmitter { + + _getBareJID(userID) { + return `${userID.replace(/@/g, '\\40')}@sig.ricohapi.com`; + } + + _getUserID(jid) { + return jid.split('@')[0].replace(/\\40/g, '@'); + } + + _onConnect(resolve, reject, status) { + console.info(MSGS[status]); + + if ((status === Strophe.Status.AUTHFAIL) || + (status === Strophe.Status.CONNFAIL) || + (status === Strophe.Status.ERROR)) { + reject(MSGS[status]); + return true; + } + if (status === Strophe.Status.CONNECTED) { + this._strophe.send(new Strophe.Builder('presence')); + resolve(MSGS[status]); + } + return true; + } + + _onMessage(stanza) { + const jid = Strophe.getBareJidFromJid(stanza.attributes.from.value); + const userID = this._getUserID(jid); + if (stanza.firstChild.nodeType !== 3) return true; + // nodeType:3 NODE.TEXT_NODE + this.emit('message', userID, stanza.firstChild.textContent); + return true; + } + + constructor(authClient) { + super(); + this._authClient = authClient; + this._strophe = null; + this._onMessageHandler = null; + } + + connect(userID, userPass) { + this.id = userID; + return new Promise((resolve, reject) => { + this._authClient.setResourceOwnerCreds(userID.split('+')[0], userPass); + this._authClient.session(AuthClient.SCOPES.VStream) + .then(result => { + this._strophe = new Strophe.Connection('https://sig.ricohapi.com/http-bind/'); + this._onMessageHandler = + this._strophe.addHandler(this._onMessage.bind(this), null, 'message'); + const jid = `${this._getBareJID(userID)}@sig.ricohapi.com/boshsdk`; + this._strophe.connect(jid, result.access_token, + this._onConnect.bind(this, resolve, reject)); + }) + .catch(reject); + }); + } + + disconnect() { + if (!this._strophe) return; + this._strophe.disconnect('normal'); + if (this._onMessageHandler) { + this._strophe.deleteHandler(this._onMessageHandler); + } + this._onMessageHandler = null; + this._strophe = null; + } + + send(to, message) { + const jid = this._getBareJID(to); + this._strophe.send(new Strophe.Builder('message', { to: jid }).t(message)); + } +} + +exports.AuthClient = AuthClient; +exports.BoshClient = BoshClient; diff --git a/samples/thetaview.js b/samples/common/thetaview.js similarity index 50% rename from samples/thetaview.js rename to samples/common/thetaview.js index 8c1d8c6..3ed5b13 100644 --- a/samples/thetaview.js +++ b/samples/common/thetaview.js @@ -36,14 +36,13 @@ class ThetaView { const fishRad = 0.883; const fishRad2 = fishRad * 0.88888888888888; const fishCenter = 1.0 - 0.44444444444444; - const ret = (lens === 0) ? - (new THREE.Vector2( - fishRad * ar.y * Math.cos(ar.x) * 0.5 + 0.25, - fishRad2 * ar.y * Math.sin(ar.x) + fishCenter)) : - (new THREE.Vector2( - fishRad * (1.0 - ar.y) * Math.cos(-1.0 * ar.x + Math.PI) * 0.5 + 0.75, - fishRad2 * (1.0 - ar.y) * Math.sin(-1.0 * ar.x + Math.PI) + fishCenter)); - return ret; + const x = (lens === 0) ? + fishRad * ar.y * Math.cos(ar.x) * 0.5 + 0.25 : + fishRad * (1.0 - ar.y) * Math.cos(-1.0 * ar.x + Math.PI) * 0.5 + 0.75; + const y = (lens === 0) ? + fishRad2 * ar.y * Math.sin(ar.x) + fishCenter : + fishRad2 * (1.0 - ar.y) * Math.sin(-1.0 * ar.x + Math.PI) + fishCenter; + return (new THREE.Vector2(x, y)); } _createGeometry() { @@ -92,6 +91,73 @@ class ThetaView { }); } + _animate() { + this._timer = requestAnimationFrame(this._animate.bind(this)); + if (this._camera === null) return; + + this._lat = Math.max(-85, Math.min(85, this._lat)); + const phi = THREE.Math.degToRad(90 - this._lat); + const theta = THREE.Math.degToRad(this._lon); + this._camera.target.x = 500 * Math.sin(phi) * Math.cos(theta); + this._camera.target.y = 500 * Math.cos(phi); + this._camera.target.z = 500 * Math.sin(phi) * Math.sin(theta); + this._camera.lookAt(this._camera.target); + this._renderer.render(this._scene, this._camera); + } + + _onWheel(e) { + if (!e) return; + if (e.preventDefault) e.preventDefault(); + const k = this._isChrome ? 0.05 : 1.0; + this._camera.fov += (e.deltaY * k); + this._camera.updateProjectionMatrix(); + } + + _onMouseUp(e) { + if (!e) return; + if (e.preventDefault) e.preventDefault(); + this._isUserInteracting = false; + } + + _onTouchEnd(e) { + this._onMouseUp(e.touches[0]); + return false; + } + + _onMouseDown(e) { + if (!e) return; + if (e.preventDefault) e.preventDefault(); + this._isUserInteracting = true; + this._onPointerDownPointerX = e.clientX; + this._onPointerDownPointerY = e.clientY; + this._onPointerDownLon = this._lon; + this._onPointerDownLat = this._lat; + } + + _onTouchStart(e) { + this._onMouseDown(e.touches[0]); + return false; + } + + _onMove(e, d) { + if (!this._isUserInteracting) return; + this._lon = (this._onPointerDownPointerX - e.clientX) * d + this._onPointerDownLon; + this._lat = (e.clientY - this._onPointerDownPointerY) * d + this._onPointerDownLat; + } + + _onMouseMove(e) { + if (!e) return; + if (e.preventDefault) e.preventDefault(); + this._onMove(e, 0.1); + } + + _onTouchMove(e) { + if (!e) return false; + if (e.preventDefault) e.preventDefault(); + this._onMove(e.touches[0], 1.0); + return false; + } + constructor() { this._isUserInteracting = false; this._lon = 0; @@ -103,10 +169,18 @@ class ThetaView { this._camera = null; this._scene = null; this._renderer = null; - }; + this._container = undefined; + this._timer = undefined; + const ua = window.navigator.userAgent.toLowerCase(); + this._isChrome = (ua.indexOf('chrome') !== -1); + } + + start(videoDOM) { + if (!this._container) return; + if (this._timer) return; + const w = this._container.clientWidth; + const h = this._container.clientHeight; - start(videoDOM, containerDOM, w, h) { - const self = this; // create Camera this._camera = new THREE.PerspectiveCamera(75, w / h, 1, 1100); this._camera.target = new THREE.Vector3(0, 0, 0); @@ -120,71 +194,43 @@ class ThetaView { this._renderer.setPixelRatio(window.devicePixelRatio); this._renderer.setSize(w, h); - function onWheel(e) { - e.preventDefault(); - let diff = 0; - if (event.wheelDeltaY) diff = event.wheelDeltaY * -0.05; // webkit - else if (event.wheelDelta) diff = event.wheelDelta * -0.05; // Opera / Explorer 9 - else if (event.detail) diff = event.detail * 1.0; // Firefox - self._camera.fov += diff; - self._camera.updateProjectionMatrix(); - } - - function onMouseUp(e) { - e.preventDefault(); - self._isUserInteracting = false; - } - - function onMouseDown(e) { - e.preventDefault(); - self._isUserInteracting = true; - self._onPointerDownPointerX = e.clientX; - self._onPointerDownPointerY = e.clientY; - self._onPointerDownLon = self._lon; - self._onPointerDownLat = self._lat; - } - - function onMouseMove(e) { - if (self._isUserInteracting !== true) return; - self._lon = (self._onPointerDownPointerX - e.clientX) * 0.1 + self._onPointerDownLon; - self._lat = (e.clientY - self._onPointerDownPointerY) * 0.1 + self._onPointerDownLat; - } - - this._renderer.domElement.addEventListener('mousewheel', onWheel, false); - this._renderer.domElement.addEventListener('MozMousePixelScroll', onWheel, false); - this._renderer.domElement.addEventListener('mouseup', onMouseUp, false); - this._renderer.domElement.addEventListener('mousedown', onMouseDown, false); - this._renderer.domElement.addEventListener('mousemove', onMouseMove, false); - - containerDOM.appendChild(this._renderer.domElement); - }; - - stop(containerDOM) { - const child = containerDOM.lastChild; - if (child) containerDOM.removeChild(child); + this._renderer.domElement.addEventListener('wheel', this._onWheel.bind(this), false); + this._renderer.domElement.addEventListener('mouseup', this._onMouseUp.bind(this), false); + this._renderer.domElement.addEventListener('mousedown', this._onMouseDown.bind(this), false); + this._renderer.domElement.addEventListener('mousemove', this._onMouseMove.bind(this), false); + this._renderer.domElement.addEventListener('touchend', this._onTouchEnd.bind(this), false); + this._renderer.domElement.addEventListener('touchstart', this._onTouchStart.bind(this), false); + this._renderer.domElement.addEventListener('touchmove', this._onTouchMove.bind(this), false); + + this._container.appendChild(this._renderer.domElement); + const dom = videoDOM; + dom.style.display = 'none'; + this._animate(); } - animate() { - let phi = 0; - let theta = 0; - if (this._camera === null) return; - this._lat = Math.max(-85, Math.min(85, this._lat)); - phi = THREE.Math.degToRad(90 - this._lat); - theta = THREE.Math.degToRad(this._lon); - this._camera.target.x = 500 * Math.sin(phi) * Math.cos(theta); - this._camera.target.y = 500 * Math.cos(phi); - this._camera.target.z = 500 * Math.sin(phi) * Math.sin(theta); - this._camera.lookAt(this._camera.target); - this._renderer.render(this._scene, this._camera); - }; + stop(videoDOM) { + if (!this._timer) return; + cancelAnimationFrame(this._timer); + this._timer = undefined; - resize(w, h) { - if (this._camera === null) return; - this._camera.aspect = w / h; - this._camera.updateProjectionMatrix(); - this._renderer.setSize(w, h); - }; + const child = this._container.lastChild; + if (child) this._container.removeChild(child); + const dom = videoDOM; + dom.style.display = 'block'; + } -}; + setContainer(elm) { + this._container = elm; + window.onresize = () => { + if (this._camera === null) return; + const w = this._container.clientWidth; + const ww = this._renderer.domElement.width; + const hh = this._renderer.domElement.height; + this._camera.aspect = ww / hh; + this._camera.updateProjectionMatrix(); + this._renderer.setSize(w, w / this._camera.aspect); + }; + } +} exports.ThetaView = ThetaView; diff --git a/samples/config_template.js b/samples/config_template.js index 1b0f4b0..4afa07f 100644 --- a/samples/config_template.js +++ b/samples/config_template.js @@ -1,8 +1,9 @@ -'use strict'; - const CONFIG = { clientId: '', clientSecret: '', }; -module.exports.CONFIG = CONFIG; +const USER = { + userId: '', + userPass: '', +}; diff --git a/samples/gulpfile.js b/samples/gulpfile.js new file mode 100644 index 0000000..449b935 --- /dev/null +++ b/samples/gulpfile.js @@ -0,0 +1,20 @@ +const gulp = require('gulp'); +const eslint = require('gulp-eslint'); +const webserver = require('gulp-webserver'); +const webpack = require('webpack-stream'); +const webpackConfig = require('./webpack.config.js'); + +gulp.task('build', function() { + return gulp.src('') + .pipe(webpack(webpackConfig)) + .pipe(gulp.dest('./oneway-broadcast')) + .pipe(gulp.dest('./oneway-watch')) + .pipe(gulp.dest('./twoway')); +}); + +gulp.task('lint', function() { + return gulp.src(['common/**/*.js']) + .pipe(eslint()) + .pipe(eslint.format()) + .pipe(eslint.failOnError()); +}); diff --git a/samples/index.html b/samples/index.html deleted file mode 100644 index 6610419..0000000 --- a/samples/index.html +++ /dev/null @@ -1,106 +0,0 @@ - - - -
- -ID:
-
-
-
PASS:
-
-
-
Choose your webcam:
- -Please click `allow` on the top of the screen so we can access your webcam for calls.
- - - -