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.

RemoteVideo.js 12KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402
  1. /* global $, APP, 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 { i18next } from '../../../react/features/base/i18n';
  10. import {
  11. JitsiParticipantConnectionStatus
  12. } from '../../../react/features/base/lib-jitsi-meet';
  13. import { getParticipantById } from '../../../react/features/base/participants';
  14. import { isTestModeEnabled } from '../../../react/features/base/testing';
  15. import { updateLastTrackVideoMediaEvent } from '../../../react/features/base/tracks';
  16. import { PresenceLabel } from '../../../react/features/presence-status';
  17. import { stopController, requestRemoteControl } from '../../../react/features/remote-control';
  18. import { RemoteVideoMenuTriggerButton } from '../../../react/features/remote-video-menu';
  19. /* eslint-enable no-unused-vars */
  20. import UIUtils from '../util/UIUtil';
  21. import SmallVideo from './SmallVideo';
  22. const logger = Logger.getLogger(__filename);
  23. /**
  24. * List of container events that we are going to process, will be added as listener to the
  25. * container for every event in the list. The latest event will be stored in redux.
  26. */
  27. const containerEvents = [
  28. 'abort', 'canplay', 'canplaythrough', 'emptied', 'ended', 'error', 'loadeddata', 'loadedmetadata', 'loadstart',
  29. 'pause', 'play', 'playing', 'ratechange', 'stalled', 'suspend', 'waiting'
  30. ];
  31. /**
  32. *
  33. * @param {*} spanId
  34. */
  35. function createContainer(spanId) {
  36. const container = document.createElement('span');
  37. container.id = spanId;
  38. container.className = 'videocontainer';
  39. container.innerHTML = `
  40. <div class = 'videocontainer__background'></div>
  41. <div class = 'videocontainer__toptoolbar'></div>
  42. <div class = 'videocontainer__toolbar'></div>
  43. <div class = 'videocontainer__hoverOverlay'></div>
  44. <div class = 'displayNameContainer'></div>
  45. <div class = 'avatar-container'></div>
  46. <div class ='presence-label-container'></div>
  47. <span class = 'remotevideomenu'></span>`;
  48. const remoteVideosContainer
  49. = document.getElementById('filmstripRemoteVideosContainer');
  50. const localVideoContainer
  51. = document.getElementById('localVideoTileViewContainer');
  52. remoteVideosContainer.insertBefore(container, localVideoContainer);
  53. return container;
  54. }
  55. /**
  56. *
  57. */
  58. export default class RemoteVideo extends SmallVideo {
  59. /**
  60. * Creates new instance of the <tt>RemoteVideo</tt>.
  61. * @param user {JitsiParticipant} the user for whom remote video instance will
  62. * be created.
  63. * @param {VideoLayout} VideoLayout the video layout instance.
  64. * @constructor
  65. */
  66. constructor(user, VideoLayout) {
  67. super(VideoLayout);
  68. this.user = user;
  69. this.id = user.getId();
  70. this.videoSpanId = `participant_${this.id}`;
  71. this._audioStreamElement = null;
  72. this.statsPopoverLocation = interfaceConfig.VERTICAL_FILMSTRIP ? 'left bottom' : 'top center';
  73. this.addRemoteVideoContainer();
  74. this.updateIndicators();
  75. this.updateDisplayName();
  76. this.bindHoverHandler();
  77. this.flipX = false;
  78. this.isLocal = false;
  79. /**
  80. * The flag is set to <tt>true</tt> after the 'canplay' event has been
  81. * triggered on the current video element. It goes back to <tt>false</tt>
  82. * when the stream is removed. It is used to determine whether the video
  83. * playback has ever started.
  84. * @type {boolean}
  85. */
  86. this._canPlayEventReceived = false;
  87. // Bind event handlers so they are only bound once for every instance.
  88. // TODO The event handlers should be turned into actions so changes can be
  89. // handled through reducers and middleware.
  90. this._setAudioVolume = this._setAudioVolume.bind(this);
  91. this.container.onclick = this._onContainerClick;
  92. }
  93. /**
  94. *
  95. */
  96. addRemoteVideoContainer() {
  97. this.container = createContainer(this.videoSpanId);
  98. this.$container = $(this.container);
  99. this.initializeAvatar();
  100. this._setThumbnailSize();
  101. this.initBrowserSpecificProperties();
  102. this.updateRemoteVideoMenu();
  103. this.updateStatusBar();
  104. this.addAudioLevelIndicator();
  105. this.addPresenceLabel();
  106. return this.container;
  107. }
  108. /**
  109. * Generates the popup menu content.
  110. *
  111. * @returns {Element|*} the constructed element, containing popup menu items
  112. * @private
  113. */
  114. _generatePopupContent() {
  115. const remoteVideoMenuContainer
  116. = this.container.querySelector('.remotevideomenu');
  117. if (!remoteVideoMenuContainer) {
  118. return;
  119. }
  120. const initialVolumeValue = this._audioStreamElement && this._audioStreamElement.volume;
  121. // hide volume when in silent mode
  122. const onVolumeChange
  123. = APP.store.getState()['features/base/config'].startSilent ? undefined : this._setAudioVolume;
  124. ReactDOM.render(
  125. <Provider store = { APP.store }>
  126. <I18nextProvider i18n = { i18next }>
  127. <AtlasKitThemeProvider mode = 'dark'>
  128. <RemoteVideoMenuTriggerButton
  129. initialVolumeValue = { initialVolumeValue }
  130. onMenuDisplay
  131. = {this._onRemoteVideoMenuDisplay.bind(this)}
  132. onVolumeChange = { onVolumeChange }
  133. participantID = { this.id } />
  134. </AtlasKitThemeProvider>
  135. </I18nextProvider>
  136. </Provider>,
  137. remoteVideoMenuContainer);
  138. }
  139. /**
  140. *
  141. */
  142. _onRemoteVideoMenuDisplay() {
  143. this.updateRemoteVideoMenu();
  144. }
  145. /**
  146. * Change the remote participant's volume level.
  147. *
  148. * @param {int} newVal - The value to set the slider to.
  149. */
  150. _setAudioVolume(newVal) {
  151. if (this._audioStreamElement) {
  152. this._audioStreamElement.volume = newVal;
  153. }
  154. }
  155. /**
  156. * Updates the remote video menu.
  157. */
  158. updateRemoteVideoMenu() {
  159. this._generatePopupContent();
  160. }
  161. /**
  162. * Removes the remote stream element corresponding to the given stream and
  163. * parent container.
  164. *
  165. * @param stream the MediaStream
  166. * @param isVideo <tt>true</tt> if given <tt>stream</tt> is a video one.
  167. */
  168. removeRemoteStreamElement(stream) {
  169. if (!this.container) {
  170. return false;
  171. }
  172. const isVideo = stream.isVideoTrack();
  173. const elementID = SmallVideo.getStreamElementID(stream);
  174. const select = $(`#${elementID}`);
  175. select.remove();
  176. if (isVideo) {
  177. this._canPlayEventReceived = false;
  178. }
  179. logger.info(`${isVideo ? 'Video' : 'Audio'} removed ${this.id}`, select);
  180. if (stream === this.videoStream) {
  181. this.videoStream = null;
  182. }
  183. this.updateView();
  184. }
  185. /**
  186. * The remote video is considered "playable" once the can play event has been received.
  187. *
  188. * @inheritdoc
  189. * @override
  190. */
  191. isVideoPlayable() {
  192. const participant = getParticipantById(APP.store.getState(), this.id);
  193. const { connectionStatus } = participant || {};
  194. return (
  195. super.isVideoPlayable()
  196. && this._canPlayEventReceived
  197. && connectionStatus === JitsiParticipantConnectionStatus.ACTIVE
  198. );
  199. }
  200. /**
  201. * @inheritDoc
  202. */
  203. updateView() {
  204. this.$container.toggleClass('audio-only', APP.conference.isAudioOnly());
  205. super.updateView();
  206. }
  207. /**
  208. * Removes RemoteVideo from the page.
  209. */
  210. remove() {
  211. super.remove();
  212. this.removePresenceLabel();
  213. this.removeRemoteVideoMenu();
  214. }
  215. /**
  216. *
  217. * @param {*} streamElement
  218. * @param {*} stream
  219. */
  220. waitForPlayback(streamElement, stream) {
  221. $(streamElement).hide();
  222. const webRtcStream = stream.getOriginalStream();
  223. const isVideo = stream.isVideoTrack();
  224. if (!isVideo || webRtcStream.id === 'mixedmslabel') {
  225. return;
  226. }
  227. const listener = () => {
  228. this._canPlayEventReceived = true;
  229. logger.info(`${this.id} video is now active`, streamElement);
  230. if (streamElement) {
  231. $(streamElement).show();
  232. }
  233. streamElement.removeEventListener('canplay', listener);
  234. // Refresh to show the video
  235. this.updateView();
  236. };
  237. streamElement.addEventListener('canplay', listener);
  238. }
  239. /**
  240. *
  241. * @param {*} stream
  242. */
  243. addRemoteStreamElement(stream) {
  244. if (!this.container) {
  245. logger.debug('Not attaching remote stream due to no container');
  246. return;
  247. }
  248. const isVideo = stream.isVideoTrack();
  249. if (isVideo) {
  250. this.videoStream = stream;
  251. } else {
  252. this.audioStream = stream;
  253. }
  254. if (!stream.getOriginalStream()) {
  255. logger.debug('Remote video stream has no original stream');
  256. return;
  257. }
  258. let streamElement = SmallVideo.createStreamElement(stream);
  259. // Put new stream element always in front
  260. streamElement = UIUtils.prependChild(this.container, streamElement);
  261. this.waitForPlayback(streamElement, stream);
  262. stream.attach(streamElement);
  263. if (!isVideo) {
  264. this._audioStreamElement = streamElement;
  265. // If the remote video menu was created before the audio stream was
  266. // attached we need to update the menu in order to show the volume
  267. // slider.
  268. this.updateRemoteVideoMenu();
  269. } else if (isTestModeEnabled(APP.store.getState())) {
  270. const cb = name => APP.store.dispatch(updateLastTrackVideoMediaEvent(stream, name));
  271. containerEvents.forEach(event => {
  272. streamElement.addEventListener(event, cb.bind(this, event));
  273. });
  274. }
  275. }
  276. /**
  277. * Triggers re-rendering of the display name using current instance state.
  278. *
  279. * @returns {void}
  280. */
  281. updateDisplayName() {
  282. if (!this.container) {
  283. logger.warn(`Unable to set displayName - ${this.videoSpanId} does not exist`);
  284. return;
  285. }
  286. this._renderDisplayName({
  287. elementID: `${this.videoSpanId}_name`,
  288. participantID: this.id
  289. });
  290. }
  291. /**
  292. * Removes remote video menu element from video element identified by
  293. * given <tt>videoElementId</tt>.
  294. *
  295. * @param videoElementId the id of local or remote video element.
  296. */
  297. removeRemoteVideoMenu() {
  298. const menuSpan = this.$container.find('.remotevideomenu');
  299. if (menuSpan.length) {
  300. ReactDOM.unmountComponentAtNode(menuSpan.get(0));
  301. menuSpan.remove();
  302. }
  303. }
  304. /**
  305. * Mounts the {@code PresenceLabel} for displaying the participant's current
  306. * presence status.
  307. *
  308. * @return {void}
  309. */
  310. addPresenceLabel() {
  311. const presenceLabelContainer = this.container.querySelector('.presence-label-container');
  312. if (presenceLabelContainer) {
  313. ReactDOM.render(
  314. <Provider store = { APP.store }>
  315. <I18nextProvider i18n = { i18next }>
  316. <PresenceLabel
  317. participantID = { this.id }
  318. className = 'presence-label' />
  319. </I18nextProvider>
  320. </Provider>,
  321. presenceLabelContainer);
  322. }
  323. }
  324. /**
  325. * Unmounts the {@code PresenceLabel} component.
  326. *
  327. * @return {void}
  328. */
  329. removePresenceLabel() {
  330. const presenceLabelContainer = this.container.querySelector('.presence-label-container');
  331. if (presenceLabelContainer) {
  332. ReactDOM.unmountComponentAtNode(presenceLabelContainer);
  333. }
  334. }
  335. }