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 18KB

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