Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

FollowMe.js 14KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381
  1. /*
  2. * Copyright @ 2015 Atlassian Pty Ltd
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. */
  16. import UIEvents from '../service/UI/UIEvents';
  17. import VideoLayout from './UI/videolayout/VideoLayout';
  18. import FilmStrip from './UI/videolayout/FilmStrip';
  19. /**
  20. * The (name of the) command which transports the state (represented by
  21. * {State} for the local state at the time of this writing) of a {FollowMe}
  22. * (instance) between participants.
  23. */
  24. const _COMMAND = "follow-me";
  25. /**
  26. * The timeout after which a follow-me command that has been received will be
  27. * ignored if not consumed.
  28. *
  29. * @type {number} in seconds
  30. * @private
  31. */
  32. const _FOLLOW_ME_RECEIVED_TIMEOUT = 30;
  33. /**
  34. * Represents the set of {FollowMe}-related states (properties and their
  35. * respective values) which are to be followed by a participant. {FollowMe}
  36. * will send {_COMMAND} whenever a property of {State} changes (if the local
  37. * participant is in her right to issue such a command, of course).
  38. */
  39. class State {
  40. /**
  41. * Initializes a new {State} instance.
  42. *
  43. * @param propertyChangeCallback {Function} which is to be called when a
  44. * property of the new instance has its value changed from an old value
  45. * into a (different) new value. The function is supplied with the name of
  46. * the property, the old value of the property before the change, and the
  47. * new value of the property after the change.
  48. */
  49. constructor (propertyChangeCallback) {
  50. this._propertyChangeCallback = propertyChangeCallback;
  51. }
  52. get filmStripVisible () { return this._filmStripVisible; }
  53. set filmStripVisible (b) {
  54. var oldValue = this._filmStripVisible;
  55. if (oldValue !== b) {
  56. this._filmStripVisible = b;
  57. this._firePropertyChange('filmStripVisible', oldValue, b);
  58. }
  59. }
  60. get nextOnStage() { return this._nextOnStage; }
  61. set nextOnStage(id) {
  62. var oldValue = this._nextOnStage;
  63. if (oldValue !== id) {
  64. this._nextOnStage = id;
  65. this._firePropertyChange('nextOnStage', oldValue, id);
  66. }
  67. }
  68. get sharedDocumentVisible () { return this._sharedDocumentVisible; }
  69. set sharedDocumentVisible (b) {
  70. var oldValue = this._sharedDocumentVisible;
  71. if (oldValue !== b) {
  72. this._sharedDocumentVisible = b;
  73. this._firePropertyChange('sharedDocumentVisible', oldValue, b);
  74. }
  75. }
  76. /**
  77. * Invokes {_propertyChangeCallback} to notify it that {property} had its
  78. * value changed from {oldValue} to {newValue}.
  79. *
  80. * @param property the name of the property which had its value changed
  81. * from {oldValue} to {newValue}
  82. * @param oldValue the value of {property} before the change
  83. * @param newValue the value of {property} after the change
  84. */
  85. _firePropertyChange (property, oldValue, newValue) {
  86. var propertyChangeCallback = this._propertyChangeCallback;
  87. if (propertyChangeCallback)
  88. propertyChangeCallback(property, oldValue, newValue);
  89. }
  90. }
  91. /**
  92. * Represents the "Follow Me" feature which enables a moderator to
  93. * (partially) control the user experience/interface (e.g. film strip
  94. * visibility) of (other) non-moderator particiapnts.
  95. *
  96. * @author Lyubomir Marinov
  97. */
  98. class FollowMe {
  99. /**
  100. * Initializes a new {FollowMe} instance.
  101. *
  102. * @param conference the {conference} which is to transport
  103. * {FollowMe}-related information between participants
  104. * @param UI the {UI} which is the source (model/state) to be sent to
  105. * remote participants if the local participant is the moderator or the
  106. * destination (model/state) to receive from the remote moderator if the
  107. * local participant is not the moderator
  108. */
  109. constructor (conference, UI) {
  110. this._conference = conference;
  111. this._UI = UI;
  112. this.nextOnStageTimer = 0;
  113. // The states of the local participant which are to be followed (by the
  114. // remote participants when the local participant is in her right to
  115. // issue such commands).
  116. this._local = new State(this._localPropertyChange.bind(this));
  117. // Listen to "Follow Me" commands. I'm not sure whether a moderator can
  118. // (in lib-jitsi-meet and/or Meet) become a non-moderator. If that's
  119. // possible, then it may be easiest to always listen to commands. The
  120. // listener will validate received commands before acting on them.
  121. conference.commands.addCommandListener(
  122. _COMMAND,
  123. this._onFollowMeCommand.bind(this));
  124. }
  125. /**
  126. * Adds listeners for the UI states of the local participant which are
  127. * to be followed (by the remote participants). A non-moderator (very
  128. * likely) can become a moderator so it may be easiest to always track
  129. * the states of interest.
  130. * @private
  131. */
  132. _addFollowMeListeners () {
  133. this.filmStripEventHandler = this._filmStripToggled.bind(this);
  134. this._UI.addListener(UIEvents.TOGGLED_FILM_STRIP,
  135. this.filmStripEventHandler);
  136. var self = this;
  137. this.pinnedEndpointEventHandler = function (smallVideo, isPinned) {
  138. self._nextOnStage(smallVideo, isPinned);
  139. };
  140. this._UI.addListener(UIEvents.PINNED_ENDPOINT,
  141. this.pinnedEndpointEventHandler);
  142. this.sharedDocEventHandler = this._sharedDocumentToggled.bind(this);
  143. this._UI.addListener( UIEvents.TOGGLED_SHARED_DOCUMENT,
  144. this.sharedDocEventHandler);
  145. }
  146. /**
  147. * Removes all follow me listeners.
  148. * @private
  149. */
  150. _removeFollowMeListeners () {
  151. this._UI.removeListener(UIEvents.TOGGLED_FILM_STRIP,
  152. this.filmStripEventHandler);
  153. this._UI.removeListener(UIEvents.TOGGLED_SHARED_DOCUMENT,
  154. this.sharedDocEventHandler);
  155. this._UI.removeListener(UIEvents.PINNED_ENDPOINT,
  156. this.pinnedEndpointEventHandler);
  157. }
  158. /**
  159. * Enables or disabled the follow me functionality
  160. *
  161. * @param enable {true} to enable the follow me functionality, {false} -
  162. * to disable it
  163. */
  164. enableFollowMe (enable) {
  165. this.isEnabled = enable;
  166. if (this.isEnabled)
  167. this._addFollowMeListeners();
  168. else
  169. this._removeFollowMeListeners();
  170. }
  171. /**
  172. * Notifies this instance that the (visibility of the) film strip was
  173. * toggled (in the user interface of the local participant).
  174. *
  175. * @param filmStripVisible {Boolean} {true} if the film strip was shown (as
  176. * a result of the toggle) or {false} if the film strip was hidden
  177. */
  178. _filmStripToggled (filmStripVisible) {
  179. this._local.filmStripVisible = filmStripVisible;
  180. }
  181. /**
  182. * Notifies this instance that the (visibility of the) shared document was
  183. * toggled (in the user interface of the local participant).
  184. *
  185. * @param sharedDocumentVisible {Boolean} {true} if the shared document was
  186. * shown (as a result of the toggle) or {false} if it was hidden
  187. */
  188. _sharedDocumentToggled (sharedDocumentVisible) {
  189. this._local.sharedDocumentVisible = sharedDocumentVisible;
  190. }
  191. /**
  192. * Changes the nextOnPage property value.
  193. *
  194. * @param smallVideo the {SmallVideo} that was pinned or unpinned
  195. * @param isPinned indicates if the given {SmallVideo} was pinned or
  196. * unpinned
  197. * @private
  198. */
  199. _nextOnStage (smallVideo, isPinned) {
  200. if (!this._conference.isModerator)
  201. return;
  202. var nextOnStage = null;
  203. if(isPinned)
  204. nextOnStage = smallVideo.getId();
  205. this._local.nextOnStage = nextOnStage;
  206. }
  207. /**
  208. * Sends the follow-me command, when a local property change occurs.
  209. *
  210. * @param property the property name
  211. * @param oldValue the old value
  212. * @param newValue the new value
  213. * @private
  214. */
  215. _localPropertyChange (property, oldValue, newValue) {
  216. // Only a moderator is allowed to send commands.
  217. var conference = this._conference;
  218. if (!conference.isModerator)
  219. return;
  220. var commands = conference.commands;
  221. // XXX The "Follow Me" command represents a snapshot of all states
  222. // which are to be followed so don't forget to removeCommand before
  223. // sendCommand!
  224. commands.removeCommand(_COMMAND);
  225. var self = this;
  226. commands.sendCommandOnce(
  227. _COMMAND,
  228. {
  229. attributes: {
  230. filmStripVisible: self._local.filmStripVisible,
  231. nextOnStage: self._local.nextOnStage,
  232. sharedDocumentVisible: self._local.sharedDocumentVisible
  233. }
  234. });
  235. }
  236. /**
  237. * Notifies this instance about a &qout;Follow Me&qout; command (delivered
  238. * by the Command(s) API of {this._conference}).
  239. *
  240. * @param attributes the attributes {Object} carried by the command
  241. * @param id the identifier of the participant who issued the command. A
  242. * notable idiosyncrasy of the Command(s) API to be mindful of here is that
  243. * the command may be issued by the local participant.
  244. */
  245. _onFollowMeCommand ({ attributes }, id) {
  246. // We require to know who issued the command because (1) only a
  247. // moderator is allowed to send commands and (2) a command MUST be
  248. // issued by a defined commander.
  249. if (typeof id === 'undefined')
  250. return;
  251. // The Command(s) API will send us our own commands and we don't want
  252. // to act upon them.
  253. if (this._conference.isLocalId(id))
  254. return;
  255. if (!this._conference.isParticipantModerator(id))
  256. {
  257. console.warn('Received follow-me command ' +
  258. 'not from moderator');
  259. return;
  260. }
  261. // Applies the received/remote command to the user experience/interface
  262. // of the local participant.
  263. this._onFilmStripVisible(attributes.filmStripVisible);
  264. this._onNextOnStage(attributes.nextOnStage);
  265. this._onSharedDocumentVisible(attributes.sharedDocumentVisible);
  266. }
  267. _onFilmStripVisible(filmStripVisible) {
  268. if (typeof filmStripVisible !== 'undefined') {
  269. // XXX The Command(s) API doesn't preserve the types (of
  270. // attributes, at least) at the time of this writing so take into
  271. // account that what originated as a Boolean may be a String on
  272. // receipt.
  273. filmStripVisible = (filmStripVisible == 'true');
  274. // FIXME The UI (module) very likely doesn't (want to) expose its
  275. // eventEmitter as a public field. I'm not sure at the time of this
  276. // writing whether calling UI.toggleFilmStrip() is acceptable (from
  277. // a design standpoint) either.
  278. if (filmStripVisible !== FilmStrip.isFilmStripVisible())
  279. this._UI.eventEmitter.emit(UIEvents.TOGGLE_FILM_STRIP);
  280. }
  281. }
  282. _onNextOnStage(id) {
  283. var clickId = null;
  284. var pin;
  285. if(typeof id !== 'undefined' && !VideoLayout.isPinned(id)) {
  286. clickId = id;
  287. pin = true;
  288. }
  289. else if (typeof id == 'undefined' && VideoLayout.getPinnedId()) {
  290. clickId = VideoLayout.getPinnedId();
  291. pin = false;
  292. }
  293. if (clickId)
  294. this._pinVideoThumbnailById(clickId, pin);
  295. }
  296. _onSharedDocumentVisible(sharedDocumentVisible) {
  297. if (typeof sharedDocumentVisible !== 'undefined') {
  298. // XXX The Command(s) API doesn't preserve the types (of
  299. // attributes, at least) at the time of this writing so take into
  300. // account that what originated as a Boolean may be a String on
  301. // receipt.
  302. sharedDocumentVisible = (sharedDocumentVisible == 'true');
  303. if (sharedDocumentVisible
  304. !== this._UI.getSharedDocumentManager().isVisible())
  305. this._UI.getSharedDocumentManager().toggleEtherpad();
  306. }
  307. }
  308. /**
  309. * Pins / unpins the video thumbnail given by clickId.
  310. *
  311. * @param clickId the identifier of the video thumbnail to pin or unpin
  312. * @param pin {true} to pin, {false} to unpin
  313. * @private
  314. */
  315. _pinVideoThumbnailById(clickId, pin) {
  316. var self = this;
  317. var smallVideo = VideoLayout.getSmallVideo(clickId);
  318. // If the SmallVideo for the given clickId exists we proceed with the
  319. // pin/unpin.
  320. if (smallVideo) {
  321. this.nextOnStageTimer = 0;
  322. clearTimeout(this.nextOnStageTimout);
  323. if (pin && !VideoLayout.isPinned(clickId)
  324. || !pin && VideoLayout.isPinned(clickId))
  325. VideoLayout.handleVideoThumbClicked(clickId);
  326. }
  327. // If there's no SmallVideo object for the given id, lets wait and see
  328. // if it's going to be created in the next 30sec.
  329. else {
  330. this.nextOnStageTimout = setTimeout(function () {
  331. if (self.nextOnStageTimer > _FOLLOW_ME_RECEIVED_TIMEOUT) {
  332. self.nextOnStageTimer = 0;
  333. return;
  334. }
  335. this.nextOnStageTimer++;
  336. self._pinVideoThumbnailById(clickId, pin);
  337. }, 1000);
  338. }
  339. }
  340. }
  341. export default FollowMe;