You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

SharedVideo.js 26KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844
  1. /* global $, APP, YT, interfaceConfig, onPlayerReady, onPlayerStateChange,
  2. onPlayerError */
  3. const logger = require('jitsi-meet-logger').getLogger(__filename);
  4. import UIUtil from '../util/UIUtil';
  5. import UIEvents from '../../../service/UI/UIEvents';
  6. import VideoLayout from '../videolayout/VideoLayout';
  7. import LargeContainer from '../videolayout/LargeContainer';
  8. import Filmstrip from '../videolayout/Filmstrip';
  9. import {
  10. createSharedVideoEvent as createEvent,
  11. sendAnalytics
  12. } from '../../../react/features/analytics';
  13. import {
  14. participantJoined,
  15. participantLeft
  16. } from '../../../react/features/base/participants';
  17. import { dockToolbox, showToolbox } from '../../../react/features/toolbox';
  18. import SharedVideoThumb from './SharedVideoThumb';
  19. export const SHARED_VIDEO_CONTAINER_TYPE = 'sharedvideo';
  20. /**
  21. * Example shared video link.
  22. * @type {string}
  23. */
  24. const defaultSharedVideoLink = 'https://www.youtube.com/watch?v=xNXN7CZk8X0';
  25. const updateInterval = 5000; // milliseconds
  26. /**
  27. * The dialog for user input (video link).
  28. * @type {null}
  29. */
  30. let dialog = null;
  31. /**
  32. * Manager of shared video.
  33. */
  34. export default class SharedVideoManager {
  35. /**
  36. *
  37. */
  38. constructor(emitter) {
  39. this.emitter = emitter;
  40. this.isSharedVideoShown = false;
  41. this.isPlayerAPILoaded = false;
  42. this.mutedWithUserInteraction = false;
  43. }
  44. /**
  45. * Indicates if the player volume is currently on. This will return true if
  46. * we have an available player, which is currently in a PLAYING state,
  47. * which isn't muted and has it's volume greater than 0.
  48. *
  49. * @returns {boolean} indicating if the volume of the shared video is
  50. * currently on.
  51. */
  52. isSharedVideoVolumeOn() {
  53. return this.player
  54. && this.player.getPlayerState() === YT.PlayerState.PLAYING
  55. && !this.player.isMuted()
  56. && this.player.getVolume() > 0;
  57. }
  58. /**
  59. * Indicates if the local user is the owner of the shared video.
  60. * @returns {*|boolean}
  61. */
  62. isSharedVideoOwner() {
  63. return this.from && APP.conference.isLocalId(this.from);
  64. }
  65. /**
  66. * Starts shared video by asking user for url, or if its already working
  67. * asks whether the user wants to stop sharing the video.
  68. */
  69. toggleSharedVideo() {
  70. if (dialog) {
  71. return;
  72. }
  73. if (!this.isSharedVideoShown) {
  74. requestVideoLink().then(
  75. url => {
  76. this.emitter.emit(
  77. UIEvents.UPDATE_SHARED_VIDEO, url, 'start');
  78. sendAnalytics(createEvent('started'));
  79. },
  80. err => {
  81. logger.log('SHARED VIDEO CANCELED', err);
  82. sendAnalytics(createEvent('canceled'));
  83. }
  84. );
  85. return;
  86. }
  87. if (APP.conference.isLocalId(this.from)) {
  88. showStopVideoPropmpt().then(
  89. () => {
  90. // make sure we stop updates for playing before we send stop
  91. // if we stop it after receiving self presence, we can end
  92. // up sending stop playing, and on the other end it will not
  93. // stop
  94. if (this.intervalId) {
  95. clearInterval(this.intervalId);
  96. this.intervalId = null;
  97. }
  98. this.emitter.emit(
  99. UIEvents.UPDATE_SHARED_VIDEO, this.url, 'stop');
  100. sendAnalytics(createEvent('stopped'));
  101. },
  102. () => {}); // eslint-disable-line no-empty-function
  103. } else {
  104. APP.UI.messageHandler.showWarning({
  105. descriptionKey: 'dialog.alreadySharedVideoMsg',
  106. titleKey: 'dialog.alreadySharedVideoTitle'
  107. });
  108. sendAnalytics(createEvent('already.shared'));
  109. }
  110. }
  111. /**
  112. * Shows the player component and starts the process that will be sending
  113. * updates, if we are the one shared the video.
  114. *
  115. * @param id the id of the sender of the command
  116. * @param url the video url
  117. * @param attributes
  118. */
  119. onSharedVideoStart(id, url, attributes) {
  120. if (this.isSharedVideoShown) {
  121. return;
  122. }
  123. this.isSharedVideoShown = true;
  124. // the video url
  125. this.url = url;
  126. // the owner of the video
  127. this.from = id;
  128. this.mutedWithUserInteraction = APP.conference.isLocalAudioMuted();
  129. // listen for local audio mute events
  130. this.localAudioMutedListener = this.onLocalAudioMuted.bind(this);
  131. this.emitter.on(UIEvents.AUDIO_MUTED, this.localAudioMutedListener);
  132. // This code loads the IFrame Player API code asynchronously.
  133. const tag = document.createElement('script');
  134. tag.src = 'https://www.youtube.com/iframe_api';
  135. const firstScriptTag = document.getElementsByTagName('script')[0];
  136. firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
  137. // sometimes we receive errors like player not defined
  138. // or player.pauseVideo is not a function
  139. // we need to operate with player after start playing
  140. // self.player will be defined once it start playing
  141. // and will process any initial attributes if any
  142. this.initialAttributes = attributes;
  143. const self = this;
  144. if (self.isPlayerAPILoaded) {
  145. window.onYouTubeIframeAPIReady();
  146. } else {
  147. window.onYouTubeIframeAPIReady = function() {
  148. self.isPlayerAPILoaded = true;
  149. const showControls
  150. = APP.conference.isLocalId(self.from) ? 1 : 0;
  151. const p = new YT.Player('sharedVideoIFrame', {
  152. height: '100%',
  153. width: '100%',
  154. videoId: self.url,
  155. playerVars: {
  156. 'origin': location.origin,
  157. 'fs': '0',
  158. 'autoplay': 0,
  159. 'controls': showControls,
  160. 'rel': 0
  161. },
  162. events: {
  163. 'onReady': onPlayerReady,
  164. 'onStateChange': onPlayerStateChange,
  165. 'onError': onPlayerError
  166. }
  167. });
  168. // add listener for volume changes
  169. p.addEventListener(
  170. 'onVolumeChange', 'onVolumeChange');
  171. if (APP.conference.isLocalId(self.from)) {
  172. // adds progress listener that will be firing events
  173. // while we are paused and we change the progress of the
  174. // video (seeking forward or backward on the video)
  175. p.addEventListener(
  176. 'onVideoProgress', 'onVideoProgress');
  177. }
  178. };
  179. }
  180. /**
  181. * Indicates that a change in state has occurred for the shared video.
  182. * @param event the event notifying us of the change
  183. */
  184. window.onPlayerStateChange = function(event) {
  185. // eslint-disable-next-line eqeqeq
  186. if (event.data == YT.PlayerState.PLAYING) {
  187. self.player = event.target;
  188. if (self.initialAttributes) {
  189. // If a network update has occurred already now is the
  190. // time to process it.
  191. self.processVideoUpdate(
  192. self.player,
  193. self.initialAttributes);
  194. self.initialAttributes = null;
  195. }
  196. self.smartAudioMute();
  197. // eslint-disable-next-line eqeqeq
  198. } else if (event.data == YT.PlayerState.PAUSED) {
  199. self.smartAudioUnmute();
  200. sendAnalytics(createEvent('paused'));
  201. }
  202. // eslint-disable-next-line eqeqeq
  203. self.fireSharedVideoEvent(event.data == YT.PlayerState.PAUSED);
  204. };
  205. /**
  206. * Track player progress while paused.
  207. * @param event
  208. */
  209. window.onVideoProgress = function(event) {
  210. const state = event.target.getPlayerState();
  211. // eslint-disable-next-line eqeqeq
  212. if (state == YT.PlayerState.PAUSED) {
  213. self.fireSharedVideoEvent(true);
  214. }
  215. };
  216. /**
  217. * Gets notified for volume state changed.
  218. * @param event
  219. */
  220. window.onVolumeChange = function(event) {
  221. self.fireSharedVideoEvent();
  222. // let's check, if player is not muted lets mute locally
  223. if (event.data.volume > 0 && !event.data.muted) {
  224. self.smartAudioMute();
  225. } else if (event.data.volume <= 0 || event.data.muted) {
  226. self.smartAudioUnmute();
  227. }
  228. sendAnalytics(createEvent(
  229. 'volume.changed',
  230. {
  231. volume: event.data.volume,
  232. muted: event.data.muted
  233. }));
  234. };
  235. window.onPlayerReady = function(event) {
  236. const player = event.target;
  237. // do not relay on autoplay as it is not sending all of the events
  238. // in onPlayerStateChange
  239. player.playVideo();
  240. const thumb = new SharedVideoThumb(
  241. self.url, SHARED_VIDEO_CONTAINER_TYPE, VideoLayout);
  242. thumb.setDisplayName('YouTube');
  243. VideoLayout.addRemoteVideoContainer(self.url, thumb);
  244. const iframe = player.getIframe();
  245. // eslint-disable-next-line no-use-before-define
  246. self.sharedVideo = new SharedVideoContainer(
  247. { url,
  248. iframe,
  249. player });
  250. // prevents pausing participants not sharing the video
  251. // to pause the video
  252. if (!APP.conference.isLocalId(self.from)) {
  253. $('#sharedVideo').css('pointer-events', 'none');
  254. }
  255. VideoLayout.addLargeVideoContainer(
  256. SHARED_VIDEO_CONTAINER_TYPE, self.sharedVideo);
  257. APP.store.dispatch(participantJoined({
  258. id: self.url,
  259. isBot: true,
  260. name: 'YouTube'
  261. }));
  262. VideoLayout.handleVideoThumbClicked(self.url);
  263. // If we are sending the command and we are starting the player
  264. // we need to continuously send the player current time position
  265. if (APP.conference.isLocalId(self.from)) {
  266. self.intervalId = setInterval(
  267. self.fireSharedVideoEvent.bind(self),
  268. updateInterval);
  269. }
  270. };
  271. window.onPlayerError = function(event) {
  272. logger.error('Error in the player:', event.data);
  273. // store the error player, so we can remove it
  274. self.errorInPlayer = event.target;
  275. };
  276. }
  277. /**
  278. * Process attributes, whether player needs to be paused or seek.
  279. * @param player the player to operate over
  280. * @param attributes the attributes with the player state we want
  281. */
  282. processVideoUpdate(player, attributes) {
  283. if (!attributes) {
  284. return;
  285. }
  286. // eslint-disable-next-line eqeqeq
  287. if (attributes.state == 'playing') {
  288. const isPlayerPaused
  289. = this.player.getPlayerState() === YT.PlayerState.PAUSED;
  290. // If our player is currently paused force the seek.
  291. this.processTime(player, attributes, isPlayerPaused);
  292. // Process mute.
  293. const isAttrMuted = attributes.muted === 'true';
  294. if (player.isMuted() !== isAttrMuted) {
  295. this.smartPlayerMute(isAttrMuted, true);
  296. }
  297. // Process volume
  298. if (!isAttrMuted
  299. && attributes.volume !== undefined
  300. // eslint-disable-next-line eqeqeq
  301. && player.getVolume() != attributes.volume) {
  302. player.setVolume(attributes.volume);
  303. logger.info(`Player change of volume:${attributes.volume}`);
  304. this.showSharedVideoMutedPopup(false);
  305. }
  306. if (isPlayerPaused) {
  307. player.playVideo();
  308. }
  309. // eslint-disable-next-line eqeqeq
  310. } else if (attributes.state == 'pause') {
  311. // if its not paused, pause it
  312. player.pauseVideo();
  313. this.processTime(player, attributes, true);
  314. }
  315. }
  316. /**
  317. * Check for time in attributes and if needed seek in current player
  318. * @param player the player to operate over
  319. * @param attributes the attributes with the player state we want
  320. * @param forceSeek whether seek should be forced
  321. */
  322. processTime(player, attributes, forceSeek) {
  323. if (forceSeek) {
  324. logger.info('Player seekTo:', attributes.time);
  325. player.seekTo(attributes.time);
  326. return;
  327. }
  328. // check received time and current time
  329. const currentPosition = player.getCurrentTime();
  330. const diff = Math.abs(attributes.time - currentPosition);
  331. // if we drift more than the interval for checking
  332. // sync, the interval is in milliseconds
  333. if (diff > updateInterval / 1000) {
  334. logger.info('Player seekTo:', attributes.time,
  335. ' current time is:', currentPosition, ' diff:', diff);
  336. player.seekTo(attributes.time);
  337. }
  338. }
  339. /**
  340. * Checks current state of the player and fire an event with the values.
  341. */
  342. fireSharedVideoEvent(sendPauseEvent) {
  343. // ignore update checks if we are not the owner of the video
  344. // or there is still no player defined or we are stopped
  345. // (in a process of stopping)
  346. if (!APP.conference.isLocalId(this.from) || !this.player
  347. || !this.isSharedVideoShown) {
  348. return;
  349. }
  350. const state = this.player.getPlayerState();
  351. // if its paused and haven't been pause - send paused
  352. if (state === YT.PlayerState.PAUSED && sendPauseEvent) {
  353. this.emitter.emit(UIEvents.UPDATE_SHARED_VIDEO,
  354. this.url, 'pause', this.player.getCurrentTime());
  355. } else if (state === YT.PlayerState.PLAYING) {
  356. // if its playing and it was paused - send update with time
  357. // if its playing and was playing just send update with time
  358. this.emitter.emit(UIEvents.UPDATE_SHARED_VIDEO,
  359. this.url, 'playing',
  360. this.player.getCurrentTime(),
  361. this.player.isMuted(),
  362. this.player.getVolume());
  363. }
  364. }
  365. /**
  366. * Updates video, if it's not playing and needs starting or if it's playing
  367. * and needs to be paused.
  368. * @param id the id of the sender of the command
  369. * @param url the video url
  370. * @param attributes
  371. */
  372. onSharedVideoUpdate(id, url, attributes) {
  373. // if we are sending the event ignore
  374. if (APP.conference.isLocalId(this.from)) {
  375. return;
  376. }
  377. if (!this.isSharedVideoShown) {
  378. this.onSharedVideoStart(id, url, attributes);
  379. return;
  380. }
  381. // eslint-disable-next-line no-negated-condition
  382. if (!this.player) {
  383. this.initialAttributes = attributes;
  384. } else {
  385. this.processVideoUpdate(this.player, attributes);
  386. }
  387. }
  388. /**
  389. * Stop shared video if it is currently showed. If the user started the
  390. * shared video is the one in the id (called when user
  391. * left and we want to remove video if the user sharing it left).
  392. * @param id the id of the sender of the command
  393. */
  394. onSharedVideoStop(id, attributes) {
  395. if (!this.isSharedVideoShown) {
  396. return;
  397. }
  398. if (this.from !== id) {
  399. return;
  400. }
  401. if (!this.player) {
  402. // if there is no error in the player till now,
  403. // store the initial attributes
  404. if (!this.errorInPlayer) {
  405. this.initialAttributes = attributes;
  406. return;
  407. }
  408. }
  409. this.emitter.removeListener(UIEvents.AUDIO_MUTED,
  410. this.localAudioMutedListener);
  411. this.localAudioMutedListener = null;
  412. VideoLayout.removeParticipantContainer(this.url);
  413. VideoLayout.showLargeVideoContainer(SHARED_VIDEO_CONTAINER_TYPE, false)
  414. .then(() => {
  415. VideoLayout.removeLargeVideoContainer(
  416. SHARED_VIDEO_CONTAINER_TYPE);
  417. if (this.player) {
  418. this.player.destroy();
  419. this.player = null;
  420. } else if (this.errorInPlayer) {
  421. // if there is an error in player, remove that instance
  422. this.errorInPlayer.destroy();
  423. this.errorInPlayer = null;
  424. }
  425. this.smartAudioUnmute();
  426. // revert to original behavior (prevents pausing
  427. // for participants not sharing the video to pause it)
  428. $('#sharedVideo').css('pointer-events', 'auto');
  429. this.emitter.emit(
  430. UIEvents.UPDATE_SHARED_VIDEO, null, 'removed');
  431. });
  432. APP.store.dispatch(participantLeft(this.url));
  433. this.url = null;
  434. this.isSharedVideoShown = false;
  435. this.initialAttributes = null;
  436. }
  437. /**
  438. * Receives events for local audio mute/unmute by local user.
  439. * @param muted boolena whether it is muted or not.
  440. * @param {boolean} indicates if this mute was a result of user interaction,
  441. * i.e. pressing the mute button or it was programatically triggerred
  442. */
  443. onLocalAudioMuted(muted, userInteraction) {
  444. if (!this.player) {
  445. return;
  446. }
  447. if (muted) {
  448. this.mutedWithUserInteraction = userInteraction;
  449. } else if (this.player.getPlayerState() !== YT.PlayerState.PAUSED) {
  450. this.smartPlayerMute(true, false);
  451. // Check if we need to update other participants
  452. this.fireSharedVideoEvent();
  453. }
  454. }
  455. /**
  456. * Mutes / unmutes the player.
  457. * @param mute true to mute the shared video, false - otherwise.
  458. * @param {boolean} Indicates if this mute is a consequence of a network
  459. * video update or is called locally.
  460. */
  461. smartPlayerMute(mute, isVideoUpdate) {
  462. if (!this.player.isMuted() && mute) {
  463. this.player.mute();
  464. if (isVideoUpdate) {
  465. this.smartAudioUnmute();
  466. }
  467. } else if (this.player.isMuted() && !mute) {
  468. this.player.unMute();
  469. if (isVideoUpdate) {
  470. this.smartAudioMute();
  471. }
  472. }
  473. this.showSharedVideoMutedPopup(mute);
  474. }
  475. /**
  476. * Smart mike unmute. If the mike is currently muted and it wasn't muted
  477. * by the user via the mike button and the volume of the shared video is on
  478. * we're unmuting the mike automatically.
  479. */
  480. smartAudioUnmute() {
  481. if (APP.conference.isLocalAudioMuted()
  482. && !this.mutedWithUserInteraction
  483. && !this.isSharedVideoVolumeOn()) {
  484. sendAnalytics(createEvent('audio.unmuted'));
  485. logger.log('Shared video: audio unmuted');
  486. this.emitter.emit(UIEvents.AUDIO_MUTED, false, false);
  487. this.showMicMutedPopup(false);
  488. }
  489. }
  490. /**
  491. * Smart mike mute. If the mike isn't currently muted and the shared video
  492. * volume is on we mute the mike.
  493. */
  494. smartAudioMute() {
  495. if (!APP.conference.isLocalAudioMuted()
  496. && this.isSharedVideoVolumeOn()) {
  497. sendAnalytics(createEvent('audio.muted'));
  498. logger.log('Shared video: audio muted');
  499. this.emitter.emit(UIEvents.AUDIO_MUTED, true, false);
  500. this.showMicMutedPopup(true);
  501. }
  502. }
  503. /**
  504. * Shows a popup under the microphone toolbar icon that notifies the user
  505. * of automatic mute after a shared video has started.
  506. * @param show boolean, show or hide the notification
  507. */
  508. showMicMutedPopup(show) {
  509. if (show) {
  510. this.showSharedVideoMutedPopup(false);
  511. }
  512. APP.UI.showCustomToolbarPopup(
  513. 'microphone', 'micMutedPopup', show, 5000);
  514. }
  515. /**
  516. * Shows a popup under the shared video toolbar icon that notifies the user
  517. * of automatic mute of the shared video after the user has unmuted their
  518. * mic.
  519. * @param show boolean, show or hide the notification
  520. */
  521. showSharedVideoMutedPopup(show) {
  522. if (show) {
  523. this.showMicMutedPopup(false);
  524. }
  525. APP.UI.showCustomToolbarPopup(
  526. 'sharedvideo', 'sharedVideoMutedPopup', show, 5000);
  527. }
  528. }
  529. /**
  530. * Container for shared video iframe.
  531. */
  532. class SharedVideoContainer extends LargeContainer {
  533. /**
  534. *
  535. */
  536. constructor({ url, iframe, player }) {
  537. super();
  538. this.$iframe = $(iframe);
  539. this.url = url;
  540. this.player = player;
  541. }
  542. /**
  543. *
  544. */
  545. show() {
  546. const self = this;
  547. return new Promise(resolve => {
  548. this.$iframe.fadeIn(300, () => {
  549. self.bodyBackground = document.body.style.background;
  550. document.body.style.background = 'black';
  551. this.$iframe.css({ opacity: 1 });
  552. APP.store.dispatch(dockToolbox(true));
  553. resolve();
  554. });
  555. });
  556. }
  557. /**
  558. *
  559. */
  560. hide() {
  561. const self = this;
  562. APP.store.dispatch(dockToolbox(false));
  563. return new Promise(resolve => {
  564. this.$iframe.fadeOut(300, () => {
  565. document.body.style.background = self.bodyBackground;
  566. this.$iframe.css({ opacity: 0 });
  567. resolve();
  568. });
  569. });
  570. }
  571. /**
  572. *
  573. */
  574. onHoverIn() {
  575. APP.store.dispatch(showToolbox());
  576. }
  577. /**
  578. *
  579. */
  580. get id() {
  581. return this.url;
  582. }
  583. /**
  584. *
  585. */
  586. resize(containerWidth, containerHeight) {
  587. let height, width;
  588. if (interfaceConfig.VERTICAL_FILMSTRIP) {
  589. height = containerHeight;
  590. width = containerWidth - Filmstrip.getFilmstripWidth();
  591. } else {
  592. height = containerHeight - Filmstrip.getFilmstripHeight();
  593. width = containerWidth;
  594. }
  595. this.$iframe.width(width).height(height);
  596. }
  597. /**
  598. * @return {boolean} do not switch on dominant speaker event if on stage.
  599. */
  600. stayOnStage() {
  601. return false;
  602. }
  603. }
  604. /**
  605. * Checks if given string is youtube url.
  606. * @param {string} url string to check.
  607. * @returns {boolean}
  608. */
  609. function getYoutubeLink(url) {
  610. const p = /^(?:https?:\/\/)?(?:www\.)?(?:youtu\.be\/|youtube\.com\/(?:embed\/|v\/|watch\?v=|watch\?.+&v=))((\w|-){11})(?:\S+)?$/;// eslint-disable-line max-len
  611. return url.match(p) ? RegExp.$1 : false;
  612. }
  613. /**
  614. * Ask user if he want to close shared video.
  615. */
  616. function showStopVideoPropmpt() {
  617. return new Promise((resolve, reject) => {
  618. const submitFunction = function(e, v) {
  619. if (v) {
  620. resolve();
  621. } else {
  622. reject();
  623. }
  624. };
  625. const closeFunction = function() {
  626. dialog = null;
  627. };
  628. dialog = APP.UI.messageHandler.openTwoButtonDialog({
  629. titleKey: 'dialog.removeSharedVideoTitle',
  630. msgKey: 'dialog.removeSharedVideoMsg',
  631. leftButtonKey: 'dialog.Remove',
  632. submitFunction,
  633. closeFunction
  634. });
  635. });
  636. }
  637. /**
  638. * Ask user for shared video url to share with others.
  639. * Dialog validates client input to allow only youtube urls.
  640. */
  641. function requestVideoLink() {
  642. const i18n = APP.translation;
  643. const cancelButton = i18n.generateTranslationHTML('dialog.Cancel');
  644. const shareButton = i18n.generateTranslationHTML('dialog.Share');
  645. const backButton = i18n.generateTranslationHTML('dialog.Back');
  646. const linkError
  647. = i18n.generateTranslationHTML('dialog.shareVideoLinkError');
  648. return new Promise((resolve, reject) => {
  649. dialog = APP.UI.messageHandler.openDialogWithStates({
  650. state0: {
  651. titleKey: 'dialog.shareVideoTitle',
  652. html: `
  653. <input name='sharedVideoUrl' type='text'
  654. class='input-control'
  655. data-i18n='[placeholder]defaultLink'
  656. autofocus>`,
  657. persistent: false,
  658. buttons: [
  659. { title: cancelButton,
  660. value: false },
  661. { title: shareButton,
  662. value: true }
  663. ],
  664. focus: ':input:first',
  665. defaultButton: 1,
  666. submit(e, v, m, f) { // eslint-disable-line max-params
  667. e.preventDefault();
  668. if (!v) {
  669. reject('cancelled');
  670. dialog.close();
  671. return;
  672. }
  673. const sharedVideoUrl = f.sharedVideoUrl;
  674. if (!sharedVideoUrl) {
  675. return;
  676. }
  677. const urlValue
  678. = encodeURI(UIUtil.escapeHtml(sharedVideoUrl));
  679. const yVideoId = getYoutubeLink(urlValue);
  680. if (!yVideoId) {
  681. dialog.goToState('state1');
  682. return false;
  683. }
  684. resolve(yVideoId);
  685. dialog.close();
  686. }
  687. },
  688. state1: {
  689. titleKey: 'dialog.shareVideoTitle',
  690. html: linkError,
  691. persistent: false,
  692. buttons: [
  693. { title: cancelButton,
  694. value: false },
  695. { title: backButton,
  696. value: true }
  697. ],
  698. focus: ':input:first',
  699. defaultButton: 1,
  700. submit(e, v) {
  701. e.preventDefault();
  702. if (v === 0) {
  703. reject();
  704. dialog.close();
  705. } else {
  706. dialog.goToState('state0');
  707. }
  708. }
  709. }
  710. }, {
  711. close() {
  712. dialog = null;
  713. }
  714. }, {
  715. url: defaultSharedVideoLink
  716. });
  717. });
  718. }