Nelze vybrat více než 25 témat Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a může být dlouhé až 35 znaků.

SmallVideo.js 32KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985
  1. /* global $, APP, config, interfaceConfig */
  2. /* eslint-disable no-unused-vars */
  3. import { AtlasKitThemeProvider } from '@atlaskit/theme';
  4. import Logger from 'jitsi-meet-logger';
  5. import React from 'react';
  6. import ReactDOM from 'react-dom';
  7. import { I18nextProvider } from 'react-i18next';
  8. import { Provider } from 'react-redux';
  9. import { AudioLevelIndicator } from '../../../react/features/audio-level-indicator';
  10. import { Avatar as AvatarDisplay } from '../../../react/features/base/avatar';
  11. import { i18next } from '../../../react/features/base/i18n';
  12. import {
  13. getParticipantCount,
  14. getPinnedParticipant,
  15. pinParticipant
  16. } from '../../../react/features/base/participants';
  17. import { ConnectionIndicator } from '../../../react/features/connection-indicator';
  18. import { DisplayName } from '../../../react/features/display-name';
  19. import {
  20. DominantSpeakerIndicator,
  21. RaisedHandIndicator,
  22. StatusIndicators
  23. } from '../../../react/features/filmstrip';
  24. import {
  25. LAYOUTS,
  26. getCurrentLayout,
  27. setTileView,
  28. shouldDisplayTileView
  29. } from '../../../react/features/video-layout';
  30. import {CornerObj,DevHook} from '../../../rdev/hooks/Hooks';
  31. /* eslint-enable no-unused-vars */
  32. const logger = Logger.getLogger(__filename);
  33. /**
  34. * Display mode constant used when video is being displayed on the small video.
  35. * @type {number}
  36. * @constant
  37. */
  38. const DISPLAY_VIDEO = 0;
  39. /**
  40. * Display mode constant used when the user's avatar is being displayed on
  41. * the small video.
  42. * @type {number}
  43. * @constant
  44. */
  45. const DISPLAY_AVATAR = 1;
  46. /**
  47. * Display mode constant used when neither video nor avatar is being displayed
  48. * on the small video. And we just show the display name.
  49. * @type {number}
  50. * @constant
  51. */
  52. const DISPLAY_BLACKNESS_WITH_NAME = 2;
  53. /**
  54. * Display mode constant used when video is displayed and display name
  55. * at the same time.
  56. * @type {number}
  57. * @constant
  58. */
  59. const DISPLAY_VIDEO_WITH_NAME = 3;
  60. /**
  61. * Display mode constant used when neither video nor avatar is being displayed
  62. * on the small video. And we just show the display name.
  63. * @type {number}
  64. * @constant
  65. */
  66. const DISPLAY_AVATAR_WITH_NAME = 4;
  67. /**
  68. *
  69. */
  70. // export default class SmallVideo {
  71. class SmallVideoOrig {
  72. /**
  73. * Constructor.
  74. */
  75. constructor(VideoLayout) {
  76. this.isAudioMuted = false;
  77. this.isVideoMuted = false;
  78. this.videoStream = null;
  79. this.audioStream = null;
  80. this.VideoLayout = VideoLayout;
  81. this.videoIsHovered = false;
  82. /**
  83. * The current state of the user's bridge connection. The value should be
  84. * a string as enumerated in the library's participantConnectionStatus
  85. * constants.
  86. *
  87. * @private
  88. * @type {string|null}
  89. */
  90. this._connectionStatus = null;
  91. /**
  92. * Whether or not the ConnectionIndicator's popover is hovered. Modifies
  93. * how the video overlays display based on hover state.
  94. *
  95. * @private
  96. * @type {boolean}
  97. */
  98. this._popoverIsHovered = false;
  99. /**
  100. * Whether or not the connection indicator should be displayed.
  101. *
  102. * @private
  103. * @type {boolean}
  104. */
  105. this._showConnectionIndicator = !interfaceConfig.CONNECTION_INDICATOR_DISABLED;
  106. /**
  107. * Whether or not the dominant speaker indicator should be displayed.
  108. *
  109. * @private
  110. * @type {boolean}
  111. */
  112. this._showDominantSpeaker = false;
  113. /**
  114. * Whether or not the raised hand indicator should be displayed.
  115. *
  116. * @private
  117. * @type {boolean}
  118. */
  119. this._showRaisedHand = false;
  120. // Bind event handlers so they are only bound once for every instance.
  121. this._onPopoverHover = this._onPopoverHover.bind(this);
  122. this.updateView = this.updateView.bind(this);
  123. this._onContainerClick = this._onContainerClick.bind(this);
  124. }
  125. /**
  126. * Returns the identifier of this small video.
  127. *
  128. * @returns the identifier of this small video
  129. */
  130. getId() {
  131. return this.id;
  132. }
  133. /**
  134. * Indicates if this small video is currently visible.
  135. *
  136. * @return <tt>true</tt> if this small video isn't currently visible and
  137. * <tt>false</tt> - otherwise.
  138. */
  139. isVisible() {
  140. return this.$container.is(':visible');
  141. }
  142. /**
  143. * Creates an audio or video element for a particular MediaStream.
  144. */
  145. static createStreamElement(stream) {
  146. const isVideo = stream.isVideoTrack();
  147. const element = isVideo ? document.createElement('video') : document.createElement('audio');
  148. if (isVideo) {
  149. element.setAttribute('muted', 'true');
  150. element.setAttribute('playsInline', 'true'); /* for Safari on iOS to work */
  151. } else if (config.startSilent) {
  152. element.muted = true;
  153. }
  154. element.autoplay = !config.testing?.noAutoPlayVideo;
  155. element.id = SmallVideo.getStreamElementID(stream);
  156. return element;
  157. }
  158. /**
  159. * Returns the element id for a particular MediaStream.
  160. */
  161. static getStreamElementID(stream) {
  162. return (stream.isVideoTrack() ? 'remoteVideo_' : 'remoteAudio_') + stream.getId();
  163. }
  164. /**
  165. * Configures hoverIn/hoverOut handlers. Depends on connection indicator.
  166. */
  167. bindHoverHandler() {
  168. // Add hover handler
  169. this.$container.hover(
  170. () => {
  171. this.videoIsHovered = true;
  172. this.updateView();
  173. this.updateIndicators();
  174. },
  175. () => {
  176. this.videoIsHovered = false;
  177. this.updateView();
  178. this.updateIndicators();
  179. }
  180. );
  181. }
  182. /**
  183. * Unmounts the ConnectionIndicator component.
  184. * @returns {void}
  185. */
  186. removeConnectionIndicator() {
  187. this._showConnectionIndicator = false;
  188. this.updateIndicators();
  189. }
  190. /**
  191. * Updates the connectionStatus stat which displays in the ConnectionIndicator.
  192. * @returns {void}
  193. */
  194. updateConnectionStatus(connectionStatus) {
  195. this._connectionStatus = connectionStatus;
  196. this.updateIndicators();
  197. }
  198. /**
  199. * Shows / hides the audio muted indicator over small videos.
  200. *
  201. * @param {boolean} isMuted indicates if the muted element should be shown
  202. * or hidden
  203. */
  204. showAudioIndicator(isMuted) {
  205. this.isAudioMuted = isMuted;
  206. this.updateStatusBar();
  207. }
  208. /**
  209. * Shows video muted indicator over small videos and disables/enables avatar
  210. * if video muted.
  211. *
  212. * @param {boolean} isMuted indicates if we should set the view to muted view
  213. * or not
  214. */
  215. setVideoMutedView(isMuted) {
  216. this.isVideoMuted = isMuted;
  217. this.updateView();
  218. this.updateStatusBar();
  219. }
  220. /**
  221. * Create or updates the ReactElement for displaying status indicators about
  222. * audio mute, video mute, and moderator status.
  223. *
  224. * @returns {void}
  225. */
  226. updateStatusBar() {
  227. const statusBarContainer = this.container.querySelector('.videocontainer__toolbar');
  228. if (!statusBarContainer) {
  229. return;
  230. }
  231. ReactDOM.render(
  232. <Provider store = { APP.store }>
  233. <DevHook type="div" className="indicator_trc t2 jdiv"></DevHook>
  234. <I18nextProvider i18n = { i18next }>
  235. <StatusIndicators
  236. showAudioMutedIndicator = { this.isAudioMuted }
  237. showVideoMutedIndicator = { this.isVideoMuted }
  238. participantID = { this.id } />
  239. </I18nextProvider>
  240. <DevHook type="div" className="indicator_trc t3 jdiv"></DevHook>
  241. </Provider>,
  242. statusBarContainer);
  243. }
  244. /**
  245. * Adds the element indicating the audio level of the participant.
  246. *
  247. * @returns {void}
  248. */
  249. addAudioLevelIndicator() {
  250. let audioLevelContainer = this._getAudioLevelContainer();
  251. if (audioLevelContainer) {
  252. return;
  253. }
  254. audioLevelContainer = document.createElement('span');
  255. audioLevelContainer.className = 'audioindicator-container';
  256. this.container.appendChild(audioLevelContainer);
  257. this.updateAudioLevelIndicator();
  258. }
  259. /**
  260. * Removes the element indicating the audio level of the participant.
  261. *
  262. * @returns {void}
  263. */
  264. removeAudioLevelIndicator() {
  265. const audioLevelContainer = this._getAudioLevelContainer();
  266. if (audioLevelContainer) {
  267. ReactDOM.unmountComponentAtNode(audioLevelContainer);
  268. }
  269. }
  270. /**
  271. * Updates the audio level for this small video.
  272. *
  273. * @param lvl the new audio level to set
  274. * @returns {void}
  275. */
  276. updateAudioLevelIndicator(lvl = 0) {
  277. const audioLevelContainer = this._getAudioLevelContainer();
  278. if (audioLevelContainer) {
  279. ReactDOM.render(<AudioLevelIndicator audioLevel = { lvl }/>, audioLevelContainer);
  280. }
  281. }
  282. /**
  283. * Queries the component's DOM for the element that should be the parent to the
  284. * AudioLevelIndicator.
  285. *
  286. * @returns {HTMLElement} The DOM element that holds the AudioLevelIndicator.
  287. */
  288. _getAudioLevelContainer() {
  289. return this.container.querySelector('.audioindicator-container');
  290. }
  291. /**
  292. * This is an especially interesting function. A naive reader might think that
  293. * it returns this SmallVideo's "video" element. But it is much more exciting.
  294. * It first finds this video's parent element using jquery, then uses a utility
  295. * from lib-jitsi-meet to extract the video element from it (with two more
  296. * jquery calls), and finally uses jquery again to encapsulate the video element
  297. * in an array. This last step allows (some might prefer "forces") users of
  298. * this function to access the video element via the 0th element of the returned
  299. * array (after checking its length of course!).
  300. */
  301. selectVideoElement() {
  302. return $($(this.container).find('video')[0]);
  303. }
  304. /**
  305. * Selects the HTML image element which displays user's avatar.
  306. *
  307. * @return {jQuery|HTMLElement} a jQuery selector pointing to the HTML image
  308. * element which displays the user's avatar.
  309. */
  310. $avatar() {
  311. return this.$container.find('.avatar-container');
  312. }
  313. /**
  314. * Returns the display name element, which appears on the video thumbnail.
  315. *
  316. * @return {jQuery} a jQuery selector pointing to the display name element of
  317. * the video thumbnail
  318. */
  319. $displayName() {
  320. return this.$container.find('.displayNameContainer');
  321. }
  322. /**
  323. * Creates or updates the participant's display name that is shown over the
  324. * video preview.
  325. *
  326. * @param {Object} props - The React {@code Component} props to pass into the
  327. * {@code DisplayName} component.
  328. * @returns {void}
  329. */
  330. _renderDisplayName(props) {
  331. const displayNameContainer = this.container.querySelector('.displayNameContainer');
  332. if (displayNameContainer) {
  333. ReactDOM.render(
  334. <Provider store = { APP.store }>
  335. <I18nextProvider i18n = { i18next }>
  336. <DisplayName { ...props } />
  337. </I18nextProvider>
  338. </Provider>,
  339. displayNameContainer);
  340. }
  341. }
  342. /**
  343. * Removes the component responsible for showing the participant's display name,
  344. * if its container is present.
  345. *
  346. * @returns {void}
  347. */
  348. removeDisplayName() {
  349. const displayNameContainer = this.container.querySelector('.displayNameContainer');
  350. if (displayNameContainer) {
  351. ReactDOM.unmountComponentAtNode(displayNameContainer);
  352. }
  353. }
  354. /**
  355. * Enables / disables the css responsible for focusing/pinning a video
  356. * thumbnail.
  357. *
  358. * @param isFocused indicates if the thumbnail should be focused/pinned or not
  359. */
  360. focus(isFocused) {
  361. const focusedCssClass = 'videoContainerFocused';
  362. const isFocusClassEnabled = this.$container.hasClass(focusedCssClass);
  363. if (!isFocused && isFocusClassEnabled) {
  364. this.$container.removeClass(focusedCssClass);
  365. } else if (isFocused && !isFocusClassEnabled) {
  366. this.$container.addClass(focusedCssClass);
  367. }
  368. }
  369. /**
  370. *
  371. */
  372. hasVideo() {
  373. return this.selectVideoElement().length !== 0;
  374. }
  375. /**
  376. * Checks whether the user associated with this <tt>SmallVideo</tt> is currently
  377. * being displayed on the "large video".
  378. *
  379. * @return {boolean} <tt>true</tt> if the user is displayed on the large video
  380. * or <tt>false</tt> otherwise.
  381. */
  382. isCurrentlyOnLargeVideo() {
  383. return APP.store.getState()['features/large-video']?.participantId === this.id;
  384. }
  385. /**
  386. * Checks whether there is a playable video stream available for the user
  387. * associated with this <tt>SmallVideo</tt>.
  388. *
  389. * @return {boolean} <tt>true</tt> if there is a playable video stream available
  390. * or <tt>false</tt> otherwise.
  391. */
  392. isVideoPlayable() {
  393. return this.videoStream && !this.isVideoMuted && !APP.conference.isAudioOnly();
  394. }
  395. /**
  396. * Determines what should be display on the thumbnail.
  397. *
  398. * @return {number} one of <tt>DISPLAY_VIDEO</tt>,<tt>DISPLAY_AVATAR</tt>
  399. * or <tt>DISPLAY_BLACKNESS_WITH_NAME</tt>.
  400. */
  401. selectDisplayMode(input) {
  402. // Display name is always and only displayed when user is on the stage
  403. if (input.isCurrentlyOnLargeVideo && !input.tileViewEnabled) {
  404. return input.isVideoPlayable && !input.isAudioOnly ? DISPLAY_BLACKNESS_WITH_NAME : DISPLAY_AVATAR_WITH_NAME;
  405. } else if (input.isVideoPlayable && input.hasVideo && !input.isAudioOnly) {
  406. // check hovering and change state to video with name
  407. return input.isHovered ? DISPLAY_VIDEO_WITH_NAME : DISPLAY_VIDEO;
  408. }
  409. // check hovering and change state to avatar with name
  410. return input.isHovered ? DISPLAY_AVATAR_WITH_NAME : DISPLAY_AVATAR;
  411. }
  412. /**
  413. * Computes information that determine the display mode.
  414. *
  415. * @returns {Object}
  416. */
  417. computeDisplayModeInput() {
  418. return {
  419. isCurrentlyOnLargeVideo: this.isCurrentlyOnLargeVideo(),
  420. isHovered: this._isHovered(),
  421. isAudioOnly: APP.conference.isAudioOnly(),
  422. tileViewEnabled: shouldDisplayTileView(APP.store.getState()),
  423. isVideoPlayable: this.isVideoPlayable(),
  424. hasVideo: Boolean(this.selectVideoElement().length),
  425. connectionStatus: APP.conference.getParticipantConnectionStatus(this.id),
  426. mutedWhileDisconnected: this.mutedWhileDisconnected,
  427. canPlayEventReceived: this._canPlayEventReceived,
  428. videoStream: Boolean(this.videoStream),
  429. isVideoMuted: this.isVideoMuted,
  430. videoStreamMuted: this.videoStream ? this.videoStream.isMuted() : 'no stream'
  431. };
  432. }
  433. /**
  434. * Checks whether current video is considered hovered. Currently it is hovered
  435. * if the mouse is over the video, or if the connection
  436. * indicator is shown(hovered).
  437. * @private
  438. */
  439. _isHovered() {
  440. return this.videoIsHovered || this._popoverIsHovered;
  441. }
  442. /**
  443. * Updates the css classes of the thumbnail based on the current state.
  444. */
  445. updateView() {
  446. this.$container.removeClass((index, classNames) =>
  447. classNames.split(' ').filter(name => name.startsWith('display-')));
  448. const oldDisplayMode = this.displayMode;
  449. let displayModeString = '';
  450. const displayModeInput = this.computeDisplayModeInput();
  451. // Determine whether video, avatar or blackness should be displayed
  452. this.displayMode = this.selectDisplayMode(displayModeInput);
  453. switch (this.displayMode) {
  454. case DISPLAY_AVATAR_WITH_NAME:
  455. displayModeString = 'avatar-with-name';
  456. this.$container.addClass('display-avatar-with-name');
  457. break;
  458. case DISPLAY_BLACKNESS_WITH_NAME:
  459. displayModeString = 'blackness-with-name';
  460. this.$container.addClass('display-name-on-black');
  461. break;
  462. case DISPLAY_VIDEO:
  463. displayModeString = 'video';
  464. this.$container.addClass('display-video');
  465. break;
  466. case DISPLAY_VIDEO_WITH_NAME:
  467. displayModeString = 'video-with-name';
  468. this.$container.addClass('display-name-on-video');
  469. break;
  470. case DISPLAY_AVATAR:
  471. default:
  472. displayModeString = 'avatar';
  473. this.$container.addClass('display-avatar-only');
  474. break;
  475. }
  476. if (this.displayMode !== oldDisplayMode) {
  477. logger.debug(`Displaying ${displayModeString} for ${this.id}, data: [${JSON.stringify(displayModeInput)}]`);
  478. }
  479. }
  480. /**
  481. * Updates the react component displaying the avatar with the passed in avatar
  482. * url.
  483. *
  484. * @returns {void}
  485. */
  486. initializeAvatar() {
  487. const thumbnail = this.$avatar().get(0);
  488. if (thumbnail) {
  489. // Maybe add a special case for local participant, as on init of
  490. // LocalVideo.js the id is set to "local" but will get updated later.
  491. ReactDOM.render(
  492. <Provider store = { APP.store }>
  493. <AvatarDisplay
  494. className = 'userAvatar'
  495. participantId = { this.id } />
  496. </Provider>,
  497. thumbnail
  498. );
  499. }
  500. }
  501. /**
  502. * Unmounts any attached react components (particular the avatar image) from
  503. * the avatar container.
  504. *
  505. * @returns {void}
  506. */
  507. removeAvatar() {
  508. const thumbnail = this.$avatar().get(0);
  509. if (thumbnail) {
  510. ReactDOM.unmountComponentAtNode(thumbnail);
  511. }
  512. }
  513. /**
  514. * Shows or hides the dominant speaker indicator.
  515. * @param show whether to show or hide.
  516. */
  517. showDominantSpeakerIndicator(show) {
  518. // Don't create and show dominant speaker indicator if
  519. // DISABLE_DOMINANT_SPEAKER_INDICATOR is true
  520. if (interfaceConfig.DISABLE_DOMINANT_SPEAKER_INDICATOR) {
  521. return;
  522. }
  523. if (!this.container) {
  524. logger.warn(`Unable to set dominant speaker indicator - ${this.videoSpanId} does not exist`);
  525. return;
  526. }
  527. if (this._showDominantSpeaker === show) {
  528. return;
  529. }
  530. this._showDominantSpeaker = show;
  531. this.$container.toggleClass('active-speaker', this._showDominantSpeaker);
  532. this.updateIndicators();
  533. this.updateView();
  534. }
  535. /**
  536. * Shows or hides the raised hand indicator.
  537. * @param show whether to show or hide.
  538. */
  539. showRaisedHandIndicator(show) {
  540. if (!this.container) {
  541. logger.warn(`Unable to raised hand indication - ${
  542. this.videoSpanId} does not exist`);
  543. return;
  544. }
  545. this._showRaisedHand = show;
  546. this.updateIndicators();
  547. }
  548. /**
  549. * Initalizes any browser specific properties. Currently sets the overflow
  550. * property for Qt browsers on Windows to hidden, thus fixing the following
  551. * problem:
  552. * Some browsers don't have full support of the object-fit property for the
  553. * video element and when we set video object-fit to "cover" the video
  554. * actually overflows the boundaries of its container, so it's important
  555. * to indicate that the "overflow" should be hidden.
  556. *
  557. * Setting this property for all browsers will result in broken audio levels,
  558. * which makes this a temporary solution, before reworking audio levels.
  559. */
  560. initBrowserSpecificProperties() {
  561. const userAgent = window.navigator.userAgent;
  562. if (userAgent.indexOf('QtWebEngine') > -1
  563. && (userAgent.indexOf('Windows') > -1 || userAgent.indexOf('Linux') > -1)) {
  564. this.$container.css('overflow', 'hidden');
  565. }
  566. }
  567. /**
  568. * Cleans up components on {@code SmallVideo} and removes itself from the DOM.
  569. *
  570. * @returns {void}
  571. */
  572. remove() {
  573. logger.log('Remove thumbnail', this.id);
  574. this.removeAudioLevelIndicator();
  575. const toolbarContainer
  576. = this.container.querySelector('.videocontainer__toolbar');
  577. if (toolbarContainer) {
  578. ReactDOM.unmountComponentAtNode(toolbarContainer);
  579. }
  580. this.removeConnectionIndicator();
  581. this.removeDisplayName();
  582. this.removeAvatar();
  583. this._unmountIndicators();
  584. // Remove whole container
  585. if (this.container.parentNode) {
  586. this.container.parentNode.removeChild(this.container);
  587. }
  588. }
  589. /**
  590. * Helper function for re-rendering multiple react components of the small
  591. * video.
  592. *
  593. * @returns {void}
  594. */
  595. rerender() {
  596. this.updateIndicators();
  597. this.updateStatusBar();
  598. this.updateView();
  599. }
  600. /**
  601. * Updates the React element responsible for showing connection status, dominant
  602. * speaker, and raised hand icons. Uses instance variables to get the necessary
  603. * state to display. Will create the React element if not already created.
  604. *
  605. * @private
  606. * @returns {void}
  607. */
  608. updateIndicators() {
  609. const indicatorToolbar = this.container.querySelector('.videocontainer__toptoolbar');
  610. if (!indicatorToolbar) {
  611. return;
  612. }
  613. const { NORMAL = 8 } = interfaceConfig.INDICATOR_FONT_SIZES || {};
  614. const iconSize = NORMAL;
  615. const showConnectionIndicator = this.videoIsHovered || !interfaceConfig.CONNECTION_INDICATOR_AUTO_HIDE_ENABLED;
  616. const state = APP.store.getState();
  617. const currentLayout = getCurrentLayout(state);
  618. const participantCount = getParticipantCount(state);
  619. let statsPopoverPosition, tooltipPosition;
  620. if (currentLayout === LAYOUTS.TILE_VIEW) {
  621. statsPopoverPosition = 'right top';
  622. tooltipPosition = 'right';
  623. } else if (currentLayout === LAYOUTS.VERTICAL_FILMSTRIP_VIEW) {
  624. statsPopoverPosition = this.statsPopoverLocation;
  625. tooltipPosition = 'left';
  626. } else {
  627. statsPopoverPosition = this.statsPopoverLocation;
  628. tooltipPosition = 'top';
  629. }
  630. // dev mod
  631. statsPopoverPosition = 'right top';
  632. ReactDOM.render(
  633. <Provider store = { APP.store }>
  634. <DevHook type="div" className="indicator_hook vid_toptoolbar_hook jdiv t0"></DevHook>
  635. <I18nextProvider i18n = { i18next }>
  636. <div>
  637. <AtlasKitThemeProvider mode = 'dark'>
  638. { this._showConnectionIndicator
  639. ? <ConnectionIndicator
  640. alwaysVisible = { showConnectionIndicator }
  641. connectionStatus = { this._connectionStatus }
  642. iconSize = { iconSize }
  643. isLocalVideo = { this.isLocal }
  644. enableStatsDisplay = { !interfaceConfig.filmStripOnly }
  645. participantId = { this.id }
  646. statsPopoverPosition = { statsPopoverPosition } />
  647. : null }
  648. <RaisedHandIndicator
  649. iconSize = { iconSize }
  650. participantId = { this.id }
  651. tooltipPosition = { tooltipPosition } />
  652. { this._showDominantSpeaker && participantCount > 2
  653. ? <DominantSpeakerIndicator
  654. iconSize = { iconSize }
  655. tooltipPosition = { tooltipPosition } />
  656. : null }
  657. </AtlasKitThemeProvider>
  658. </div>
  659. </I18nextProvider>
  660. <DevHook type="div" className="indicator_hook vid_toptoolbar_hook_after jdiv t1"></DevHook>
  661. </Provider>,
  662. indicatorToolbar
  663. );
  664. }
  665. /**
  666. * Callback invoked when the thumbnail is clicked and potentially trigger
  667. * pinning of the participant.
  668. *
  669. * @param {MouseEvent} event - The click event to intercept.
  670. * @private
  671. * @returns {void}
  672. */
  673. _onContainerClick(event) {
  674. const triggerPin = this._shouldTriggerPin(event);
  675. if (event.stopPropagation && triggerPin) {
  676. event.stopPropagation();
  677. event.preventDefault();
  678. }
  679. if (triggerPin) {
  680. this.togglePin();
  681. }
  682. return false;
  683. }
  684. /**
  685. * Returns whether or not a click event is targeted at certain elements which
  686. * should not trigger a pin.
  687. *
  688. * @param {MouseEvent} event - The click event to intercept.
  689. * @private
  690. * @returns {boolean}
  691. */
  692. _shouldTriggerPin(event) {
  693. // TODO Checking the classes is a workround to allow events to bubble into
  694. // the DisplayName component if it was clicked. React's synthetic events
  695. // will fire after jQuery handlers execute, so stop propogation at this
  696. // point will prevent DisplayName from getting click events. This workaround
  697. // should be removeable once LocalVideo is a React Component because then
  698. // the components share the same eventing system.
  699. const $source = $(event.target || event.srcElement);
  700. return $source.parents('.displayNameContainer').length === 0
  701. && $source.parents('.popover').length === 0
  702. && !event.target.classList.contains('popover');
  703. }
  704. /**
  705. * Pins the participant displayed by this thumbnail or unpins if already pinned.
  706. *
  707. * @returns {void}
  708. */
  709. togglePin() {
  710. const pinnedParticipant = getPinnedParticipant(APP.store.getState()) || {};
  711. const participantIdToPin = pinnedParticipant && pinnedParticipant.id === this.id ? null : this.id;
  712. APP.store.dispatch(pinParticipant(participantIdToPin));
  713. }
  714. /**
  715. * Removes the React element responsible for showing connection status, dominant
  716. * speaker, and raised hand icons.
  717. *
  718. * @private
  719. * @returns {void}
  720. */
  721. _unmountIndicators() {
  722. const indicatorToolbar = this.container.querySelector('.videocontainer__toptoolbar');
  723. if (indicatorToolbar) {
  724. ReactDOM.unmountComponentAtNode(indicatorToolbar);
  725. }
  726. }
  727. /**
  728. * Updates the current state of the connection indicator popover being hovered.
  729. * If hovered, display the small video as if it is hovered.
  730. *
  731. * @param {boolean} popoverIsHovered - Whether or not the mouse cursor is
  732. * currently over the connection indicator popover.
  733. * @returns {void}
  734. */
  735. _onPopoverHover(popoverIsHovered) {
  736. this._popoverIsHovered = popoverIsHovered;
  737. this.updateView();
  738. }
  739. /**
  740. * Sets the size of the thumbnail.
  741. */
  742. _setThumbnailSize() {
  743. const layout = getCurrentLayout(APP.store.getState());
  744. const heightToWidthPercent = 100
  745. / (this.isLocal ? interfaceConfig.LOCAL_THUMBNAIL_RATIO : interfaceConfig.REMOTE_THUMBNAIL_RATIO);
  746. switch (layout) {
  747. case LAYOUTS.VERTICAL_FILMSTRIP_VIEW: {
  748. this.$container.css('padding-top', `${heightToWidthPercent}%`);
  749. this.$avatar().css({
  750. height: '50%',
  751. width: `${heightToWidthPercent / 2}%`
  752. });
  753. break;
  754. }
  755. case LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW: {
  756. const state = APP.store.getState();
  757. const { local, remote } = state['features/filmstrip'].horizontalViewDimensions;
  758. const size = this.isLocal ? local : remote;
  759. if (typeof size !== 'undefined') {
  760. const { height, width } = size;
  761. const avatarSize = height / 2;
  762. this.$container.css({
  763. height: `${height}px`,
  764. 'min-height': `${height}px`,
  765. 'min-width': `${width}px`,
  766. width: `${width}px`
  767. });
  768. this.$avatar().css({
  769. height: `${avatarSize}px`,
  770. width: `${avatarSize}px`
  771. });
  772. }
  773. break;
  774. }
  775. case LAYOUTS.TILE_VIEW: {
  776. const state = APP.store.getState();
  777. const { thumbnailSize } = state['features/filmstrip'].tileViewDimensions;
  778. if (typeof thumbnailSize !== 'undefined') {
  779. const { height, width } = thumbnailSize;
  780. const avatarSize = height / 2;
  781. this.$container.css({
  782. height: `${height}px`,
  783. 'min-height': `${height}px`,
  784. 'min-width': `${width}px`,
  785. width: `${width}px`
  786. });
  787. this.$avatar().css({
  788. height: `${avatarSize}px`,
  789. width: `${avatarSize}px`
  790. });
  791. }
  792. break;
  793. }
  794. }
  795. }
  796. }
  797. function sv_dec(fn) {
  798. return function() {
  799. // dec_fns[fn.name] && dec_fns[fn.name].pre ? dec_fns[fn.name].pre({that:this, arguments}) : 1
  800. const ret = fn.apply(this, arguments);
  801. // clog("SVD",fn.name,{ret,that:this,args:[...arguments]})
  802. window.react_trc_log.SmallVideoOrig.push(fn.name)
  803. window.react_trc_log.SmallVideoOrigSet.add(fn.name)
  804. window.react_trc_log.SmallVideoOrigCnt[fn.name] ? 0 : window.react_trc_log.SmallVideoOrigCnt[fn.name] = 0
  805. window.react_trc_log.SmallVideoOrigCnt[fn.name] += 1
  806. window.react_trc_log.SmallVideoTrc.includes(fn.name) ? window.log_tb(new Error(),fn.name) : 1
  807. // console.log('FSD',fn.name,ret, [this,...arguments]);
  808. // const ret2 = dec_fns[fn.name] && dec_fns[fn.name].post ? dec_fns[fn.name].post({that:this, arguments}) : 0
  809. // if (ret2){
  810. // return ret2.ret
  811. // }
  812. // const result = fn.apply(this, arguments);
  813. // console.log('Finished');
  814. return ret;
  815. }
  816. }
  817. // _getAudioLevelContainer {ret: span.audioindicator-container, that: LocalVideo, args: Array(0)}
  818. // SmallVideo.js:900 SVD updateAudioLevelIndicator
  819. function dec_cls(){
  820. var k,v,p
  821. const skip = ["_getAudioLevelContainer","updateAudioLevelIndicator"]
  822. window.react_trc_log.SmallVideoOrig = []
  823. window.react_trc_log.SmallVideoOrigSet = new Set()
  824. window.react_trc_log.SmallVideoOrigCnt = {}
  825. for ([k,p] of Object.entries(Object.getOwnPropertyDescriptors(SmallVideoOrig.prototype))) {
  826. if (skip.includes(k)){continue}
  827. // clog("~",k,v)
  828. v=p.value
  829. clog("~",k,typeof(v))
  830. if (typeof(v) == "function"){
  831. SmallVideoOrig.prototype[k] = sv_dec(v)
  832. }
  833. }
  834. }
  835. // dec_cls()
  836. glob_dev_hooks.smallvids = []
  837. export default class SmallVideo extends SmallVideoOrig {
  838. constructor(VideoLayout){
  839. super(...arguments);
  840. glob_dev_hooks.smallvids.push(this)
  841. clog("NEW SmallVideo...",arguments,this.$container)
  842. }
  843. _setThumbnailSize(){
  844. super._setThumbnailSize(...arguments);
  845. clog("ACsv",this)
  846. // if ()
  847. this.isLocal ? this.$container.addClass("iloc") : this.$container.addClass("rloc")
  848. this.isLocal ? this.$container.addClass("local_vid") : this.$container.addClass("remote_vid")
  849. this.$container.addClass("small_vid")
  850. }
  851. _onContainerClick(event) {
  852. console.log("ON container click",this,event,[...arguments])
  853. if (window.msto.conference.force_follow && !window.amimod()){
  854. console.log("ON container click force focus")
  855. return
  856. }
  857. // return
  858. const triggerPin = this._shouldTriggerPin(event);
  859. if (event.stopPropagation && triggerPin) {
  860. event.stopPropagation();
  861. event.preventDefault();
  862. }
  863. if (triggerPin) {
  864. this.togglePin();
  865. }
  866. return false;
  867. }
  868. }