Você não pode selecionar mais de 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.

LargeVideo.js 15KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500
  1. /* global $, APP, interfaceConfig */
  2. /* jshint -W101 */
  3. import UIUtil from "../util/UIUtil";
  4. import UIEvents from "../../../service/UI/UIEvents";
  5. import LargeContainer from './LargeContainer';
  6. import BottomToolbar from '../toolbars/BottomToolbar';
  7. import Avatar from "../avatar/Avatar";
  8. import {createDeferred} from '../../util/helpers';
  9. const RTCBrowserType = require("../../RTC/RTCBrowserType");
  10. const avatarSize = interfaceConfig.DOMINANT_SPEAKER_AVATAR_SIZE;
  11. const FADE_DURATION_MS = 300;
  12. function getStreamOwnerId(stream) {
  13. if (!stream) {
  14. return;
  15. }
  16. if (stream.isLocal()) { // local stream doesn't have method "getParticipantId"
  17. return APP.conference.localId;
  18. } else {
  19. return stream.getParticipantId();
  20. }
  21. }
  22. /**
  23. * Returns an array of the video dimensions, so that it keeps it's aspect
  24. * ratio and fits available area with it's larger dimension. This method
  25. * ensures that whole video will be visible and can leave empty areas.
  26. *
  27. * @return an array with 2 elements, the video width and the video height
  28. */
  29. function getDesktopVideoSize(videoWidth,
  30. videoHeight,
  31. videoSpaceWidth,
  32. videoSpaceHeight) {
  33. let aspectRatio = videoWidth / videoHeight;
  34. let availableWidth = Math.max(videoWidth, videoSpaceWidth);
  35. let availableHeight = Math.max(videoHeight, videoSpaceHeight);
  36. videoSpaceHeight -= BottomToolbar.getFilmStripHeight();
  37. if (availableWidth / aspectRatio >= videoSpaceHeight) {
  38. availableHeight = videoSpaceHeight;
  39. availableWidth = availableHeight * aspectRatio;
  40. }
  41. if (availableHeight * aspectRatio >= videoSpaceWidth) {
  42. availableWidth = videoSpaceWidth;
  43. availableHeight = availableWidth / aspectRatio;
  44. }
  45. return { availableWidth, availableHeight };
  46. }
  47. /**
  48. * Returns an array of the video dimensions. It respects the
  49. * VIDEO_LAYOUT_FIT config, to fit the video to the screen, by hiding some parts
  50. * of it, or to fit it to the height or width.
  51. *
  52. * @param videoWidth the original video width
  53. * @param videoHeight the original video height
  54. * @param videoSpaceWidth the width of the video space
  55. * @param videoSpaceHeight the height of the video space
  56. * @return an array with 2 elements, the video width and the video height
  57. */
  58. function getCameraVideoSize(videoWidth,
  59. videoHeight,
  60. videoSpaceWidth,
  61. videoSpaceHeight) {
  62. let aspectRatio = videoWidth / videoHeight;
  63. let availableWidth = videoWidth;
  64. let availableHeight = videoHeight;
  65. if (interfaceConfig.VIDEO_LAYOUT_FIT == 'height') {
  66. availableHeight = videoSpaceHeight;
  67. availableWidth = availableHeight*aspectRatio;
  68. }
  69. else if (interfaceConfig.VIDEO_LAYOUT_FIT == 'width') {
  70. availableWidth = videoSpaceWidth;
  71. availableHeight = availableWidth/aspectRatio;
  72. }
  73. else if (interfaceConfig.VIDEO_LAYOUT_FIT == 'both') {
  74. availableWidth = Math.max(videoWidth, videoSpaceWidth);
  75. availableHeight = Math.max(videoHeight, videoSpaceHeight);
  76. if (availableWidth / aspectRatio < videoSpaceHeight) {
  77. availableHeight = videoSpaceHeight;
  78. availableWidth = availableHeight * aspectRatio;
  79. }
  80. if (availableHeight * aspectRatio < videoSpaceWidth) {
  81. availableWidth = videoSpaceWidth;
  82. availableHeight = availableWidth / aspectRatio;
  83. }
  84. }
  85. return { availableWidth, availableHeight };
  86. }
  87. /**
  88. * Returns an array of the video horizontal and vertical indents,
  89. * so that if fits its parent.
  90. *
  91. * @return an array with 2 elements, the horizontal indent and the vertical
  92. * indent
  93. */
  94. function getCameraVideoPosition(videoWidth,
  95. videoHeight,
  96. videoSpaceWidth,
  97. videoSpaceHeight) {
  98. // Parent height isn't completely calculated when we position the video in
  99. // full screen mode and this is why we use the screen height in this case.
  100. // Need to think it further at some point and implement it properly.
  101. if (UIUtil.isFullScreen()) {
  102. videoSpaceHeight = window.innerHeight;
  103. }
  104. let horizontalIndent = (videoSpaceWidth - videoWidth) / 2;
  105. let verticalIndent = (videoSpaceHeight - videoHeight) / 2;
  106. return { horizontalIndent, verticalIndent };
  107. }
  108. /**
  109. * Returns an array of the video horizontal and vertical indents.
  110. * Centers horizontally and top aligns vertically.
  111. *
  112. * @return an array with 2 elements, the horizontal indent and the vertical
  113. * indent
  114. */
  115. function getDesktopVideoPosition(videoWidth,
  116. videoHeight,
  117. videoSpaceWidth,
  118. videoSpaceHeight) {
  119. let horizontalIndent = (videoSpaceWidth - videoWidth) / 2;
  120. let verticalIndent = 0;// Top aligned
  121. return { horizontalIndent, verticalIndent };
  122. }
  123. export const VideoContainerType = "video";
  124. class VideoContainer extends LargeContainer {
  125. // FIXME: With Temasys we have to re-select everytime
  126. get $video () {
  127. return $('#largeVideo');
  128. }
  129. get id () {
  130. return getStreamOwnerId(this.stream);
  131. }
  132. constructor (onPlay) {
  133. super();
  134. this.stream = null;
  135. this.videoType = null;
  136. this.$avatar = $('#dominantSpeaker');
  137. this.$wrapper = $('#largeVideoWrapper');
  138. if (!RTCBrowserType.isIExplorer()) {
  139. this.$video.volume = 0;
  140. }
  141. this.$video.on('play', onPlay);
  142. }
  143. getStreamSize () {
  144. let video = this.$video[0];
  145. return {
  146. width: video.videoWidth,
  147. height: video.videoHeight
  148. };
  149. }
  150. getVideoSize (containerWidth, containerHeight) {
  151. let { width, height } = this.getStreamSize();
  152. if (this.stream && this.isScreenSharing()) {
  153. return getDesktopVideoSize( width,
  154. height,
  155. containerWidth,
  156. containerHeight);
  157. } else {
  158. return getCameraVideoSize( width,
  159. height,
  160. containerWidth,
  161. containerHeight);
  162. }
  163. }
  164. getVideoPosition (width, height, containerWidth, containerHeight) {
  165. if (this.stream && this.isScreenSharing()) {
  166. return getDesktopVideoPosition( width,
  167. height,
  168. containerWidth,
  169. containerHeight);
  170. } else {
  171. return getCameraVideoPosition( width,
  172. height,
  173. containerWidth,
  174. containerHeight);
  175. }
  176. }
  177. resize (containerWidth, containerHeight, animate = false) {
  178. let { width, height }
  179. = this.getVideoSize(containerWidth, containerHeight);
  180. let { horizontalIndent, verticalIndent }
  181. = this.getVideoPosition(width, height,
  182. containerWidth, containerHeight);
  183. // update avatar position
  184. let top = containerHeight / 2 - avatarSize / 4 * 3;
  185. this.$avatar.css('top', top);
  186. this.$wrapper.animate({
  187. width,
  188. height,
  189. top: verticalIndent,
  190. bottom: verticalIndent,
  191. left: horizontalIndent,
  192. right: horizontalIndent
  193. }, {
  194. queue: false,
  195. duration: animate ? 500 : 0
  196. });
  197. }
  198. setStream (stream, videoType) {
  199. this.stream = stream;
  200. this.videoType = videoType;
  201. stream.attach(this.$video);
  202. let flipX = stream.isLocal() && !this.isScreenSharing();
  203. this.$video.css({
  204. transform: flipX ? 'scaleX(-1)' : 'none'
  205. });
  206. }
  207. isScreenSharing () {
  208. return this.videoType === 'desktop';
  209. }
  210. showAvatar (show) {
  211. this.$avatar.css("visibility", show ? "visible" : "hidden");
  212. }
  213. // We are doing fadeOut/fadeIn animations on parent div which wraps
  214. // largeVideo, because when Temasys plugin is in use it replaces
  215. // <video> elements with plugin <object> tag. In Safari jQuery is
  216. // unable to store values on this plugin object which breaks all
  217. // animation effects performed on it directly.
  218. show () {
  219. let $wrapper = this.$wrapper;
  220. return new Promise(function(resolve) {
  221. $wrapper.css({visibility: 'visible'});
  222. $wrapper.fadeIn(FADE_DURATION_MS, function () {
  223. $('.watermark').css({visibility: 'visible'});
  224. resolve();
  225. });
  226. });
  227. }
  228. hide () {
  229. let $wrapper = this.$wrapper;
  230. let id = this.id;
  231. return new Promise(function(resolve) {
  232. $wrapper.fadeOut(id ? FADE_DURATION_MS : 1, function () {
  233. $wrapper.css({visibility: 'hidden'});
  234. $('.watermark').css({visibility: 'hidden'});
  235. resolve();
  236. });
  237. });
  238. }
  239. }
  240. export default class LargeVideoManager {
  241. constructor () {
  242. this.containers = {};
  243. this.state = VideoContainerType;
  244. this.videoContainer = new VideoContainer(() => this.resizeContainer(VideoContainerType));
  245. this.addContainer(VideoContainerType, this.videoContainer);
  246. this.width = 0;
  247. this.height = 0;
  248. this.$container = $('#largeVideoContainer');
  249. this.$container.css({
  250. display: 'inline-block'
  251. });
  252. if (interfaceConfig.SHOW_JITSI_WATERMARK) {
  253. let leftWatermarkDiv = this.$container.find("div.watermark.leftwatermark");
  254. leftWatermarkDiv.css({display: 'block'});
  255. leftWatermarkDiv.parent().attr('href', interfaceConfig.JITSI_WATERMARK_LINK);
  256. }
  257. if (interfaceConfig.SHOW_BRAND_WATERMARK) {
  258. let rightWatermarkDiv = this.$container.find("div.watermark.rightwatermark");
  259. rightWatermarkDiv.css({
  260. display: 'block',
  261. backgroundImage: 'url(images/rightwatermark.png)'
  262. });
  263. rightWatermarkDiv.parent().attr('href', interfaceConfig.BRAND_WATERMARK_LINK);
  264. }
  265. if (interfaceConfig.SHOW_POWERED_BY) {
  266. this.$container.children("a.poweredby").css({display: 'block'});
  267. }
  268. this.$container.hover(
  269. e => this.onHoverIn(e),
  270. e => this.onHoverOut(e)
  271. );
  272. }
  273. onHoverIn (e) {
  274. if (!this.state) {
  275. return;
  276. }
  277. let container = this.getContainer(this.state);
  278. container.onHoverIn(e);
  279. }
  280. onHoverOut (e) {
  281. if (!this.state) {
  282. return;
  283. }
  284. let container = this.getContainer(this.state);
  285. container.onHoverOut(e);
  286. }
  287. get id () {
  288. return this.videoContainer.id;
  289. }
  290. scheduleLargeVideoUpdate () {
  291. if (this.updateInProcess || !this.newStreamData) {
  292. return;
  293. }
  294. this.updateInProcess = true;
  295. let container = this.getContainer(this.state);
  296. container.hide().then(() => {
  297. let {id, stream, videoType, resolve} = this.newStreamData;
  298. this.newStreamData = null;
  299. console.info("hover in %s", id);
  300. this.state = VideoContainerType;
  301. this.videoContainer.setStream(stream, videoType);
  302. // change the avatar url on large
  303. this.updateAvatar(Avatar.getAvatarUrl(id));
  304. let isVideoMuted = stream.isMuted();
  305. // show the avatar on large if needed
  306. this.videoContainer.showAvatar(isVideoMuted);
  307. // do not show stream if video is muted
  308. let promise = isVideoMuted ? Promise.resolve() : this.videoContainer.show();
  309. // resolve updateLargeVideo promise after everything is done
  310. promise.then(resolve);
  311. return promise;
  312. }).then(() => {
  313. // after everything is done check again if there are any pending new streams.
  314. this.updateInProcess = false;
  315. this.scheduleLargeVideoUpdate();
  316. });
  317. }
  318. updateLargeVideo (stream, videoType) {
  319. let id = getStreamOwnerId(stream);
  320. if (this.newStreamData) {
  321. this.newStreamData.reject();
  322. }
  323. this.newStreamData = createDeferred();
  324. this.newStreamData.id = id;
  325. this.newStreamData.stream = stream;
  326. this.newStreamData.videoType = videoType;
  327. this.scheduleLargeVideoUpdate();
  328. return this.newStreamData.promise;
  329. }
  330. updateContainerSize (isSideBarVisible) {
  331. this.width = UIUtil.getAvailableVideoWidth(isSideBarVisible);
  332. this.height = window.innerHeight;
  333. }
  334. resizeContainer (type, animate = false) {
  335. let container = this.getContainer(type);
  336. container.resize(this.width, this.height, animate);
  337. }
  338. resize (animate) {
  339. // resize all containers
  340. Object.keys(this.containers)
  341. .forEach(type => this.resizeContainer(type, animate));
  342. this.$container.animate({
  343. width: this.width,
  344. height: this.height
  345. }, {
  346. queue: false,
  347. duration: animate ? 500 : 0
  348. });
  349. }
  350. /**
  351. * Enables/disables the filter indicating a video problem to the user.
  352. *
  353. * @param enable <tt>true</tt> to enable, <tt>false</tt> to disable
  354. */
  355. enableVideoProblemFilter (enable) {
  356. this.videoContainer.$video.toggleClass("videoProblemFilter", enable);
  357. }
  358. /**
  359. * Updates the src of the dominant speaker avatar
  360. */
  361. updateAvatar (avatarUrl) {
  362. $("#dominantSpeakerAvatar").attr('src', avatarUrl);
  363. }
  364. showAvatar (show) {
  365. this.videoContainer.showAvatar(show);
  366. }
  367. addContainer (type, container) {
  368. if (this.containers[type]) {
  369. throw new Error(`container of type ${type} already exist`);
  370. }
  371. this.containers[type] = container;
  372. this.resizeContainer(type);
  373. }
  374. getContainer (type) {
  375. let container = this.containers[type];
  376. if (!container) {
  377. throw new Error(`container of type ${type} doesn't exist`);
  378. }
  379. return container;
  380. }
  381. removeContainer (type) {
  382. if (!this.containers[type]) {
  383. throw new Error(`container of type ${type} doesn't exist`);
  384. }
  385. delete this.containers[type];
  386. }
  387. showContainer (type) {
  388. if (this.state === type) {
  389. return Promise.resolve();
  390. }
  391. let oldContainer = this.containers[this.state];
  392. oldContainer.hide();
  393. this.state = type;
  394. let container = this.getContainer(type);
  395. return container.show();
  396. }
  397. }