您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

LargeVideoManager.js 19KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576
  1. /* global $, APP, config, JitsiMeetJS */
  2. const logger = require("jitsi-meet-logger").getLogger(__filename);
  3. import { setLargeVideoHDStatus } from '../../../react/features/base/conference';
  4. import Avatar from "../avatar/Avatar";
  5. import {createDeferred} from '../../util/helpers';
  6. import UIEvents from "../../../service/UI/UIEvents";
  7. import UIUtil from "../util/UIUtil";
  8. import {VideoContainer, VIDEO_CONTAINER_TYPE} from "./VideoContainer";
  9. import AudioLevels from "../audio_levels/AudioLevels";
  10. const ParticipantConnectionStatus
  11. = JitsiMeetJS.constants.participantConnectionStatus;
  12. const DESKTOP_CONTAINER_TYPE = 'desktop';
  13. /**
  14. * The time interval in milliseconds to check the video resolution of the video
  15. * being displayed.
  16. *
  17. * @type {number}
  18. */
  19. const VIDEO_RESOLUTION_POLL_INTERVAL = 2000;
  20. /**
  21. * Manager for all Large containers.
  22. */
  23. export default class LargeVideoManager {
  24. constructor (emitter) {
  25. /**
  26. * The map of <tt>LargeContainer</tt>s where the key is the video
  27. * container type.
  28. * @type {Object.<string, LargeContainer>}
  29. */
  30. this.containers = {};
  31. this.eventEmitter = emitter;
  32. this.state = VIDEO_CONTAINER_TYPE;
  33. // FIXME: We are passing resizeContainer as parameter which is calling
  34. // Container.resize. Probably there's better way to implement this.
  35. this.videoContainer = new VideoContainer(
  36. () => this.resizeContainer(VIDEO_CONTAINER_TYPE), emitter);
  37. this.addContainer(VIDEO_CONTAINER_TYPE, this.videoContainer);
  38. // use the same video container to handle desktop tracks
  39. this.addContainer(DESKTOP_CONTAINER_TYPE, this.videoContainer);
  40. this.width = 0;
  41. this.height = 0;
  42. this.$container = $('#largeVideoContainer');
  43. this.$container.css({
  44. display: 'inline-block'
  45. });
  46. this.$container.hover(
  47. e => this.onHoverIn(e),
  48. e => this.onHoverOut(e)
  49. );
  50. // Bind event handler so it is only bound once for every instance.
  51. this._updateVideoResolutionStatus
  52. = this._updateVideoResolutionStatus.bind(this);
  53. this.videoContainer.addResizeListener(
  54. this._updateVideoResolutionStatus);
  55. if (!JitsiMeetJS.util.RTCUIHelper.isResizeEventSupported()) {
  56. /**
  57. * An interval for polling if the displayed video resolution is or
  58. * is not high-definition. For browsers that do not support video
  59. * resize events, polling is the fallback.
  60. *
  61. * @private
  62. * @type {timeoutId}
  63. */
  64. this._updateVideoResolutionInterval = window.setInterval(
  65. this._updateVideoResolutionStatus,
  66. VIDEO_RESOLUTION_POLL_INTERVAL);
  67. }
  68. }
  69. /**
  70. * Stops any polling intervals on the instance and and removes any
  71. * listeners registered on child components.
  72. *
  73. * @returns {void}
  74. */
  75. destroy() {
  76. window.clearInterval(this._updateVideoResolutionInterval);
  77. this.videoContainer.removeResizeListener(
  78. this._updateVideoResolutionStatus);
  79. }
  80. onHoverIn (e) {
  81. if (!this.state) {
  82. return;
  83. }
  84. let container = this.getContainer(this.state);
  85. container.onHoverIn(e);
  86. }
  87. onHoverOut (e) {
  88. if (!this.state) {
  89. return;
  90. }
  91. let container = this.getContainer(this.state);
  92. container.onHoverOut(e);
  93. }
  94. /**
  95. * Called when the media connection has been interrupted.
  96. */
  97. onVideoInterrupted () {
  98. this.enableLocalConnectionProblemFilter(true);
  99. this._setLocalConnectionMessage("connection.RECONNECTING");
  100. // Show the message only if the video is currently being displayed
  101. this.showLocalConnectionMessage(this.state === VIDEO_CONTAINER_TYPE);
  102. }
  103. /**
  104. * Called when the media connection has been restored.
  105. */
  106. onVideoRestored () {
  107. this.enableLocalConnectionProblemFilter(false);
  108. this.showLocalConnectionMessage(false);
  109. }
  110. get id () {
  111. let container = this.getContainer(this.state);
  112. return container.id;
  113. }
  114. scheduleLargeVideoUpdate () {
  115. if (this.updateInProcess || !this.newStreamData) {
  116. return;
  117. }
  118. this.updateInProcess = true;
  119. // Include hide()/fadeOut only if we're switching between users
  120. const isUserSwitch = this.newStreamData.id != this.id;
  121. const container = this.getContainer(this.state);
  122. const preUpdate = isUserSwitch ? container.hide() : Promise.resolve();
  123. preUpdate.then(() => {
  124. const { id, stream, videoType, resolve } = this.newStreamData;
  125. const isVideoFromCamera = videoType === VIDEO_CONTAINER_TYPE;
  126. this.newStreamData = null;
  127. logger.info("hover in %s", id);
  128. this.state = videoType;
  129. const container = this.getContainer(this.state);
  130. container.setStream(stream, videoType);
  131. // change the avatar url on large
  132. this.updateAvatar(Avatar.getAvatarUrl(id));
  133. // FIXME that does not really make sense, because the videoType
  134. // (camera or desktop) is a completely different thing than
  135. // the video container type (Etherpad, SharedVideo, VideoContainer).
  136. // ----------------------------------------------------------------
  137. // If the container is VIDEO_CONTAINER_TYPE, we need to check
  138. // its stream whether exist and is muted to set isVideoMuted
  139. // in rest of the cases it is false
  140. let showAvatar = isVideoFromCamera && (!stream || stream.isMuted());
  141. // If the user's connection is disrupted then the avatar will be
  142. // displayed in case we have no video image cached. That is if
  143. // there was a user switch(image is lost on stream detach) or if
  144. // the video was not rendered, before the connection has failed.
  145. const isConnectionActive = this._isConnectionActive(id);
  146. if (isVideoFromCamera
  147. && !isConnectionActive
  148. && (isUserSwitch || !container.wasVideoRendered)) {
  149. showAvatar = true;
  150. }
  151. // If audio only mode is enabled, always show the avatar for
  152. // videos from another participant.
  153. if (APP.conference.isAudioOnly()
  154. && (isVideoFromCamera
  155. || videoType === DESKTOP_CONTAINER_TYPE)) {
  156. showAvatar = true;
  157. }
  158. let promise;
  159. // do not show stream if video is muted
  160. // but we still should show watermark
  161. if (showAvatar) {
  162. this.showWatermark(true);
  163. // If the intention of this switch is to show the avatar
  164. // we need to make sure that the video is hidden
  165. promise = container.hide();
  166. } else {
  167. promise = container.show();
  168. }
  169. // show the avatar on large if needed
  170. container.showAvatar(showAvatar);
  171. // Clean up audio level after previous speaker.
  172. if (showAvatar) {
  173. this.updateLargeVideoAudioLevel(0);
  174. }
  175. // Make sure no notification about remote failure is shown as
  176. // its UI conflicts with the one for local connection interrupted.
  177. // For the purposes of UI indicators, audio only is considered as
  178. // an "active" connection.
  179. const isConnected
  180. = APP.conference.isAudioOnly()
  181. || APP.conference.isConnectionInterrupted()
  182. || isConnectionActive;
  183. // when isHavingConnectivityIssues, state can be inactive,
  184. // interrupted or restoring. We show different message for
  185. // interrupted and the rest.
  186. const isConnectionInterrupted =
  187. APP.conference.getParticipantConnectionStatus(id)
  188. === ParticipantConnectionStatus.INTERRUPTED;
  189. this.updateParticipantConnStatusIndication(
  190. id,
  191. isConnected,
  192. (isConnectionInterrupted)
  193. ? "connection.USER_CONNECTION_INTERRUPTED"
  194. : "connection.LOW_BANDWIDTH");
  195. // resolve updateLargeVideo promise after everything is done
  196. promise.then(resolve);
  197. return promise;
  198. }).then(() => {
  199. // after everything is done check again if there are any pending
  200. // new streams.
  201. this.updateInProcess = false;
  202. this.eventEmitter.emit(UIEvents.LARGE_VIDEO_ID_CHANGED, this.id);
  203. this.scheduleLargeVideoUpdate();
  204. });
  205. }
  206. /**
  207. * Checks whether a participant's peer connection status is active.
  208. * There is no JitsiParticipant for local id we skip checking local
  209. * participant and report it as having no connectivity issues.
  210. *
  211. * @param {string} id the id of participant(MUC nickname)
  212. * @returns {boolean} <tt>true</tt> when participant connection status is
  213. * {@link ParticipantConnectionStatus.ACTIVE} and <tt>false</tt> otherwise.
  214. * @private
  215. */
  216. _isConnectionActive(id) {
  217. return APP.conference.isLocalId(id)
  218. || APP.conference.getParticipantConnectionStatus(this.id)
  219. === ParticipantConnectionStatus.ACTIVE;
  220. }
  221. /**
  222. * Shows/hides notification about participant's connectivity issues to be
  223. * shown on the large video area.
  224. *
  225. * @param {string} id the id of remote participant(MUC nickname)
  226. * @param {boolean} isConnected true if the connection is active or false
  227. * when the user is having connectivity issues.
  228. * @param {string} messageKey the i18n key of the message
  229. *
  230. * @private
  231. */
  232. updateParticipantConnStatusIndication (id, isConnected, messageKey) {
  233. // Apply grey filter on the large video
  234. this.videoContainer.showRemoteConnectionProblemIndicator(!isConnected);
  235. if (isConnected) {
  236. // Hide the message
  237. this.showRemoteConnectionMessage(false);
  238. } else {
  239. // Get user's display name
  240. let displayName
  241. = APP.conference.getParticipantDisplayName(id);
  242. this._setRemoteConnectionMessage(
  243. messageKey,
  244. { displayName: displayName });
  245. // Show it now only if the VideoContainer is on top
  246. this.showRemoteConnectionMessage(
  247. this.state === VIDEO_CONTAINER_TYPE);
  248. }
  249. }
  250. /**
  251. * Update large video.
  252. * Switches to large video even if previously other container was visible.
  253. * @param userID the userID of the participant associated with the stream
  254. * @param {JitsiTrack?} stream new stream
  255. * @param {string?} videoType new video type
  256. * @returns {Promise}
  257. */
  258. updateLargeVideo (userID, stream, videoType) {
  259. if (this.newStreamData) {
  260. this.newStreamData.reject();
  261. }
  262. this.newStreamData = createDeferred();
  263. this.newStreamData.id = userID;
  264. this.newStreamData.stream = stream;
  265. this.newStreamData.videoType = videoType;
  266. this.scheduleLargeVideoUpdate();
  267. return this.newStreamData.promise;
  268. }
  269. /**
  270. * Update container size.
  271. */
  272. updateContainerSize () {
  273. this.width = UIUtil.getAvailableVideoWidth();
  274. this.height = window.innerHeight;
  275. }
  276. /**
  277. * Resize Large container of specified type.
  278. * @param {string} type type of container which should be resized.
  279. * @param {boolean} [animate=false] if resize process should be animated.
  280. */
  281. resizeContainer (type, animate = false) {
  282. let container = this.getContainer(type);
  283. container.resize(this.width, this.height, animate);
  284. }
  285. /**
  286. * Resize all Large containers.
  287. * @param {boolean} animate if resize process should be animated.
  288. */
  289. resize (animate) {
  290. // resize all containers
  291. Object.keys(this.containers)
  292. .forEach(type => this.resizeContainer(type, animate));
  293. this.$container.animate({
  294. width: this.width,
  295. height: this.height
  296. }, {
  297. queue: false,
  298. duration: animate ? 500 : 0
  299. });
  300. }
  301. /**
  302. * Enables/disables the filter indicating a video problem to the user caused
  303. * by the problems with local media connection.
  304. *
  305. * @param enable <tt>true</tt> to enable, <tt>false</tt> to disable
  306. */
  307. enableLocalConnectionProblemFilter (enable) {
  308. this.videoContainer.enableLocalConnectionProblemFilter(enable);
  309. }
  310. /**
  311. * Updates the src of the dominant speaker avatar
  312. */
  313. updateAvatar (avatarUrl) {
  314. $("#dominantSpeakerAvatar").attr('src', avatarUrl);
  315. }
  316. /**
  317. * Updates the audio level indicator of the large video.
  318. *
  319. * @param lvl the new audio level to set
  320. */
  321. updateLargeVideoAudioLevel (lvl) {
  322. AudioLevels.updateLargeVideoAudioLevel("dominantSpeaker", lvl);
  323. }
  324. /**
  325. * Show or hide watermark.
  326. * @param {boolean} show
  327. */
  328. showWatermark (show) {
  329. $('.watermark').css('visibility', show ? 'visible' : 'hidden');
  330. }
  331. /**
  332. * Shows/hides the message indicating problems with local media connection.
  333. * @param {boolean|null} show(optional) tells whether the message is to be
  334. * displayed or not. If missing the condition will be based on the value
  335. * obtained from {@link APP.conference.isConnectionInterrupted}.
  336. */
  337. showLocalConnectionMessage (show) {
  338. if (typeof show !== 'boolean') {
  339. show = APP.conference.isConnectionInterrupted();
  340. }
  341. let id = 'localConnectionMessage';
  342. UIUtil.setVisible(id, show);
  343. if (show) {
  344. // Avatar message conflicts with 'videoConnectionMessage',
  345. // so it must be hidden
  346. this.showRemoteConnectionMessage(false);
  347. }
  348. }
  349. /**
  350. * Shows hides the "avatar" message which is to be displayed either in
  351. * the middle of the screen or below the avatar image.
  352. *
  353. * @param {null|boolean} show (optional) <tt>true</tt> to show the avatar
  354. * message or <tt>false</tt> to hide it. If not provided then the connection
  355. * status of the user currently on the large video will be obtained form
  356. * "APP.conference" and the message will be displayed if the user's
  357. * connection is interrupted.
  358. */
  359. showRemoteConnectionMessage (show) {
  360. if (typeof show !== 'boolean') {
  361. show = !this._isConnectionActive(this.id);
  362. }
  363. if (show) {
  364. $('#remoteConnectionMessage').css({display: "block"});
  365. // 'videoConnectionMessage' message conflicts with 'avatarMessage',
  366. // so it must be hidden
  367. this.showLocalConnectionMessage(false);
  368. } else {
  369. $('#remoteConnectionMessage').hide();
  370. }
  371. }
  372. /**
  373. * Updates the text which describes that the remote user is having
  374. * connectivity issues.
  375. *
  376. * @param {string} msgKey the translation key which will be used to get
  377. * the message text.
  378. * @param {object} msgOptions translation options object.
  379. *
  380. * @private
  381. */
  382. _setRemoteConnectionMessage (msgKey, msgOptions) {
  383. if (msgKey) {
  384. $('#remoteConnectionMessage')
  385. .attr("data-i18n", msgKey)
  386. .attr("data-i18n-options", JSON.stringify(msgOptions));
  387. APP.translation.translateElement(
  388. $('#remoteConnectionMessage'), msgOptions);
  389. }
  390. this.videoContainer.positionRemoteConnectionMessage();
  391. }
  392. /**
  393. * Updated the text which is to be shown on the top of large video, when
  394. * local media connection is interrupted.
  395. *
  396. * @param {string} msgKey the translation key which will be used to get
  397. * the message text to be displayed on the large video.
  398. *
  399. * @private
  400. */
  401. _setLocalConnectionMessage (msgKey) {
  402. $('#localConnectionMessage')
  403. .attr("data-i18n", msgKey);
  404. APP.translation.translateElement($('#localConnectionMessage'));
  405. }
  406. /**
  407. * Add container of specified type.
  408. * @param {string} type container type
  409. * @param {LargeContainer} container container to add.
  410. */
  411. addContainer (type, container) {
  412. if (this.containers[type]) {
  413. throw new Error(`container of type ${type} already exist`);
  414. }
  415. this.containers[type] = container;
  416. this.resizeContainer(type);
  417. }
  418. /**
  419. * Get Large container of specified type.
  420. * @param {string} type container type.
  421. * @returns {LargeContainer}
  422. */
  423. getContainer (type) {
  424. let container = this.containers[type];
  425. if (!container) {
  426. throw new Error(`container of type ${type} doesn't exist`);
  427. }
  428. return container;
  429. }
  430. /**
  431. * Remove Large container of specified type.
  432. * @param {string} type container type.
  433. */
  434. removeContainer (type) {
  435. if (!this.containers[type]) {
  436. throw new Error(`container of type ${type} doesn't exist`);
  437. }
  438. delete this.containers[type];
  439. }
  440. /**
  441. * Show Large container of specified type.
  442. * Does nothing if such container is already visible.
  443. * @param {string} type container type.
  444. * @returns {Promise}
  445. */
  446. showContainer (type) {
  447. if (this.state === type) {
  448. return Promise.resolve();
  449. }
  450. let oldContainer = this.containers[this.state];
  451. // FIXME when video is being replaced with other content we need to hide
  452. // companion icons/messages. It would be best if the container would
  453. // be taking care of it by itself, but that is a bigger refactoring
  454. if (this.state === VIDEO_CONTAINER_TYPE) {
  455. this.showWatermark(false);
  456. this.showLocalConnectionMessage(false);
  457. this.showRemoteConnectionMessage(false);
  458. }
  459. oldContainer.hide();
  460. this.state = type;
  461. let container = this.getContainer(type);
  462. return container.show().then(() => {
  463. if (type === VIDEO_CONTAINER_TYPE) {
  464. // FIXME when video appears on top of other content we need to
  465. // show companion icons/messages. It would be best if
  466. // the container would be taking care of it by itself, but that
  467. // is a bigger refactoring
  468. this.showWatermark(true);
  469. // "avatar" and "video connection" can not be displayed both
  470. // at the same time, but the latter is of higher priority and it
  471. // will hide the avatar one if will be displayed.
  472. this.showRemoteConnectionMessage(/* fetch the current state */);
  473. this.showLocalConnectionMessage(/* fetch the current state */);
  474. }
  475. });
  476. }
  477. /**
  478. * Changes the flipX state of the local video.
  479. * @param val {boolean} true if flipped.
  480. */
  481. onLocalFlipXChange(val) {
  482. this.videoContainer.setLocalFlipX(val);
  483. }
  484. /**
  485. * Dispatches an action to update the known resolution state of the
  486. * large video.
  487. *
  488. * @private
  489. * @returns {void}
  490. */
  491. _updateVideoResolutionStatus() {
  492. const { height, width } = this.videoContainer.getStreamSize();
  493. const isCurrentlyHD = Math.min(height, width) >= config.minHDHeight;
  494. APP.store.dispatch(setLargeVideoHDStatus(isCurrentlyHD));
  495. }
  496. }