Merge pull request #2624 from AngelFQC/2622

Add vrview plugin for mediaelement #2622
pull/2629/head
Yannick Warnier 7 years ago committed by GitHub
commit 6b8d1f7506
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. BIN
      app/Resources/public/assets/mediaelement/plugins/vrview/cardboard.png
  2. 1
      app/Resources/public/assets/mediaelement/plugins/vrview/cardboard.svg
  3. 4
      app/Resources/public/assets/mediaelement/plugins/vrview/vrview.css
  4. 383
      app/Resources/public/assets/mediaelement/plugins/vrview/vrview.js
  5. 1
      app/Resources/public/assets/mediaelement/plugins/vrview/vrview.min.css
  6. 12
      app/Resources/public/assets/mediaelement/plugins/vrview/vrview.min.js
  7. 27
      app/Resources/public/assets/vrview/CONTRIBUTING
  8. 202
      app/Resources/public/assets/vrview/LICENSE
  9. 73
      app/Resources/public/assets/vrview/README.md
  10. 1
      app/Resources/public/assets/vrview/build/device-motion-sender.min.js
  11. 12341
      app/Resources/public/assets/vrview/build/embed.js
  12. 1
      app/Resources/public/assets/vrview/build/embed.min.js
  13. 24312
      app/Resources/public/assets/vrview/build/three.js
  14. 511
      app/Resources/public/assets/vrview/build/three.min.js
  15. 995
      app/Resources/public/assets/vrview/build/vrview.js
  16. 1
      app/Resources/public/assets/vrview/build/vrview.min.js
  17. 5
      app/Resources/public/assets/vrview/images/ic_info_outline_black_24dp.svg
  18. 4
      app/Resources/public/assets/vrview/images/ic_play_arrow_white_24dp.svg
  19. BIN
      app/Resources/public/assets/vrview/images/loading.gif
  20. 41
      app/Resources/public/assets/vrview/index-minified.html
  21. 33
      app/Resources/public/assets/vrview/index.html
  22. 2836
      app/Resources/public/assets/vrview/package-lock.json
  23. 56
      app/Resources/public/assets/vrview/package.json
  24. 21
      app/Resources/public/assets/vrview/scripts/acl.txt
  25. 21
      app/Resources/public/assets/vrview/scripts/deploy-2.0.sh
  26. 88
      app/Resources/public/assets/vrview/scripts/js/device-motion-sender.js
  27. 2
      app/Resources/public/assets/vrview/src/api/README.md
  28. 80
      app/Resources/public/assets/vrview/src/api/iframe-message-sender.js
  29. 22
      app/Resources/public/assets/vrview/src/api/main.js
  30. 316
      app/Resources/public/assets/vrview/src/api/player.js
  31. 147
      app/Resources/public/assets/vrview/src/embed/adaptive-player.js
  32. 56
      app/Resources/public/assets/vrview/src/embed/analytics.js
  33. 20
      app/Resources/public/assets/vrview/src/embed/eyes.js
  34. 403
      app/Resources/public/assets/vrview/src/embed/hotspot-renderer.js
  35. 68
      app/Resources/public/assets/vrview/src/embed/iframe-message-receiver.js
  36. 54
      app/Resources/public/assets/vrview/src/embed/loading-indicator.js
  37. 369
      app/Resources/public/assets/vrview/src/embed/main.js
  38. 41
      app/Resources/public/assets/vrview/src/embed/reticle-renderer.js
  39. 125
      app/Resources/public/assets/vrview/src/embed/scene-info.js
  40. 205
      app/Resources/public/assets/vrview/src/embed/sphere-renderer.js
  41. 130
      app/Resources/public/assets/vrview/src/embed/video-proxy.js
  42. 20
      app/Resources/public/assets/vrview/src/embed/with-analytics.js
  43. 372
      app/Resources/public/assets/vrview/src/embed/world-renderer.js
  44. 33
      app/Resources/public/assets/vrview/src/message.js
  45. 29
      app/Resources/public/assets/vrview/src/third_party/three/Three.js
  46. BIN
      app/Resources/public/assets/vrview/src/third_party/three/closure-compiler-v20160713.jar
  47. 10
      app/Resources/public/assets/vrview/src/third_party/three/externs.js
  48. 34
      app/Resources/public/assets/vrview/src/third_party/three/rollup.config.js
  49. 241
      app/Resources/public/assets/vrview/src/util.js
  50. 25
      app/Resources/public/assets/vrview/src/video-type.js
  51. 100
      app/Resources/public/assets/vrview/style.css
  52. 63
      main/inc/lib/javascript/ckeditor/plugins/video/dialogs/video.js
  53. 12
      main/inc/lib/javascript/ckeditor/plugins/video/plugin.js
  54. 2
      main/install/configuration.dist.php
  55. 5
      main/template/default/layout/main.js.tpl

Binary file not shown.

After

Width:  |  Height:  |  Size: 668 B

@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" x="0" y="0" width="18" height="12" viewBox="0 0 17.62 11.4"><defs><style>.cls-1{fill:#fff;}</style></defs><title>6</title><path id="cardboard" class="cls-1" d="M157.8,3.9H142.3A1.11,1.11,0,0,0,141.2,5v9.2a1.11,1.11,0,0,0,1.1,1.1h4.2a1,1,0,0,0,1-.7l1.2-3.1a1.39,1.39,0,0,1,2.6,0l1.2,3.1a1.09,1.09,0,0,0,1,.7h4.2a1.11,1.11,0,0,0,1.1-1.1V5.1a1,1,0,0,0-1-1.2m-11.9,7.6a2,2,0,1,1,2-2,2.07,2.07,0,0,1-2,2m8.4,0a2,2,0,1,1,2-2A2,2,0,0,1,154.3,11.5Z" transform="translate(-141.2 -3.9)"/></svg>

After

Width:  |  Height:  |  Size: 557 B

@ -0,0 +1,4 @@
.mejs__vrview-button > button,
.mejs-vrview-button > button {
background: url('cardboard.svg') no-repeat 0 4px;
}

@ -0,0 +1,383 @@
/*!
* MediaElement.js
* http://www.mediaelementjs.com/
*
* Wrapper that mimics native HTML5 MediaElement (audio and video)
* using a variety of technologies (pure JavaScript, Flash, iframe)
*
* Copyright 2010-2017, John Dyer (http://j.hn/)
* License: MIT
*
*/(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(_dereq_,module,exports){
'use strict';
var VrAPI = {
isMediaStarted: false,
isMediaLoaded: false,
creationQueue: [],
prepareSettings: function prepareSettings(settings) {
if (VrAPI.isLoaded) {
VrAPI.createInstance(settings);
} else {
VrAPI.loadScript(settings);
VrAPI.creationQueue.push(settings);
}
},
loadScript: function loadScript(settings) {
if (!VrAPI.isMediaStarted) {
if (typeof VRView !== 'undefined') {
VrAPI.createInstance(settings);
} else {
var script = document.createElement('script'),
firstScriptTag = document.getElementsByTagName('script')[0];
var done = false;
settings.options.path = typeof settings.options.path === 'string' ? settings.options.path : 'https://googlevr.github.io/vrview/build/vrview.min.js';
script.src = settings.options.path;
script.onload = script.onreadystatechange = function () {
if (!done && (!this.readyState || this.readyState === undefined || this.readyState === 'loaded' || this.readyState === 'complete')) {
done = true;
VrAPI.mediaReady();
script.onload = script.onreadystatechange = null;
}
};
firstScriptTag.parentNode.insertBefore(script, firstScriptTag);
}
VrAPI.isMediaStarted = true;
}
},
mediaReady: function mediaReady() {
VrAPI.isLoaded = true;
VrAPI.isMediaLoaded = true;
while (VrAPI.creationQueue.length > 0) {
var settings = VrAPI.creationQueue.pop();
VrAPI.createInstance(settings);
}
},
createInstance: function createInstance(settings) {
var player = new VRView.Player('#' + settings.id, settings.options);
window['__ready__' + settings.id](player);
}
};
var VrRenderer = {
name: 'vrview',
options: {
prefix: 'vrview'
},
canPlayType: function canPlayType(type) {
return ~['video/mp4', 'application/x-mpegurl', 'vnd.apple.mpegurl', 'application/dash+xml'].indexOf(type.toLowerCase());
},
create: function create(mediaElement, options, mediaFiles) {
var stack = [],
vr = {},
readyState = 4;
var vrPlayer = null,
paused = true,
volume = 1,
oldVolume = volume,
bufferedTime = 0,
ended = false,
duration = 0,
url = '';
vr.options = options;
vr.id = mediaElement.id + '_' + options.prefix;
vr.mediaElement = mediaElement;
var props = mejs.html5media.properties,
assignGettersSetters = function assignGettersSetters(propName) {
var capName = propName.substring(0, 1).toUpperCase() + propName.substring(1);
vr['get' + capName] = function () {
if (vrPlayer !== null) {
var value = null;
switch (propName) {
case 'currentTime':
return vrPlayer.getCurrentTime();
case 'duration':
return vrPlayer.getDuration();
case 'volume':
volume = vrPlayer.getVolume();
return volume;
case 'muted':
return volume === 0;
case 'paused':
paused = vrPlayer.isPaused;
return paused;
case 'ended':
return ended;
case 'src':
return url;
case 'buffered':
return {
start: function start() {
return 0;
},
end: function end() {
return bufferedTime * duration;
},
length: 1
};
case 'readyState':
return readyState;
}
return value;
} else {
return null;
}
};
vr['set' + capName] = function (value) {
if (vrPlayer !== null) {
switch (propName) {
case 'src':
var _url = typeof value === 'string' ? value : value[0].src;
vrPlayer.setContentInfo({ video: _url });
break;
case 'currentTime':
vrPlayer.setCurrentTime(value);
setTimeout(function () {
var event = mejs.Utils.createEvent('timeupdate', vr);
mediaElement.dispatchEvent(event);
}, 50);
break;
case 'volume':
vrPlayer.setVolume(value);
setTimeout(function () {
var event = mejs.Utils.createEvent('volumechange', vr);
mediaElement.dispatchEvent(event);
}, 50);
break;
case 'muted':
volume = value ? 0 : oldVolume;
vrPlayer.setVolume(volume);
setTimeout(function () {
var event = mejs.Utils.createEvent('volumechange', vr);
mediaElement.dispatchEvent(event);
}, 50);
break;
case 'readyState':
var event = mejs.Utils.createEvent('canplay', vr);
mediaElement.dispatchEvent(event);
break;
default:
console.log('VRView ' + vr.id, propName, 'UNSUPPORTED property');
break;
}
} else {
stack.push({ type: 'set', propName: propName, value: value });
}
};
};
for (var i = 0, il = props.length; i < il; i++) {
assignGettersSetters(props[i]);
}
var methods = mejs.html5media.methods,
assignMethods = function assignMethods(methodName) {
vr[methodName] = function () {
if (vrPlayer !== null) {
switch (methodName) {
case 'play':
return vrPlayer.play();
case 'pause':
return vrPlayer.pause();
case 'load':
return null;
}
} else {
stack.push({ type: 'call', methodName: methodName });
}
};
};
for (var _i = 0, _il = methods.length; _i < _il; _i++) {
assignMethods(methods[_i]);
}
var vrContainer = document.createElement('div');
vrContainer.setAttribute('id', vr.id);
vrContainer.style.width = '100%';
vrContainer.style.height = '100%';
window['__ready__' + vr.id] = function (_vrPlayer) {
mediaElement.vrPlayer = vrPlayer = _vrPlayer;
var iframe = vrContainer.querySelector('iframe');
iframe.style.width = '100%';
iframe.style.height = '100%';
if (stack.length) {
for (var _i2 = 0, _il2 = stack.length; _i2 < _il2; _i2++) {
var stackItem = stack[_i2];
if (stackItem.type === 'set') {
var propName = stackItem.propName,
capName = '' + propName.substring(0, 1).toUpperCase() + propName.substring(1);
vr['set' + capName](stackItem.value);
} else if (stackItem.type === 'call') {
vr[stackItem.methodName]();
}
}
}
vrPlayer.on('ready', function () {
var events = mejs.html5media.events.concat(['mouseover', 'mouseout']);
var _loop = function _loop(_i3, _il3) {
vrPlayer.on(events[_i3], function () {
var event = mejs.Utils.createEvent(events[_i3], vr);
mediaElement.dispatchEvent(event);
});
};
for (var _i3 = 0, _il3 = events.length; _i3 < _il3; _i3++) {
_loop(_i3, _il3);
}
});
};
mediaElement.originalNode.parentNode.insertBefore(vrContainer, mediaElement.originalNode);
mediaElement.originalNode.style.display = 'none';
if (mediaElement.originalNode.getAttribute('data-360video-stereo') === 'false') {
options.vrIsStereo = false;
}
var vrSettings = {
path: options.vrPath,
is_stereo: options.vrIsStereo,
is_autopan_off: options.vrIsAutopanOff,
is_debug: options.vrDebug,
default_yaw: options.vrDefaultYaw,
is_yaw_only: options.vrIsYawOnly,
loop: options.loop
};
if (mediaFiles && mediaFiles.length > 0) {
for (var _i4 = 0, _il4 = mediaFiles.length; _i4 < _il4; _i4++) {
if (mejs.Renderers.renderers[options.prefix].canPlayType(mediaFiles[_i4].type)) {
vrSettings.video = mediaFiles[_i4].src;
vrSettings.width = '100%';
vrSettings.height = '100%';
break;
}
}
}
VrAPI.prepareSettings({
options: vrSettings,
id: vr.id
});
vr.hide = function () {
vr.pause();
if (vrPlayer) {
vrContainer.style.display = 'none';
}
};
vr.setSize = function () {};
vr.show = function () {
if (vrPlayer) {
vrContainer.style.display = '';
}
};
return vr;
}
};
mejs.Renderers.add(VrRenderer);
Object.assign(mejs.MepDefaults, {
vrPath: null,
vrIsStereo: true,
vrIsAutopanOff: true,
vrDebug: false,
vrDefaultYaw: 0,
vrIsYawOnly: false
});
Object.assign(MediaElementPlayer.prototype, {
buildvrview: function buildvrview(player, controls, layers, media) {
var t = this;
if (!t.isVideo || t.isVideo && t.media.rendererName !== null && !t.media.rendererName.match(/(native\_(dash|hls)|html5)/)) {
return;
}
if (player.node.getAttribute('data-360video') !== 'true') {
return;
}
var button = document.createElement('div');
player.detectFullscreenMode();
button.className = t.options.classPrefix + 'button ' + t.options.classPrefix + 'vrview-button';
button.innerHTML = '<button type="button" aria-controls="' + t.id + '" title="VR" aria-label="VR" tabindex="0"></button>';
button.addEventListener('click', function () {
var isFullScreen = mejs.Features.HAS_TRUE_NATIVE_FULLSCREEN && mejs.Features.IS_FULLSCREEN || player.isFullScreen;
if (isFullScreen) {
player.exitFullScreen();
} else {
player.enterFullScreen();
}
});
t.globalBind('keydown', function (e) {
var key = e.which || e.keyCode || 0;
if (key === 27 && (mejs.Features.HAS_TRUE_NATIVE_FULLSCREEN && mejs.Features.IS_FULLSCREEN || player.isFullScreen)) {
player.exitFullScreen();
}
});
// t.addControlElement(button, 'vrview');
var url = media.getSrc(),
mediaFiles = [{ src: url, type: mejs.Utils.getTypeFromFile(url) }],
renderInfo = mejs.Renderers.select(mediaFiles, ['vrview']);
media.changeRenderer(renderInfo.rendererName, mediaFiles);
}
});
},{}]},{},[1]);

@ -0,0 +1 @@
.mejs-vrview-button>button,.mejs__vrview-button>button{background:url(cardboard.svg) no-repeat 0 4px}

File diff suppressed because one or more lines are too long

