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 @@ - - - - - - Video streaming sample - - - - -
-
-

Video streaming sample

-

ID: -
- -

-

PASS: -
- -

-

Choose your webcam:

-
-
-
-
S
-
e
-
a
-
r
-
c
-
h
-
i
-
n
-
g
-
.
-
-
- - -

Please click `allow` on the top of the screen so we can access your webcam for calls.

- - - -
-
- - - - - - - - diff --git a/samples/main.js b/samples/main.js deleted file mode 100644 index 8f23337..0000000 --- a/samples/main.js +++ /dev/null @@ -1,237 +0,0 @@ -'use strict'; -/* - * Copyright (c) 2016 Ricoh Company, Ltd. All Rights Reserved. - * See LICENSE for more information - * - * main.js for browser sample - */ - -const UDCStrophe = require('./UDCStrophe').UDCStrophe; -const StrophePeer = require('./strophePeer').StrophePeer; -const Webcam = require('./webcam').Webcam; -const ThetaView = require('./thetaview').ThetaView; -const CONFIG = require('./config').CONFIG; - -const xmpp = new UDCStrophe(CONFIG.clientId, CONFIG.clientSecret); -const webrtc = new StrophePeer(); -const webcam = new Webcam(); -const thetaview = new ThetaView(); - -let _view = 'INITIAL'; -let _app = {}; -let _thetatimer = null; - -function hide(id) { - const elm = document.querySelector(id); - if (elm.style.display === '') elm.style.display = 'none'; -} - -function show(id) { - const elm = document.querySelector(id); - if (elm.style.display === 'none') elm.style.display = ''; -} - -function getSelected(id) { - const ret = Array.prototype.slice.call(document.getElementsByName(id)) - .filter(o => (o.checked))[0]; - return ret? ret.value : undefined; -} - -function camera2elm(camera) { - const radio = document.createElement('input'); - radio.setAttribute('type', 'radio'); - radio.setAttribute('name', 'radio'); - radio.setAttribute('value', camera.id); - const label = document.createElement('label'); - label.appendChild(radio); - label.appendChild(document.createTextNode(camera.label)); - return label; -} - -function nextView(view, evt) { - const SM_DEFINE = { - INITIAL: { - ok: 'CAMERA_READY', - ng: 'CAMERA_ERROR', - }, - CAMERA_READY: { - evtClickLogin: 'LOGING_IN', - }, - CAMERA_ERROR: { - // dead end - }, - LOGING_IN: { - ok: 'READY', - ng: 'CAMERA_READY', - }, - READY: { - evtClickConnect: 'CONNECTING', - evtOnCall: 'CONNECTING', - evtClickLogout: 'CAMERA_READY', - }, - CONNECTING: { - evtArrivePeer: 'CHATTING', - evtClickLogout: 'CAMERA_READY', - }, - CHATTING: { - evtClickLogout: 'CAMERA_READY', - evtThetaView: 'CHATTING', - } - }; - return SM_DEFINE[view][evt]; -} - -// DOM read write -function update(view, app) { - if (view === 'CAMERA_READY') { - if (app.cameras.length === 0) { - show('#nocamera'); - return; // dead end - } - if (app.error) { - show('#loginerr'); - app.error = false; - } - const radios = document.getElementById('cameras'); - radios.innerHTML = ''; - radios.appendChild( - app.cameras.map(camera2elm) - .reduce((fragment, elm) => fragment.appendChild(elm).parentNode, - document.createDocumentFragment())).parentNode; - ['#viewpage', '#loader', '#loader2'].forEach(hide); - ['#loginpage', '#login'].forEach(show); - } else if (view === 'CAMERA_ERROR') { - ['#loader'].forEach(hide); - ['#camerr'].forEach(show); - } else if (view === 'LOGING_IN') { - app.id = document.querySelector('#id').value; - app.pass = document.querySelector('#pass').value; - ['#loginerr'].forEach(hide); - ['#loader2'].forEach(show); - } else if (view === 'READY') { - document.querySelector('#my-video').src = app.myVideoUrl; - document.querySelector('#myid').textContent = app.xmppid; - ['#loginpage', '#loader2', '#loader3'].forEach(hide); - ['#viewpage', '#connect'].forEach(show); - } else if (view === 'CONNECTING') { - app.peerId = document.querySelector('#peer-id').value; - ['#loader3'].forEach(show); - } else if (view === 'CHATTING') { - app.peerW = document.querySelector('#peer-container').clientWidth; - app.peerH = document.querySelector('#peer-container').clientHeight; - document.querySelector('#peer-video').src = app.peerVideoUrl; - document.querySelector('#peer-video').style.width = app.peerW + 'px'; - app.peerVideoDOM = document.getElementById('peer-video'); - app.peerContainerDOM = document.getElementById('peer-container'); - if (app.thetaMode) { - hide('#peer-video'); - } else { - show('#peer-video'); - } - ['#connect','#loader3'].forEach(hide); - } -} - -function occur(evt) { - _view = nextView(_view, evt); - update(_view, _app); -} - -function waitCall(call) { - call.on('stream', s => { - _app.peerVideoUrl = URL.createObjectURL(s); - occur('evtArrivePeer'); - }); -} - -function stopThetaView() { - if (_thetatimer) return; - cancelAnimationFrame(_thetatimer); - _thetatimer = undefined; -} - -function quit() { - stopThetaView(); - if (_app.peerVideoUrl) URL.revokeObjectURL(_app.peerVideoUrl); - if (_app.myVideoUrl) URL.revokeObjectURL(_app.myVideoUrl); - webrtc.stopStream(); - webrtc.disconnect(); - xmpp.disconnect(); -} - -function animate() { - _thetatimer = requestAnimationFrame(animate); - thetaview.animate(); -} - -document.addEventListener('DOMContentLoaded', () => { - - webrtc.on('error', e => console.error(e)); - - webrtc.on('call', call => { - occur('evtOnCall'); - webrtc.answer(call); - waitCall(call); - }); - - window.onload = () => { - webcam.getUserMedia() // for force allow dialog. - .then(() => webcam.init()) - .then(cameras => { - _app.cameras = cameras; - occur('ok'); - }) - .catch(() => occur('ng')); - }; - - window.onresize = () => { - const w = document.querySelector('#peer-container').clientWidth; - const h = document.querySelector('#peer-container').clientHeight; - document.querySelector('#peer-video').style.width = w + 'px'; - document.querySelector('#peer-video').style.height = h + 'px'; - thetaview.resize(w, h); - }; - - document.querySelector('#login').addEventListener('click', () => { - occur('evtClickLogin'); - xmpp.connect(_app.id, _app.pass) - .then(() => webrtc.connect(xmpp.connection)) - .then(() => webcam.getUserMedia(getSelected('radio'))) - .then(s => { - _app.xmppid = xmpp.id; - _app.myVideoUrl = URL.createObjectURL(s); - webrtc.stopStream(); - webrtc.setStream(s); - occur('ok'); - }) - .catch(e => { - console.error(e); - _app.error = true; - occur('ng'); - }); - }); - - document.querySelector('#connect').addEventListener('click', () => { - occur('evtClickConnect'); - waitCall(webrtc.call(_app.peerId)); - }); - - - document.querySelector('#logout').addEventListener('click', () => { - occur('evtClickLogout'); - quit(); - }); - - document.querySelector('#theta').addEventListener("click", () => { - _app.thetaMode = !_app.thetaMode; - occur('evtThetaView'); - if (_app.thetaMode) { - thetaview.start(_app.peerVideoDOM, _app.peerContainerDOM, _app.peerW, _app.peerH); - animate(); - } else { - stopThetaView(); - thetaview.stop(_app.peerContainerDOM); - } - }); - -}); diff --git a/samples/oneway-broadcast/README.md b/samples/oneway-broadcast/README.md new file mode 100644 index 0000000..a9a2188 --- /dev/null +++ b/samples/oneway-broadcast/README.md @@ -0,0 +1,49 @@ +# Ricoh One-way Video Streaming Sample (Broadcast) +In this sample, you can try Ricoh One-way Video Streaming distribution. +See [oneway-watch](https://github.com/ricohapi/video-streaming-sample-app/tree/master/samples/oneway-watch) for Viewing Ricoh One-way Video Streaming. + +## Requirements +* Raspbian Jessie or another Operating System similar to it. +* Node.js 5.0 or newer. +* Web Camera accessible from your browser. + +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/samples +$ npm install +$ npm run build +``` + +Make config.js from config_template.js. + +```sh +$ cd oneway-broadcast +$ cp ../config_template.js ./config.js +``` +and put your credentials into the `config.js`. + +Install Mozilla Firefox(Iceweasel), Xvfb and other packages you need. +In Raspbian Jessie, the following command will be executed: + +```sh +$ apt-get install firefox-esr xvfb +$ npm install +``` + +## Video Streaming distribution +Connect to your Web camera. +Execute the command `npm start`, then a Web browser is started in the background. +When receiving the Streaming request, it start Video Streaming distribution. + +```sh +$ npm start +``` diff --git a/samples/oneway-broadcast/gulpfile.js b/samples/oneway-broadcast/gulpfile.js new file mode 100644 index 0000000..ec68e03 --- /dev/null +++ b/samples/oneway-broadcast/gulpfile.js @@ -0,0 +1,9 @@ +const gulp = require('gulp'); +const eslint = require('gulp-eslint'); + +gulp.task('lint', function() { + return gulp.src(['src/**/*.js']) + .pipe(eslint()) + .pipe(eslint.format()) + .pipe(eslint.failOnError()); +}); diff --git a/samples/oneway-broadcast/index.html b/samples/oneway-broadcast/index.html new file mode 100644 index 0000000..256738e --- /dev/null +++ b/samples/oneway-broadcast/index.html @@ -0,0 +1,23 @@ + + + + + + Video streaming sample + + + + + + + +
+ + + + + diff --git a/samples/oneway-broadcast/package.json b/samples/oneway-broadcast/package.json new file mode 100644 index 0000000..b3f013c --- /dev/null +++ b/samples/oneway-broadcast/package.json @@ -0,0 +1,34 @@ +{ + "name": "ricohapi-oneway-video-streaming-sample", + "version": "1.0.0", + "description": "RICOH API headless video streaming sample", + "main": "./src/headless.js", + "scripts": { + "start": "node ./src/headless.js" + }, + "dependencies": { + "selenium-webdriver": "^2.53.2", + "xvfb": "^0.2.3" + }, + "devDependencies": { + "eslint": "^2.2.0", + "eslint-config-airbnb": "^6.0.2", + "eslint-plugin-react": "^4.1.0", + "gulp": "^3.9.1", + "gulp-eslint": "^2.0.0" + }, + "engines": { + "node": ">=5.0.0", + "npm": ">=3.0.0" + }, + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/ricohapi/video-streaming-sample-app.git" + }, + "keywords": [], + "bugs": { + "url": "https://github.com/ricohapi/video-streaming-sample-app/issues" + }, + "homepage": "https://github.com/ricohapi/video-streaming-sample-app" +} diff --git a/samples/oneway-broadcast/src/.eslintrc b/samples/oneway-broadcast/src/.eslintrc new file mode 100644 index 0000000..f53364c --- /dev/null +++ b/samples/oneway-broadcast/src/.eslintrc @@ -0,0 +1,7 @@ +{ + "extends": "airbnb", + "rules": { + "strict": 0, + "no-underscore-dangle": ["error", { "allowAfterThis": true }] + } +} diff --git a/samples/oneway-broadcast/src/headless.js b/samples/oneway-broadcast/src/headless.js new file mode 100644 index 0000000..55dbdd8 --- /dev/null +++ b/samples/oneway-broadcast/src/headless.js @@ -0,0 +1,87 @@ +'use strict'; + +/** + * Copyright (c) 2016 Ricoh Company, Ltd. All Rights Reserved. + * See LICENSE for more information + */ + +const path = require('path'); +const EventEmitter = require('events'); +const Xvfb = require('xvfb'); +const firefox = require('selenium-webdriver/firefox'); +const Console = console; + +class HeadlessBrowser extends EventEmitter { + constructor() { + super(); + this._driver = null; + this._xvfb = null; + } + + start() { + this._xvfb = new Xvfb(); + this._xvfb.startSync(); + const profile = new firefox.Profile(); + profile.setPreference('media.navigator.permission.disabled', true); + const options = new firefox.Options().setProfile(profile); + this._driver = new firefox.Driver(options); + const url = `file://${path.resolve(__dirname, '../index.html')}`; + this._driver.get(url); + this._loop(); + } + + stop() { + if (this._xvfb) { + this._xvfb.stopSync(); + this._xvfb = null; + } + if (this._driver) { + this._driver.quit(); + this._driver = null; + } + } + + _loop() { + if (this._driver) { + this._printLogs(); + setTimeout(this._loop.bind(this), 1000); + } + } + + _printLogs() { + const script = 'return typeof(getLogs) === "function" ? getLogs() : [];'; + this._driver.executeScript(script) + .then(logs => { + for (const log of logs) { + switch (log.type) { + case 'error': + this.emit('error', log.message); + break; + case 'warn': + this.emit('warn', log.message); + break; + case 'info': + this.emit('info', log.message); + break; + case 'debug': + this.emit('debug', log.message); + break; + default: break; + } + } + }); + } +} + +const headless = new HeadlessBrowser(); +headless.on('error', Console.error); +headless.on('warn', Console.warn); +headless.on('info', Console.info); +// headless.on('debug', Console.log); +headless.start(); +process.on('SIGINT', () => { + Console.info('exit'); + headless.stop(); + process.exit(); +}); +Console.info('hit ctrl+c to quit'); diff --git a/samples/oneway-broadcast/src/log-hook.js b/samples/oneway-broadcast/src/log-hook.js new file mode 100644 index 0000000..1cedf81 --- /dev/null +++ b/samples/oneway-broadcast/src/log-hook.js @@ -0,0 +1,44 @@ +'use strict'; + +/** + * Copyright (c) 2016 Ricoh Company, Ltd. All Rights Reserved. + * See LICENSE for more information + */ + +(() => { + const logs = []; + const MAX_LENGTH = 100; + const Console = console; + + window.getLogs = () => { + const result = logs.concat(); + logs.length = 0; + return result; + }; + + window.onerror = message => { + logs.push({ + type: 'error', + message, + }); + }; + + function hookConsole(name) { + const func = Console[name]; + Console[name] = (...args) => { + func.apply(Console, args); + if (logs.length < MAX_LENGTH) { + logs.push({ + type: name, + message: args.join(', '), + }); + } + }; + } + + hookConsole('error'); + hookConsole('warn'); + hookConsole('info'); + hookConsole('debug'); + Console.log = Console.debug; +})(); diff --git a/samples/oneway-watch/README.md b/samples/oneway-watch/README.md new file mode 100644 index 0000000..0991174 --- /dev/null +++ b/samples/oneway-watch/README.md @@ -0,0 +1,41 @@ +# Ricoh One-way Video Streaming Sample (Watch) +In this sample, you can try Viewing Ricoh One-way Video Streaming. +See [oneway-broadcast](https://github.com/ricohapi/video-streaming-sample-app/tree/master/samples/oneway-broadcast) for Ricoh One-way Video Streaming distribution. + +## Requirements +* Google Chrome 51+ or Mozilla Firefox 47+ +* Node.js 5.0 or newer. + +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/samples +$ npm install +$ npm run build +``` + +Make config.js from config_template.js. +```sh +$ cd oneway-watch +$ cp ../config_template.js ./config.js +``` +and put your credentials into the `config.js`. + +## Viewing Streaming Video +Execute `npm start`, then the browser will be opened. +Put your Ricoh ID & password, and submit the login button. +Then put the sender's User ID to the Peer-ID field and submit Connect button, then streaming connection will start between sender and reciever. + +```sh +$ npm start +``` + +## THETA View +If the peer user is using THETA, push THETA View button, then you'll see the draggable & zoomable 360° view. diff --git a/samples/oneway-watch/gulpfile.js b/samples/oneway-watch/gulpfile.js new file mode 100644 index 0000000..a1a32cf --- /dev/null +++ b/samples/oneway-watch/gulpfile.js @@ -0,0 +1,12 @@ +const gulp = require('gulp'), + webserver = require('gulp-webserver'); + +gulp.task('server', function () { + gulp.src('') + .pipe(webserver({ + host: 'localhost', + port: 8035, + open: true, + fallback: './index.html' + })); +}); diff --git a/samples/oneway-watch/index.html b/samples/oneway-watch/index.html new file mode 100644 index 0000000..9c35504 --- /dev/null +++ b/samples/oneway-watch/index.html @@ -0,0 +1,32 @@ + + + + + + Video streaming sample + + + + + + + + + + + + + +
+ + + + + + diff --git a/samples/oneway-watch/package.json b/samples/oneway-watch/package.json new file mode 100644 index 0000000..cc76368 --- /dev/null +++ b/samples/oneway-watch/package.json @@ -0,0 +1,5 @@ +{ + "scripts": { + "start": "gulp server" + } +} diff --git a/samples/oneway-watch/styles/style.css b/samples/oneway-watch/styles/style.css new file mode 100644 index 0000000..0d19981 --- /dev/null +++ b/samples/oneway-watch/styles/style.css @@ -0,0 +1,44 @@ +body { + background-color: #333; + height: 100%; + margin: 0; + padding: 0; + width: 98%; +} + +.form-login { + max-width: 400px; + padding: 15px; + margin: 50px auto; +} + +.form-call { + padding: 15px; + margin: 50px auto; +} + +.glyphicon-spin { + font-size:22px; + -webkit-animation: spin 1000ms infinite linear; + animation: spin 1000ms infinite linear; +} +@-webkit-keyframes spin { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } + 100% { + -webkit-transform: rotate(359deg); + transform: rotate(359deg); + } +} +@-moz-keyframes spin { + 0% { + -moz-transform: rotate(0deg); + transform: rotate(0deg); + } + 100% { + -moz-transform: rotate(359deg); + transform: rotate(359deg); + } +} diff --git a/package.json b/samples/package.json similarity index 51% rename from package.json rename to samples/package.json index 238ad2b..01788b9 100644 --- a/package.json +++ b/samples/package.json @@ -1,18 +1,32 @@ { "name": "ricohapi-video-streaming-sample", - "version": "1.0.0", + "version": "2.0.0", "description": "RICOH API video streaming sample", - "main": "index.html", + "main": "common/main.js", "dependencies": { - "ricohapi-auth": "1.0.1" + "ricohapi-auth": "1.1.1" }, "devDependencies": { "gulp": "^3.9.1", - "webpack-stream": "^3.1.0", + "webpack-stream": "^3.2.0", "gulp-webserver": "^0.9.1", "babel-loader": "^6.2.4", - "babel-core": "^6.7.0", - "babel-preset-es2015": "^6.6.0" + "babel-core": "^6.9.1", + "babel-preset-es2015": "^6.9.0", + "strip-loader": "^0.1.2", + "gulp-eslint": "^2.0.0", + "eslint": "^2.12.0", + "eslint-config-airbnb": "^9.0.1", + "eslint-plugin-react": "^5.1.1", + "eslint-plugin-jsx-a11y": "^1.4.2", + "eslint-plugin-import": "^1.8.1" + }, + "engines": { + "node": ">=5.0.0", + "npm": ">=3.0.0" + }, + "scripts": { + "build": "gulp build" }, "license": "MIT", "repository": { diff --git a/samples/strophePeer.js b/samples/strophePeer.js deleted file mode 100644 index 848bec0..0000000 --- a/samples/strophePeer.js +++ /dev/null @@ -1,112 +0,0 @@ -'use strict'; - -/** - * Copyright (c) 2016 Ricoh Company, Ltd. All Rights Reserved. - * See LICENSE for more information - * - * use xmpp(strophe.connection) on peerjs - * override peerjs Sock class - */ - -const EventEmitter = require('events'); - -function _log(typ, obj) { -// console.log({ type: typ, data: obj }); -} - -class StrophePeer extends EventEmitter { - - constructor() { - super(); - } - - connect(strophe, cid) { - _log('log', 'connect called'); - this.strophe = strophe; - - return new Promise((resolve, reject) => { - - // inject strophe in peerJS - Socket.prototype.close = () => {}; - Socket.prototype.start = () => {}; - Socket.prototype.send = data => { - _log('send', data) - this.strophe.send($msg({ - to: data.dst + '@sig.ricohapi.com' - }).t(JSON.stringify(data))); - } - - this.peer = new Peer(cid); - this.peer.on('open', () => { - _log('log', 'webrtc connected') - resolve(); - }); - this.peer.on('error', reject); - this.peer.on('call', call => this.emit('call', call)); - - const msgHandler = message => { - _log('onMessage', message) - try { - // change XMPP message for PeerJS - const id = Strophe.getBareJidFromJid(message.attributes['from'].value).split('@')[0]; - // to judge if peerjs message or not, and fire events for each listeners. - // - // { type: 'OPEN' | 'OFFER' | 'ANSWER' | 'CANDIDATE' - // src: jid - // ... - // } - // - if (message.firstChild.nodeType == 3 /* NODE.TEXT_NODE */ ) { - const json = JSON.parse(message.firstChild.textContent); - if (json.type) { - json.src = id; - this.peer.socket.emit('message', json); - } - } - } catch (e) { - _log(e); - } - return true; - }; - this.strophe.addHandler(msgHandler, null, 'message', null); - this.peer.socket.emit('message', { "type": "OPEN" }); - }); - } - - disconnect() { - this.peer.disconnect(); - } - - setStream(stream) { - this._localStream = stream; - }; - - call(cid) { - _log('log', 'call called'); - if (!this._localStream) { - _log('log', 'no localstream'); - return; - } - return this.peer.call(cid.replace(/@/g, '\\40'), this._localStream); - } - - answer(call) { - if (!this._localStream) { - _log('log', 'no localstream'); - return; - } - call.answer(this._localStream); - } - - stopStream() { - if (!this._localStream) { - _log('log', 'no localstream'); - return; - } - this._localStream.getVideoTracks()[0].stop(); - this._localStream = undefined; - }; - -}; - -exports.StrophePeer = StrophePeer; diff --git a/samples/styles/style.css b/samples/styles/style.css deleted file mode 100644 index e2a93ff..0000000 --- a/samples/styles/style.css +++ /dev/null @@ -1,165 +0,0 @@ -.wrapper { - width: 98%; - margin: 0 auto; -} - -.loginform { - width: 400px; - margin: 30px auto; - padding: 20px; - border: 1px solid #555; -} - -input[type="text"],input[type="password"] { - width: 300px; - padding: 4px; - font-size: 14px; -} - -.header { - float: left; - height: 10px; - width: 100%; -} - -.wrapleft { - float: left; - width: 100%; -} - -.left { - margin-right: 387px; -} - -#peer-container { - background-color: #111; - height: 800px; -} - -.right { - float: right; - width: 353px; - margin-top: 30px; - margin-left: -369px; - background-color: #eee; - height: auto; - padding: 8px; -} - -.footer { - float: left; - width: 100%; -} - -#camerr,#nocamera,#loginerr { - color: #F33; -} - -.li-avail { - color: white; - border-radius: 4px; - text-shadow: 0 1px 1px rgba(0, 0, 0, 0.2); - background: rgb(28, 184, 65); -} - -body { - padding: 0px; - margin: 10px; - height: 1000px; - font-family: 'sans-selif'; -} - -button { - color: white; - border-radius: 4px; - text-shadow: 0 1px 1px rgba(0, 0, 0, 0.2); - background: rgb(28, 184, 65); - border: 2px; - margin: 2px; - padding: 6px; -} - -button:active { - transform: scale(0.95); -} - -li { - list-style-position: inside; - border: 1px solid #ccc; - list-style-type: none; - border-radius: 4px; - margin: 10px; - padding: 8px; -} - -label { - display: block; -} - -/* http://codepen.io/Manoz/pen/pydxK */ -/* https://blog.codepen.io/legal/licensing/ */ -.letter-holder { - padding: 8px; - margin-bottom: 30px; -} - -.letter { - float: left; - color: #777; -} - -.load-6 .letter { - animation-name: loadingF; - animation-duration: 1.6s; - animation-iteration-count: infinite; - animation-direction: linear; -} - -.l-1 { - animation-delay: .48s; -} - -.l-2 { - animation-delay: .6s; -} - -.l-3 { - animation-delay: .72s; -} - -.l-4 { - animation-delay: .84s; -} - -.l-5 { - animation-delay: .96s; -} - -.l-6 { - animation-delay: 1.08s; -} - -.l-7 { - animation-delay: 1.2s; -} - -.l-8 { - animation-delay: 1.32s; -} - -.l-9 { - animation-delay: 1.44s; -} - -.l-10 { - animation-delay: 1.56s; -} - -@keyframes loadingF { - 0% { - opacity: 0; - } - 100% { - opacity: 1; - } -} diff --git a/samples/twoway/README.md b/samples/twoway/README.md new file mode 100644 index 0000000..3d52dd5 --- /dev/null +++ b/samples/twoway/README.md @@ -0,0 +1,45 @@ +# Ricoh Two-way Video Streaming Sample +Bidirectional video streaming sample. + +## Requirements +* Google Chrome 51+ or Mozilla Firefox 47+ +* Web Camera accessible from your browser. +* Enable Web Camera access in your browser setting. +* Node.js 5.0 or newer. + +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/samples +$ npm install +$ npm run build +``` +Make config.js from config_template.js. + +```sh +$ cd twoway +$ cp ../config_template.js ./config.js +``` +and put your credentials into the `config.js`. + +## Video Streaming +Connect the Web Camera and execute `npm start`, 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 and click Open button, +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 +$ npm start +``` + +## THETA View +If the peer user is using THETA, push THETA View button, then you'll see the draggable & zoomable 360° view. diff --git a/samples/twoway/gulpfile.js b/samples/twoway/gulpfile.js new file mode 100644 index 0000000..b8b5aad --- /dev/null +++ b/samples/twoway/gulpfile.js @@ -0,0 +1,12 @@ +const gulp = require('gulp'), + webserver = require('gulp-webserver'); + +gulp.task('server', function () { + gulp.src('') + .pipe(webserver({ + host: 'localhost', + port: 8034, + open: true, + fallback: './index.html' + })); +}); diff --git a/samples/twoway/index.html b/samples/twoway/index.html new file mode 100644 index 0000000..25d268d --- /dev/null +++ b/samples/twoway/index.html @@ -0,0 +1,25 @@ + + + + + + Video streaming sample + + + + + + + + + + + + +
+ + + + + + diff --git a/samples/twoway/package.json b/samples/twoway/package.json new file mode 100644 index 0000000..cc76368 --- /dev/null +++ b/samples/twoway/package.json @@ -0,0 +1,5 @@ +{ + "scripts": { + "start": "gulp server" + } +} diff --git a/samples/twoway/styles/style.css b/samples/twoway/styles/style.css new file mode 100644 index 0000000..374f7c9 --- /dev/null +++ b/samples/twoway/styles/style.css @@ -0,0 +1,44 @@ +body { + background-color: #333; + height: 100%; + margin: 0; + padding: 0; + width: 98%; +} + +.form-login { + max-width: 400px; + padding: 15px; + margin: 50px auto; +} + +.form-call { + padding: 15px; + margin: 50px auto; +} + +.glyphicon-spin { + font-size:22px; + -webkit-animation: spin 1000ms infinite linear; + animation: spin 1000ms infinite linear; +} +@-webkit-keyframes spin { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } + 100% { + -webkit-transform: rotate(359deg); + transform: rotate(359deg); + } +} +@-moz-keyframes spin { + 0% { + -moz-transform: rotate(0deg); + transform: rotate(0deg); + } + 100% { + -moz-transform: rotate(359deg); + transform: rotate(359deg); + } +} diff --git a/samples/webcam.js b/samples/webcam.js deleted file mode 100644 index c4b5925..0000000 --- a/samples/webcam.js +++ /dev/null @@ -1,59 +0,0 @@ -'use strict'; -/** - * Copyright (c) 2016 Ricoh Company, Ltd. All Rights Reserved. - * See LICENSE for more information - */ - -class Webcam { - constructor() { - navigator.getUserMedia = navigator.getUserMedia || - navigator.webkitGetUserMedia || navigator.mozGetUserMedia; - }; - - _enumerateDevice() { - return new Promise((resolve, reject) => { - navigator.mediaDevices.enumerateDevices() - .then(devices => { - const valid = devices.filter(o => (o.kind === 'videoinput') && (o.label !== '')); - resolve(valid.map(o => ({ id: o.deviceId, label: o.label }))); - }) - .catch(e => reject(e)); - }); - } - - _getSources() { - return new Promise((resolve, reject) => { - if (typeof MediaStreamTrack.getSources === 'undefined') { - console.log('MediaStreamTrack.getSources not supported'); - return reject; - } - MediaStreamTrack.getSources(data => { - const valid = data.filter(o => (o.kind === 'video') && (o.label !== '')); - return resolve(valid.map(o => ({ id: o.id, label: o.label }))); - }); - }); - } - - init() { - return (!navigator.mediaDevices || !navigator.mediaDevices.enumerateDevices) ? - this._getSources() : this._enumerateDevice(); - }; - - getUserMedia(src) { - let option = {}; - if (src) { - option = { - video: { mandatory: { sourceId: src } }, - audio: false, - }; - } else { - option = { video: true, audio: false }; - } - return new Promise((resolve, reject) => { - navigator.getUserMedia(option, s => resolve(s), e => reject(e)); - }); - }; - -}; - -exports.Webcam = Webcam; diff --git a/samples/webpack.config.js b/samples/webpack.config.js new file mode 100644 index 0000000..b9df579 --- /dev/null +++ b/samples/webpack.config.js @@ -0,0 +1,35 @@ +var webpack = require('webpack'); +var path = require('path'); +module.exports = { + entry: './common/main.js', + output: { + filename: './build/ricohapi-webrtc.js', + library: "RicohAPIWebRTC", + libraryTarget: "umd" + }, + module: { + loaders: [{ + test: /\.js$/, + loader: 'babel', + exclude: /^(?!.*ricoh).*(?=node_modules).*$/, + query: { + presets: ['es2015'], + compact: false, + cacheDirectory: true + } + }, + { + test: /\.js$/, + loader: "strip-loader?strip[]=console.log" + } + ], + noParse: [/validate\.js/] + }, + resolve: { + extensions: ['', '.js'], + modulesDirectories: ['node_modules'], + module: { + noParse: [ /\.\/data\//, /\.\/nightwatch\// ], + }, + }, +}; diff --git a/webpack.config.js b/webpack.config.js deleted file mode 100644 index f45972e..0000000 --- a/webpack.config.js +++ /dev/null @@ -1,19 +0,0 @@ -module.exports = { - entry: './samples/main.js', - output: { - filename: './build/bundle.js' - }, - module: { - loaders: [{ - test: /\.js$/, - loader: 'babel', - query: { - presets: ['es2015'], - compact: false - } - }] - }, - resolve: { - extensions: ['', '.js'] - } -};