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.

actions.js 20KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703
  1. // @flow
  2. import UIEvents from '../../../../service/UI/UIEvents';
  3. import {
  4. createStartMutedConfigurationEvent,
  5. sendAnalytics
  6. } from '../../analytics';
  7. import { getName } from '../../app';
  8. import { JitsiConferenceEvents } from '../lib-jitsi-meet';
  9. import { setAudioMuted, setVideoMuted } from '../media';
  10. import {
  11. MAX_DISPLAY_NAME_LENGTH,
  12. dominantSpeakerChanged,
  13. participantConnectionStatusChanged,
  14. participantJoined,
  15. participantLeft,
  16. participantPresenceChanged,
  17. participantRoleChanged,
  18. participantUpdated
  19. } from '../participants';
  20. import { getLocalTracks, trackAdded, trackRemoved } from '../tracks';
  21. import { getJitsiMeetGlobalNS } from '../util';
  22. import {
  23. AUTH_STATUS_CHANGED,
  24. CONFERENCE_FAILED,
  25. CONFERENCE_JOINED,
  26. CONFERENCE_LEFT,
  27. CONFERENCE_WILL_JOIN,
  28. CONFERENCE_WILL_LEAVE,
  29. DATA_CHANNEL_OPENED,
  30. KICKED_OUT,
  31. LOCK_STATE_CHANGED,
  32. P2P_STATUS_CHANGED,
  33. SET_AUDIO_ONLY,
  34. SET_DESKTOP_SHARING_ENABLED,
  35. SET_FOLLOW_ME,
  36. SET_LASTN,
  37. SET_PASSWORD,
  38. SET_PASSWORD_FAILED,
  39. SET_RECEIVE_VIDEO_QUALITY,
  40. SET_ROOM,
  41. SET_START_MUTED_POLICY
  42. } from './actionTypes';
  43. import {
  44. AVATAR_ID_COMMAND,
  45. AVATAR_URL_COMMAND,
  46. EMAIL_COMMAND,
  47. JITSI_CONFERENCE_URL_KEY
  48. } from './constants';
  49. import {
  50. _addLocalTracksToConference,
  51. getCurrentConference,
  52. sendLocalParticipant
  53. } from './functions';
  54. import type { Dispatch } from 'redux';
  55. const logger = require('jitsi-meet-logger').getLogger(__filename);
  56. declare var APP: Object;
  57. /**
  58. * Adds conference (event) listeners.
  59. *
  60. * @param {JitsiConference} conference - The JitsiConference instance.
  61. * @param {Dispatch} dispatch - The Redux dispatch function.
  62. * @private
  63. * @returns {void}
  64. */
  65. function _addConferenceListeners(conference, dispatch) {
  66. // Dispatches into features/base/conference follow:
  67. conference.on(
  68. JitsiConferenceEvents.CONFERENCE_FAILED,
  69. (...args) => dispatch(conferenceFailed(conference, ...args)));
  70. conference.on(
  71. JitsiConferenceEvents.CONFERENCE_JOINED,
  72. (...args) => dispatch(conferenceJoined(conference, ...args)));
  73. conference.on(
  74. JitsiConferenceEvents.CONFERENCE_LEFT,
  75. (...args) => dispatch(conferenceLeft(conference, ...args)));
  76. conference.on(
  77. JitsiConferenceEvents.KICKED,
  78. () => dispatch(kickedOut(conference)));
  79. conference.on(
  80. JitsiConferenceEvents.LOCK_STATE_CHANGED,
  81. (...args) => dispatch(lockStateChanged(conference, ...args)));
  82. // Dispatches into features/base/media follow:
  83. conference.on(
  84. JitsiConferenceEvents.STARTED_MUTED,
  85. () => {
  86. const audioMuted = Boolean(conference.startAudioMuted);
  87. const videoMuted = Boolean(conference.startVideoMuted);
  88. sendAnalytics(createStartMutedConfigurationEvent(
  89. 'remote', audioMuted, videoMuted));
  90. logger.log(`Start muted: ${audioMuted ? 'audio, ' : ''}${
  91. videoMuted ? 'video' : ''}`);
  92. // XXX Jicofo tells lib-jitsi-meet to start with audio and/or video
  93. // muted i.e. Jicofo expresses an intent. Lib-jitsi-meet has turned
  94. // Jicofo's intent into reality by actually muting the respective
  95. // tracks. The reality is expressed in base/tracks already so what
  96. // is left is to express Jicofo's intent in base/media.
  97. // TODO Maybe the app needs to learn about Jicofo's intent and
  98. // transfer that intent to lib-jitsi-meet instead of lib-jitsi-meet
  99. // acting on Jicofo's intent without the app's knowledge.
  100. dispatch(setAudioMuted(audioMuted));
  101. dispatch(setVideoMuted(videoMuted));
  102. });
  103. // Dispatches into features/base/tracks follow:
  104. conference.on(
  105. JitsiConferenceEvents.TRACK_ADDED,
  106. t => t && !t.isLocal() && dispatch(trackAdded(t)));
  107. conference.on(
  108. JitsiConferenceEvents.TRACK_REMOVED,
  109. t => t && !t.isLocal() && dispatch(trackRemoved(t)));
  110. // Dispatches into features/base/participants follow:
  111. conference.on(
  112. JitsiConferenceEvents.DISPLAY_NAME_CHANGED,
  113. (id, displayName) => dispatch(participantUpdated({
  114. conference,
  115. id,
  116. name: displayName.substr(0, MAX_DISPLAY_NAME_LENGTH)
  117. })));
  118. conference.on(
  119. JitsiConferenceEvents.DOMINANT_SPEAKER_CHANGED,
  120. id => dispatch(dominantSpeakerChanged(id, conference)));
  121. conference.on(
  122. JitsiConferenceEvents.PARTICIPANT_CONN_STATUS_CHANGED,
  123. (...args) => dispatch(participantConnectionStatusChanged(...args)));
  124. conference.on(
  125. JitsiConferenceEvents.USER_JOINED,
  126. (id, user) => !user.isHidden() && dispatch(participantJoined({
  127. conference,
  128. id,
  129. name: user.getDisplayName(),
  130. presence: user.getStatus(),
  131. role: user.getRole()
  132. })));
  133. conference.on(
  134. JitsiConferenceEvents.USER_LEFT,
  135. (id, user) => !user.isHidden()
  136. && dispatch(participantLeft(id, conference)));
  137. conference.on(
  138. JitsiConferenceEvents.USER_ROLE_CHANGED,
  139. (...args) => dispatch(participantRoleChanged(...args)));
  140. conference.on(
  141. JitsiConferenceEvents.USER_STATUS_CHANGED,
  142. (...args) => dispatch(participantPresenceChanged(...args)));
  143. conference.addCommandListener(
  144. AVATAR_ID_COMMAND,
  145. (data, id) => dispatch(participantUpdated({
  146. conference,
  147. id,
  148. avatarID: data.value
  149. })));
  150. conference.addCommandListener(
  151. AVATAR_URL_COMMAND,
  152. (data, id) => dispatch(participantUpdated({
  153. conference,
  154. id,
  155. avatarURL: data.value
  156. })));
  157. conference.addCommandListener(
  158. EMAIL_COMMAND,
  159. (data, id) => dispatch(participantUpdated({
  160. conference,
  161. id,
  162. email: data.value
  163. })));
  164. }
  165. /**
  166. * Updates the current known state of server-side authentication.
  167. *
  168. * @param {boolean} authEnabled - Whether or not server authentication is
  169. * enabled.
  170. * @param {string} authLogin - The current name of the logged in user, if any.
  171. * @returns {{
  172. * type: AUTH_STATUS_CHANGED,
  173. * authEnabled: boolean,
  174. * authLogin: string
  175. * }}
  176. */
  177. export function authStatusChanged(authEnabled: boolean, authLogin: string) {
  178. return {
  179. type: AUTH_STATUS_CHANGED,
  180. authEnabled,
  181. authLogin
  182. };
  183. }
  184. /**
  185. * Signals that a specific conference has failed.
  186. *
  187. * @param {JitsiConference} conference - The JitsiConference that has failed.
  188. * @param {string} error - The error describing/detailing the cause of the
  189. * failure.
  190. * @returns {{
  191. * type: CONFERENCE_FAILED,
  192. * conference: JitsiConference,
  193. * error: Error
  194. * }}
  195. * @public
  196. */
  197. export function conferenceFailed(conference: Object, error: string) {
  198. return {
  199. type: CONFERENCE_FAILED,
  200. conference,
  201. // Make the error resemble an Error instance (to the extent that
  202. // jitsi-meet needs it).
  203. error: {
  204. name: error,
  205. recoverable: undefined
  206. }
  207. };
  208. }
  209. /**
  210. * Signals that a specific conference has been joined.
  211. *
  212. * @param {JitsiConference} conference - The JitsiConference instance which was
  213. * joined by the local participant.
  214. * @returns {{
  215. * type: CONFERENCE_JOINED,
  216. * conference: JitsiConference
  217. * }}
  218. */
  219. export function conferenceJoined(conference: Object) {
  220. return {
  221. type: CONFERENCE_JOINED,
  222. conference
  223. };
  224. }
  225. /**
  226. * Signals that a specific conference has been left.
  227. *
  228. * @param {JitsiConference} conference - The JitsiConference instance which was
  229. * left by the local participant.
  230. * @returns {{
  231. * type: CONFERENCE_LEFT,
  232. * conference: JitsiConference
  233. * }}
  234. */
  235. export function conferenceLeft(conference: Object) {
  236. return {
  237. type: CONFERENCE_LEFT,
  238. conference
  239. };
  240. }
  241. /**
  242. * Adds any existing local tracks to a specific conference before the conference
  243. * is joined. Then signals the intention of the application to have the local
  244. * participant join the specified conference.
  245. *
  246. * @param {JitsiConference} conference - The {@code JitsiConference} instance
  247. * the local participant will (try to) join.
  248. * @returns {Function}
  249. */
  250. function _conferenceWillJoin(conference: Object) {
  251. return (dispatch: Dispatch<*>, getState: Function) => {
  252. const localTracks
  253. = getLocalTracks(getState()['features/base/tracks'])
  254. .map(t => t.jitsiTrack);
  255. if (localTracks.length) {
  256. _addLocalTracksToConference(conference, localTracks);
  257. }
  258. dispatch(conferenceWillJoin(conference));
  259. };
  260. }
  261. /**
  262. * Signals the intention of the application to have the local participant
  263. * join the specified conference.
  264. *
  265. * @param {JitsiConference} conference - The {@code JitsiConference} instance
  266. * the local participant will (try to) join.
  267. * @returns {{
  268. * type: CONFERENCE_WILL_JOIN,
  269. * conference: JitsiConference
  270. * }}
  271. */
  272. export function conferenceWillJoin(conference: Object) {
  273. return {
  274. type: CONFERENCE_WILL_JOIN,
  275. conference
  276. };
  277. }
  278. /**
  279. * Signals the intention of the application to have the local participant leave
  280. * a specific conference. Similar in fashion to CONFERENCE_LEFT. Contrary to it
  281. * though, it's not guaranteed because CONFERENCE_LEFT may be triggered by
  282. * lib-jitsi-meet and not the application.
  283. *
  284. * @param {JitsiConference} conference - The JitsiConference instance which will
  285. * be left by the local participant.
  286. * @returns {{
  287. * type: CONFERENCE_LEFT,
  288. * conference: JitsiConference
  289. * }}
  290. */
  291. export function conferenceWillLeave(conference: Object) {
  292. return {
  293. type: CONFERENCE_WILL_LEAVE,
  294. conference
  295. };
  296. }
  297. /**
  298. * Initializes a new conference.
  299. *
  300. * @returns {Function}
  301. */
  302. export function createConference() {
  303. return (dispatch: Function, getState: Function) => {
  304. const state = getState();
  305. const { connection, locationURL } = state['features/base/connection'];
  306. if (!connection) {
  307. throw new Error('Cannot create a conference without a connection!');
  308. }
  309. const { password, room } = state['features/base/conference'];
  310. if (!room) {
  311. throw new Error('Cannot join a conference without a room name!');
  312. }
  313. const conference
  314. = connection.initJitsiConference(
  315. // XXX Lib-jitsi-meet does not accept uppercase letters.
  316. room.toLowerCase(), {
  317. ...state['features/base/config'],
  318. applicationName: getName(),
  319. getWiFiStatsMethod: getJitsiMeetGlobalNS().getWiFiStats
  320. });
  321. conference[JITSI_CONFERENCE_URL_KEY] = locationURL;
  322. dispatch(_conferenceWillJoin(conference));
  323. _addConferenceListeners(conference, dispatch);
  324. sendLocalParticipant(state, conference);
  325. conference.join(password);
  326. };
  327. }
  328. /**
  329. * Will try to join the conference again in case it failed earlier with
  330. * {@link JitsiConferenceErrors.AUTHENTICATION_REQUIRED}. It means that Jicofo
  331. * did not allow to create new room from anonymous domain, but it can be tried
  332. * again later in case authenticated user created it in the meantime.
  333. *
  334. * @returns {Function}
  335. */
  336. export function checkIfCanJoin() {
  337. return (dispatch: Dispatch<*>, getState: Function) => {
  338. const { authRequired, password }
  339. = getState()['features/base/conference'];
  340. authRequired && authRequired.join(password);
  341. };
  342. }
  343. /**
  344. * Signals the data channel with the bridge has successfully opened.
  345. *
  346. * @returns {{
  347. * type: DATA_CHANNEL_OPENED
  348. * }}
  349. */
  350. export function dataChannelOpened() {
  351. return {
  352. type: DATA_CHANNEL_OPENED
  353. };
  354. }
  355. /**
  356. * Signals that we've been kicked out of the conference.
  357. *
  358. * @param {JitsiConference} conference - The {@link JitsiConference} instance
  359. * for which the event is being signaled.
  360. * @returns {{
  361. * type: KICKED_OUT,
  362. * conference: JitsiConference
  363. * }}
  364. */
  365. export function kickedOut(conference: Object) {
  366. return {
  367. type: KICKED_OUT,
  368. conference
  369. };
  370. }
  371. /**
  372. * Signals that the lock state of a specific JitsiConference changed.
  373. *
  374. * @param {JitsiConference} conference - The JitsiConference which had its lock
  375. * state changed.
  376. * @param {boolean} locked - If the specified conference became locked, true;
  377. * otherwise, false.
  378. * @returns {{
  379. * type: LOCK_STATE_CHANGED,
  380. * conference: JitsiConference,
  381. * locked: boolean
  382. * }}
  383. */
  384. export function lockStateChanged(conference: Object, locked: boolean) {
  385. return {
  386. type: LOCK_STATE_CHANGED,
  387. conference,
  388. locked
  389. };
  390. }
  391. /**
  392. * Updates the known state of start muted policies.
  393. *
  394. * @param {boolean} audioMuted - Whether or not members will join the conference
  395. * as audio muted.
  396. * @param {boolean} videoMuted - Whether or not members will join the conference
  397. * as video muted.
  398. * @returns {{
  399. * type: SET_START_MUTED_POLICY,
  400. * startAudioMutedPolicy: boolean,
  401. * startVideoMutedPolicy: boolean
  402. * }}
  403. */
  404. export function onStartMutedPolicyChanged(
  405. audioMuted: boolean, videoMuted: boolean) {
  406. return {
  407. type: SET_START_MUTED_POLICY,
  408. startAudioMutedPolicy: audioMuted,
  409. startVideoMutedPolicy: videoMuted
  410. };
  411. }
  412. /**
  413. * Sets whether or not peer2peer is currently enabled.
  414. *
  415. * @param {boolean} p2p - Whether or not peer2peer is currently active.
  416. * @returns {{
  417. * type: P2P_STATUS_CHANGED,
  418. * p2p: boolean
  419. * }}
  420. */
  421. export function p2pStatusChanged(p2p: boolean) {
  422. return {
  423. type: P2P_STATUS_CHANGED,
  424. p2p
  425. };
  426. }
  427. /**
  428. * Sets the audio-only flag for the current JitsiConference.
  429. *
  430. * @param {boolean} audioOnly - True if the conference should be audio only;
  431. * false, otherwise.
  432. * @param {boolean} ensureVideoTrack - Define if conference should ensure
  433. * to create a video track.
  434. * @returns {{
  435. * type: SET_AUDIO_ONLY,
  436. * audioOnly: boolean,
  437. * ensureVideoTrack: boolean
  438. * }}
  439. */
  440. export function setAudioOnly(
  441. audioOnly: boolean,
  442. ensureVideoTrack: boolean = false) {
  443. return {
  444. type: SET_AUDIO_ONLY,
  445. audioOnly,
  446. ensureVideoTrack
  447. };
  448. }
  449. /**
  450. * Sets the flag for indicating if desktop sharing is enabled.
  451. *
  452. * @param {boolean} desktopSharingEnabled - True if desktop sharing is enabled.
  453. * @returns {{
  454. * type: SET_DESKTOP_SHARING_ENABLED,
  455. * desktopSharingEnabled: boolean
  456. * }}
  457. */
  458. export function setDesktopSharingEnabled(desktopSharingEnabled: boolean) {
  459. return {
  460. type: SET_DESKTOP_SHARING_ENABLED,
  461. desktopSharingEnabled
  462. };
  463. }
  464. /**
  465. * Enables or disables the Follow Me feature.
  466. *
  467. * @param {boolean} enabled - Whether or not Follow Me should be enabled.
  468. * @returns {{
  469. * type: SET_FOLLOW_ME,
  470. * enabled: boolean
  471. * }}
  472. */
  473. export function setFollowMe(enabled: boolean) {
  474. if (typeof APP !== 'undefined') {
  475. APP.UI.emitEvent(UIEvents.FOLLOW_ME_ENABLED, enabled);
  476. }
  477. return {
  478. type: SET_FOLLOW_ME,
  479. enabled
  480. };
  481. }
  482. /**
  483. * Sets the video channel's last N (value) of the current conference. A value of
  484. * undefined shall be used to reset it to the default value.
  485. *
  486. * @param {(number|undefined)} lastN - The last N value to be set.
  487. * @returns {Function}
  488. */
  489. export function setLastN(lastN: ?number) {
  490. return (dispatch: Dispatch<*>, getState: Function) => {
  491. if (typeof lastN === 'undefined') {
  492. const config = getState()['features/base/config'];
  493. /* eslint-disable no-param-reassign */
  494. lastN = config.channelLastN;
  495. if (typeof lastN === 'undefined') {
  496. lastN = -1;
  497. }
  498. /* eslint-enable no-param-reassign */
  499. }
  500. dispatch({
  501. type: SET_LASTN,
  502. lastN
  503. });
  504. };
  505. }
  506. /**
  507. * Sets the password to join or lock a specific JitsiConference.
  508. *
  509. * @param {JitsiConference} conference - The JitsiConference which requires a
  510. * password to join or is to be locked with the specified password.
  511. * @param {Function} method - The JitsiConference method of password protection
  512. * such as join or lock.
  513. * @param {string} password - The password with which the specified conference
  514. * is to be joined or locked.
  515. * @returns {Function}
  516. */
  517. export function setPassword(
  518. conference: Object,
  519. method: Function,
  520. password: string) {
  521. return (dispatch: Dispatch<*>, getState: Function): ?Promise<void> => {
  522. switch (method) {
  523. case conference.join: {
  524. let state = getState()['features/base/conference'];
  525. // Make sure that the action will set a password for a conference
  526. // that the application wants joined.
  527. if (state.passwordRequired === conference) {
  528. dispatch({
  529. type: SET_PASSWORD,
  530. conference,
  531. method,
  532. password
  533. });
  534. // Join the conference with the newly-set password.
  535. // Make sure that the action did set the password.
  536. state = getState()['features/base/conference'];
  537. if (state.password === password
  538. && !state.passwordRequired
  539. // Make sure that the application still wants the
  540. // conference joined.
  541. && !state.conference) {
  542. method.call(conference, password);
  543. }
  544. }
  545. break;
  546. }
  547. case conference.lock: {
  548. const state = getState()['features/base/conference'];
  549. if (state.conference === conference) {
  550. return (
  551. method.call(conference, password)
  552. .then(() => dispatch({
  553. type: SET_PASSWORD,
  554. conference,
  555. method,
  556. password
  557. }))
  558. .catch(error => dispatch({
  559. type: SET_PASSWORD_FAILED,
  560. error
  561. }))
  562. );
  563. }
  564. return Promise.reject();
  565. }
  566. }
  567. };
  568. }
  569. /**
  570. * Sets the max frame height to receive from remote participant videos.
  571. *
  572. * @param {number} receiveVideoQuality - The max video resolution to receive.
  573. * @returns {{
  574. * type: SET_RECEIVE_VIDEO_QUALITY,
  575. * receiveVideoQuality: number
  576. * }}
  577. */
  578. export function setReceiveVideoQuality(receiveVideoQuality: number) {
  579. return {
  580. type: SET_RECEIVE_VIDEO_QUALITY,
  581. receiveVideoQuality
  582. };
  583. }
  584. /**
  585. * Sets (the name of) the room of the conference to be joined.
  586. *
  587. * @param {(string|undefined)} room - The name of the room of the conference to
  588. * be joined.
  589. * @returns {{
  590. * type: SET_ROOM,
  591. * room: string
  592. * }}
  593. */
  594. export function setRoom(room: ?string) {
  595. return {
  596. type: SET_ROOM,
  597. room
  598. };
  599. }
  600. /**
  601. * Sets whether or not members should join audio and/or video muted.
  602. *
  603. * @param {boolean} startAudioMuted - Whether or not members will join the
  604. * conference as audio muted.
  605. * @param {boolean} startVideoMuted - Whether or not members will join the
  606. * conference as video muted.
  607. * @returns {Function}
  608. */
  609. export function setStartMutedPolicy(
  610. startAudioMuted: boolean, startVideoMuted: boolean) {
  611. return (dispatch: Dispatch<*>, getState: Function) => {
  612. const conference = getCurrentConference(getState());
  613. conference && conference.setStartMutedPolicy({
  614. audio: startAudioMuted,
  615. video: startVideoMuted
  616. });
  617. return dispatch(
  618. onStartMutedPolicyChanged(startAudioMuted, startVideoMuted));
  619. };
  620. }
  621. /**
  622. * Toggles the audio-only flag for the current JitsiConference.
  623. *
  624. * @returns {Function}
  625. */
  626. export function toggleAudioOnly() {
  627. return (dispatch: Dispatch<*>, getState: Function) => {
  628. const { audioOnly } = getState()['features/base/conference'];
  629. return dispatch(setAudioOnly(!audioOnly, true));
  630. };
  631. }