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.

LargeVideo.js 13KB

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