Du kannst nicht mehr als 25 Themen auswählen Themen müssen mit entweder einem Buchstaben oder einer Ziffer beginnen. Sie können Bindestriche („-“) enthalten und bis zu 35 Zeichen lang sein.

conference.js 38KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165
  1. /* global $, APP, JitsiMeetJS, config, interfaceConfig */
  2. import {openConnection} from './connection';
  3. //FIXME:
  4. import createRoomLocker from './modules/UI/authentication/RoomLocker';
  5. //FIXME:
  6. import AuthHandler from './modules/UI/authentication/AuthHandler';
  7. import ConnectionQuality from './modules/connectionquality/connectionquality';
  8. import Recorder from './modules/recorder/Recorder';
  9. import CQEvents from './service/connectionquality/CQEvents';
  10. import UIEvents from './service/UI/UIEvents';
  11. const ConnectionEvents = JitsiMeetJS.events.connection;
  12. const ConnectionErrors = JitsiMeetJS.errors.connection;
  13. const ConferenceEvents = JitsiMeetJS.events.conference;
  14. const ConferenceErrors = JitsiMeetJS.errors.conference;
  15. const TrackEvents = JitsiMeetJS.events.track;
  16. const TrackErrors = JitsiMeetJS.errors.track;
  17. let room, connection, localAudio, localVideo, roomLocker;
  18. import {VIDEO_CONTAINER_TYPE} from "./modules/UI/videolayout/LargeVideo";
  19. /**
  20. * Open Connection. When authentication failed it shows auth dialog.
  21. * @param roomName the room name to use
  22. * @returns Promise<JitsiConnection>
  23. */
  24. function connect(roomName) {
  25. return openConnection({retry: true, roomName: roomName})
  26. .catch(function (err) {
  27. if (err === ConnectionErrors.PASSWORD_REQUIRED) {
  28. APP.UI.notifyTokenAuthFailed();
  29. } else {
  30. APP.UI.notifyConnectionFailed(err);
  31. }
  32. throw err;
  33. });
  34. }
  35. /**
  36. * Share email with other users.
  37. * @param emailCommand the email command
  38. * @param {string} email new email
  39. */
  40. function sendEmail (emailCommand, email) {
  41. room.sendCommand(emailCommand, {
  42. value: email,
  43. attributes: {
  44. id: room.myUserId()
  45. }
  46. });
  47. }
  48. /**
  49. * Get user nickname by user id.
  50. * @param {string} id user id
  51. * @returns {string?} user nickname or undefined if user is unknown.
  52. */
  53. function getDisplayName (id) {
  54. if (APP.conference.isLocalId(id)) {
  55. return APP.settings.getDisplayName();
  56. }
  57. let participant = room.getParticipantById(id);
  58. if (participant && participant.getDisplayName()) {
  59. return participant.getDisplayName();
  60. }
  61. }
  62. /**
  63. * Mute or unmute local audio stream if it exists.
  64. * @param {boolean} muted if audio stream should be muted or unmuted.
  65. * @param {boolean} indicates if this local audio mute was a result of user
  66. * interaction
  67. *
  68. */
  69. function muteLocalAudio (muted, userInteraction) {
  70. if (!localAudio) {
  71. return;
  72. }
  73. if (muted) {
  74. localAudio.mute().then(function(value) {},
  75. function(value) {
  76. console.warn('Audio Mute was rejected:', value);
  77. }
  78. );
  79. } else {
  80. localAudio.unmute().then(function(value) {},
  81. function(value) {
  82. console.warn('Audio unmute was rejected:', value);
  83. }
  84. );
  85. }
  86. }
  87. /**
  88. * Mute or unmute local video stream if it exists.
  89. * @param {boolean} muted if video stream should be muted or unmuted.
  90. */
  91. function muteLocalVideo (muted) {
  92. if (!localVideo) {
  93. return;
  94. }
  95. if (muted) {
  96. localVideo.mute().then(function(value) {},
  97. function(value) {
  98. console.warn('Video mute was rejected:', value);
  99. }
  100. );
  101. } else {
  102. localVideo.unmute().then(function(value) {},
  103. function(value) {
  104. console.warn('Video unmute was rejected:', value);
  105. }
  106. );
  107. }
  108. }
  109. /**
  110. * Disconnect from the conference and optionally request user feedback.
  111. * @param {boolean} [requestFeedback=false] if user feedback should be requested
  112. */
  113. function hangup (requestFeedback = false) {
  114. APP.conference._room.leave().then(() => {
  115. connection.disconnect();
  116. if (requestFeedback) {
  117. return APP.UI.requestFeedback();
  118. } else {
  119. return Promise.resolve();
  120. }
  121. }).then(function () {
  122. if (!config.enableWelcomePage) {
  123. return;
  124. }
  125. // redirect to welcome page
  126. setTimeout(() => {
  127. APP.settings.setWelcomePageEnabled(true);
  128. window.location.pathname = "/";
  129. }, 3000);
  130. }, function (err) {
  131. console.error('Failed to hangup the call:', err);
  132. });
  133. }
  134. /**
  135. * Create local tracks of specified types.
  136. * @param {string[]} devices required track types ('audio', 'video' etc.)
  137. * @returns {Promise<JitsiLocalTrack[]>}
  138. */
  139. function createLocalTracks (...devices) {
  140. return JitsiMeetJS.createLocalTracks({
  141. // copy array to avoid mutations inside library
  142. devices: devices.slice(0),
  143. resolution: config.resolution,
  144. cameraDeviceId: APP.settings.getCameraDeviceId(),
  145. micDeviceId: APP.settings.getMicDeviceId(),
  146. // adds any ff fake device settings if any
  147. firefox_fake_device: config.firefox_fake_device
  148. }).catch(function (err) {
  149. console.error('failed to create local tracks', ...devices, err);
  150. return Promise.reject(err);
  151. });
  152. }
  153. class ConferenceConnector {
  154. constructor(resolve, reject) {
  155. this._resolve = resolve;
  156. this._reject = reject;
  157. this.reconnectTimeout = null;
  158. room.on(ConferenceEvents.CONFERENCE_JOINED,
  159. this._handleConferenceJoined.bind(this));
  160. room.on(ConferenceEvents.CONFERENCE_FAILED,
  161. this._onConferenceFailed.bind(this));
  162. room.on(ConferenceEvents.CONFERENCE_ERROR,
  163. this._onConferenceError.bind(this));
  164. }
  165. _handleConferenceFailed(err, msg) {
  166. this._unsubscribe();
  167. this._reject(err);
  168. }
  169. _onConferenceFailed(err, ...params) {
  170. console.error('CONFERENCE FAILED:', err, ...params);
  171. switch (err) {
  172. // room is locked by the password
  173. case ConferenceErrors.PASSWORD_REQUIRED:
  174. APP.UI.markRoomLocked(true);
  175. roomLocker.requirePassword().then(function () {
  176. room.join(roomLocker.password);
  177. });
  178. break;
  179. case ConferenceErrors.CONNECTION_ERROR:
  180. {
  181. let [msg] = params;
  182. APP.UI.notifyConnectionFailed(msg);
  183. }
  184. break;
  185. case ConferenceErrors.VIDEOBRIDGE_NOT_AVAILABLE:
  186. APP.UI.notifyBridgeDown();
  187. break;
  188. // not enough rights to create conference
  189. case ConferenceErrors.AUTHENTICATION_REQUIRED:
  190. // schedule reconnect to check if someone else created the room
  191. this.reconnectTimeout = setTimeout(function () {
  192. room.join();
  193. }, 5000);
  194. // notify user that auth is required
  195. AuthHandler.requireAuth(room, roomLocker.password);
  196. break;
  197. case ConferenceErrors.RESERVATION_ERROR:
  198. {
  199. let [code, msg] = params;
  200. APP.UI.notifyReservationError(code, msg);
  201. }
  202. break;
  203. case ConferenceErrors.GRACEFUL_SHUTDOWN:
  204. APP.UI.notifyGracefulShutdown();
  205. break;
  206. case ConferenceErrors.JINGLE_FATAL_ERROR:
  207. APP.UI.notifyInternalError();
  208. break;
  209. case ConferenceErrors.CONFERENCE_DESTROYED:
  210. {
  211. let [reason] = params;
  212. APP.UI.hideStats();
  213. APP.UI.notifyConferenceDestroyed(reason);
  214. }
  215. break;
  216. case ConferenceErrors.FOCUS_DISCONNECTED:
  217. {
  218. let [focus, retrySec] = params;
  219. APP.UI.notifyFocusDisconnected(focus, retrySec);
  220. }
  221. break;
  222. case ConferenceErrors.FOCUS_LEFT:
  223. room.leave().then(() => connection.disconnect());
  224. APP.UI.notifyFocusLeft();
  225. break;
  226. case ConferenceErrors.CONFERENCE_MAX_USERS:
  227. connection.disconnect();
  228. APP.UI.notifyMaxUsersLimitReached();
  229. break;
  230. default:
  231. this._handleConferenceFailed(err, ...params);
  232. }
  233. }
  234. _onConferenceError(err, ...params) {
  235. console.error('CONFERENCE Error:', err, params);
  236. switch (err) {
  237. case ConferenceErrors.CHAT_ERROR:
  238. {
  239. let [code, msg] = params;
  240. APP.UI.showChatError(code, msg);
  241. }
  242. break;
  243. default:
  244. console.error("Unknown error.");
  245. }
  246. }
  247. _unsubscribe() {
  248. room.off(
  249. ConferenceEvents.CONFERENCE_JOINED, this._handleConferenceJoined);
  250. room.off(
  251. ConferenceEvents.CONFERENCE_FAILED, this._onConferenceFailed);
  252. if (this.reconnectTimeout !== null) {
  253. clearTimeout(this.reconnectTimeout);
  254. }
  255. AuthHandler.closeAuth();
  256. }
  257. _handleConferenceJoined() {
  258. this._unsubscribe();
  259. this._resolve();
  260. }
  261. connect() {
  262. room.join();
  263. }
  264. }
  265. export default {
  266. localId: undefined,
  267. isModerator: false,
  268. audioMuted: false,
  269. videoMuted: false,
  270. isSharingScreen: false,
  271. isDesktopSharingEnabled: false,
  272. /**
  273. * Open new connection and join to the conference.
  274. * @param {object} options
  275. * @param {string} roomName name of the conference
  276. * @returns {Promise}
  277. */
  278. init(options) {
  279. this.roomName = options.roomName;
  280. JitsiMeetJS.setLogLevel(JitsiMeetJS.logLevels.TRACE);
  281. // attaches global error handler, if there is already one, respect it
  282. if(JitsiMeetJS.getGlobalOnErrorHandler){
  283. var oldOnErrorHandler = window.onerror;
  284. window.onerror = function (message, source, lineno, colno, error) {
  285. JitsiMeetJS.getGlobalOnErrorHandler(
  286. message, source, lineno, colno, error);
  287. if(oldOnErrorHandler)
  288. oldOnErrorHandler(message, source, lineno, colno, error);
  289. };
  290. }
  291. return JitsiMeetJS.init(config).then(() => {
  292. return Promise.all([
  293. // try to retrieve audio and video
  294. createLocalTracks('audio', 'video')
  295. // if failed then try to retrieve only audio
  296. .catch(() => createLocalTracks('audio'))
  297. // if audio also failed then just return empty array
  298. .catch(() => []),
  299. connect(options.roomName)
  300. ]);
  301. }).then(([tracks, con]) => {
  302. console.log('initialized with %s local tracks', tracks.length);
  303. APP.connection = connection = con;
  304. this._createRoom(tracks);
  305. this.isDesktopSharingEnabled =
  306. JitsiMeetJS.isDesktopSharingEnabled();
  307. // update list of available devices
  308. if (JitsiMeetJS.isDeviceListAvailable() &&
  309. JitsiMeetJS.isDeviceChangeAvailable()) {
  310. JitsiMeetJS.enumerateDevices(
  311. devices => APP.UI.onAvailableDevicesChanged(devices)
  312. );
  313. }
  314. if (config.iAmRecorder)
  315. this.recorder = new Recorder();
  316. // XXX The API will take care of disconnecting from the XMPP server
  317. // (and, thus, leaving the room) on unload.
  318. return new Promise((resolve, reject) => {
  319. (new ConferenceConnector(resolve, reject)).connect();
  320. });
  321. });
  322. },
  323. /**
  324. * Check if id is id of the local user.
  325. * @param {string} id id to check
  326. * @returns {boolean}
  327. */
  328. isLocalId (id) {
  329. return this.localId === id;
  330. },
  331. /**
  332. * Simulates toolbar button click for audio mute. Used by shortcuts and API.
  333. * @param mute true for mute and false for unmute.
  334. */
  335. muteAudio (mute) {
  336. muteLocalAudio(mute);
  337. },
  338. /**
  339. * Returns whether local audio is muted or not.
  340. * @returns {boolean}
  341. */
  342. isLocalAudioMuted() {
  343. return this.audioMuted;
  344. },
  345. /**
  346. * Simulates toolbar button click for audio mute. Used by shortcuts and API.
  347. */
  348. toggleAudioMuted () {
  349. this.muteAudio(!this.audioMuted);
  350. },
  351. /**
  352. * Simulates toolbar button click for video mute. Used by shortcuts and API.
  353. * @param mute true for mute and false for unmute.
  354. */
  355. muteVideo (mute) {
  356. muteLocalVideo(mute);
  357. },
  358. /**
  359. * Simulates toolbar button click for video mute. Used by shortcuts and API.
  360. */
  361. toggleVideoMuted () {
  362. this.muteVideo(!this.videoMuted);
  363. },
  364. /**
  365. * Retrieve list of conference participants (without local user).
  366. * @returns {JitsiParticipant[]}
  367. */
  368. listMembers () {
  369. return room.getParticipants();
  370. },
  371. /**
  372. * Retrieve list of ids of conference participants (without local user).
  373. * @returns {string[]}
  374. */
  375. listMembersIds () {
  376. return room.getParticipants().map(p => p.getId());
  377. },
  378. /**
  379. * Checks whether the participant identified by id is a moderator.
  380. * @id id to search for participant
  381. * @return {boolean} whether the participant is moderator
  382. */
  383. isParticipantModerator (id) {
  384. let user = room.getParticipantById(id);
  385. return user && user.isModerator();
  386. },
  387. /**
  388. * Check if SIP is supported.
  389. * @returns {boolean}
  390. */
  391. sipGatewayEnabled () {
  392. return room.isSIPCallingSupported();
  393. },
  394. get membersCount () {
  395. return room.getParticipants().length + 1;
  396. },
  397. /**
  398. * Returns true if the callstats integration is enabled, otherwise returns
  399. * false.
  400. *
  401. * @returns true if the callstats integration is enabled, otherwise returns
  402. * false.
  403. */
  404. isCallstatsEnabled () {
  405. return room.isCallstatsEnabled();
  406. },
  407. /**
  408. * Sends the given feedback through CallStats if enabled.
  409. *
  410. * @param overallFeedback an integer between 1 and 5 indicating the
  411. * user feedback
  412. * @param detailedFeedback detailed feedback from the user. Not yet used
  413. */
  414. sendFeedback (overallFeedback, detailedFeedback) {
  415. return room.sendFeedback (overallFeedback, detailedFeedback);
  416. },
  417. // used by torture currently
  418. isJoined () {
  419. return this._room
  420. && this._room.isJoined();
  421. },
  422. getConnectionState () {
  423. return this._room
  424. && this._room.getConnectionState();
  425. },
  426. getMyUserId () {
  427. return this._room
  428. && this._room.myUserId();
  429. },
  430. /**
  431. * Indicates if recording is supported in this conference.
  432. */
  433. isRecordingSupported() {
  434. return this._room && this._room.isRecordingSupported();
  435. },
  436. /**
  437. * Returns the recording state or undefined if the room is not defined.
  438. */
  439. getRecordingState() {
  440. return (this._room) ? this._room.getRecordingState() : undefined;
  441. },
  442. /**
  443. * Will be filled with values only when config.debug is enabled.
  444. * Its used by torture to check audio levels.
  445. */
  446. audioLevelsMap: {},
  447. /**
  448. * Returns the stored audio level (stored only if config.debug is enabled)
  449. * @param id the id for the user audio level to return (the id value is
  450. * returned for the participant using getMyUserId() method)
  451. */
  452. getPeerSSRCAudioLevel (id) {
  453. return this.audioLevelsMap[id];
  454. },
  455. /**
  456. * @return {number} the number of participants in the conference with at
  457. * least one track.
  458. */
  459. getNumberOfParticipantsWithTracks() {
  460. return this._room.getParticipants()
  461. .filter((p) => p.getTracks().length > 0)
  462. .length;
  463. },
  464. /**
  465. * Returns the stats.
  466. */
  467. getStats() {
  468. return ConnectionQuality.getStats();
  469. },
  470. // end used by torture
  471. getLogs () {
  472. return room.getLogs();
  473. },
  474. /**
  475. * Exposes a Command(s) API on this instance. It is necessitated by (1) the
  476. * desire to keep room private to this instance and (2) the need of other
  477. * modules to send and receive commands to and from participants.
  478. * Eventually, this instance remains in control with respect to the
  479. * decision whether the Command(s) API of room (i.e. lib-jitsi-meet's
  480. * JitsiConference) is to be used in the implementation of the Command(s)
  481. * API of this instance.
  482. */
  483. commands: {
  484. /**
  485. * Known custom conference commands.
  486. */
  487. defaults: {
  488. CONNECTION_QUALITY: "stats",
  489. EMAIL: "email",
  490. ETHERPAD: "etherpad",
  491. SHARED_VIDEO: "shared-video",
  492. CUSTOM_ROLE: "custom-role"
  493. },
  494. /**
  495. * Receives notifications from other participants about commands aka
  496. * custom events (sent by sendCommand or sendCommandOnce methods).
  497. * @param command {String} the name of the command
  498. * @param handler {Function} handler for the command
  499. */
  500. addCommandListener () {
  501. room.addCommandListener.apply(room, arguments);
  502. },
  503. /**
  504. * Removes command.
  505. * @param name {String} the name of the command.
  506. */
  507. removeCommand () {
  508. room.removeCommand.apply(room, arguments);
  509. },
  510. /**
  511. * Sends command.
  512. * @param name {String} the name of the command.
  513. * @param values {Object} with keys and values that will be sent.
  514. */
  515. sendCommand () {
  516. room.sendCommand.apply(room, arguments);
  517. },
  518. /**
  519. * Sends command one time.
  520. * @param name {String} the name of the command.
  521. * @param values {Object} with keys and values that will be sent.
  522. */
  523. sendCommandOnce () {
  524. room.sendCommandOnce.apply(room, arguments);
  525. }
  526. },
  527. _createRoom (localTracks) {
  528. room = connection.initJitsiConference(APP.conference.roomName,
  529. this._getConferenceOptions());
  530. this.localId = room.myUserId();
  531. localTracks.forEach((track) => {
  532. if (track.isAudioTrack()) {
  533. this.useAudioStream(track);
  534. } else if (track.isVideoTrack()) {
  535. this.useVideoStream(track);
  536. } else {
  537. console.error(
  538. "Ignored not an audio nor a video track: ", track);
  539. }
  540. });
  541. roomLocker = createRoomLocker(room);
  542. this._room = room; // FIXME do not use this
  543. let email = APP.settings.getEmail();
  544. email && sendEmail(this.commands.defaults.EMAIL, email);
  545. let nick = APP.settings.getDisplayName();
  546. if (config.useNicks && !nick) {
  547. nick = APP.UI.askForNickname();
  548. APP.settings.setDisplayName(nick);
  549. }
  550. nick && room.setDisplayName(nick);
  551. this._setupListeners();
  552. },
  553. _getConferenceOptions() {
  554. let options = config;
  555. if(config.enableRecording && !config.recordingType) {
  556. options.recordingType = (config.hosts &&
  557. (typeof config.hosts.jirecon != "undefined"))?
  558. "jirecon" : "colibri";
  559. }
  560. return options;
  561. },
  562. /**
  563. * Start using provided video stream.
  564. * Stops previous video stream.
  565. * @param {JitsiLocalTrack} [stream] new stream to use or null
  566. * @returns {Promise}
  567. */
  568. useVideoStream (stream) {
  569. let promise = Promise.resolve();
  570. if (localVideo) {
  571. // this calls room.removeTrack internally
  572. // so we don't need to remove it manually
  573. promise = localVideo.dispose();
  574. }
  575. localVideo = stream;
  576. return promise.then(function () {
  577. if (stream) {
  578. return room.addTrack(stream);
  579. }
  580. }).then(() => {
  581. if (stream) {
  582. this.videoMuted = stream.isMuted();
  583. this.isSharingScreen = stream.videoType === 'desktop';
  584. APP.UI.addLocalStream(stream);
  585. } else {
  586. this.videoMuted = false;
  587. this.isSharingScreen = false;
  588. }
  589. APP.UI.setVideoMuted(this.localId, this.videoMuted);
  590. APP.UI.updateDesktopSharingButtons();
  591. });
  592. },
  593. /**
  594. * Start using provided audio stream.
  595. * Stops previous audio stream.
  596. * @param {JitsiLocalTrack} [stream] new stream to use or null
  597. * @returns {Promise}
  598. */
  599. useAudioStream (stream) {
  600. let promise = Promise.resolve();
  601. if (localAudio) {
  602. // this calls room.removeTrack internally
  603. // so we don't need to remove it manually
  604. promise = localAudio.dispose();
  605. }
  606. localAudio = stream;
  607. return promise.then(function () {
  608. if (stream) {
  609. return room.addTrack(stream);
  610. }
  611. }).then(() => {
  612. if (stream) {
  613. this.audioMuted = stream.isMuted();
  614. APP.UI.addLocalStream(stream);
  615. } else {
  616. this.audioMuted = false;
  617. }
  618. APP.UI.setAudioMuted(this.localId, this.audioMuted);
  619. });
  620. },
  621. videoSwitchInProgress: false,
  622. toggleScreenSharing (shareScreen = !this.isSharingScreen) {
  623. if (this.videoSwitchInProgress) {
  624. console.warn("Switch in progress.");
  625. return;
  626. }
  627. if (!this.isDesktopSharingEnabled) {
  628. console.warn("Cannot toggle screen sharing: not supported.");
  629. return;
  630. }
  631. this.videoSwitchInProgress = true;
  632. if (shareScreen) {
  633. createLocalTracks('desktop').then(([stream]) => {
  634. stream.on(
  635. TrackEvents.LOCAL_TRACK_STOPPED,
  636. () => {
  637. // if stream was stopped during screensharing session
  638. // then we should switch to video
  639. // otherwise we stopped it because we already switched
  640. // to video, so nothing to do here
  641. if (this.isSharingScreen) {
  642. this.toggleScreenSharing(false);
  643. }
  644. }
  645. );
  646. return this.useVideoStream(stream);
  647. }).then(() => {
  648. this.videoSwitchInProgress = false;
  649. console.log('sharing local desktop');
  650. }).catch((err) => {
  651. this.videoSwitchInProgress = false;
  652. this.toggleScreenSharing(false);
  653. if(err === TrackErrors.CHROME_EXTENSION_USER_CANCELED)
  654. return;
  655. console.error('failed to share local desktop', err);
  656. if (err === TrackErrors.FIREFOX_EXTENSION_NEEDED) {
  657. APP.UI.showExtensionRequiredDialog(
  658. config.desktopSharingFirefoxExtensionURL
  659. );
  660. return;
  661. }
  662. // Handling:
  663. // TrackErrors.CHROME_EXTENSION_INSTALLATION_ERROR
  664. // TrackErrors.GENERAL
  665. // and any other
  666. let dialogTxt = APP.translation
  667. .generateTranslationHTML("dialog.failtoinstall");
  668. let dialogTitle = APP.translation
  669. .generateTranslationHTML("dialog.error");
  670. APP.UI.messageHandler.openDialog(
  671. dialogTitle,
  672. dialogTxt,
  673. false
  674. );
  675. });
  676. } else {
  677. createLocalTracks('video').then(
  678. ([stream]) => this.useVideoStream(stream)
  679. ).then(() => {
  680. this.videoSwitchInProgress = false;
  681. console.log('sharing local video');
  682. }).catch((err) => {
  683. this.useVideoStream(null);
  684. this.videoSwitchInProgress = false;
  685. console.error('failed to share local video', err);
  686. });
  687. }
  688. },
  689. /**
  690. * Setup interaction between conference and UI.
  691. */
  692. _setupListeners () {
  693. // add local streams when joined to the conference
  694. room.on(ConferenceEvents.CONFERENCE_JOINED, () => {
  695. APP.UI.mucJoined();
  696. });
  697. room.on(
  698. ConferenceEvents.AUTH_STATUS_CHANGED,
  699. function (authEnabled, authLogin) {
  700. APP.UI.updateAuthInfo(authEnabled, authLogin);
  701. }
  702. );
  703. room.on(ConferenceEvents.USER_JOINED, (id, user) => {
  704. if (user.isHidden())
  705. return;
  706. console.log('USER %s connnected', id, user);
  707. APP.API.notifyUserJoined(id);
  708. APP.UI.addUser(id, user.getDisplayName());
  709. // check the roles for the new user and reflect them
  710. APP.UI.updateUserRole(user);
  711. });
  712. room.on(ConferenceEvents.USER_LEFT, (id, user) => {
  713. console.log('USER %s LEFT', id, user);
  714. APP.API.notifyUserLeft(id);
  715. APP.UI.removeUser(id, user.getDisplayName());
  716. APP.UI.onSharedVideoStop(id);
  717. });
  718. room.on(ConferenceEvents.USER_ROLE_CHANGED, (id, role) => {
  719. if (this.isLocalId(id)) {
  720. console.info(`My role changed, new role: ${role}`);
  721. this.isModerator = room.isModerator();
  722. APP.UI.updateLocalRole(room.isModerator());
  723. } else {
  724. let user = room.getParticipantById(id);
  725. if (user) {
  726. APP.UI.updateUserRole(user);
  727. }
  728. }
  729. });
  730. room.on(ConferenceEvents.TRACK_ADDED, (track) => {
  731. if(!track || track.isLocal())
  732. return;
  733. track.on(TrackEvents.TRACK_VIDEOTYPE_CHANGED, (type) => {
  734. APP.UI.onPeerVideoTypeChanged(track.getParticipantId(), type);
  735. });
  736. APP.UI.addRemoteStream(track);
  737. });
  738. room.on(ConferenceEvents.TRACK_REMOVED, (track) => {
  739. if(!track || track.isLocal())
  740. return;
  741. APP.UI.removeRemoteStream(track);
  742. });
  743. room.on(ConferenceEvents.TRACK_MUTE_CHANGED, (track) => {
  744. if(!track)
  745. return;
  746. const handler = (track.getType() === "audio")?
  747. APP.UI.setAudioMuted : APP.UI.setVideoMuted;
  748. let id;
  749. const mute = track.isMuted();
  750. if(track.isLocal()){
  751. id = this.localId;
  752. if(track.getType() === "audio") {
  753. this.audioMuted = mute;
  754. } else {
  755. this.videoMuted = mute;
  756. }
  757. } else {
  758. id = track.getParticipantId();
  759. }
  760. handler(id , mute);
  761. });
  762. room.on(ConferenceEvents.TRACK_AUDIO_LEVEL_CHANGED, (id, lvl) => {
  763. if(this.isLocalId(id) && localAudio && localAudio.isMuted()) {
  764. lvl = 0;
  765. }
  766. if(config.debug)
  767. {
  768. this.audioLevelsMap[id] = lvl;
  769. console.log("AudioLevel:" + id + "/" + lvl);
  770. }
  771. APP.UI.setAudioLevel(id, lvl);
  772. });
  773. room.on(ConferenceEvents.IN_LAST_N_CHANGED, (inLastN) => {
  774. //FIXME
  775. if (config.muteLocalVideoIfNotInLastN) {
  776. // TODO mute or unmute if required
  777. // mark video on UI
  778. // APP.UI.markVideoMuted(true/false);
  779. }
  780. });
  781. room.on(
  782. ConferenceEvents.LAST_N_ENDPOINTS_CHANGED, (ids, enteringIds) => {
  783. APP.UI.handleLastNEndpoints(ids, enteringIds);
  784. });
  785. room.on(ConferenceEvents.DOMINANT_SPEAKER_CHANGED, (id) => {
  786. APP.UI.markDominantSpeaker(id);
  787. });
  788. if (!interfaceConfig.filmStripOnly) {
  789. room.on(ConferenceEvents.CONNECTION_INTERRUPTED, () => {
  790. APP.UI.markVideoInterrupted(true);
  791. });
  792. room.on(ConferenceEvents.CONNECTION_RESTORED, () => {
  793. APP.UI.markVideoInterrupted(false);
  794. });
  795. room.on(ConferenceEvents.MESSAGE_RECEIVED, (id, text, ts) => {
  796. let nick = getDisplayName(id);
  797. APP.API.notifyReceivedChatMessage(id, nick, text, ts);
  798. APP.UI.addMessage(id, nick, text, ts);
  799. });
  800. }
  801. room.on(ConferenceEvents.DISPLAY_NAME_CHANGED, (id, displayName) => {
  802. APP.API.notifyDisplayNameChanged(id, displayName);
  803. APP.UI.changeDisplayName(id, displayName);
  804. });
  805. room.on(ConferenceEvents.RECORDER_STATE_CHANGED, (status, error) => {
  806. console.log("Received recorder status change: ", status, error);
  807. APP.UI.updateRecordingState(status);
  808. });
  809. room.on(ConferenceEvents.USER_STATUS_CHANGED, function (id, status) {
  810. APP.UI.updateUserStatus(id, status);
  811. });
  812. room.on(ConferenceEvents.KICKED, () => {
  813. APP.UI.hideStats();
  814. APP.UI.notifyKicked();
  815. // FIXME close
  816. });
  817. room.on(ConferenceEvents.DTMF_SUPPORT_CHANGED, (isDTMFSupported) => {
  818. APP.UI.updateDTMFSupport(isDTMFSupported);
  819. });
  820. APP.UI.addListener(UIEvents.ROOM_LOCK_CLICKED, () => {
  821. if (room.isModerator()) {
  822. let promise = roomLocker.isLocked
  823. ? roomLocker.askToUnlock()
  824. : roomLocker.askToLock();
  825. promise.then(() => {
  826. APP.UI.markRoomLocked(roomLocker.isLocked);
  827. });
  828. } else {
  829. roomLocker.notifyModeratorRequired();
  830. }
  831. });
  832. APP.UI.addListener(UIEvents.AUDIO_MUTED, muteLocalAudio);
  833. APP.UI.addListener(UIEvents.VIDEO_MUTED, muteLocalVideo);
  834. if (!interfaceConfig.filmStripOnly) {
  835. APP.UI.addListener(UIEvents.MESSAGE_CREATED, (message) => {
  836. APP.API.notifySendingChatMessage(message);
  837. room.sendTextMessage(message);
  838. });
  839. }
  840. room.on(ConferenceEvents.CONNECTION_STATS, function (stats) {
  841. ConnectionQuality.updateLocalStats(stats);
  842. });
  843. ConnectionQuality.addListener(
  844. CQEvents.LOCALSTATS_UPDATED,
  845. (percent, stats) => {
  846. APP.UI.updateLocalStats(percent, stats);
  847. // send local stats to other users
  848. room.sendCommandOnce(this.commands.defaults.CONNECTION_QUALITY,
  849. {
  850. children: ConnectionQuality.convertToMUCStats(stats),
  851. attributes: {
  852. xmlns: 'http://jitsi.org/jitmeet/stats'
  853. }
  854. });
  855. }
  856. );
  857. // listen to remote stats
  858. room.addCommandListener(this.commands.defaults.CONNECTION_QUALITY,
  859. (values, from) => {
  860. ConnectionQuality.updateRemoteStats(from, values);
  861. });
  862. ConnectionQuality.addListener(CQEvents.REMOTESTATS_UPDATED,
  863. (id, percent, stats) => {
  864. APP.UI.updateRemoteStats(id, percent, stats);
  865. });
  866. room.addCommandListener(this.commands.defaults.ETHERPAD, ({value}) => {
  867. APP.UI.initEtherpad(value);
  868. });
  869. APP.UI.addListener(UIEvents.EMAIL_CHANGED, (email = '') => {
  870. email = email.trim();
  871. if (email === APP.settings.getEmail()) {
  872. return;
  873. }
  874. APP.settings.setEmail(email);
  875. APP.UI.setUserAvatar(room.myUserId(), email);
  876. sendEmail(this.commands.defaults.EMAIL, email);
  877. });
  878. room.addCommandListener(this.commands.defaults.EMAIL, (data) => {
  879. APP.UI.setUserAvatar(data.attributes.id, data.value);
  880. });
  881. APP.UI.addListener(UIEvents.NICKNAME_CHANGED, (nickname = '') => {
  882. nickname = nickname.trim();
  883. if (nickname === APP.settings.getDisplayName()) {
  884. return;
  885. }
  886. APP.settings.setDisplayName(nickname);
  887. room.setDisplayName(nickname);
  888. APP.UI.changeDisplayName(APP.conference.localId, nickname);
  889. });
  890. APP.UI.addListener(UIEvents.START_MUTED_CHANGED,
  891. (startAudioMuted, startVideoMuted) => {
  892. room.setStartMutedPolicy({
  893. audio: startAudioMuted,
  894. video: startVideoMuted
  895. });
  896. }
  897. );
  898. room.on(
  899. ConferenceEvents.START_MUTED_POLICY_CHANGED,
  900. ({ audio, video }) => {
  901. APP.UI.onStartMutedChanged(audio, video);
  902. }
  903. );
  904. room.on(ConferenceEvents.STARTED_MUTED, () => {
  905. (room.isStartAudioMuted() || room.isStartVideoMuted())
  906. && APP.UI.notifyInitiallyMuted();
  907. });
  908. APP.UI.addListener(UIEvents.USER_INVITED, (roomUrl) => {
  909. APP.UI.inviteParticipants(
  910. roomUrl,
  911. APP.conference.roomName,
  912. roomLocker.password,
  913. APP.settings.getDisplayName()
  914. );
  915. });
  916. room.on(
  917. ConferenceEvents.AVAILABLE_DEVICES_CHANGED, function (id, devices) {
  918. APP.UI.updateDevicesAvailability(id, devices);
  919. }
  920. );
  921. // call hangup
  922. APP.UI.addListener(UIEvents.HANGUP, () => {
  923. hangup(true);
  924. });
  925. // logout
  926. APP.UI.addListener(UIEvents.LOGOUT, () => {
  927. AuthHandler.logout(room).then(function (url) {
  928. if (url) {
  929. window.location.href = url;
  930. } else {
  931. hangup(true);
  932. }
  933. });
  934. });
  935. APP.UI.addListener(UIEvents.SIP_DIAL, (sipNumber) => {
  936. room.dial(sipNumber);
  937. });
  938. // Starts or stops the recording for the conference.
  939. APP.UI.addListener(UIEvents.RECORDING_TOGGLED, (options) => {
  940. room.toggleRecording(options);
  941. });
  942. APP.UI.addListener(UIEvents.SUBJECT_CHANGED, (topic) => {
  943. room.setSubject(topic);
  944. });
  945. room.on(ConferenceEvents.SUBJECT_CHANGED, function (subject) {
  946. APP.UI.setSubject(subject);
  947. });
  948. APP.UI.addListener(UIEvents.USER_KICKED, (id) => {
  949. room.kickParticipant(id);
  950. });
  951. APP.UI.addListener(UIEvents.REMOTE_AUDIO_MUTED, (id) => {
  952. room.muteParticipant(id);
  953. });
  954. APP.UI.addListener(UIEvents.AUTH_CLICKED, () => {
  955. AuthHandler.authenticate(room);
  956. });
  957. APP.UI.addListener(UIEvents.SELECTED_ENDPOINT, (id) => {
  958. room.selectParticipant(id);
  959. });
  960. APP.UI.addListener(UIEvents.PINNED_ENDPOINT, (smallVideo, isPinned) => {
  961. var smallVideoId = smallVideo.getId();
  962. if (smallVideo.getVideoType() === VIDEO_CONTAINER_TYPE
  963. && !APP.conference.isLocalId(smallVideoId))
  964. if (isPinned)
  965. room.pinParticipant(smallVideoId);
  966. // When the library starts supporting multiple pins we would
  967. // pass the isPinned parameter together with the identifier,
  968. // but currently we send null to indicate that we unpin the
  969. // last pinned.
  970. else
  971. room.pinParticipant(null);
  972. });
  973. APP.UI.addListener(
  974. UIEvents.VIDEO_DEVICE_CHANGED,
  975. (cameraDeviceId) => {
  976. APP.settings.setCameraDeviceId(cameraDeviceId);
  977. createLocalTracks('video').then(([stream]) => {
  978. this.useVideoStream(stream);
  979. console.log('switched local video device');
  980. });
  981. }
  982. );
  983. APP.UI.addListener(
  984. UIEvents.AUDIO_DEVICE_CHANGED,
  985. (micDeviceId) => {
  986. APP.settings.setMicDeviceId(micDeviceId);
  987. createLocalTracks('audio').then(([stream]) => {
  988. this.useAudioStream(stream);
  989. console.log('switched local audio device');
  990. });
  991. }
  992. );
  993. APP.UI.addListener(
  994. UIEvents.TOGGLE_SCREENSHARING, this.toggleScreenSharing.bind(this)
  995. );
  996. APP.UI.addListener(UIEvents.UPDATE_SHARED_VIDEO,
  997. (url, state, time, isMuted, volume) => {
  998. // send start and stop commands once, and remove any updates
  999. // that had left
  1000. if (state === 'stop' || state === 'start' || state === 'playing') {
  1001. room.removeCommand(this.commands.defaults.SHARED_VIDEO);
  1002. room.sendCommandOnce(this.commands.defaults.SHARED_VIDEO, {
  1003. value: url,
  1004. attributes: {
  1005. state: state,
  1006. time: time,
  1007. muted: isMuted,
  1008. volume: volume
  1009. }
  1010. });
  1011. }
  1012. else {
  1013. // in case of paused, in order to allow late users to join
  1014. // paused
  1015. room.removeCommand(this.commands.defaults.SHARED_VIDEO);
  1016. room.sendCommand(this.commands.defaults.SHARED_VIDEO, {
  1017. value: url,
  1018. attributes: {
  1019. state: state,
  1020. time: time,
  1021. muted: isMuted,
  1022. volume: volume
  1023. }
  1024. });
  1025. }
  1026. });
  1027. room.addCommandListener(
  1028. this.commands.defaults.SHARED_VIDEO, ({value, attributes}, id) => {
  1029. if (attributes.state === 'stop') {
  1030. APP.UI.onSharedVideoStop(id, attributes);
  1031. }
  1032. else if (attributes.state === 'start') {
  1033. APP.UI.onSharedVideoStart(id, value, attributes);
  1034. }
  1035. else if (attributes.state === 'playing'
  1036. || attributes.state === 'pause') {
  1037. APP.UI.onSharedVideoUpdate(id, value, attributes);
  1038. }
  1039. });
  1040. },
  1041. /**
  1042. * Adss any room listener.
  1043. * @param eventName one of the ConferenceEvents
  1044. * @param callBack the function to be called when the event occurs
  1045. */
  1046. addConferenceListener(eventName, callBack) {
  1047. room.on(eventName, callBack);
  1048. }
  1049. };