Source: ui/vr_manager.js

  1. /*! @license
  2. * Shaka Player
  3. * Copyright 2016 Google LLC
  4. * SPDX-License-Identifier: Apache-2.0
  5. */
  6. goog.provide('shaka.ui.VRManager');
  7. goog.require('shaka.device.DeviceFactory');
  8. goog.require('shaka.device.IDevice');
  9. goog.require('shaka.log');
  10. goog.require('shaka.ui.VRWebgl');
  11. goog.require('shaka.util.Dom');
  12. goog.require('shaka.util.EventManager');
  13. goog.require('shaka.util.FakeEvent');
  14. goog.require('shaka.util.FakeEventTarget');
  15. goog.require('shaka.util.IReleasable');
  16. goog.requireType('shaka.Player');
  17. /**
  18. * @implements {shaka.util.IReleasable}
  19. */
  20. shaka.ui.VRManager = class extends shaka.util.FakeEventTarget {
  21. /**
  22. * @param {!HTMLElement} container
  23. * @param {?HTMLCanvasElement} canvas
  24. * @param {!HTMLMediaElement} video
  25. * @param {!shaka.Player} player
  26. * @param {shaka.extern.UIConfiguration} config
  27. */
  28. constructor(container, canvas, video, player, config) {
  29. super();
  30. /** @private {!HTMLElement} */
  31. this.container_ = container;
  32. /** @private {?HTMLCanvasElement} */
  33. this.canvas_ = canvas;
  34. /** @private {!HTMLMediaElement} */
  35. this.video_ = video;
  36. /** @private {!shaka.Player} */
  37. this.player_ = player;
  38. /** @private {shaka.extern.UIConfiguration} */
  39. this.config_ = config;
  40. /** @private {shaka.util.EventManager} */
  41. this.loadEventManager_ = new shaka.util.EventManager();
  42. /** @private {shaka.util.EventManager} */
  43. this.eventManager_ = new shaka.util.EventManager();
  44. /** @private {?WebGLRenderingContext} */
  45. this.gl_ = this.getGL_(this.canvas_);
  46. /** @private {?shaka.ui.VRWebgl} */
  47. this.vrWebgl_ = null;
  48. /** @private {boolean} */
  49. this.onGesture_ = false;
  50. /** @private {number} */
  51. this.prevX_ = 0;
  52. /** @private {number} */
  53. this.prevY_ = 0;
  54. /** @private {number} */
  55. this.prevAlpha_ = 0;
  56. /** @private {number} */
  57. this.prevBeta_ = 0;
  58. /** @private {number} */
  59. this.prevGamma_ = 0;
  60. /** @private {?string} */
  61. this.vrAsset_ = null;
  62. this.loadEventManager_.listen(player, 'loading', () => {
  63. if (this.vrWebgl_) {
  64. this.vrWebgl_.reset();
  65. }
  66. this.checkVrStatus_();
  67. });
  68. this.loadEventManager_.listen(player, 'spatialvideoinfo', (event) => {
  69. /** @type {shaka.extern.SpatialVideoInfo} */
  70. const spatialInfo = event['detail'];
  71. let unsupported = false;
  72. switch (spatialInfo.projection) {
  73. case 'rect':
  74. // Rectilinear content is the flat rectangular media.
  75. this.vrAsset_ = null;
  76. break;
  77. case 'equi':
  78. this.vrAsset_ = 'equirectangular';
  79. break;
  80. case 'hequ':
  81. switch (spatialInfo.hfov) {
  82. case 360:
  83. this.vrAsset_ = 'equirectangular';
  84. break;
  85. case 180:
  86. this.vrAsset_ = 'halfequirectangular';
  87. break;
  88. default:
  89. if (spatialInfo.hfov == null) {
  90. this.vrAsset_ = 'halfequirectangular';
  91. } else {
  92. this.vrAsset_ = null;
  93. unsupported = true;
  94. }
  95. break;
  96. }
  97. break;
  98. case 'fish':
  99. // It's not really the same thing, but the difference is very subtle
  100. // and allows us to tolerate it.
  101. this.vrAsset_ = 'halfequirectangular';
  102. break;
  103. default:
  104. this.vrAsset_ = null;
  105. unsupported = true;
  106. break;
  107. }
  108. if (unsupported) {
  109. shaka.log.warning('Unsupported VR projection or hfov', spatialInfo);
  110. }
  111. this.checkVrStatus_();
  112. });
  113. this.loadEventManager_.listen(player, 'nospatialvideoinfo', () => {
  114. this.vrAsset_ = null;
  115. this.checkVrStatus_();
  116. });
  117. this.loadEventManager_.listen(player, 'unloading', () => {
  118. this.vrAsset_ = null;
  119. this.checkVrStatus_();
  120. });
  121. this.checkVrStatus_();
  122. }
  123. /**
  124. * @override
  125. */
  126. release() {
  127. if (this.loadEventManager_) {
  128. this.loadEventManager_.release();
  129. this.loadEventManager_ = null;
  130. }
  131. if (this.eventManager_) {
  132. this.eventManager_.release();
  133. this.eventManager_ = null;
  134. }
  135. if (this.vrWebgl_) {
  136. this.vrWebgl_.release();
  137. this.vrWebgl_ = null;
  138. }
  139. // FakeEventTarget implements IReleasable
  140. super.release();
  141. }
  142. /**
  143. * @param {!shaka.extern.UIConfiguration} config
  144. */
  145. configure(config) {
  146. this.config_ = config;
  147. this.checkVrStatus_();
  148. }
  149. /**
  150. * Returns if a VR is capable.
  151. *
  152. * @return {boolean}
  153. */
  154. canPlayVR() {
  155. if (this.canvas_) {
  156. return !!this.gl_;
  157. }
  158. const canvas =
  159. shaka.util.Dom.asHTMLCanvasElement(document.createElement('canvas'));
  160. return !!this.getGL_(canvas);
  161. }
  162. /**
  163. * Returns if a VR is supported.
  164. *
  165. * @return {boolean}
  166. */
  167. isPlayingVR() {
  168. return !!this.vrWebgl_;
  169. }
  170. /**
  171. * Reset VR view.
  172. */
  173. reset() {
  174. if (!this.vrWebgl_) {
  175. shaka.log.alwaysWarn('Not playing VR content');
  176. return;
  177. }
  178. this.vrWebgl_.reset();
  179. }
  180. /**
  181. * Get the angle of the north.
  182. *
  183. * @return {?number}
  184. */
  185. getNorth() {
  186. if (!this.vrWebgl_) {
  187. shaka.log.alwaysWarn('Not playing VR content');
  188. return null;
  189. }
  190. return this.vrWebgl_.getNorth();
  191. }
  192. /**
  193. * Returns the field of view.
  194. *
  195. * @return {?number}
  196. */
  197. getFieldOfView() {
  198. if (!this.vrWebgl_) {
  199. shaka.log.alwaysWarn('Not playing VR content');
  200. return null;
  201. }
  202. return this.vrWebgl_.getFieldOfView();
  203. }
  204. /**
  205. * Set the field of view.
  206. *
  207. * @param {number} fieldOfView
  208. */
  209. setFieldOfView(fieldOfView) {
  210. if (!this.vrWebgl_) {
  211. shaka.log.alwaysWarn('Not playing VR content');
  212. return;
  213. }
  214. if (fieldOfView < 0) {
  215. shaka.log.alwaysWarn('Field of view should be greater than 0');
  216. fieldOfView = 0;
  217. } else if (fieldOfView > 100) {
  218. shaka.log.alwaysWarn('Field of view should be less than 100');
  219. fieldOfView = 100;
  220. }
  221. this.vrWebgl_.setFieldOfView(fieldOfView);
  222. }
  223. /**
  224. * Toggle stereoscopic mode.
  225. */
  226. toggleStereoscopicMode() {
  227. if (!this.vrWebgl_) {
  228. shaka.log.alwaysWarn('Not playing VR content');
  229. return;
  230. }
  231. this.vrWebgl_.toggleStereoscopicMode();
  232. }
  233. /**
  234. * Returns true if stereoscopic mode is enabled.
  235. *
  236. * @return {boolean}
  237. */
  238. isStereoscopicModeEnabled() {
  239. if (!this.vrWebgl_) {
  240. shaka.log.alwaysWarn('Not playing VR content');
  241. return false;
  242. }
  243. return this.vrWebgl_.isStereoscopicModeEnabled();
  244. }
  245. /**
  246. * Increment the yaw in X angle in degrees.
  247. *
  248. * @param {number} angle
  249. */
  250. incrementYaw(angle) {
  251. if (!this.vrWebgl_) {
  252. shaka.log.alwaysWarn('Not playing VR content');
  253. return;
  254. }
  255. this.vrWebgl_.rotateViewGlobal(
  256. angle * shaka.ui.VRManager.TO_RADIANS_, 0, 0);
  257. }
  258. /**
  259. * Increment the pitch in X angle in degrees.
  260. *
  261. * @param {number} angle
  262. */
  263. incrementPitch(angle) {
  264. if (!this.vrWebgl_) {
  265. shaka.log.alwaysWarn('Not playing VR content');
  266. return;
  267. }
  268. this.vrWebgl_.rotateViewGlobal(
  269. 0, angle * shaka.ui.VRManager.TO_RADIANS_, 0);
  270. }
  271. /**
  272. * Increment the roll in X angle in degrees.
  273. *
  274. * @param {number} angle
  275. */
  276. incrementRoll(angle) {
  277. if (!this.vrWebgl_) {
  278. shaka.log.alwaysWarn('Not playing VR content');
  279. return;
  280. }
  281. this.vrWebgl_.rotateViewGlobal(
  282. 0, 0, angle * shaka.ui.VRManager.TO_RADIANS_);
  283. }
  284. /**
  285. * @private
  286. */
  287. checkVrStatus_() {
  288. if ((this.config_.displayInVrMode || this.vrAsset_)) {
  289. if (!this.canvas_) {
  290. this.canvas_ = shaka.util.Dom.asHTMLCanvasElement(
  291. document.createElement('canvas'));
  292. this.canvas_.classList.add('shaka-vr-canvas-container');
  293. this.video_.parentElement.insertBefore(
  294. this.canvas_, this.video_.nextElementSibling);
  295. this.gl_ = this.getGL_(this.canvas_);
  296. }
  297. const newProjectionMode =
  298. this.vrAsset_ || this.config_.defaultVrProjectionMode;
  299. if (!this.vrWebgl_) {
  300. this.canvas_.style.display = '';
  301. this.init_(newProjectionMode);
  302. this.dispatchEvent(new shaka.util.FakeEvent(
  303. 'vrstatuschanged',
  304. (new Map()).set('newStatus', this.isPlayingVR())));
  305. } else {
  306. const currentProjectionMode = this.vrWebgl_.getProjectionMode();
  307. if (currentProjectionMode != newProjectionMode) {
  308. this.eventManager_.removeAll();
  309. this.vrWebgl_.release();
  310. this.init_(newProjectionMode);
  311. // Re-initialization the status does not change.
  312. }
  313. }
  314. } else if (!this.config_.displayInVrMode && !this.vrAsset_ &&
  315. this.canvas_ && this.vrWebgl_) {
  316. this.canvas_.style.display = 'none';
  317. this.eventManager_.removeAll();
  318. this.vrWebgl_.release();
  319. this.vrWebgl_ = null;
  320. this.dispatchEvent(new shaka.util.FakeEvent(
  321. 'vrstatuschanged',
  322. (new Map()).set('newStatus', this.isPlayingVR())));
  323. }
  324. }
  325. /**
  326. * @param {string} projectionMode
  327. * @private
  328. */
  329. init_(projectionMode) {
  330. if (this.gl_ && this.canvas_) {
  331. this.vrWebgl_ = new shaka.ui.VRWebgl(
  332. this.video_, this.player_, this.canvas_, this.gl_, projectionMode);
  333. this.setupVRListeners_();
  334. }
  335. }
  336. /**
  337. * @param {?HTMLCanvasElement} canvas
  338. * @return {?WebGLRenderingContext}
  339. * @private
  340. */
  341. getGL_(canvas) {
  342. if (!canvas) {
  343. return null;
  344. }
  345. // The user interface is not intended for devices that are controlled with
  346. // a remote control, and WebGL may run slowly on these devices.
  347. const device = shaka.device.DeviceFactory.getDevice();
  348. const deviceType = device.getDeviceType();
  349. if (deviceType == shaka.device.IDevice.DeviceType.TV ||
  350. deviceType == shaka.device.IDevice.DeviceType.CONSOLE ||
  351. deviceType == shaka.device.IDevice.DeviceType.CAST) {
  352. return null;
  353. }
  354. const webglContexts = [
  355. 'webgl2',
  356. 'webgl',
  357. ];
  358. for (const webgl of webglContexts) {
  359. const gl = canvas.getContext(webgl);
  360. if (gl) {
  361. return /** @type {!WebGLRenderingContext} */(gl);
  362. }
  363. }
  364. return null;
  365. }
  366. /**
  367. * @private
  368. */
  369. setupVRListeners_() {
  370. // Start
  371. this.eventManager_.listen(this.container_, 'mousedown', (event) => {
  372. if (!this.onGesture_) {
  373. this.gestureStart_(event.clientX, event.clientY);
  374. }
  375. });
  376. if (navigator.maxTouchPoints > 0) {
  377. this.eventManager_.listen(this.container_, 'touchstart', (e) => {
  378. if (!this.onGesture_) {
  379. const event = /** @type {!TouchEvent} */(e);
  380. this.gestureStart_(
  381. event.touches[0].clientX, event.touches[0].clientY);
  382. }
  383. });
  384. }
  385. // Zoom
  386. this.eventManager_.listen(this.container_, 'wheel', (e) => {
  387. if (!this.onGesture_) {
  388. const event = /** @type {!WheelEvent} */(e);
  389. this.vrWebgl_.zoom(event.deltaY);
  390. event.preventDefault();
  391. event.stopPropagation();
  392. }
  393. });
  394. // Move
  395. this.eventManager_.listen(this.container_, 'mousemove', (event) => {
  396. if (this.onGesture_) {
  397. this.gestureMove_(event.clientX, event.clientY);
  398. }
  399. });
  400. if (navigator.maxTouchPoints > 0) {
  401. this.eventManager_.listen(this.container_, 'touchmove', (e) => {
  402. if (this.onGesture_) {
  403. const event = /** @type {!TouchEvent} */(e);
  404. this.gestureMove_(
  405. event.touches[0].clientX, event.touches[0].clientY);
  406. }
  407. e.preventDefault();
  408. });
  409. }
  410. // End
  411. this.eventManager_.listen(this.container_, 'mouseleave', () => {
  412. this.onGesture_ = false;
  413. });
  414. this.eventManager_.listen(this.container_, 'mouseup', () => {
  415. this.onGesture_ = false;
  416. });
  417. if (navigator.maxTouchPoints > 0) {
  418. this.eventManager_.listen(this.container_, 'touchend', () => {
  419. this.onGesture_ = false;
  420. });
  421. }
  422. // Detect device movement
  423. let deviceOrientationListener = false;
  424. if (window.DeviceOrientationEvent) {
  425. // See: https://dev.to/li/how-to-requestpermission-for-devicemotion-and-deviceorientation-events-in-ios-13-46g2
  426. if (typeof DeviceMotionEvent.requestPermission == 'function') {
  427. const userGestureListener = () => {
  428. DeviceMotionEvent.requestPermission().then((newPermissionState) => {
  429. if (newPermissionState !== 'granted' ||
  430. deviceOrientationListener) {
  431. return;
  432. }
  433. deviceOrientationListener = true;
  434. this.setupDeviceOrientationListener_();
  435. });
  436. };
  437. DeviceMotionEvent.requestPermission().then((permissionState) => {
  438. this.eventManager_.unlisten(
  439. this.container_, 'click', userGestureListener);
  440. this.eventManager_.unlisten(
  441. this.container_, 'mouseup', userGestureListener);
  442. if (navigator.maxTouchPoints > 0) {
  443. this.eventManager_.unlisten(
  444. this.container_, 'touchend', userGestureListener);
  445. }
  446. if (permissionState !== 'granted') {
  447. this.eventManager_.listenOnce(
  448. this.container_, 'click', userGestureListener);
  449. this.eventManager_.listenOnce(
  450. this.container_, 'mouseup', userGestureListener);
  451. if (navigator.maxTouchPoints > 0) {
  452. this.eventManager_.listenOnce(
  453. this.container_, 'touchend', userGestureListener);
  454. }
  455. return;
  456. }
  457. deviceOrientationListener = true;
  458. this.setupDeviceOrientationListener_();
  459. }).catch(() => {
  460. this.eventManager_.unlisten(
  461. this.container_, 'click', userGestureListener);
  462. this.eventManager_.unlisten(
  463. this.container_, 'mouseup', userGestureListener);
  464. if (navigator.maxTouchPoints > 0) {
  465. this.eventManager_.unlisten(
  466. this.container_, 'touchend', userGestureListener);
  467. }
  468. this.eventManager_.listenOnce(
  469. this.container_, 'click', userGestureListener);
  470. this.eventManager_.listenOnce(
  471. this.container_, 'mouseup', userGestureListener);
  472. if (navigator.maxTouchPoints > 0) {
  473. this.eventManager_.listenOnce(
  474. this.container_, 'touchend', userGestureListener);
  475. }
  476. });
  477. } else {
  478. deviceOrientationListener = true;
  479. this.setupDeviceOrientationListener_();
  480. }
  481. }
  482. }
  483. /**
  484. * @private
  485. */
  486. setupDeviceOrientationListener_() {
  487. this.eventManager_.listen(window, 'deviceorientation', (e) => {
  488. if (!this.vrWebgl_) {
  489. return;
  490. }
  491. const event = /** @type {!DeviceOrientationEvent} */(e);
  492. let alphaDif = (event.alpha || 0) - this.prevAlpha_;
  493. let betaDif = (event.beta || 0) - this.prevBeta_;
  494. let gammaDif = (event.gamma || 0) - this.prevGamma_;
  495. if (Math.abs(alphaDif) > 10 || Math.abs(betaDif) > 10 ||
  496. Math.abs(gammaDif) > 5) {
  497. alphaDif = 0;
  498. gammaDif = 0;
  499. betaDif = 0;
  500. }
  501. this.prevAlpha_ = event.alpha || 0;
  502. this.prevBeta_ = event.beta || 0;
  503. this.prevGamma_ = event.gamma || 0;
  504. const toRadians = shaka.ui.VRManager.TO_RADIANS_;
  505. const orientation = screen.orientation.angle;
  506. if (orientation == 90 || orientation == -90) {
  507. this.vrWebgl_.rotateViewGlobal(
  508. alphaDif * toRadians * -1, gammaDif * toRadians * -1, 0);
  509. } else {
  510. this.vrWebgl_.rotateViewGlobal(
  511. alphaDif * toRadians * -1, betaDif * toRadians, 0);
  512. }
  513. });
  514. }
  515. /**
  516. * @param {number} x
  517. * @param {number} y
  518. * @private
  519. */
  520. gestureStart_(x, y) {
  521. this.onGesture_ = true;
  522. this.prevX_ = x;
  523. this.prevY_ = y;
  524. }
  525. /**
  526. * @param {number} x
  527. * @param {number} y
  528. * @private
  529. */
  530. gestureMove_(x, y) {
  531. const touchScaleFactor = -0.60 * Math.PI / 180;
  532. this.vrWebgl_.rotateViewGlobal((x - this.prevX_) * touchScaleFactor,
  533. (y - this.prevY_) * -1 * touchScaleFactor, 0);
  534. this.prevX_ = x;
  535. this.prevY_ = y;
  536. }
  537. };
  538. /**
  539. * @const {number}
  540. * @private
  541. */
  542. shaka.ui.VRManager.TO_RADIANS_ = Math.PI / 180;