@ -0,0 +1,27 @@
Want to contribute? Great! First, read this page (including the small print at the end).
### Before you contribute
Before we can use your code, you must sign the
[Google Individual Contributor License Agreement]
(https://cla.developers.google.com/about/google-individual)
(CLA), which you can do online. The CLA is necessary mainly because you own the
copyright to your changes, even after your contribution becomes part of our
codebase, so we need your permission to use and distribute your code. We also
need to be sure of various other things—for instance that you'll tell us if you
know that your code infringes on other people's patents. You don't have to sign
the CLA until after you've submitted your code for review and a member has
approved it, but you must do it before we can put your code into our codebase.
Before you start working on a larger contribution, you should get in touch with
us first through the issue tracker with your idea so that we can help out and
possibly guide you. Coordinating up front makes it much easier to avoid
frustration later on.
### Code reviews
All submissions, including submissions by project members, require review. We
use GitHub pull requests for this purpose.
### The small print
Contributions made by corporations are covered by a different agreement than
the one above, the
[Software Grant and Corporate Contributor License Agreement]
(https://cla.developers.google.com/about/google-corporate).

@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2015 Google Inc
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

@ -0,0 +1,73 @@
# VR View
[![Build Status](https://travis-ci.org/googlevr/vrview.svg?branch=master)](https://travis-ci.org/googlevr/vrview)
[![dependencies Status](https://david-dm.org/googlevr/vrview/status.svg)](https://david-dm.org/googlevr/vrview)
[![devDependencies Status](https://david-dm.org/googlevr/vrview/dev-status.svg)](https://david-dm.org/googlevr/vrview?type=dev)
VR View allows you to embed 360 degree VR media into websites on desktop and
mobile. For more information, please read the documentation available at
<http://developers.google.com/cardboard/vrview>.
# Configuration
A complete list of VR View parameters can be found in the table below.
Name | Type | Parameter description
---- | ---- | ---------------------
`video` | String | URL to a 360° video file or an adaptive streaming manifest file (.mpd or .m3u8). Exactly one of video or image is required.
`image` | String | URL to a 360° image file. Exactly one of video or image is required.
`width` | String | String value for the iframe's width attribute.
`height` | String | String value for the iframe's height attribute.
`preview` | String | (Optional) URL to a preview image for a 360° image file.
`is_stereo` | Boolean | (Optional) Indicates whether the content at the image or video URL is stereo or not.
`is_debug` | Boolean | (Optional) When true, turns on debug features like rendering hotspots ad showing the FPS meter.
`is_vr_off` | Boolean | (Optional) When true, disables the VR mode button.
`is_autopan_off` | Boolean | (Optional) When true, disables the autopan introduction on desktop.
`default_yaw` | Number | (Optional) Numeric angle in degrees of the initial heading for the 360° content. By default, the camera points at the center of the underlying image.
`is_yaw_only` | Boolean | (Optional) When true, prevents roll and pitch. This is intended for stereo panoramas.
`loop` | Boolean | (Optional) When false, stops the loop in the video.
`hide_fullscreen_button` | Boolean | (Optional) When true, the fullscreen button contained inside the VR View iframe will be hidden. This parameter is useful if the user wants to use VR View's fullscreen workflow (via `vrView.setFullscreen()` callback) with an element outside the iframe.
`volume` | Number | (Optional) The initial volume of the media; it ranges between 0 and 1; zero equals muted.
`muted` | Boolean | (Optional) When true, mutes the sound of the video.
# Downloading files
The `gh-pages` branch hosts the built files. Download these instead of linking to these
locations, since the directory structure of the repo may change in the future.
* [https://googlevr.github.io/vrview/build/vrview.js](https://googlevr.github.io/vrview/build/vrview.js)
* [https://googlevr.github.io/vrview/build/vrview.min.js](https://googlevr.github.io/vrview/build/vrview.min.js)
* [https://googlevr.github.io/vrview/build/embed.js](https://googlevr.github.io/vrview/build/embed.js)
* [https://googlevr.github.io/vrview/build/embed.min.js](https://googlevr.github.io/vrview/build/embed.min.js)
* [https://googlevr.github.io/vrview/build/three.js](https://googlevr.github.io/vrview/build/three.js)
* [https://googlevr.github.io/vrview/build/three.min.js](https://googlevr.github.io/vrview/build/three.min.js)
* [https://googlevr.github.io/vrview/build/device-motion-sender.min.js](https://googlevr.github.io/vrview/build/device-motion-sender.min.js)
# Building
This project uses `browserify` to manage dependencies and build. `watchify` is
especially convenient to preserve the write-and-reload model of development.
This package lives in the npm index.
**Current builds are not working on Windows ([#261](https://github.com/googlevr/vrview/issues/261))**
Relevant commands:
```shell
$ npm run build # builds the iframe embed and JS API (full and minified versions).
$ npm run build-api # builds the JS API (full and minified versions).
$ npm run build-min # builds the minified iframe embed.
$ npm run build-dev # builds the full iframe embed.
$ npm run build-api-min # builds the minified JS API.
$ npm run build-api-dev # builds the full JS API.
$ npm run watch # auto-builds the iframe embed whenever any source changes.
$ npm run watch-api # auto-builds the JS API code whenever any source changes.
```
As of 2017/06/13, the pre-built js artifacts have been removed from source
control. You must run `npm run build` prior to trying any of the examples.
# Release Notes
## 2.0.2
Close vulnerability with unsanitized user strings being injected into DOM

@ -0,0 +1 @@
function DeviceMotionSender(){if(!this.isIOS_()){return}window.addEventListener("devicemotion",this.onDeviceMotion_.bind(this),false);this.iframes=document.querySelectorAll("iframe.vrview")}DeviceMotionSender.prototype.onDeviceMotion_=function(e){var message={type:"DeviceMotion",deviceMotionEvent:this.cloneDeviceMotionEvent_(e)};for(var i=0;i<this.iframes.length;i++){var iframe=this.iframes[i];var iframeWindow=iframe.contentWindow;if(this.isCrossDomainIframe_(iframe)){iframeWindow.postMessage(message,"*")}}};DeviceMotionSender.prototype.cloneDeviceMotionEvent_=function(e){return{acceleration:{x:e.acceleration.x,y:e.acceleration.y,z:e.acceleration.z},accelerationIncludingGravity:{x:e.accelerationIncludingGravity.x,y:e.accelerationIncludingGravity.y,z:e.accelerationIncludingGravity.z},rotationRate:{alpha:e.rotationRate.alpha,beta:e.rotationRate.beta,gamma:e.rotationRate.gamma},interval:e.interval}};DeviceMotionSender.prototype.isIOS_=function(){return/iPad|iPhone|iPod/.test(navigator.userAgent)&&!window.MSStream};DeviceMotionSender.prototype.isCrossDomainIframe_=function(iframe){var html=null;try{var doc=iframe.contentDocument||iframe.contentWindow.document;html=doc.body.innerHTML}catch(err){}return html===null};var dms=new DeviceMotionSender;

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -0,0 +1,995 @@
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.VRView = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(_dereq_,module,exports){
'use strict';
var has = Object.prototype.hasOwnProperty;
//
// We store our EE objects in a plain object whose properties are event names.
// If `Object.create(null)` is not supported we prefix the event names with a
// `~` to make sure that the built-in object properties are not overridden or
// used as an attack vector.
// We also assume that `Object.create(null)` is available when the event name
// is an ES6 Symbol.
//
var prefix = typeof Object.create !== 'function' ? '~' : false;
/**
* Representation of a single EventEmitter function.
*
* @param {Function} fn Event handler to be called.
* @param {Mixed} context Context for function execution.
* @param {Boolean} [once=false] Only emit once
* @api private
*/
function EE(fn, context, once) {
this.fn = fn;
this.context = context;
this.once = once || false;
}
/**
* Minimal EventEmitter interface that is molded against the Node.js
* EventEmitter interface.
*
* @constructor
* @api public
*/
function EventEmitter() { /* Nothing to set */ }
/**
* Hold the assigned EventEmitters by name.
*
* @type {Object}
* @private
*/
EventEmitter.prototype._events = undefined;
/**
* Return an array listing the events for which the emitter has registered
* listeners.
*
* @returns {Array}
* @api public
*/
EventEmitter.prototype.eventNames = function eventNames() {
var events = this._events
, names = []
, name;
if (!events) return names;
for (name in events) {
if (has.call(events, name)) names.push(prefix ? name.slice(1) : name);
}
if (Object.getOwnPropertySymbols) {
return names.concat(Object.getOwnPropertySymbols(events));
}
return names;
};
/**
* Return a list of assigned event listeners.
*
* @param {String} event The events that should be listed.
* @param {Boolean} exists We only need to know if there are listeners.
* @returns {Array|Boolean}
* @api public
*/
EventEmitter.prototype.listeners = function listeners(event, exists) {
var evt = prefix ? prefix + event : event
, available = this._events && this._events[evt];
if (exists) return !!available;
if (!available) return [];
if (available.fn) return [available.fn];
for (var i = 0, l = available.length, ee = new Array(l); i < l; i++) {
ee[i] = available[i].fn;
}
return ee;
};
/**
* Emit an event to all registered event listeners.
*
* @param {String} event The name of the event.
* @returns {Boolean} Indication if we've emitted an event.
* @api public
*/
EventEmitter.prototype.emit = function emit(event, a1, a2, a3, a4, a5) {
var evt = prefix ? prefix + event : event;
if (!this._events || !this._events[evt]) return false;
var listeners = this._events[evt]
, len = arguments.length
, args
, i;
if ('function' === typeof listeners.fn) {
if (listeners.once) this.removeListener(event, listeners.fn, undefined, true);
switch (len) {
case 1: return listeners.fn.call(listeners.context), true;
case 2: return listeners.fn.call(listeners.context, a1), true;
case 3: return listeners.fn.call(listeners.context, a1, a2), true;
case 4: return listeners.fn.call(listeners.context, a1, a2, a3), true;
case 5: return listeners.fn.call(listeners.context, a1, a2, a3, a4), true;
case 6: return listeners.fn.call(listeners.context, a1, a2, a3, a4, a5), true;
}
for (i = 1, args = new Array(len -1); i < len; i++) {
args[i - 1] = arguments[i];
}
listeners.fn.apply(listeners.context, args);
} else {
var length = listeners.length
, j;
for (i = 0; i < length; i++) {
if (listeners[i].once) this.removeListener(event, listeners[i].fn, undefined, true);
switch (len) {
case 1: listeners[i].fn.call(listeners[i].context); break;
case 2: listeners[i].fn.call(listeners[i].context, a1); break;
case 3: listeners[i].fn.call(listeners[i].context, a1, a2); break;
default:
if (!args) for (j = 1, args = new Array(len -1); j < len; j++) {
args[j - 1] = arguments[j];
}
listeners[i].fn.apply(listeners[i].context, args);
}
}
}
return true;
};
/**
* Register a new EventListener for the given event.
*
* @param {String} event Name of the event.
* @param {Function} fn Callback function.
* @param {Mixed} [context=this] The context of the function.
* @api public
*/
EventEmitter.prototype.on = function on(event, fn, context) {
var listener = new EE(fn, context || this)
, evt = prefix ? prefix + event : event;
if (!this._events) this._events = prefix ? {} : Object.create(null);
if (!this._events[evt]) this._events[evt] = listener;
else {
if (!this._events[evt].fn) this._events[evt].push(listener);
else this._events[evt] = [
this._events[evt], listener
];
}
return this;
};
/**
* Add an EventListener that's only called once.
*
* @param {String} event Name of the event.
* @param {Function} fn Callback function.
* @param {Mixed} [context=this] The context of the function.
* @api public
*/
EventEmitter.prototype.once = function once(event, fn, context) {
var listener = new EE(fn, context || this, true)
, evt = prefix ? prefix + event : event;
if (!this._events) this._events = prefix ? {} : Object.create(null);
if (!this._events[evt]) this._events[evt] = listener;
else {
if (!this._events[evt].fn) this._events[evt].push(listener);
else this._events[evt] = [
this._events[evt], listener
];
}
return this;
};
/**
* Remove event listeners.
*
* @param {String} event The event we want to remove.
* @param {Function} fn The listener that we need to find.
* @param {Mixed} context Only remove listeners matching this context.
* @param {Boolean} once Only remove once listeners.
* @api public
*/
EventEmitter.prototype.removeListener = function removeListener(event, fn, context, once) {
var evt = prefix ? prefix + event : event;
if (!this._events || !this._events[evt]) return this;
var listeners = this._events[evt]
, events = [];
if (fn) {
if (listeners.fn) {
if (
listeners.fn !== fn
|| (once && !listeners.once)
|| (context && listeners.context !== context)
) {
events.push(listeners);
}
} else {
for (var i = 0, length = listeners.length; i < length; i++) {
if (
listeners[i].fn !== fn
|| (once && !listeners[i].once)
|| (context && listeners[i].context !== context)
) {
events.push(listeners[i]);
}
}
}
}
//
// Reset the array, or remove it completely if we have no more listeners.
//
if (events.length) {
this._events[evt] = events.length === 1 ? events[0] : events;
} else {
delete this._events[evt];
}
return this;
};
/**
* Remove all listeners or only the listeners for the specified event.
*
* @param {String} event The event want to remove all listeners for.
* @api public
*/
EventEmitter.prototype.removeAllListeners = function removeAllListeners(event) {
if (!this._events) return this;
if (event) delete this._events[prefix ? prefix + event : event];
else this._events = prefix ? {} : Object.create(null);
return this;
};
//
// Alias methods names because people roll like that.
//
EventEmitter.prototype.off = EventEmitter.prototype.removeListener;
EventEmitter.prototype.addListener = EventEmitter.prototype.on;
//
// This function doesn't apply anymore.
//
EventEmitter.prototype.setMaxListeners = function setMaxListeners() {
return this;
};
//
// Expose the prefix.
//
EventEmitter.prefixed = prefix;
//
// Expose the module.
//
if ('undefined' !== typeof module) {
module.exports = EventEmitter;
}
},{}],2:[function(_dereq_,module,exports){
/*
* Copyright 2016 Google Inc. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
var Message = _dereq_('../message');
/**
* Sends events to the embedded VR view IFrame via postMessage. Also handles
* messages sent back from the IFrame:
*
* click: When a hotspot was clicked.
* modechange: When the user changes viewing mode (VR|Fullscreen|etc).
*/
function IFrameMessageSender(iframe) {
if (!iframe) {
console.error('No iframe specified');
return;
}
this.iframe = iframe;
// On iOS, if the iframe is across domains, also send DeviceMotion data.
if (this.isIOS_()) {
window.addEventListener('devicemotion', this.onDeviceMotion_.bind(this), false);
}
}
/**
* Sends a message to the associated VR View IFrame.
*/
IFrameMessageSender.prototype.send = function(message) {
var iframeWindow = this.iframe.contentWindow;
iframeWindow.postMessage(message, '*');
};
IFrameMessageSender.prototype.onDeviceMotion_ = function(e) {
var message = {
type: Message.DEVICE_MOTION,
deviceMotionEvent: this.cloneDeviceMotionEvent_(e)
};
this.send(message);
};
IFrameMessageSender.prototype.cloneDeviceMotionEvent_ = function(e) {
return {
acceleration: {
x: e.acceleration.x,
y: e.acceleration.y,
z: e.acceleration.z,
},
accelerationIncludingGravity: {
x: e.accelerationIncludingGravity.x,
y: e.accelerationIncludingGravity.y,
z: e.accelerationIncludingGravity.z,
},
rotationRate: {
alpha: e.rotationRate.alpha,
beta: e.rotationRate.beta,
gamma: e.rotationRate.gamma,
},
interval: e.interval,
timeStamp: e.timeStamp
};
};
IFrameMessageSender.prototype.isIOS_ = function() {
return /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
};
module.exports = IFrameMessageSender;
},{"../message":5}],3:[function(_dereq_,module,exports){
/*
* Copyright 2016 Google Inc. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
var Player = _dereq_('./player');
var VRView = {
Player: Player
};
module.exports = VRView;
},{"./player":4}],4:[function(_dereq_,module,exports){
/*
* Copyright 2016 Google Inc. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
var EventEmitter = _dereq_('eventemitter3');
var IFrameMessageSender = _dereq_('./iframe-message-sender');
var Message = _dereq_('../message');
var Util = _dereq_('../util');
// Save the executing script. This will be used to calculate the embed URL.
var CURRENT_SCRIPT_SRC = Util.getCurrentScript().src;
var FAKE_FULLSCREEN_CLASS = 'vrview-fake-fullscreen';
/**
* Entry point for the VR View JS API.
*
* Emits the following events:
* ready: When the player is loaded.
* modechange: When the viewing mode changes (normal, fullscreen, VR).
* click (id): When a hotspot is clicked.
*/
function Player(selector, contentInfo) {
// Create a VR View iframe depending on the parameters.
var iframe = this.createIframe_(contentInfo);
this.iframe = iframe;
var parentEl = document.querySelector(selector);
parentEl.appendChild(iframe);
// Make a sender as well, for relying commands to the child IFrame.
this.sender = new IFrameMessageSender(iframe);
// Listen to messages from the IFrame.
window.addEventListener('message', this.onMessage_.bind(this), false);
// Expose a public .isPaused attribute.
this.isPaused = false;
// Expose a public .isMuted attribute.
this.isMuted = false;
if (typeof contentInfo.muted !== 'undefined') {
this.isMuted = contentInfo.muted;
}
// Other public attributes
this.currentTime = 0;
this.duration = 0;
this.volume = contentInfo.volume != undefined ? contentInfo.volume : 1;
if (Util.isIOS()) {
this.injectFullscreenStylesheet_();
}
}
Player.prototype = new EventEmitter();
/**
* @param pitch {Number} The latitude of center, specified in degrees, between
* -90 and 90, with 0 at the horizon.
* @param yaw {Number} The longitude of center, specified in degrees, between
* -180 and 180, with 0 at the image center.
* @param radius {Number} The radius of the hotspot, specified in meters.
* @param distance {Number} The distance of the hotspot from camera, specified
* in meters.
* @param hotspotId {String} The ID of the hotspot.
*/
Player.prototype.addHotspot = function(hotspotId, params) {
// TODO: Add validation to params.
var data = {
pitch: params.pitch,
yaw: params.yaw,
radius: params.radius,
distance: params.distance,
id: hotspotId
};
this.sender.send({type: Message.ADD_HOTSPOT, data: data});
};
Player.prototype.play = function() {
this.sender.send({type: Message.PLAY});
};
Player.prototype.pause = function() {
this.sender.send({type: Message.PAUSE});
};
/**
* Equivalent of HTML5 setSrc().
* @param {String} contentInfo
*/
Player.prototype.setContent = function(contentInfo) {
this.absolutifyPaths_(contentInfo);
var data = {
contentInfo: contentInfo
};
this.sender.send({type: Message.SET_CONTENT, data: data});
};
/**
* Sets the software volume of the video. 0 is mute, 1 is max.
*/
Player.prototype.setVolume = function(volumeLevel) {
var data = {
volumeLevel: volumeLevel
};
this.sender.send({type: Message.SET_VOLUME, data: data});
};
Player.prototype.getVolume = function() {
return this.volume;
};
/**
* Sets the mute state of the video element. true is muted, false is unmuted.
*/
Player.prototype.mute = function(muteState) {
var data = {
muteState: muteState
};
this.sender.send({type: Message.MUTED, data: data});
};
/**
* Set the current time of the media being played
* @param {Number} time
*/
Player.prototype.setCurrentTime = function(time) {
var data = {
currentTime: time
};
this.sender.send({type: Message.SET_CURRENT_TIME, data: data});
};
Player.prototype.getCurrentTime = function() {
return this.currentTime;
};
Player.prototype.getDuration = function() {
return this.duration;
};
Player.prototype.setFullscreen = function() {
this.sender.send({type: Message.SET_FULLSCREEN});
};
/**
* Helper for creating an iframe.
*
* @return {IFrameElement} The iframe.
*/
Player.prototype.createIframe_ = function(contentInfo) {
this.absolutifyPaths_(contentInfo);
var iframe = document.createElement('iframe');
iframe.setAttribute('allowfullscreen', true);
iframe.setAttribute('scrolling', 'no');
iframe.style.border = 0;
// Handle iframe size if width and height are specified.
if (contentInfo.hasOwnProperty('width')) {
iframe.setAttribute('width', contentInfo.width);
delete contentInfo.width;
}
if (contentInfo.hasOwnProperty('height')) {
iframe.setAttribute('height', contentInfo.height);
delete contentInfo.height;
}
var url = this.getEmbedUrl_() + Util.createGetParams(contentInfo);
iframe.src = url;
return iframe;
};
Player.prototype.onMessage_ = function(event) {
var message = event.data;
if (!message || !message.type) {
console.warn('Received message with no type.');
return;
}
var type = message.type.toLowerCase();
var data = message.data;
switch (type) {
case 'ready':
if (data !== undefined && data.duration !== undefined) {
this.duration = data.duration;
}
case 'modechange':
case 'error':
case 'click':
case 'ended':
case 'getposition':
this.emit(type, data);
break;
case 'volumechange':
this.volume = data;
this.emit('volumechange', data);
break;
case 'muted':
this.isMuted = data;
this.emit('mute', data);
break;
case 'timeupdate':
this.currentTime = data;
this.emit('timeupdate', {
currentTime: this.currentTime,
duration: this.duration
});
break;
case 'play':
case 'paused':
this.isPaused = data;
if (this.isPaused) {
this.emit('pause', data);
} else {
this.emit('play', data);
}
break;
case 'enter-fullscreen':
case 'enter-vr':
this.setFakeFullscreen_(true);
break;
case 'exit-fullscreen':
this.setFakeFullscreen_(false);
break;
default:
console.warn('Got unknown message of type %s from %s', message.type, message.origin);
}
};
/**
* Note: iOS doesn't support the fullscreen API.
* In standalone <iframe> mode, VR View emulates fullscreen by redirecting to
* another page.
* In JS API mode, we stretch the iframe to cover the extent of the page using
* CSS. To do this cleanly, we also inject a stylesheet.
*/
Player.prototype.setFakeFullscreen_ = function(isFullscreen) {
if (isFullscreen) {
this.iframe.classList.add(FAKE_FULLSCREEN_CLASS);
} else {
this.iframe.classList.remove(FAKE_FULLSCREEN_CLASS);
}
};
Player.prototype.injectFullscreenStylesheet_ = function() {
var styleString = [
'iframe.' + FAKE_FULLSCREEN_CLASS,
'{',
'position: fixed !important;',
'display: block !important;',
'z-index: 9999999999 !important;',
'top: 0 !important;',
'left: 0 !important;',
'width: 100% !important;',
'height: 100% !important;',
'margin: 0 !important;',
'}',
].join('\n');
var style = document.createElement('style');
style.innerHTML = styleString;
document.body.appendChild(style);
};
Player.prototype.getEmbedUrl_ = function() {
// Assume that the script is in $ROOT/build/something.js, and that the iframe
// HTML is in $ROOT/index.html.
//
// E.g: /vrview/2.0/build/vrview.min.js => /vrview/2.0/index.html.
var path = CURRENT_SCRIPT_SRC;
var split = path.split('/');
var rootSplit = split.slice(0, split.length - 2);
var rootPath = rootSplit.join('/');
return rootPath + '/index.html';
};
Player.prototype.getDirName_ = function() {
var path = window.location.pathname;
path = path.substring(0, path.lastIndexOf('/'));
return location.protocol + '//' + location.host + path;
};
/**
* Make all of the URLs inside contentInfo absolute instead of relative.
*/
Player.prototype.absolutifyPaths_ = function(contentInfo) {
var dirName = this.getDirName_();
var urlParams = ['image', 'preview', 'video'];
for (var i = 0; i < urlParams.length; i++) {
var name = urlParams[i];
var path = contentInfo[name];
if (path && Util.isPathAbsolute(path)) {
var absolute = Util.relativeToAbsolutePath(dirName, path);
contentInfo[name] = absolute;
//console.log('Converted to absolute: %s', absolute);
}
}
};
/**
* Get position YAW, PITCH
*/
Player.prototype.getPosition = function() {
this.sender.send({type: Message.GET_POSITION, data: {}});
};
module.exports = Player;
},{"../message":5,"../util":6,"./iframe-message-sender":2,"eventemitter3":1}],5:[function(_dereq_,module,exports){
/*
* Copyright 2016 Google Inc. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Messages from the API to the embed.
*/
var Message = {
PLAY: 'play',
PAUSE: 'pause',
TIMEUPDATE: 'timeupdate',
ADD_HOTSPOT: 'addhotspot',
SET_CONTENT: 'setimage',
SET_VOLUME: 'setvolume',
MUTED: 'muted',
SET_CURRENT_TIME: 'setcurrenttime',
DEVICE_MOTION: 'devicemotion',
GET_POSITION: 'getposition',
SET_FULLSCREEN: 'setfullscreen',
};
module.exports = Message;
},{}],6:[function(_dereq_,module,exports){
/*
* Copyright 2016 Google Inc. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
var Util = window.Util || {};
Util.isDataURI = function(src) {
return src && src.indexOf('data:') == 0;
};
Util.generateUUID = function() {
function s4() {
return Math.floor((1 + Math.random()) * 0x10000)
.toString(16)
.substring(1);
}
return s4() + s4() + '-' + s4() + '-' + s4() + '-' +
s4() + '-' + s4() + s4() + s4();
};
Util.isMobile = function() {
var check = false;
(function(a){if(/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i.test(a)||/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(a.substr(0,4)))check = true})(navigator.userAgent||navigator.vendor||window.opera);
return check;
};
Util.isIOS = function() {
return /(iPad|iPhone|iPod)/g.test(navigator.userAgent);
};
Util.isSafari = function() {
return /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
};
Util.cloneObject = function(obj) {
var out = {};
for (key in obj) {
out[key] = obj[key];
}
return out;
};
Util.hashCode = function(s) {
return s.split("").reduce(function(a,b){a=((a<<5)-a)+b.charCodeAt(0);return a&a},0);
};
Util.loadTrackSrc = function(context, src, callback, opt_progressCallback) {
var request = new XMLHttpRequest();
request.open('GET', src, true);
request.responseType = 'arraybuffer';
// Decode asynchronously.
request.onload = function() {
context.decodeAudioData(request.response, function(buffer) {
callback(buffer);
}, function(e) {
console.error(e);
});
};
if (opt_progressCallback) {
request.onprogress = function(e) {
var percent = e.loaded / e.total;
opt_progressCallback(percent);
};
}
request.send();
};
Util.isPow2 = function(n) {
return (n & (n - 1)) == 0;
};
Util.capitalize = function(s) {
return s.charAt(0).toUpperCase() + s.slice(1);
};
Util.isIFrame = function() {
try {
return window.self !== window.top;
} catch (e) {
return true;
}
};
// From http://goo.gl/4WX3tg
Util.getQueryParameter = function(name) {
name = name.replace(/[\[]/, "\\[").replace(/[\]]/, "\\]");
var regex = new RegExp("[\\?&]" + name + "=([^&#]*)"),
results = regex.exec(location.search);
return results === null ? "" : decodeURIComponent(results[1].replace(/\+/g, " "));
};
// From http://stackoverflow.com/questions/11871077/proper-way-to-detect-webgl-support.
Util.isWebGLEnabled = function() {
var canvas = document.createElement('canvas');
try { gl = canvas.getContext("webgl"); }
catch (x) { gl = null; }
if (gl == null) {
try { gl = canvas.getContext("experimental-webgl"); experimental = true; }
catch (x) { gl = null; }
}
return !!gl;
};
Util.clone = function(obj) {
return JSON.parse(JSON.stringify(obj));
};
// From http://stackoverflow.com/questions/10140604/fastest-hypotenuse-in-javascript
Util.hypot = Math.hypot || function(x, y) {
return Math.sqrt(x*x + y*y);
};
// From http://stackoverflow.com/a/17447718/693934
Util.isIE11 = function() {
return navigator.userAgent.match(/Trident/);
};
Util.getRectCenter = function(rect) {
return new THREE.Vector2(rect.x + rect.width/2, rect.y + rect.height/2);
};
Util.getScreenWidth = function() {
return Math.max(window.screen.width, window.screen.height) *
window.devicePixelRatio;
};
Util.getScreenHeight = function() {
return Math.min(window.screen.width, window.screen.height) *
window.devicePixelRatio;
};
Util.isIOS9OrLess = function() {
if (!Util.isIOS()) {
return false;
}
var re = /(iPhone|iPad|iPod) OS ([\d_]+)/;
var iOSVersion = navigator.userAgent.match(re);
if (!iOSVersion) {
return false;
}
// Get the last group.
var versionString = iOSVersion[iOSVersion.length - 1];
var majorVersion = parseFloat(versionString);
return majorVersion <= 9;
};
Util.getExtension = function(url) {
return url.split('.').pop().split('?')[0];
};
Util.createGetParams = function(params) {
var out = '?';
for (var k in params) {
var paramString = k + '=' + params[k] + '&';
out += paramString;
}
// Remove the trailing ampersand.
out.substring(0, params.length - 2);
return out;
};
Util.sendParentMessage = function(message) {
if (window.parent) {
parent.postMessage(message, '*');
}
};
Util.parseBoolean = function(value) {
if (value == 'false' || value == 0) {
return false;
} else if (value == 'true' || value == 1) {
return true;
} else {
return !!value;
}
};
/**
* @param base {String} An absolute directory root.
* @param relative {String} A relative path.
*
* @returns {String} An absolute path corresponding to the rootPath.
*
* From http://stackoverflow.com/a/14780463/693934.
*/
Util.relativeToAbsolutePath = function(base, relative) {
var stack = base.split('/');
var parts = relative.split('/');
for (var i = 0; i < parts.length; i++) {
if (parts[i] == '.') {
continue;
}
if (parts[i] == '..') {
stack.pop();
} else {
stack.push(parts[i]);
}
}
return stack.join('/');
};
/**
* @return {Boolean} True iff the specified path is an absolute path.
*/
Util.isPathAbsolute = function(path) {
return ! /^(?:\/|[a-z]+:\/\/)/.test(path);
}
Util.isEmptyObject = function(obj) {
return Object.getOwnPropertyNames(obj).length == 0;
};
Util.isDebug = function() {
return Util.parseBoolean(Util.getQueryParameter('debug'));
};
Util.getCurrentScript = function() {
// Note: in IE11, document.currentScript doesn't work, so we fall back to this
// hack, taken from https://goo.gl/TpExuH.
if (!document.currentScript) {
console.warn('This browser does not support document.currentScript. Trying fallback.');
}
return document.currentScript || document.scripts[document.scripts.length - 1];
}
module.exports = Util;
},{}]},{},[3])(3)
});

File diff suppressed because one or more lines are too long

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24px" height="24px" viewBox="0 0 24 24" fill="#000000">
<circle cx="12" cy="12" r="10" fill="white"/>
<path d="M0 0h24v24H0z" fill="none"/>
<path d="M11 17h2v-6h-2v6zm1-15C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zM11 9h2V7h-2v2z"/>
</svg>

After

Width:  |  Height:  |  Size: 375 B

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24px" height="24px" viewBox="0 0 24 24" fill="#FFFFFF">
<path d="M8 5v14l11-7z"/>
<path d="M0 0h24v24H0z" fill="none"/>
</svg>

After

Width:  |  Height:  |  Size: 182 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 188 KiB

@ -0,0 +1,41 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>VR view</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<link rel="stylesheet" href="style.css">
</head>
<body>
<div id="error" class="dialog">
<div class="wrap">
<h1 class="title">Error</h1>
<p class="message">An unknown error occurred.</p>
</div>
</div>
<div id="play-overlay">
<img src="images/ic_play_arrow_white_24dp.svg" />
</div>
<a id="watermark" href="http://g.co/vr/view" target="_blank">
<img src="images/ic_info_outline_black_24dp.svg" />
</a>
<script>
WebVRConfig = {
BUFFER_SCALE: 0.5,
TOUCH_PANNER_DISABLED: false
};
</script>
<script src="build/three.min.js"></script>
<script src="build/embed.min.js"></script>
</body>
</html>

@ -0,0 +1,33 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>VR view</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<link rel="stylesheet" href="style.css">
</head>
<body>
<div id="error" class="dialog">
<div class="wrap">
<h1 class="title">Error</h1>
<p class="message">An unknown error occurred.</p>
</div>
</div>
<script>
WebVRConfig = {
BUFFER_SCALE: 0.5,
TOUCH_PANNER_DISABLED: false
};
</script>
<script src="build/three.js"></script>
<script src="build/embed.js"></script>
</body>
</html>

File diff suppressed because it is too large Load Diff

@ -0,0 +1,56 @@
{
"name": "vrview",
"version": "2.0.2",
"description": "Embed VR content into your webpage.",
"main": "index.js",
"dependencies": {
"@tweenjs/tween.js": "^16.8.0",
"es6-promise": "^3.0.2",
"eventemitter3": "^1.2.0",
"lodash": "^4.17.5",
"shaka-player": "^2.0.0",
"stats-js": "^1.0.0-alpha1",
"three": "^0.84.0",
"urijs": "^1.18.2",
"webvr-boilerplate": "^0.4.6",
"webvr-polyfill": "^0.9.38"
},
"devDependencies": {
"browserify": "^13.1.1",
"debug": "^2.6.9",
"derequire": "^2.0.6",
"eslint": "^4.2.0",
"eslint-config-google": "^0.9.1",
"rollup": "^0.37.2",
"uglify-es": "^3.0.24",
"watchify": "^3.8.0"
},
"scripts": {
"_mkdir": "mkdir -p build",
"build-min": "npm run _mkdir && browserify src/embed/main.js | derequire | uglifyjs -c > build/embed.min.js && npm run build-three-closure",
"build-dev": "npm run _mkdir && browserify src/embed/main.js | derequire > build/embed.js && npm run build-three-closure",
"build-analytics-min": "npm run _mkdir && browserify src/embed/with-analytics.js | derequire | uglifyjs -c > build/embed.min.js && npm run build-three-closure",
"build-analytics-dev": "npm run _mkdir && browserify src/embed/with-analytics.js | derequire > build/embed.js && npm run build-three-closure",
"build-api-min": "npm run _mkdir && browserify --standalone VRView src/api/main.js | derequire | uglifyjs -c > build/vrview.min.js",
"build-api-dev": "npm run _mkdir && browserify --standalone VRView src/api/main.js | derequire > build/vrview.js",
"build": "npm run build-min; npm run build-dev; npm run build-api",
"build-analytics": "npm run build-analytics-min; npm run build-analytics-dev; npm run build-api",
"build-api": "npm run build-api-min; npm run build-api-dev",
"watch": "npm run _mkdir && watchify src/embed/main.js -v -d -o build/embed.js",
"watch-api": "npm run _mkdir && watchify --standalone VRView src/api/main.js -v -d -o build/vrview.js",
"build-three-closure": "npm run _mkdir && rollup -c src/third_party/three/rollup.config.js && java -jar src/third_party/three/closure-compiler-v20160713.jar --warning_level=VERBOSE --jscomp_off=globalThis --jscomp_off=checkTypes --externs src/third_party/three/externs.js --language_in=ECMASCRIPT5_STRICT --js build/three.js --js_output_file build/three.min.js",
"build-dms": "npm run _mkdir && uglifyjs scripts/js/device-motion-sender.js > build/device-motion-sender.min.js",
"build-all": "npm run build-analytics; npm run build-three-closure; npm run build-dms",
"test": "npm run build"
},
"repository": {
"type": "git",
"url": "git+https://github.com/google/vrview.git"
},
"author": "",
"license": "Apache-2.0",
"bugs": {
"url": "https://github.com/google/vrview/issues"
},
"homepage": "https://github.com/google/vrview#readme"
}

@ -0,0 +1,21 @@
[
{
"entity": "allUsers",
"role": "READER"
},
{
"email": "smus@google.com",
"entity": "user-smus@google.com",
"role": "OWNER"
},
{
"email": "bwuest@google.com",
"entity": "user-bwuest@google.com",
"role": "OWNER"
},
{
"email": "nathanmartz@google.com",
"entity": "user-nathanmartz@google.com",
"role": "OWNER"
}
]

@ -0,0 +1,21 @@
#!/usr/bin/env sh
#
# Copyright 2016 Google Inc. All Rights Reserved.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
SCRIPT_DIR=`dirname $BASH_SOURCE`
gsutil cp index-minified.html gs://vrview/2.0/index.html
gsutil cp -r style.css images/ build/ examples/ gs://vrview/2.0/
gsutil -m acl set -r $SCRIPT_DIR/acl.txt gs://vrview/2.0/

@ -0,0 +1,88 @@
/*
* Copyright 2016 Google Inc. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Sends DeviceMotion events to all embedded VR views via postMessage. Note:
* each iframe must have a class 'vrview'.
*
* This is a workaround for https://bugs.webkit.org/show_bug.cgi?id=150072.
*/
function DeviceMotionSender() {
// This is an iOS-specific workaround.
if (!this.isIOS_()) {
return;
}
window.addEventListener('devicemotion', this.onDeviceMotion_.bind(this), false);
// Find the right iFrame to send data to.
this.iframes = document.querySelectorAll('iframe.vrview');
}
DeviceMotionSender.prototype.onDeviceMotion_ = function(e) {
var message = {
type: 'DeviceMotion',
deviceMotionEvent: this.cloneDeviceMotionEvent_(e)
};
for (var i = 0; i < this.iframes.length; i++) {
// Only send data if we're on iOS and we are dealing with a cross-domain
// iframe.
var iframe = this.iframes[i];
var iframeWindow = iframe.contentWindow;
if (this.isCrossDomainIframe_(iframe)) {
iframeWindow.postMessage(message, '*');
}
}
};
DeviceMotionSender.prototype.cloneDeviceMotionEvent_ = function(e) {
return {
acceleration: {
x: e.acceleration.x,
y: e.acceleration.y,
z: e.acceleration.z,
},
accelerationIncludingGravity: {
x: e.accelerationIncludingGravity.x,
y: e.accelerationIncludingGravity.y,
z: e.accelerationIncludingGravity.z,
},
rotationRate: {
alpha: e.rotationRate.alpha,
beta: e.rotationRate.beta,
gamma: e.rotationRate.gamma,
},
interval: e.interval
};
};
DeviceMotionSender.prototype.isIOS_ = function() {
return /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
};
// From http://stackoverflow.com/questions/12381334/foolproof-way-to-detect-if-iframe-is-cross-domain.
DeviceMotionSender.prototype.isCrossDomainIframe_ = function(iframe) {
var html = null;
try {
var doc = iframe.contentDocument || iframe.contentWindow.document;
html = doc.body.innerHTML;
} catch (err) {
}
return (html === null);
};
var dms = new DeviceMotionSender();

@ -0,0 +1,2 @@
This directory contains all of the JavaScript that is required for the
VR View JavaScript API.

@ -0,0 +1,80 @@
/*
* Copyright 2016 Google Inc. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
var Message = require('../message');
/**
* Sends events to the embedded VR view IFrame via postMessage. Also handles
* messages sent back from the IFrame:
*
* click: When a hotspot was clicked.
* modechange: When the user changes viewing mode (VR|Fullscreen|etc).
*/
function IFrameMessageSender(iframe) {
if (!iframe) {
console.error('No iframe specified');
return;
}
this.iframe = iframe;
// On iOS, if the iframe is across domains, also send DeviceMotion data.
if (this.isIOS_()) {
window.addEventListener('devicemotion', this.onDeviceMotion_.bind(this), false);
}
}
/**
* Sends a message to the associated VR View IFrame.
*/
IFrameMessageSender.prototype.send = function(message) {
var iframeWindow = this.iframe.contentWindow;
iframeWindow.postMessage(message, '*');
};
IFrameMessageSender.prototype.onDeviceMotion_ = function(e) {
var message = {
type: Message.DEVICE_MOTION,
deviceMotionEvent: this.cloneDeviceMotionEvent_(e)
};
this.send(message);
};
IFrameMessageSender.prototype.cloneDeviceMotionEvent_ = function(e) {
return {
acceleration: {
x: e.acceleration.x,
y: e.acceleration.y,
z: e.acceleration.z,
},
accelerationIncludingGravity: {
x: e.accelerationIncludingGravity.x,
y: e.accelerationIncludingGravity.y,
z: e.accelerationIncludingGravity.z,
},
rotationRate: {
alpha: e.rotationRate.alpha,
beta: e.rotationRate.beta,
gamma: e.rotationRate.gamma,
},
interval: e.interval,
timeStamp: e.timeStamp
};
};
IFrameMessageSender.prototype.isIOS_ = function() {
return /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
};
module.exports = IFrameMessageSender;

@ -0,0 +1,22 @@
/*
* Copyright 2016 Google Inc. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
var Player = require('./player');
var VRView = {
Player: Player
};
module.exports = VRView;

@ -0,0 +1,316 @@
/*
* Copyright 2016 Google Inc. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
var EventEmitter = require('eventemitter3');
var IFrameMessageSender = require('./iframe-message-sender');
var Message = require('../message');
var Util = require('../util');
// Save the executing script. This will be used to calculate the embed URL.
var CURRENT_SCRIPT_SRC = Util.getCurrentScript().src;
var FAKE_FULLSCREEN_CLASS = 'vrview-fake-fullscreen';
/**
* Entry point for the VR View JS API.
*
* Emits the following events:
* ready: When the player is loaded.
* modechange: When the viewing mode changes (normal, fullscreen, VR).
* click (id): When a hotspot is clicked.
*/
function Player(selector, contentInfo) {
// Create a VR View iframe depending on the parameters.
var iframe = this.createIframe_(contentInfo);
this.iframe = iframe;
var parentEl = document.querySelector(selector);
parentEl.appendChild(iframe);
// Make a sender as well, for relying commands to the child IFrame.
this.sender = new IFrameMessageSender(iframe);
// Listen to messages from the IFrame.
window.addEventListener('message', this.onMessage_.bind(this), false);
// Expose a public .isPaused attribute.
this.isPaused = false;
// Expose a public .isMuted attribute.
this.isMuted = false;
if (typeof contentInfo.muted !== 'undefined') {
this.isMuted = contentInfo.muted;
}
// Other public attributes
this.currentTime = 0;
this.duration = 0;
this.volume = contentInfo.volume != undefined ? contentInfo.volume : 1;
if (Util.isIOS()) {
this.injectFullscreenStylesheet_();
}
}
Player.prototype = new EventEmitter();
/**
* @param pitch {Number} The latitude of center, specified in degrees, between
* -90 and 90, with 0 at the horizon.
* @param yaw {Number} The longitude of center, specified in degrees, between
* -180 and 180, with 0 at the image center.
* @param radius {Number} The radius of the hotspot, specified in meters.
* @param distance {Number} The distance of the hotspot from camera, specified
* in meters.
* @param hotspotId {String} The ID of the hotspot.
*/
Player.prototype.addHotspot = function(hotspotId, params) {
// TODO: Add validation to params.
var data = {
pitch: params.pitch,
yaw: params.yaw,
radius: params.radius,
distance: params.distance,
id: hotspotId
};
this.sender.send({type: Message.ADD_HOTSPOT, data: data});
};
Player.prototype.play = function() {
this.sender.send({type: Message.PLAY});
};
Player.prototype.pause = function() {
this.sender.send({type: Message.PAUSE});
};
/**
* Equivalent of HTML5 setSrc().
* @param {String} contentInfo
*/
Player.prototype.setContent = function(contentInfo) {
this.absolutifyPaths_(contentInfo);
var data = {
contentInfo: contentInfo
};
this.sender.send({type: Message.SET_CONTENT, data: data});
};
/**
* Sets the software volume of the video. 0 is mute, 1 is max.
*/
Player.prototype.setVolume = function(volumeLevel) {
var data = {
volumeLevel: volumeLevel
};
this.sender.send({type: Message.SET_VOLUME, data: data});
};
Player.prototype.getVolume = function() {
return this.volume;
};
/**
* Sets the mute state of the video element. true is muted, false is unmuted.
*/
Player.prototype.mute = function(muteState) {
var data = {
muteState: muteState
};
this.sender.send({type: Message.MUTED, data: data});
};
/**
* Set the current time of the media being played
* @param {Number} time
*/
Player.prototype.setCurrentTime = function(time) {
var data = {
currentTime: time
};
this.sender.send({type: Message.SET_CURRENT_TIME, data: data});
};
Player.prototype.getCurrentTime = function() {
return this.currentTime;
};
Player.prototype.getDuration = function() {
return this.duration;
};
Player.prototype.setFullscreen = function() {
this.sender.send({type: Message.SET_FULLSCREEN});
};
/**
* Helper for creating an iframe.
*
* @return {IFrameElement} The iframe.
*/
Player.prototype.createIframe_ = function(contentInfo) {
this.absolutifyPaths_(contentInfo);
var iframe = document.createElement('iframe');
iframe.setAttribute('allowfullscreen', true);
iframe.setAttribute('scrolling', 'no');
iframe.style.border = 0;
// Handle iframe size if width and height are specified.
if (contentInfo.hasOwnProperty('width')) {
iframe.setAttribute('width', contentInfo.width);
delete contentInfo.width;
}
if (contentInfo.hasOwnProperty('height')) {
iframe.setAttribute('height', contentInfo.height);
delete contentInfo.height;
}
var url = this.getEmbedUrl_() + Util.createGetParams(contentInfo);
iframe.src = url;
return iframe;
};
Player.prototype.onMessage_ = function(event) {
var message = event.data;
if (!message || !message.type) {
console.warn('Received message with no type.');
return;
}
var type = message.type.toLowerCase();
var data = message.data;
switch (type) {
case 'ready':
if (data !== undefined && data.duration !== undefined) {
this.duration = data.duration;
}
case 'modechange':
case 'error':
case 'click':
case 'ended':
case 'getposition':
this.emit(type, data);
break;
case 'volumechange':
this.volume = data;
this.emit('volumechange', data);
break;
case 'muted':
this.isMuted = data;
this.emit('mute', data);
break;
case 'timeupdate':
this.currentTime = data;
this.emit('timeupdate', {
currentTime: this.currentTime,
duration: this.duration
});
break;
case 'play':
case 'paused':
this.isPaused = data;
if (this.isPaused) {
this.emit('pause', data);
} else {
this.emit('play', data);
}
break;
case 'enter-fullscreen':
case 'enter-vr':
this.setFakeFullscreen_(true);
break;
case 'exit-fullscreen':
this.setFakeFullscreen_(false);
break;
default:
console.warn('Got unknown message of type %s from %s', message.type, message.origin);
}
};
/**
* Note: iOS doesn't support the fullscreen API.
* In standalone <iframe> mode, VR View emulates fullscreen by redirecting to
* another page.
* In JS API mode, we stretch the iframe to cover the extent of the page using
* CSS. To do this cleanly, we also inject a stylesheet.
*/
Player.prototype.setFakeFullscreen_ = function(isFullscreen) {
if (isFullscreen) {
this.iframe.classList.add(FAKE_FULLSCREEN_CLASS);
} else {
this.iframe.classList.remove(FAKE_FULLSCREEN_CLASS);
}
};
Player.prototype.injectFullscreenStylesheet_ = function() {
var styleString = [
'iframe.' + FAKE_FULLSCREEN_CLASS,
'{',
'position: fixed !important;',
'display: block !important;',
'z-index: 9999999999 !important;',
'top: 0 !important;',
'left: 0 !important;',
'width: 100% !important;',
'height: 100% !important;',
'margin: 0 !important;',
'}',
].join('\n');
var style = document.createElement('style');
style.innerHTML = styleString;
document.body.appendChild(style);
};
Player.prototype.getEmbedUrl_ = function() {
// Assume that the script is in $ROOT/build/something.js, and that the iframe
// HTML is in $ROOT/index.html.
//
// E.g: /vrview/2.0/build/vrview.min.js => /vrview/2.0/index.html.
var path = CURRENT_SCRIPT_SRC;
var split = path.split('/');
var rootSplit = split.slice(0, split.length - 2);
var rootPath = rootSplit.join('/');
return rootPath + '/index.html';
};
Player.prototype.getDirName_ = function() {
var path = window.location.pathname;
path = path.substring(0, path.lastIndexOf('/'));
return location.protocol + '//' + location.host + path;
};
/**
* Make all of the URLs inside contentInfo absolute instead of relative.
*/
Player.prototype.absolutifyPaths_ = function(contentInfo) {
var dirName = this.getDirName_();
var urlParams = ['image', 'preview', 'video'];
for (var i = 0; i < urlParams.length; i++) {
var name = urlParams[i];
var path = contentInfo[name];
if (path && Util.isPathAbsolute(path)) {
var absolute = Util.relativeToAbsolutePath(dirName, path);
contentInfo[name] = absolute;
//console.log('Converted to absolute: %s', absolute);
}
}
};
/**
* Get position YAW, PITCH
*/
Player.prototype.getPosition = function() {
this.sender.send({type: Message.GET_POSITION, data: {}});
};
module.exports = Player;

@ -0,0 +1,147 @@
/*
* Copyright 2016 Google Inc. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
var EventEmitter = require('eventemitter3');
var shaka = require('shaka-player');
var Types = require('../video-type');
var Util = require('../util');
var DEFAULT_BITS_PER_SECOND = 1000000;
/**
* Supports regular video URLs (eg. mp4), as well as adaptive manifests like
* DASH (.mpd) and soon HLS (.m3u8).
*
* Events:
* load(video): When the video is loaded.
* error(message): If an error occurs.
*
* To play/pause/seek/etc, please use the underlying video element.
*/
function AdaptivePlayer(params) {
this.video = document.createElement('video');
// Loop by default.
if (params.loop === true) {
this.video.setAttribute('loop', true);
}
if (params.volume !== undefined) {
// XXX: .setAttribute('volume', params.volume) doesn't work for some reason.
this.video.volume = params.volume;
}
// Not muted by default.
if (params.muted === true) {
this.video.muted = params.muted;
}
// For FF, make sure we enable preload.
this.video.setAttribute('preload', 'auto');
// Enable inline video playback in iOS 10+.
this.video.setAttribute('playsinline', true);
this.video.setAttribute('crossorigin', 'anonymous');
}
AdaptivePlayer.prototype = new EventEmitter();
AdaptivePlayer.prototype.load = function(url) {
var self = this;
// TODO(smus): Investigate whether or not differentiation is best done by
// mimeType after all. Cursory research suggests that adaptive streaming
// manifest mime types aren't properly supported.
//
// For now, make determination based on extension.
var extension = Util.getExtension(url);
switch (extension) {
case 'm3u8': // HLS
this.type = Types.HLS;
if (Util.isSafari()) {
this.loadVideo_(url).then(function() {
self.emit('load', self.video, self.type);
}).catch(this.onError_.bind(this));
} else {
self.onError_('HLS is only supported on Safari.');
}
break;
case 'mpd': // MPEG-DASH
this.type = Types.DASH;
this.loadShakaVideo_(url).then(function() {
console.log('The video has now been loaded!');
self.emit('load', self.video, self.type);
}).catch(this.onError_.bind(this));
break;
default: // A regular video, not an adaptive manifest.
this.type = Types.VIDEO;
this.loadVideo_(url).then(function() {
self.emit('load', self.video, self.type);
}).catch(this.onError_.bind(this));
break;
}
};
AdaptivePlayer.prototype.destroy = function() {
this.video.pause();
this.video.src = '';
this.video = null;
};
/*** PRIVATE API ***/
AdaptivePlayer.prototype.onError_ = function(e) {
console.error(e);
this.emit('error', e);
};
AdaptivePlayer.prototype.loadVideo_ = function(url) {
var self = this, video = self.video;
return new Promise(function(resolve, reject) {
video.src = url;
video.addEventListener('canplaythrough', resolve);
video.addEventListener('loadedmetadata', function() {
self.emit('timeupdate', {
currentTime: video.currentTime,
duration: video.duration
});
});
video.addEventListener('error', reject);
video.load();
});
};
AdaptivePlayer.prototype.initShaka_ = function() {
this.player = new shaka.Player(this.video);
this.player.configure({
abr: { defaultBandwidthEstimate: DEFAULT_BITS_PER_SECOND }
});
// Listen for error events.
this.player.addEventListener('error', this.onError_);
};
AdaptivePlayer.prototype.loadShakaVideo_ = function(url) {
// Install built-in polyfills to patch browser incompatibilities.
shaka.polyfill.installAll();
if (!shaka.Player.isBrowserSupported()) {
console.error('Shaka is not supported on this browser.');
return;
}
this.initShaka_();
return this.player.load(url);
};
module.exports = AdaptivePlayer;

@ -0,0 +1,56 @@
/*
* Copyright 2016 Google Inc. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
function Analytics() {
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','//www.google-analytics.com/analytics.js','ga');
ga('create', 'UA-35315454-8', 'auto');
ga('send', 'pageview');
this.lastModeChangeTime = window.performance.now();
this.lastModeLabel = Analytics.MODE_LABELS[0];
}
Analytics.MODE_LABELS = {
0: 'UNKNOWN',
1: 'NORMAL',
2: 'MAGIC_WINDOW',
3: 'VR'
};
Analytics.prototype.logModeChanged = function(mode) {
var modeLabel = Analytics.MODE_LABELS[mode];
var lastModeLabel = Analytics.MODE_LABELS[this.lastMode];
console.log('Analytics: going from mode %s to %s', lastModeLabel, modeLabel);
ga('send', 'screenview', {
appName: 'EmbedVR',
screenName: modeLabel
});
var now = window.performance.now();
var msSinceLastModeChange = Math.round(now - this.lastModeChangeTime);
ga('send', 'timing', 'Time spent in mode', lastModeLabel, msSinceLastModeChange);
this.lastModeChangeTime = now;
this.lastMode = mode;
}
window.analytics = new Analytics();

@ -0,0 +1,20 @@
/*
* Copyright 2016 Google Inc. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
var Eyes = {
LEFT: 1,
RIGHT: 2
};
module.exports = Eyes;

@ -0,0 +1,403 @@
/*
* Copyright 2016 Google Inc. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
var EventEmitter = require('eventemitter3');
var TWEEN = require('@tweenjs/tween.js');
var Util = require('../util');
// Constants for the focus/blur animation.
var NORMAL_SCALE = new THREE.Vector3(1, 1, 1);
var FOCUS_SCALE = new THREE.Vector3(1.2, 1.2, 1.2);
var FOCUS_DURATION = 200;
// Constants for the active/inactive animation.
var INACTIVE_COLOR = new THREE.Color(1, 1, 1);
var ACTIVE_COLOR = new THREE.Color(0.8, 0, 0);
var ACTIVE_DURATION = 100;
// Constants for opacity.
var MAX_INNER_OPACITY = 0.8;
var MAX_OUTER_OPACITY = 0.5;
var FADE_START_ANGLE_DEG = 35;
var FADE_END_ANGLE_DEG = 60;
/**
* Responsible for rectangular hot spots that the user can interact with.
*
* Specific duties:
* Adding and removing hotspots.
* Rendering the hotspots (debug mode only).
* Notifying when hotspots are interacted with.
*
* Emits the following events:
* click (id): a hotspot is clicked.
* focus (id): a hotspot is focused.
* blur (id): a hotspot is no longer hovered over.
*/
function HotspotRenderer(worldRenderer) {
this.worldRenderer = worldRenderer;
this.scene = worldRenderer.scene;
// Note: this event must be added to document.body and not to window for it to
// work inside iOS iframes.
var body = document.body;
// Bind events for hotspot interaction.
if (!Util.isMobile()) {
// Only enable mouse events on desktop.
body.addEventListener('mousedown', this.onMouseDown_.bind(this), false);
body.addEventListener('mousemove', this.onMouseMove_.bind(this), false);
body.addEventListener('mouseup', this.onMouseUp_.bind(this), false);
}
body.addEventListener('touchstart', this.onTouchStart_.bind(this), false);
body.addEventListener('touchend', this.onTouchEnd_.bind(this), false);
// Add a placeholder for hotspots.
this.hotspotRoot = new THREE.Object3D();
// Align the center with the center of the camera too.
this.hotspotRoot.rotation.y = Math.PI / 2;
this.scene.add(this.hotspotRoot);
// All hotspot IDs.
this.hotspots = {};
// Currently selected hotspots.
this.selectedHotspots = {};
// Hotspots that the last touchstart / mousedown event happened for.
this.downHotspots = {};
// For raycasting. Initialize mouse to be off screen initially.
this.pointer = new THREE.Vector2(1, 1);
this.raycaster = new THREE.Raycaster();
}
HotspotRenderer.prototype = new EventEmitter();
/**
* @param pitch {Number} The latitude of center, specified in degrees, between
* -90 and 90, with 0 at the horizon.
* @param yaw {Number} The longitude of center, specified in degrees, between
* -180 and 180, with 0 at the image center.
* @param radius {Number} The radius of the hotspot, specified in meters.
* @param distance {Number} The distance of the hotspot from camera, specified
* in meters.
* @param hotspotId {String} The ID of the hotspot.
*/
HotspotRenderer.prototype.add = function(pitch, yaw, radius, distance, id) {
// If a hotspot already exists with this ID, stop.
if (this.hotspots[id]) {
// TODO: Proper error reporting.
console.error('Attempt to add hotspot with existing id %s.', id);
return;
}
var hotspot = this.createHotspot_(radius, distance);
hotspot.name = id;
// Position the hotspot based on the pitch and yaw specified.
var quat = new THREE.Quaternion();
quat.setFromEuler(new THREE.Euler(THREE.Math.degToRad(pitch), THREE.Math.degToRad(yaw), 0, 'ZYX'));
hotspot.position.applyQuaternion(quat);
hotspot.lookAt(new THREE.Vector3());
this.hotspotRoot.add(hotspot);
this.hotspots[id] = hotspot;
}
/**
* Removes a hotspot based on the ID.
*
* @param ID {String} Identifier of the hotspot to be removed.
*/
HotspotRenderer.prototype.remove = function(id) {
// If there's no hotspot with this ID, fail.
if (!this.hotspots[id]) {
// TODO: Proper error reporting.
console.error('Attempt to remove non-existing hotspot with id %s.', id);
return;
}
// Remove the mesh from the scene.
this.hotspotRoot.remove(this.hotspots[id]);
// If this hotspot was selected, make sure it gets unselected.
delete this.selectedHotspots[id];
delete this.downHotspots[id];
delete this.hotspots[id];
this.emit('blur', id);
};
/**
* Clears all hotspots from the pano. Often called when changing panos.
*/
HotspotRenderer.prototype.clearAll = function() {
for (var id in this.hotspots) {
this.remove(id);
}
};
HotspotRenderer.prototype.getCount = function() {
var count = 0;
for (var id in this.hotspots) {
count += 1;
}
return count;
};
HotspotRenderer.prototype.update = function(camera) {
if (this.worldRenderer.isVRMode()) {
this.pointer.set(0, 0);
}
// Update the picking ray with the camera and mouse position.
this.raycaster.setFromCamera(this.pointer, camera);
// Fade hotspots out if they are really far from center to avoid overly
// distorted visuals.
this.fadeOffCenterHotspots_(camera);
var hotspots = this.hotspotRoot.children;
// Go through all hotspots to see if they are currently selected.
for (var i = 0; i < hotspots.length; i++) {
var hotspot = hotspots[i];
//hotspot.lookAt(camera.position);
var id = hotspot.name;
// Check if hotspot is intersected with the picking ray.
var intersects = this.raycaster.intersectObjects(hotspot.children);
var isIntersected = (intersects.length > 0);
// If newly selected, emit a focus event.
if (isIntersected && !this.selectedHotspots[id]) {
this.emit('focus', id);
this.focus_(id);
}
// If no longer selected, emit a blur event.
if (!isIntersected && this.selectedHotspots[id]) {
this.emit('blur', id);
this.blur_(id);
}
// Update the set of selected hotspots.
if (isIntersected) {
this.selectedHotspots[id] = true;
} else {
delete this.selectedHotspots[id];
}
}
};
/**
* Toggle whether or not hotspots are visible.
*/
HotspotRenderer.prototype.setVisibility = function(isVisible) {
this.hotspotRoot.visible = isVisible;
};
HotspotRenderer.prototype.onTouchStart_ = function(e) {
// In VR mode, don't touch the pointer position.
if (!this.worldRenderer.isVRMode()) {
this.updateTouch_(e);
}
// Force a camera update to see if any hotspots were selected.
this.update(this.worldRenderer.camera);
this.downHotspots = {};
for (var id in this.selectedHotspots) {
this.downHotspots[id] = true;
this.down_(id);
}
return false;
};
HotspotRenderer.prototype.onTouchEnd_ = function(e) {
// If no hotspots are pressed, emit an empty click event.
if (Util.isEmptyObject(this.downHotspots)) {
this.emit('click');
return;
}
// Only emit a click if the finger was down on the same hotspot before.
for (var id in this.downHotspots) {
this.emit('click', id);
this.up_(id);
e.preventDefault();
}
};
HotspotRenderer.prototype.updateTouch_ = function(e) {
var size = this.getSize_();
var touch = e.touches[0];
this.pointer.x = (touch.clientX / size.width) * 2 - 1;
this.pointer.y = - (touch.clientY / size.height) * 2 + 1;
};
HotspotRenderer.prototype.onMouseDown_ = function(e) {
this.updateMouse_(e);
this.downHotspots = {};
for (var id in this.selectedHotspots) {
this.downHotspots[id] = true;
this.down_(id);
}
};
HotspotRenderer.prototype.onMouseMove_ = function(e) {
this.updateMouse_(e);
};
HotspotRenderer.prototype.onMouseUp_ = function(e) {
this.updateMouse_(e);
// If no hotspots are pressed, emit an empty click event.
if (Util.isEmptyObject(this.downHotspots)) {
this.emit('click');
return;
}
// Only emit a click if the mouse was down on the same hotspot before.
for (var id in this.selectedHotspots) {
if (id in this.downHotspots) {
this.emit('click', id);
this.up_(id);
}
}
};
HotspotRenderer.prototype.updateMouse_ = function(e) {
var size = this.getSize_();
this.pointer.x = (e.clientX / size.width) * 2 - 1;
this.pointer.y = - (e.clientY / size.height) * 2 + 1;
};
HotspotRenderer.prototype.getSize_ = function() {
var canvas = this.worldRenderer.renderer.domElement;
return this.worldRenderer.renderer.getSize();
};
HotspotRenderer.prototype.createHotspot_ = function(radius, distance) {
var innerGeometry = new THREE.CircleGeometry(radius, 32);
var innerMaterial = new THREE.MeshBasicMaterial({
color: 0xffffff, side: THREE.DoubleSide, transparent: true,
opacity: MAX_INNER_OPACITY, depthTest: false
});
var inner = new THREE.Mesh(innerGeometry, innerMaterial);
inner.name = 'inner';
var outerMaterial = new THREE.MeshBasicMaterial({
color: 0xffffff, side: THREE.DoubleSide, transparent: true,
opacity: MAX_OUTER_OPACITY, depthTest: false
});
var outerGeometry = new THREE.RingGeometry(radius * 0.85, radius, 32);
var outer = new THREE.Mesh(outerGeometry, outerMaterial);
outer.name = 'outer';
// Position at the extreme end of the sphere.
var hotspot = new THREE.Object3D();
hotspot.position.z = -distance;
hotspot.scale.copy(NORMAL_SCALE);
hotspot.add(inner);
hotspot.add(outer);
return hotspot;
};
/**
* Large aspect ratios tend to cause visually jarring distortions on the sides.
* Here we fade hotspots out to avoid them.
*/
HotspotRenderer.prototype.fadeOffCenterHotspots_ = function(camera) {
var lookAt = new THREE.Vector3(1, 0, 0);
lookAt.applyQuaternion(camera.quaternion);
// Take into account the camera parent too.
lookAt.applyQuaternion(camera.parent.quaternion);
// Go through each hotspot. Calculate how far off center it is.
for (var id in this.hotspots) {
var hotspot = this.hotspots[id];
var angle = hotspot.position.angleTo(lookAt);
var angleDeg = THREE.Math.radToDeg(angle);
var isVisible = angleDeg < 45;
var opacity;
if (angleDeg < FADE_START_ANGLE_DEG) {
opacity = 1;
} else if (angleDeg > FADE_END_ANGLE_DEG) {
opacity = 0;
} else {
// We are in the case START < angle < END. Linearly interpolate.
var range = FADE_END_ANGLE_DEG - FADE_START_ANGLE_DEG;
var value = FADE_END_ANGLE_DEG - angleDeg;
opacity = value / range;
}
// Opacity a function of angle. If angle is large, opacity is zero. At some
// point, ramp opacity down.
this.setOpacity_(id, opacity);
}
};
HotspotRenderer.prototype.focus_ = function(id) {
var hotspot = this.hotspots[id];
// Tween scale of hotspot.
this.tween = new TWEEN.Tween(hotspot.scale).to(FOCUS_SCALE, FOCUS_DURATION)
.easing(TWEEN.Easing.Quadratic.InOut)
.start();
if (this.worldRenderer.isVRMode()) {
this.timeForHospotClick = setTimeout(function () {
this.emit('click', id);
}, 1200 )
}
};
HotspotRenderer.prototype.blur_ = function(id) {
var hotspot = this.hotspots[id];
this.tween = new TWEEN.Tween(hotspot.scale).to(NORMAL_SCALE, FOCUS_DURATION)
.easing(TWEEN.Easing.Quadratic.InOut)
.start();
if (this.timeForHospotClick) {
clearTimeout( this.timeForHospotClick );
}
};
HotspotRenderer.prototype.down_ = function(id) {
// Become active.
var hotspot = this.hotspots[id];
var outer = hotspot.getObjectByName('inner');
this.tween = new TWEEN.Tween(outer.material.color).to(ACTIVE_COLOR, ACTIVE_DURATION)
.start();
};
HotspotRenderer.prototype.up_ = function(id) {
// Become inactive.
var hotspot = this.hotspots[id];
var outer = hotspot.getObjectByName('inner');
this.tween = new TWEEN.Tween(outer.material.color).to(INACTIVE_COLOR, ACTIVE_DURATION)
.start();
};
HotspotRenderer.prototype.setOpacity_ = function(id, opacity) {
var hotspot = this.hotspots[id];
var outer = hotspot.getObjectByName('outer');
var inner = hotspot.getObjectByName('inner');
outer.material.opacity = opacity * MAX_OUTER_OPACITY;
inner.material.opacity = opacity * MAX_INNER_OPACITY;
};
module.exports = HotspotRenderer;

@ -0,0 +1,68 @@
/*
* Copyright 2016 Google Inc. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
var EventEmitter = require('eventemitter3');
var Message = require('../message');
var Util = require('../util');
/**
* Sits in an embedded iframe, receiving messages from a containing
* iFrame. This facilitates an API which provides the following features:
*
* Playing and pausing content.
* Adding hotspots.
* Sending messages back to the containing iframe when hotspot is clicked
* Sending analytics events to containing iframe.
*
* Note: this script used to also respond to synthetic devicemotion events, but
* no longer does so. This is because as of iOS 9.2, Safari disallows listening
* for devicemotion events within cross-device iframes. To work around this, the
* webvr-polyfill responds to the postMessage event containing devicemotion
* information (sent by the iframe-message-sender in the VR View API).
*/
function IFrameMessageReceiver() {
window.addEventListener('message', this.onMessage_.bind(this), false);
}
IFrameMessageReceiver.prototype = new EventEmitter();
IFrameMessageReceiver.prototype.onMessage_ = function(event) {
if (Util.isDebug()) {
console.log('onMessage_', event);
}
var message = event.data;
var type = message.type.toLowerCase();
var data = message.data;
switch (type) {
case Message.SET_CONTENT:
case Message.SET_VOLUME:
case Message.MUTED:
case Message.ADD_HOTSPOT:
case Message.PLAY:
case Message.PAUSE:
case Message.SET_CURRENT_TIME:
case Message.GET_POSITION:
case Message.SET_FULLSCREEN:
this.emit(type, data);
break;
default:
if (Util.isDebug()) {
console.warn('Got unknown message of type %s from %s', message.type, message.origin);
}
}
};
module.exports = IFrameMessageReceiver;

@ -0,0 +1,54 @@
/*
* Copyright 2016 Google Inc. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Shows a 2D loading indicator while various pieces of EmbedVR load.
*/
function LoadingIndicator() {
this.el = this.build_();
document.body.appendChild(this.el);
this.show();
}
LoadingIndicator.prototype.build_ = function() {
var overlay = document.createElement('div');
var s = overlay.style;
s.position = 'fixed';
s.top = 0;
s.left = 0;
s.width = '100%';
s.height = '100%';
s.background = '#eee';
var img = document.createElement('img');
img.src = 'images/loading.gif';
var s = img.style;
s.position = 'absolute';
s.top = '50%';
s.left = '50%';
s.transform = 'translate(-50%, -50%)';
overlay.appendChild(img);
return overlay;
};
LoadingIndicator.prototype.hide = function() {
this.el.style.display = 'none';
};
LoadingIndicator.prototype.show = function() {
this.el.style.display = 'block';
};
module.exports = LoadingIndicator;

@ -0,0 +1,369 @@
/*
* Copyright 2016 Google Inc. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// Initialize the loading indicator as quickly as possible to give the user
// immediate feedback.
var LoadingIndicator = require('./loading-indicator');
var loadIndicator = new LoadingIndicator();
var ES6Promise = require('es6-promise');
// Polyfill ES6 promises for IE.
ES6Promise.polyfill();
var IFrameMessageReceiver = require('./iframe-message-receiver');
var Message = require('../message');
var SceneInfo = require('./scene-info');
var Stats = require('../../node_modules/stats-js/build/stats.min');
var Util = require('../util');
var WebVRPolyfill = require('webvr-polyfill');
var WorldRenderer = require('./world-renderer');
var receiver = new IFrameMessageReceiver();
receiver.on(Message.PLAY, onPlayRequest);
receiver.on(Message.PAUSE, onPauseRequest);
receiver.on(Message.ADD_HOTSPOT, onAddHotspot);
receiver.on(Message.SET_CONTENT, onSetContent);
receiver.on(Message.SET_VOLUME, onSetVolume);
receiver.on(Message.MUTED, onMuted);
receiver.on(Message.SET_CURRENT_TIME, onUpdateCurrentTime);
receiver.on(Message.GET_POSITION, onGetPosition);
receiver.on(Message.SET_FULLSCREEN, onSetFullscreen);
window.addEventListener('load', onLoad);
var stats = new Stats();
var scene = SceneInfo.loadFromGetParams();
var worldRenderer = new WorldRenderer(scene);
worldRenderer.on('error', onRenderError);
worldRenderer.on('load', onRenderLoad);
worldRenderer.on('modechange', onModeChange);
worldRenderer.on('ended', onEnded);
worldRenderer.on('play', onPlay);
worldRenderer.hotspotRenderer.on('click', onHotspotClick);
window.worldRenderer = worldRenderer;
var isReadySent = false;
var volume = 0;
function onLoad() {
if (!Util.isWebGLEnabled()) {
showError('WebGL not supported.');
return;
}
// Load the scene.
worldRenderer.setScene(scene);
if (scene.isDebug) {
// Show stats.
showStats();
}
if (scene.isYawOnly) {
WebVRConfig = window.WebVRConfig || {};
WebVRConfig.YAW_ONLY = true;
}
requestAnimationFrame(loop);
}
function onVideoTap() {
worldRenderer.videoProxy.play();
hidePlayButton();
// Prevent multiple play() calls on the video element.
document.body.removeEventListener('touchend', onVideoTap);
}
function onRenderLoad(event) {
if (event.videoElement) {
var scene = SceneInfo.loadFromGetParams();
// On mobile, tell the user they need to tap to start. Otherwise, autoplay.
if (Util.isMobile()) {
// Tell user to tap to start.
showPlayButton();
document.body.addEventListener('touchend', onVideoTap);
} else {
event.videoElement.play();
}
// Attach to pause and play events, to notify the API.
event.videoElement.addEventListener('pause', onPause);
event.videoElement.addEventListener('play', onPlay);
event.videoElement.addEventListener('timeupdate', onGetCurrentTime);
event.videoElement.addEventListener('ended', onEnded);
}
// Hide loading indicator.
loadIndicator.hide();
// Autopan only on desktop, for photos only, and only if autopan is enabled.
if (!Util.isMobile() && !worldRenderer.sceneInfo.video && !worldRenderer.sceneInfo.isAutopanOff) {
worldRenderer.autopan();
}
// Notify the API that we are ready, but only do this once.
if (!isReadySent) {
if (event.videoElement) {
Util.sendParentMessage({
type: 'ready',
data: {
duration: event.videoElement.duration
}
});
} else {
Util.sendParentMessage({
type: 'ready'
});
}
isReadySent = true;
}
}
function onPlayRequest() {
if (!worldRenderer.videoProxy) {
onApiError('Attempt to pause, but no video found.');
return;
}
worldRenderer.videoProxy.play();
}
function onPauseRequest() {
if (!worldRenderer.videoProxy) {
onApiError('Attempt to pause, but no video found.');
return;
}
worldRenderer.videoProxy.pause();
}
function onAddHotspot(e) {
if (Util.isDebug()) {
console.log('onAddHotspot', e);
}
// TODO: Implement some validation?
var pitch = parseFloat(e.pitch);
var yaw = parseFloat(e.yaw);
var radius = parseFloat(e.radius);
var distance = parseFloat(e.distance);
var id = e.id;
worldRenderer.hotspotRenderer.add(pitch, yaw, radius, distance, id);
}
function onSetContent(e) {
if (Util.isDebug()) {
console.log('onSetContent', e);
}
// Remove all of the hotspots.
worldRenderer.hotspotRenderer.clearAll();
// Fade to black.
worldRenderer.sphereRenderer.setOpacity(0, 500).then(function() {
// Then load the new scene.
var scene = SceneInfo.loadFromAPIParams(e.contentInfo);
worldRenderer.destroy();
// Update the URL to reflect the new scene. This is important particularily
// on iOS where we use a fake fullscreen mode.
var url = scene.getCurrentUrl();
//console.log('Updating url to be %s', url);
window.history.pushState(null, 'VR View', url);
// And set the new scene.
return worldRenderer.setScene(scene);
}).then(function() {
// Then fade the scene back in.
worldRenderer.sphereRenderer.setOpacity(1, 500);
});
}
function onSetVolume(e) {
// Only work for video. If there's no video, send back an error.
if (!worldRenderer.videoProxy) {
onApiError('Attempt to set volume, but no video found.');
return;
}
worldRenderer.videoProxy.setVolume(e.volumeLevel);
volume = e.volumeLevel;
Util.sendParentMessage({
type: 'volumechange',
data: e.volumeLevel
});
}
function onMuted(e) {
// Only work for video. If there's no video, send back an error.
if (!worldRenderer.videoProxy) {
onApiError('Attempt to mute, but no video found.');
return;
}
worldRenderer.videoProxy.mute(e.muteState);
Util.sendParentMessage({
type: 'muted',
data: e.muteState
});
}
function onUpdateCurrentTime(time) {
if (!worldRenderer.videoProxy) {
onApiError('Attempt to pause, but no video found.');
return;
}
worldRenderer.videoProxy.setCurrentTime(time);
onGetCurrentTime();
}
function onGetCurrentTime() {
var time = worldRenderer.videoProxy.getCurrentTime();
Util.sendParentMessage({
type: 'timeupdate',
data: time
});
}
function onSetFullscreen() {
if (!worldRenderer.videoProxy) {
onApiError('Attempt to set fullscreen, but no video found.');
return;
}
worldRenderer.manager.onFSClick_();
}
function onApiError(message) {
console.error(message);
Util.sendParentMessage({
type: 'error',
data: {message: message}
});
}
function onModeChange(mode) {
Util.sendParentMessage({
type: 'modechange',
data: {mode: mode}
});
}
function onHotspotClick(id) {
Util.sendParentMessage({
type: 'click',
data: {id: id}
});
}
function onPlay() {
Util.sendParentMessage({
type: 'paused',
data: false
});
}
function onPause() {
Util.sendParentMessage({
type: 'paused',
data: true
});
}
function onEnded() {
Util.sendParentMessage({
type: 'ended',
data: true
});
}
function onSceneError(message) {
showError('Loader: ' + message);
}
function onRenderError(message) {
showError('Render: ' + message);
}
function showError(message) {
// Hide loading indicator.
loadIndicator.hide();
// Sanitize `message` as it could contain user supplied
// values. Re-add the space character as to not modify the
// error messages used throughout the codebase.
message = encodeURI(message).replace(/%20/g, ' ');
var error = document.querySelector('#error');
error.classList.add('visible');
error.querySelector('.message').innerHTML = message;
error.querySelector('.title').innerHTML = 'Error';
}
function hideError() {
var error = document.querySelector('#error');
error.classList.remove('visible');
}
function showPlayButton() {
var playButton = document.querySelector('#play-overlay');
playButton.classList.add('visible');
}
function hidePlayButton() {
var playButton = document.querySelector('#play-overlay');
playButton.classList.remove('visible');
}
function showStats() {
stats.setMode(0); // 0: fps, 1: ms
// Align bottom-left.
stats.domElement.style.position = 'absolute';
stats.domElement.style.left = '0px';
stats.domElement.style.bottom = '0px';
document.body.appendChild(stats.domElement);
}
function loop(time) {
// Use the VRDisplay RAF if it is present.
if (worldRenderer.vrDisplay) {
worldRenderer.vrDisplay.requestAnimationFrame(loop);
} else {
requestAnimationFrame(loop);
}
stats.begin();
// Update the video if needed.
if (worldRenderer.videoProxy) {
worldRenderer.videoProxy.update(time);
}
worldRenderer.render(time);
worldRenderer.submitFrame();
stats.end();
}
function onGetPosition() {
Util.sendParentMessage({
type: 'getposition',
data: {
Yaw: worldRenderer.camera.rotation.y * 180 / Math.PI,
Pitch: worldRenderer.camera.rotation.x * 180 / Math.PI
}
});
}

@ -0,0 +1,41 @@
/*
* Copyright 2016 Google Inc. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
function ReticleRenderer(camera) {
this.camera = camera;
this.reticle = this.createReticle_();
// In front of the hotspot itself, which is at r=0.99.
this.reticle.position.z = -0.97;
camera.add(this.reticle);
this.setVisibility(false);
}
ReticleRenderer.prototype.setVisibility = function(isVisible) {
// TODO: Tween the transition.
this.reticle.visible = isVisible;
};
ReticleRenderer.prototype.createReticle_ = function() {
// Make a torus.
var geometry = new THREE.TorusGeometry(0.02, 0.005, 10, 20);
var material = new THREE.MeshBasicMaterial({color: 0x000000});
var torus = new THREE.Mesh(geometry, material);
return torus;
};
module.exports = ReticleRenderer;

@ -0,0 +1,125 @@
/*
* Copyright 2016 Google Inc. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
var Util = require('../util');
var CAMEL_TO_UNDERSCORE = {
video: 'video',
image: 'image',
preview: 'preview',
loop: 'loop',
volume: 'volume',
muted: 'muted',
isStereo: 'is_stereo',
defaultYaw: 'default_yaw',
isYawOnly: 'is_yaw_only',
isDebug: 'is_debug',
isVROff: 'is_vr_off',
isAutopanOff: 'is_autopan_off',
hideFullscreenButton: 'hide_fullscreen_button'
};
/**
* Contains all information about a given scene.
*/
function SceneInfo(opt_params) {
var params = opt_params || {};
params.player = {
loop: opt_params.loop,
volume: opt_params.volume,
muted: opt_params.muted
};
this.image = params.image !== undefined ? encodeURI(params.image) : undefined;
this.preview = params.preview !== undefined ? encodeURI(params.preview) : undefined;
this.video = params.video !== undefined ? encodeURI(params.video) : undefined;
this.defaultYaw = THREE.Math.degToRad(params.defaultYaw || 0);
this.isStereo = Util.parseBoolean(params.isStereo);
this.isYawOnly = Util.parseBoolean(params.isYawOnly);
this.isDebug = Util.parseBoolean(params.isDebug);
this.isVROff = Util.parseBoolean(params.isVROff);
this.isAutopanOff = Util.parseBoolean(params.isAutopanOff);
this.loop = Util.parseBoolean(params.player.loop);
this.volume = parseFloat(
params.player.volume ? params.player.volume : '1');
this.muted = Util.parseBoolean(params.player.muted);
this.hideFullscreenButton = Util.parseBoolean(params.hideFullscreenButton);
}
SceneInfo.loadFromGetParams = function() {
var params = {};
for (var camelCase in CAMEL_TO_UNDERSCORE) {
var underscore = CAMEL_TO_UNDERSCORE[camelCase];
params[camelCase] = Util.getQueryParameter(underscore)
|| ((window.WebVRConfig && window.WebVRConfig.PLAYER) ? window.WebVRConfig.PLAYER[underscore] : "");
}
var scene = new SceneInfo(params);
if (!scene.isValid()) {
console.warn('Invalid scene: %s', scene.errorMessage);
}
return scene;
};
SceneInfo.loadFromAPIParams = function(underscoreParams) {
var params = {};
for (var camelCase in CAMEL_TO_UNDERSCORE) {
var underscore = CAMEL_TO_UNDERSCORE[camelCase];
if (underscoreParams[underscore]) {
params[camelCase] = underscoreParams[underscore];
}
}
var scene = new SceneInfo(params);
if (!scene.isValid()) {
console.warn('Invalid scene: %s', scene.errorMessage);
}
return scene;
};
SceneInfo.prototype.isValid = function() {
// Either it's an image or a video.
if (!this.image && !this.video) {
this.errorMessage = 'Either image or video URL must be specified.';
return false;
}
if (this.image && !this.isValidImage_(this.image)) {
this.errorMessage = 'Invalid image URL: ' + this.image;
return false;
}
this.errorMessage = null;
return true;
};
/**
* Generates a URL to reflect this scene.
*/
SceneInfo.prototype.getCurrentUrl = function() {
var url = location.protocol + '//' + location.host + location.pathname + '?';
for (var camelCase in CAMEL_TO_UNDERSCORE) {
var underscore = CAMEL_TO_UNDERSCORE[camelCase];
var value = this[camelCase];
if (value !== undefined) {
url += underscore + '=' + value + '&';
}
}
// Chop off the trailing ampersand.
return url.substring(0, url.length - 1);
};
SceneInfo.prototype.isValidImage_ = function(imageUrl) {
return true;
};
module.exports = SceneInfo;

@ -0,0 +1,205 @@
/*
* Copyright 2016 Google Inc. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
var Eyes = require('./eyes');
var TWEEN = require('@tweenjs/tween.js');
var Util = require('../util');
var VideoType = require('../video-type');
function SphereRenderer(scene) {
this.scene = scene;
// Create a transparent mask.
this.createOpacityMask_();
}
/**
* Sets the photosphere based on the image in the source. Supports stereo and
* mono photospheres.
*
* @return {Promise}
*/
SphereRenderer.prototype.setPhotosphere = function(src, opt_params) {
return new Promise(function(resolve, reject) {
this.resolve = resolve;
this.reject = reject;
var params = opt_params || {};
this.isStereo = !!params.isStereo;
this.src = src;
// Load texture.
var loader = new THREE.TextureLoader();
loader.crossOrigin = 'anonymous';
loader.load(src, this.onTextureLoaded_.bind(this), undefined,
this.onTextureError_.bind(this));
}.bind(this));
};
/**
* @return {Promise} Yeah.
*/
SphereRenderer.prototype.set360Video = function (videoElement, videoType, opt_params) {
return new Promise(function(resolve, reject) {
this.resolve = resolve;
this.reject = reject;
var params = opt_params || {};
this.isStereo = !!params.isStereo;
// Load the video texture.
var videoTexture = new THREE.VideoTexture(videoElement);
videoTexture.minFilter = THREE.LinearFilter;
videoTexture.magFilter = THREE.LinearFilter;
videoTexture.generateMipmaps = false;
if (Util.isSafari() && videoType === VideoType.HLS) {
// fix black screen issue on safari
videoTexture.format = THREE.RGBAFormat;
videoTexture.flipY = false;
} else {
videoTexture.format = THREE.RGBFormat;
}
videoTexture.needsUpdate = true;
this.onTextureLoaded_(videoTexture);
}.bind(this));
};
/**
* Set the opacity of the panorama.
*
* @param {Number} opacity How opaque we want the panorama to be. 0 means black,
* 1 means full color.
* @param {Number} duration Number of milliseconds the transition should take.
*
* @return {Promise} When the opacity change is complete.
*/
SphereRenderer.prototype.setOpacity = function(opacity, duration) {
var scene = this.scene;
// If we want the opacity
var overlayOpacity = 1 - opacity;
return new Promise(function(resolve, reject) {
var mask = scene.getObjectByName('opacityMask');
var tween = new TWEEN.Tween({opacity: mask.material.opacity})
.to({opacity: overlayOpacity}, duration)
.easing(TWEEN.Easing.Quadratic.InOut);
tween.onUpdate(function(e) {
mask.material.opacity = this.opacity;
});
tween.onComplete(resolve).start();
});
};
SphereRenderer.prototype.onTextureLoaded_ = function(texture) {
var sphereLeft;
var sphereRight;
if (this.isStereo) {
sphereLeft = this.createPhotosphere_(texture, {offsetY: 0.5, scaleY: 0.5});
sphereRight = this.createPhotosphere_(texture, {offsetY: 0, scaleY: 0.5});
} else {
sphereLeft = this.createPhotosphere_(texture);
sphereRight = this.createPhotosphere_(texture);
}
// Display in left and right eye respectively.
sphereLeft.layers.set(Eyes.LEFT);
sphereLeft.eye = Eyes.LEFT;
sphereLeft.name = 'eyeLeft';
sphereRight.layers.set(Eyes.RIGHT);
sphereRight.eye = Eyes.RIGHT;
sphereRight.name = 'eyeRight';
this.scene.getObjectByName('photo').children = [sphereLeft, sphereRight];
this.resolve();
};
SphereRenderer.prototype.onTextureError_ = function(error) {
this.reject('Unable to load texture from "' + this.src + '"');
};
SphereRenderer.prototype.createPhotosphere_ = function(texture, opt_params) {
var p = opt_params || {};
p.scaleX = p.scaleX || 1;
p.scaleY = p.scaleY || 1;
p.offsetX = p.offsetX || 0;
p.offsetY = p.offsetY || 0;
p.phiStart = p.phiStart || 0;
p.phiLength = p.phiLength || Math.PI * 2;
p.thetaStart = p.thetaStart || 0;
p.thetaLength = p.thetaLength || Math.PI;
var geometry = new THREE.SphereGeometry(1, 48, 48,
p.phiStart, p.phiLength, p.thetaStart, p.thetaLength);
geometry.applyMatrix(new THREE.Matrix4().makeScale(-1, 1, 1));
var uvs = geometry.faceVertexUvs[0];
for (var i = 0; i < uvs.length; i ++) {
for (var j = 0; j < 3; j ++) {
uvs[i][j].x *= p.scaleX;
uvs[i][j].x += p.offsetX;
uvs[i][j].y *= p.scaleY;
uvs[i][j].y += p.offsetY;
}
}
var material;
if (texture.format === THREE.RGBAFormat && texture.flipY === false) {
material = new THREE.ShaderMaterial({
uniforms: {
texture: { value: texture }
},
vertexShader: [
"varying vec2 vUV;",
"void main() {",
" vUV = vec2( uv.x, 1.0 - uv.y );",
" gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );",
"}"
].join("\n"),
fragmentShader: [
"uniform sampler2D texture;",
"varying vec2 vUV;",
"void main() {",
" gl_FragColor = texture2D( texture, vUV )" + (Util.isIOS() ? ".bgra" : "") + ";",
"}"
].join("\n")
});
} else {
material = new THREE.MeshBasicMaterial({ map: texture });
}
var out = new THREE.Mesh(geometry, material);
//out.visible = false;
out.renderOrder = -1;
return out;
};
SphereRenderer.prototype.createOpacityMask_ = function() {
var geometry = new THREE.SphereGeometry(0.49, 48, 48);
var material = new THREE.MeshBasicMaterial({
color: 0x000000, side: THREE.DoubleSide, opacity: 0, transparent: true});
var opacityMask = new THREE.Mesh(geometry, material);
opacityMask.name = 'opacityMask';
opacityMask.renderOrder = 1;
this.scene.add(opacityMask);
return opacityMask;
};
module.exports = SphereRenderer;

@ -0,0 +1,130 @@
/*
* Copyright 2016 Google Inc. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
var Util = require('../util');
/**
* A proxy class for working around the fact that as soon as a video is play()ed
* on iOS, Safari auto-fullscreens the video.
*
* TODO(smus): The entire raison d'etre for this class is to work around this
* issue. Once Safari implements some way to suppress this fullscreen player, we
* can remove this code.
*/
function VideoProxy(videoElement) {
this.videoElement = videoElement;
// True if we're currently manually advancing the playhead (only on iOS).
this.isFakePlayback = false;
// When the video started playing.
this.startTime = null;
}
VideoProxy.prototype.play = function() {
if (Util.isIOS9OrLess()) {
this.startTime = performance.now();
this.isFakePlayback = true;
// Make an audio element to playback just the audio part.
this.audioElement = new Audio();
this.audioElement.src = this.videoElement.src;
this.audioElement.play();
} else {
this.videoElement.play().then(function(e) {
console.log('Playing video.', e);
});
}
};
VideoProxy.prototype.pause = function() {
if (Util.isIOS9OrLess() && this.isFakePlayback) {
this.isFakePlayback = true;
this.audioElement.pause();
} else {
this.videoElement.pause();
}
};
VideoProxy.prototype.setVolume = function(volumeLevel) {
if (this.videoElement) {
// On iOS 10, the VideoElement.volume property is read-only. So we special
// case muting and unmuting.
if (Util.isIOS()) {
this.videoElement.muted = (volumeLevel === 0);
} else {
this.videoElement.volume = volumeLevel;
}
}
if (this.audioElement) {
this.audioElement.volume = volumeLevel;
}
};
/**
* Set the attribute mute of the elements according with the muteState param.
*
* @param bool muteState
*/
VideoProxy.prototype.mute = function(muteState) {
if (this.videoElement) {
this.videoElement.muted = muteState;
}
if (this.audioElement) {
this.audioElement.muted = muteState;
}
};
VideoProxy.prototype.getCurrentTime = function() {
return Util.isIOS9OrLess() ? this.audioElement.currentTime : this.videoElement.currentTime;
};
/**
*
* @param {Object} time
*/
VideoProxy.prototype.setCurrentTime = function(time) {
if (this.videoElement) {
this.videoElement.currentTime = time.currentTime;
}
if (this.audioElement) {
this.audioElement.currentTime = time.currentTime;
}
};
/**
* Called on RAF to progress playback.
*/
VideoProxy.prototype.update = function() {
// Fakes playback for iOS only.
if (!this.isFakePlayback) {
return;
}
var duration = this.videoElement.duration;
var now = performance.now();
var delta = now - this.startTime;
var deltaS = delta / 1000;
this.videoElement.currentTime = deltaS;
// Loop through the video
if (deltaS > duration) {
this.startTime = now;
this.videoElement.currentTime = 0;
// Also restart the audio.
this.audioElement.currentTime = 0;
}
};
module.exports = VideoProxy;

@ -0,0 +1,20 @@
/*
* Copyright 2016 Google Inc. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// Load EmbedVR.
require('./main');
// Load Analytics for EmbedVR.
require('./analytics');

@ -0,0 +1,372 @@
/*
* Copyright 2016 Google Inc. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
var AdaptivePlayer = require('./adaptive-player');
var EventEmitter = require('eventemitter3');
var Eyes = require('./eyes');
var HotspotRenderer = require('./hotspot-renderer');
var ReticleRenderer = require('./reticle-renderer');
var SphereRenderer = require('./sphere-renderer');
var TWEEN = require('@tweenjs/tween.js');
var Util = require('../util');
var VideoProxy = require('./video-proxy');
var WebVRManager = require('webvr-boilerplate');
var AUTOPAN_DURATION = 3000;
var AUTOPAN_ANGLE = 0.4;
/**
* The main WebGL rendering entry point. Manages the scene, camera, VR-related
* rendering updates. Interacts with the WebVRManager.
*
* Coordinates the other renderers: SphereRenderer, HotspotRenderer,
* ReticleRenderer.
*
* Also manages the AdaptivePlayer and VideoProxy.
*
* Emits the following events:
* load: when the scene is loaded.
* error: if there is an error loading the scene.
* modechange(Boolean isVR): if the mode (eg. VR, fullscreen, etc) changes.
*/
function WorldRenderer(params) {
this.init_(params.hideFullscreenButton);
this.sphereRenderer = new SphereRenderer(this.scene);
this.hotspotRenderer = new HotspotRenderer(this);
this.hotspotRenderer.on('focus', this.onHotspotFocus_.bind(this));
this.hotspotRenderer.on('blur', this.onHotspotBlur_.bind(this));
this.reticleRenderer = new ReticleRenderer(this.camera);
// Get the VR Display as soon as we initialize.
navigator.getVRDisplays().then(function(displays) {
if (displays.length > 0) {
this.vrDisplay = displays[0];
}
}.bind(this));
}
WorldRenderer.prototype = new EventEmitter();
WorldRenderer.prototype.render = function(time) {
this.controls.update();
TWEEN.update(time);
this.effect.render(this.scene, this.camera);
this.hotspotRenderer.update(this.camera);
};
/**
* @return {Promise} When the scene is fully loaded.
*/
WorldRenderer.prototype.setScene = function(scene) {
var self = this;
var promise = new Promise(function(resolve, reject) {
self.sceneResolve = resolve;
self.sceneReject = reject;
});
if (!scene || !scene.isValid()) {
this.didLoadFail_(scene.errorMessage);
return;
}
var params = {
isStereo: scene.isStereo,
loop: scene.loop,
volume: scene.volume,
muted: scene.muted
};
this.setDefaultYaw_(scene.defaultYaw || 0);
// Disable VR mode if explicitly disabled, or if we're loading a video on iOS
// 9 or earlier.
if (scene.isVROff || (scene.video && Util.isIOS9OrLess())) {
this.manager.setVRCompatibleOverride(false);
}
// Set various callback overrides in iOS.
if (Util.isIOS()) {
this.manager.setFullscreenCallback(function() {
Util.sendParentMessage({type: 'enter-fullscreen'});
});
this.manager.setExitFullscreenCallback(function() {
Util.sendParentMessage({type: 'exit-fullscreen'});
});
this.manager.setVRCallback(function() {
Util.sendParentMessage({type: 'enter-vr'});
});
}
// If we're dealing with an image, and not a video.
if (scene.image && !scene.video) {
if (scene.preview) {
// First load the preview.
this.sphereRenderer.setPhotosphere(scene.preview, params).then(function() {
// As soon as something is loaded, emit the load event to hide the
// loading progress bar.
self.didLoad_();
// Then load the full resolution image.
self.sphereRenderer.setPhotosphere(scene.image, params);
}).catch(self.didLoadFail_.bind(self));
} else {
// No preview -- go straight to rendering the full image.
this.sphereRenderer.setPhotosphere(scene.image, params).then(function() {
self.didLoad_();
}).catch(self.didLoadFail_.bind(self));
}
} else if (scene.video) {
if (Util.isIE11()) {
// On IE 11, if an 'image' param is provided, load it instead of showing
// an error.
//
// TODO(smus): Once video textures are supported, remove this fallback.
if (scene.image) {
this.sphereRenderer.setPhotosphere(scene.image, params).then(function() {
self.didLoad_();
}).catch(self.didLoadFail_.bind(self));
} else {
this.didLoadFail_('Video is not supported on IE11.');
}
} else {
this.player = new AdaptivePlayer(params);
this.player.on('load', function(videoElement, videoType) {
self.sphereRenderer.set360Video(videoElement, videoType, params).then(function() {
self.didLoad_({videoElement: videoElement});
}).catch(self.didLoadFail_.bind(self));
});
this.player.on('error', function(error) {
self.didLoadFail_('Video load error: ' + error);
});
this.player.load(scene.video);
this.videoProxy = new VideoProxy(this.player.video);
}
}
this.sceneInfo = scene;
if (Util.isDebug()) {
console.log('Loaded scene', scene);
}
return promise;
};
WorldRenderer.prototype.isVRMode = function() {
return !!this.vrDisplay && this.vrDisplay.isPresenting;
};
WorldRenderer.prototype.submitFrame = function() {
if (this.isVRMode()) {
this.vrDisplay.submitFrame();
}
};
WorldRenderer.prototype.disposeEye_ = function(eye) {
if (eye) {
if (eye.material.map) {
eye.material.map.dispose();
}
eye.material.dispose();
eye.geometry.dispose();
}
};
WorldRenderer.prototype.dispose = function() {
var eyeLeft = this.scene.getObjectByName('eyeLeft');
this.disposeEye_(eyeLeft);
var eyeRight = this.scene.getObjectByName('eyeRight');
this.disposeEye_(eyeRight);
};
WorldRenderer.prototype.destroy = function() {
if (this.player) {
this.player.removeAllListeners();
this.player.destroy();
this.player = null;
}
var photo = this.scene.getObjectByName('photo');
var eyeLeft = this.scene.getObjectByName('eyeLeft');
var eyeRight = this.scene.getObjectByName('eyeRight');
if (eyeLeft) {
this.disposeEye_(eyeLeft);
photo.remove(eyeLeft);
this.scene.remove(eyeLeft);
}
if (eyeRight) {
this.disposeEye_(eyeRight);
photo.remove(eyeRight);
this.scene.remove(eyeRight);
}
};
WorldRenderer.prototype.didLoad_ = function(opt_event) {
var event = opt_event || {};
this.emit('load', event);
if (this.sceneResolve) {
this.sceneResolve();
}
};
WorldRenderer.prototype.didLoadFail_ = function(message) {
this.emit('error', message);
if (this.sceneReject) {
this.sceneReject(message);
}
};
/**
* Sets the default yaw.
* @param {Number} angleRad The yaw in radians.
*/
WorldRenderer.prototype.setDefaultYaw_ = function(angleRad) {
// Rotate the camera parent to take into account the scene's rotation.
// By default, it should be at the center of the image.
var display = this.controls.getVRDisplay();
// For desktop, we subtract the current display Y axis
var theta = display.theta_ || 0;
// For devices with orientation we make the current view center
if (display.poseSensor_) {
display.poseSensor_.resetPose();
}
this.camera.parent.rotation.y = (Math.PI / 2.0) + angleRad - theta;
};
/**
* Do the initial camera tween to rotate the camera, giving an indication that
* there is live content there (on desktop only).
*/
WorldRenderer.prototype.autopan = function(duration) {
var targetY = this.camera.parent.rotation.y - AUTOPAN_ANGLE;
var tween = new TWEEN.Tween(this.camera.parent.rotation)
.to({y: targetY}, AUTOPAN_DURATION)
.easing(TWEEN.Easing.Quadratic.Out)
.start();
};
WorldRenderer.prototype.init_ = function(hideFullscreenButton) {
var container = document.querySelector('body');
var aspect = window.innerWidth / window.innerHeight;
var camera = new THREE.PerspectiveCamera(75, aspect, 0.1, 100);
camera.layers.enable(1);
var cameraDummy = new THREE.Object3D();
cameraDummy.add(camera);
// Antialiasing disabled to improve performance.
var renderer = new THREE.WebGLRenderer({antialias: false});
renderer.setClearColor(0x000000, 0);
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(window.devicePixelRatio);
container.appendChild(renderer.domElement);
var controls = new THREE.VRControls(camera);
var effect = new THREE.VREffect(renderer);
// Disable eye separation.
effect.scale = 0;
effect.setSize(window.innerWidth, window.innerHeight);
// Present submission of frames automatically. This is done manually in
// submitFrame().
effect.autoSubmitFrame = false;
this.camera = camera;
this.renderer = renderer;
this.effect = effect;
this.controls = controls;
this.manager = new WebVRManager(renderer, effect, {predistorted: false, hideButton: hideFullscreenButton});
this.scene = this.createScene_();
this.scene.add(this.camera.parent);
// Watch the resize event.
window.addEventListener('resize', this.onResize_.bind(this));
// Prevent context menu.
window.addEventListener('contextmenu', this.onContextMenu_.bind(this));
window.addEventListener('vrdisplaypresentchange',
this.onVRDisplayPresentChange_.bind(this));
};
WorldRenderer.prototype.onResize_ = function() {
this.effect.setSize(window.innerWidth, window.innerHeight);
this.camera.aspect = window.innerWidth / window.innerHeight;
this.camera.updateProjectionMatrix();
};
WorldRenderer.prototype.onVRDisplayPresentChange_ = function(e) {
if (Util.isDebug()) {
console.log('onVRDisplayPresentChange_');
}
var isVR = this.isVRMode();
// If the mode changed to VR and there is at least one hotspot, show reticle.
var isReticleVisible = isVR && this.hotspotRenderer.getCount() > 0;
this.reticleRenderer.setVisibility(isReticleVisible);
// Resize the renderer for good measure.
this.onResize_();
// Analytics.
if (window.analytics) {
analytics.logModeChanged(isVR);
}
// When exiting VR mode from iOS, make sure we emit back an exit-fullscreen event.
if (!isVR && Util.isIOS()) {
Util.sendParentMessage({type: 'exit-fullscreen'});
}
// Emit a mode change event back to any listeners.
this.emit('modechange', isVR);
};
WorldRenderer.prototype.createScene_ = function(opt_params) {
var scene = new THREE.Scene();
// Add a group for the photosphere.
var photoGroup = new THREE.Object3D();
photoGroup.name = 'photo';
scene.add(photoGroup);
return scene;
};
WorldRenderer.prototype.onHotspotFocus_ = function(id) {
// Set the default cursor to be a pointer.
this.setCursor_('pointer');
};
WorldRenderer.prototype.onHotspotBlur_ = function(id) {
// Reset the default cursor to be the default one.
this.setCursor_('');
};
WorldRenderer.prototype.setCursor_ = function(cursor) {
this.renderer.domElement.style.cursor = cursor;
};
WorldRenderer.prototype.onContextMenu_ = function(e) {
e.preventDefault();
e.stopPropagation();
return false;
};
module.exports = WorldRenderer;

@ -0,0 +1,33 @@
/*
* Copyright 2016 Google Inc. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Messages from the API to the embed.
*/
var Message = {
PLAY: 'play',
PAUSE: 'pause',
TIMEUPDATE: 'timeupdate',
ADD_HOTSPOT: 'addhotspot',
SET_CONTENT: 'setimage',
SET_VOLUME: 'setvolume',
MUTED: 'muted',
SET_CURRENT_TIME: 'setcurrenttime',
DEVICE_MOTION: 'devicemotion',
GET_POSITION: 'getposition',
SET_FULLSCREEN: 'setfullscreen',
};
module.exports = Message;

@ -0,0 +1,29 @@
import '../../../node_modules/three/src/polyfills.js';
export { WebGLRenderer } from '../../../node_modules/three/src/renderers/WebGLRenderer.js';
export { Scene } from '../../../node_modules/three/src/scenes/Scene.js';
export { Mesh } from '../../../node_modules/three/src/objects/Mesh.js';
export { VideoTexture } from '../../../node_modules/three/src/textures/VideoTexture.js';
export { MeshBasicMaterial } from '../../../node_modules/three/src/materials/MeshBasicMaterial.js';
export { ShaderMaterial } from '../../../node_modules/three/src/materials/ShaderMaterial.js';
export { TextureLoader } from '../../../node_modules/three/src/loaders/TextureLoader.js';
export { PerspectiveCamera } from '../../../node_modules/three/src/cameras/PerspectiveCamera.js';
export { Object3D } from '../../../node_modules/three/src/core/Object3D.js';
export { Raycaster } from '../../../node_modules/three/src/core/Raycaster.js';
export { _Math as Math } from '../../../node_modules/three/src/math/Math.js';
export { Quaternion } from '../../../node_modules/three/src/math/Quaternion.js';
export { Euler } from '../../../node_modules/three/src/math/Euler.js';
export { Matrix4 } from '../../../node_modules/three/src/math/Matrix4.js';
export { Matrix3 } from '../../../node_modules/three/src/math/Matrix3.js';
export { Vector4 } from '../../../node_modules/three/src/math/Vector4.js';
export { Vector3 } from '../../../node_modules/three/src/math/Vector3.js';
export { Vector2 } from '../../../node_modules/three/src/math/Vector2.js';
export { Color } from '../../../node_modules/three/src/math/Color.js';
export { TorusGeometry } from '../../../node_modules/three/src/geometries/TorusGeometry.js';
export { SphereGeometry } from '../../../node_modules/three/src/geometries/SphereGeometry.js';
export { CircleGeometry } from '../../../node_modules/three/src/geometries/CircleGeometry.js';
export { RingGeometry } from '../../../node_modules/three/src/geometries/RingGeometry.js';
export * from '../../../node_modules/three/src/constants.js';
import '../../../node_modules/three/examples/js/controls/VRControls.js';
import '../../../node_modules/three/examples/js/effects/VREffect.js';

@ -0,0 +1,10 @@
var THREE;
var define;
var module;
var exports;
var performance;
var WebGL2RenderingContext;
var VRDisplay;
var PositionSensorVRDevice;
var HMDVRDevice;
var VRFrameData;

@ -0,0 +1,34 @@
import * as fs from 'fs';
var outro = `
Object.defineProperty( exports, 'AudioContext', {
get: function () {
return exports.getAudioContext();
}
});`;
function glsl () {
return {
transform ( code, id ) {
if ( !/\.glsl$/.test( id ) ) return;
return 'export default ' + JSON.stringify(
code
.replace( /[ \t]*\/\/.*\n/g, '' )
.replace( /[ \t]*\/\*[\s\S]*?\*\//g, '' )
.replace( /\n{2,}/g, '\n' )
) + ';';
}
};
}
export default {
entry: 'src/third_party/three/Three.js',
dest: 'build/three.js',
moduleName: 'THREE',
format: 'umd',
plugins: [
glsl()
],
outro: outro
};

@ -0,0 +1,241 @@
/*
* Copyright 2016 Google Inc. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
var Util = window.Util || {};
Util.isDataURI = function(src) {
return src && src.indexOf('data:') == 0;
};
Util.generateUUID = function() {
function s4() {
return Math.floor((1 + Math.random()) * 0x10000)
.toString(16)
.substring(1);
}
return s4() + s4() + '-' + s4() + '-' + s4() + '-' +
s4() + '-' + s4() + s4() + s4();
};
Util.isMobile = function() {
var check = false;
(function(a){if(/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i.test(a)||/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(a.substr(0,4)))check = true})(navigator.userAgent||navigator.vendor||window.opera);
return check;
};
Util.isIOS = function() {
return /(iPad|iPhone|iPod)/g.test(navigator.userAgent);
};
Util.isSafari = function() {
return /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
};
Util.cloneObject = function(obj) {
var out = {};
for (key in obj) {
out[key] = obj[key];
}
return out;
};
Util.hashCode = function(s) {
return s.split("").reduce(function(a,b){a=((a<<5)-a)+b.charCodeAt(0);return a&a},0);
};
Util.loadTrackSrc = function(context, src, callback, opt_progressCallback) {
var request = new XMLHttpRequest();
request.open('GET', src, true);
request.responseType = 'arraybuffer';
// Decode asynchronously.
request.onload = function() {
context.decodeAudioData(request.response, function(buffer) {
callback(buffer);
}, function(e) {
console.error(e);
});
};
if (opt_progressCallback) {
request.onprogress = function(e) {
var percent = e.loaded / e.total;
opt_progressCallback(percent);
};
}
request.send();
};
Util.isPow2 = function(n) {
return (n & (n - 1)) == 0;
};
Util.capitalize = function(s) {
return s.charAt(0).toUpperCase() + s.slice(1);
};
Util.isIFrame = function() {
try {
return window.self !== window.top;
} catch (e) {
return true;
}
};
// From http://goo.gl/4WX3tg
Util.getQueryParameter = function(name) {
name = name.replace(/[\[]/, "\\[").replace(/[\]]/, "\\]");
var regex = new RegExp("[\\?&]" + name + "=([^&#]*)"),
results = regex.exec(location.search);
return results === null ? "" : decodeURIComponent(results[1].replace(/\+/g, " "));
};
// From http://stackoverflow.com/questions/11871077/proper-way-to-detect-webgl-support.
Util.isWebGLEnabled = function() {
var canvas = document.createElement('canvas');
try { gl = canvas.getContext("webgl"); }
catch (x) { gl = null; }
if (gl == null) {
try { gl = canvas.getContext("experimental-webgl"); experimental = true; }
catch (x) { gl = null; }
}
return !!gl;
};
Util.clone = function(obj) {
return JSON.parse(JSON.stringify(obj));
};
// From http://stackoverflow.com/questions/10140604/fastest-hypotenuse-in-javascript
Util.hypot = Math.hypot || function(x, y) {
return Math.sqrt(x*x + y*y);
};
// From http://stackoverflow.com/a/17447718/693934
Util.isIE11 = function() {
return navigator.userAgent.match(/Trident/);
};
Util.getRectCenter = function(rect) {
return new THREE.Vector2(rect.x + rect.width/2, rect.y + rect.height/2);
};
Util.getScreenWidth = function() {
return Math.max(window.screen.width, window.screen.height) *
window.devicePixelRatio;
};
Util.getScreenHeight = function() {
return Math.min(window.screen.width, window.screen.height) *
window.devicePixelRatio;
};
Util.isIOS9OrLess = function() {
if (!Util.isIOS()) {
return false;
}
var re = /(iPhone|iPad|iPod) OS ([\d_]+)/;
var iOSVersion = navigator.userAgent.match(re);
if (!iOSVersion) {
return false;
}
// Get the last group.
var versionString = iOSVersion[iOSVersion.length - 1];
var majorVersion = parseFloat(versionString);
return majorVersion <= 9;
};
Util.getExtension = function(url) {
return url.split('.').pop().split('?')[0];
};
Util.createGetParams = function(params) {
var out = '?';
for (var k in params) {
var paramString = k + '=' + params[k] + '&';
out += paramString;
}
// Remove the trailing ampersand.
out.substring(0, params.length - 2);
return out;
};
Util.sendParentMessage = function(message) {
if (window.parent) {
parent.postMessage(message, '*');
}
};
Util.parseBoolean = function(value) {
if (value == 'false' || value == 0) {
return false;
} else if (value == 'true' || value == 1) {
return true;
} else {
return !!value;
}
};
/**
* @param base {String} An absolute directory root.
* @param relative {String} A relative path.
*
* @returns {String} An absolute path corresponding to the rootPath.
*
* From http://stackoverflow.com/a/14780463/693934.
*/
Util.relativeToAbsolutePath = function(base, relative) {
var stack = base.split('/');
var parts = relative.split('/');
for (var i = 0; i < parts.length; i++) {
if (parts[i] == '.') {
continue;
}
if (parts[i] == '..') {
stack.pop();
} else {
stack.push(parts[i]);
}
}
return stack.join('/');
};
/**
* @return {Boolean} True iff the specified path is an absolute path.
*/
Util.isPathAbsolute = function(path) {
return ! /^(?:\/|[a-z]+:\/\/)/.test(path);
}
Util.isEmptyObject = function(obj) {
return Object.getOwnPropertyNames(obj).length == 0;
};
Util.isDebug = function() {
return Util.parseBoolean(Util.getQueryParameter('debug'));
};
Util.getCurrentScript = function() {
// Note: in IE11, document.currentScript doesn't work, so we fall back to this
// hack, taken from https://goo.gl/TpExuH.
if (!document.currentScript) {
console.warn('This browser does not support document.currentScript. Trying fallback.');
}
return document.currentScript || document.scripts[document.scripts.length - 1];
}
module.exports = Util;

@ -0,0 +1,25 @@
/*
* Copyright 2016 Google Inc. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Video Types
*/
var VideoTypes = {
HLS: 1,
DASH: 2,
VIDEO: 3
};
module.exports = VideoTypes;

@ -0,0 +1,100 @@
html, body {
background-color: #000;
color: #eee;
margin: 0px;
padding: 0px;
position: fixed;
overflow: hidden;
top: 0;
bottom: 0;
left: 0;
right: 0;
/* Prevent user selection. */
user-select: none;
touch-callout: none;
-webkit-touch-callout: none;
-webkit-user-select: none;
}
.dialog {
display: none;
align-items: center;
justify-content: center;
font-family: sans-serif;
font-size: 170%;
position: absolute;
width: 100%;
height: 100%;
background: rgba(255, 255, 255, 0.2);
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
}
.dialog.visible {
display: flex;
}
.dialog .wrap {
padding: 30px 60px;
margin: 20px auto;
width: 60%;
background: rgba(0, 0, 0, 0.8);
border-radius: 5px;
}
.dialog h1 {
margin: 0;
}
a, a:visited {
color: skyblue;
}
#watermark img {
position: fixed;
overflow: hidden;
left: 0;
bottom: 0;
opacity: 0.3;
width: 24px;
height: 24px;
padding: 12px;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
}
#watermark img:hover {
opacity: 1;
-webkit-filter: drop-shadow(white 0 0 5px);
}
canvas {
cursor: -webkit-grab;
}
canvas:active {
cursor: -webkit-grabbing;
}
#play-overlay {
position: fixed;
width: 100%;
height: 100%;
left: 0;
top: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: none;
align-items: center;
justify-content: center;
}
#play-overlay.visible {
display: flex;
}
#play-overlay img {
width: 30%;
height: 30%;
}
#error .message {
font-family: monospace;
}

@ -10,10 +10,29 @@ CKEDITOR.dialog.add( 'video', function ( editor )
value = generateId();
if (value == '') {
return;
// return;
}
videoNode.setAttribute( this.id, value);
switch (this.id) {
case '360video':
if (value) {
videoNode.setAttribute( 'data-360video', 'true' );
} else {
videoNode.removeAttribute( 'data-360video' );
}
break;
case '360videostereo':
if (videoNode.getAttribute( 'data-360video' ) === 'true') {
if (!value) {
videoNode.setAttribute( 'data-360video-stereo', 'false' );
} else {
videoNode.removeAttribute( 'data-360video-stereo' );
}
}
break;
default:
videoNode.setAttribute( this.id, value);
}
if ( !value )
return;
@ -43,8 +62,18 @@ CKEDITOR.dialog.add( 'video', function ( editor )
function loadValue( videoNode )
{
if ( videoNode )
this.setValue( videoNode.getAttribute( this.id ) );
if ( videoNode ) {
switch (this.id) {
case '360video':
this.setValue(videoNode.getAttribute( 'data-360video' ) === 'true');
break;
case '360videostereo':
this.setValue(videoNode.getAttribute( 'data-360video-stereo' ) !== 'false');
break;
default:
this.setValue( videoNode.getAttribute( this.id ) );
}
}
else
{
if ( this.id == 'id')
@ -194,6 +223,7 @@ CKEDITOR.dialog.add( 'video', function ( editor )
[
{
id : 'info',
label: lang.infoLabel,
elements :
[
{
@ -347,6 +377,31 @@ CKEDITOR.dialog.add( 'video', function ( editor )
}]
}
]
},
{
id: '360',
label: '360°',
elements: [
{
type : 'html',
html : lang.html360
},
{
type : 'checkbox',
id : '360video',
label: lang.video360,
commit : commitValue,
setup : loadValue
},
{
type : 'checkbox',
id : '360videostereo',
label : lang.video360stereo,
'default': 'checked',
commit : commitValue,
setup : loadValue
}
]
}
]

@ -179,7 +179,11 @@ var en = {
sourceVideo: 'Source video',
sourceType : 'Video type',
linkTemplate : '<a href="%src%">%type%</a> ',
fallbackTemplate : 'Your browser doesn\'t support video.<br>Please download the file: %links%'
fallbackTemplate : 'Your browser doesn\'t support video.<br>Please download the file: %links%',
infoLabel: 'Information',
html360: 'Please do not add more than one 360° video on a single page.',
video360: 'Enable 360° video player',
video360stereo : 'Stereo video (1:1 aspect ratio)'
};
var es = {
@ -193,7 +197,11 @@ var es = {
sourceVideo: 'Archivo de video',
sourceType : 'Tipo',
linkTemplate : '<a href="%src%">%type%</a> ',
fallbackTemplate : 'Su navegador no soporta VIDEO.<br>Por favor, descargue el fichero: %links%'
fallbackTemplate : 'Su navegador no soporta VIDEO.<br>Por favor, descargue el fichero: %links%',
infoLabel: 'Información',
html360: 'Por favor, no agregar más de un video 360° en una sola página.',
video360: 'Habilitar reproductor de video 360°',
video360stereo : 'Video estéreo (relación de aspecto 1:1)'
};
// v3

@ -871,7 +871,7 @@ INSERT INTO settings_current(variable, subkey, type, category, selected_value, t
// ALTER TABLE c_quiz ADD autolaunch TINYINT(1) DEFAULT 0;
// Enable speed controller in video player
// $_configuration['video_features'] = ['features' => ['speed']];
// $_configuration['video_features'] = ['features' => ['speed', 'vrview']];
// Disable token verification when sending a message
// $_configuration['disable_token_in_new_message'] = false;

@ -258,10 +258,11 @@ $(function() {
if ( {{ show_media_element }} == 1) {
$('video:not(.skip), audio:not(.skip)').mediaelementplayer({
pluginPath: _p.web + 'web/assets/mediaelement/build/',
renderers: ['html5', 'flash_video', 'native_flv'],
//renderers: ['html5', 'flash_video', 'native_flv'],
features: ['{{ video_features }}'],
success: function(mediaElement, originalNode, instance) {
}
},
vrPath: _p.web + 'web/assets/vrview/build/vrview.js'
});
}

Loading…
Cancel
Save