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.any.ts 35KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167
  1. import { createStartMutedConfigurationEvent } from '../../analytics/AnalyticsEvents';
  2. import { sendAnalytics } from '../../analytics/functions';
  3. import { IReduxState, IStore } from '../../app/types';
  4. import { transcriberJoined, transcriberLeft } from '../../transcribing/actions';
  5. import { setIAmVisitor } from '../../visitors/actions';
  6. import { iAmVisitor } from '../../visitors/functions';
  7. import { overwriteConfig } from '../config/actions';
  8. import { getReplaceParticipant } from '../config/functions';
  9. import { connect, disconnect, hangup } from '../connection/actions';
  10. import { JITSI_CONNECTION_CONFERENCE_KEY } from '../connection/constants';
  11. import { hasAvailableDevices } from '../devices/functions.any';
  12. import JitsiMeetJS, { JitsiConferenceEvents, JitsiE2ePingEvents } from '../lib-jitsi-meet';
  13. import {
  14. setAudioMuted,
  15. setAudioUnmutePermissions,
  16. setVideoMuted,
  17. setVideoUnmutePermissions
  18. } from '../media/actions';
  19. import { MEDIA_TYPE, MediaType } from '../media/constants';
  20. import {
  21. dominantSpeakerChanged,
  22. participantKicked,
  23. participantMutedUs,
  24. participantPresenceChanged,
  25. participantRoleChanged,
  26. participantSourcesUpdated,
  27. participantUpdated
  28. } from '../participants/actions';
  29. import { getNormalizedDisplayName, getParticipantByIdOrUndefined } from '../participants/functions';
  30. import { IJitsiParticipant } from '../participants/types';
  31. import { toState } from '../redux/functions';
  32. import {
  33. destroyLocalTracks,
  34. replaceLocalTrack,
  35. trackAdded,
  36. trackRemoved
  37. } from '../tracks/actions.any';
  38. import { getLocalTracks } from '../tracks/functions';
  39. import { getBackendSafeRoomName } from '../util/uri';
  40. import {
  41. AUTH_STATUS_CHANGED,
  42. CONFERENCE_FAILED,
  43. CONFERENCE_JOINED,
  44. CONFERENCE_JOIN_IN_PROGRESS,
  45. CONFERENCE_LEFT,
  46. CONFERENCE_LOCAL_SUBJECT_CHANGED,
  47. CONFERENCE_PROPERTIES_CHANGED,
  48. CONFERENCE_SUBJECT_CHANGED,
  49. CONFERENCE_TIMESTAMP_CHANGED,
  50. CONFERENCE_UNIQUE_ID_SET,
  51. CONFERENCE_WILL_INIT,
  52. CONFERENCE_WILL_JOIN,
  53. CONFERENCE_WILL_LEAVE,
  54. DATA_CHANNEL_CLOSED,
  55. DATA_CHANNEL_OPENED,
  56. E2E_RTT_CHANGED,
  57. ENDPOINT_MESSAGE_RECEIVED,
  58. KICKED_OUT,
  59. LOCK_STATE_CHANGED,
  60. NON_PARTICIPANT_MESSAGE_RECEIVED,
  61. P2P_STATUS_CHANGED,
  62. SEND_TONES,
  63. SET_ASSUMED_BANDWIDTH_BPS,
  64. SET_FOLLOW_ME,
  65. SET_FOLLOW_ME_RECORDER,
  66. SET_OBFUSCATED_ROOM,
  67. SET_PASSWORD,
  68. SET_PASSWORD_FAILED,
  69. SET_PENDING_SUBJECT_CHANGE,
  70. SET_ROOM,
  71. SET_START_MUTED_POLICY,
  72. SET_START_REACTIONS_MUTED,
  73. UPDATE_CONFERENCE_METADATA
  74. } from './actionTypes';
  75. import { setupVisitorStartupMedia } from './actions';
  76. import {
  77. AVATAR_URL_COMMAND,
  78. EMAIL_COMMAND,
  79. JITSI_CONFERENCE_URL_KEY
  80. } from './constants';
  81. import {
  82. _addLocalTracksToConference,
  83. commonUserJoinedHandling,
  84. commonUserLeftHandling,
  85. getConferenceOptions,
  86. getConferenceState,
  87. getCurrentConference,
  88. getVisitorOptions,
  89. sendLocalParticipant
  90. } from './functions';
  91. import logger from './logger';
  92. import { IConferenceMetadata, IJitsiConference } from './reducer';
  93. /**
  94. * Adds conference (event) listeners.
  95. *
  96. * @param {JitsiConference} conference - The JitsiConference instance.
  97. * @param {Dispatch} dispatch - The Redux dispatch function.
  98. * @param {Object} state - The Redux state.
  99. * @private
  100. * @returns {void}
  101. */
  102. function _addConferenceListeners(conference: IJitsiConference, dispatch: IStore['dispatch'], state: IReduxState) {
  103. // A simple logger for conference errors received through
  104. // the listener. These errors are not handled now, but logged.
  105. conference.on(JitsiConferenceEvents.CONFERENCE_ERROR,
  106. (error: Error) => logger.error('Conference error.', error));
  107. // Dispatches into features/base/conference follow:
  108. // we want to ignore this event in case of tokenAuthUrl config
  109. // we are deprecating this and at some point will get rid of it
  110. if (!state['features/base/config'].tokenAuthUrl) {
  111. conference.on(
  112. JitsiConferenceEvents.AUTH_STATUS_CHANGED,
  113. (authEnabled: boolean, authLogin: string) => dispatch(authStatusChanged(authEnabled, authLogin)));
  114. }
  115. conference.on(
  116. JitsiConferenceEvents.CONFERENCE_FAILED,
  117. (err: string, ...args: any[]) => dispatch(conferenceFailed(conference, err, ...args)));
  118. conference.on(
  119. JitsiConferenceEvents.CONFERENCE_JOINED,
  120. (..._args: any[]) => dispatch(conferenceJoined(conference)));
  121. conference.on(
  122. JitsiConferenceEvents.CONFERENCE_UNIQUE_ID_SET,
  123. (..._args: any[]) => dispatch(conferenceUniqueIdSet(conference)));
  124. conference.on(
  125. JitsiConferenceEvents.CONFERENCE_JOIN_IN_PROGRESS,
  126. (..._args: any[]) => dispatch(conferenceJoinInProgress(conference)));
  127. conference.on(
  128. JitsiConferenceEvents.CONFERENCE_LEFT,
  129. (..._args: any[]) => {
  130. dispatch(conferenceTimestampChanged(0));
  131. dispatch(conferenceLeft(conference));
  132. });
  133. conference.on(JitsiConferenceEvents.SUBJECT_CHANGED,
  134. (subject: string) => dispatch(conferenceSubjectChanged(subject)));
  135. conference.on(JitsiConferenceEvents.CONFERENCE_CREATED_TIMESTAMP,
  136. (timestamp: number) => dispatch(conferenceTimestampChanged(timestamp)));
  137. conference.on(
  138. JitsiConferenceEvents.KICKED,
  139. (participant: any) => dispatch(kickedOut(conference, participant)));
  140. conference.on(
  141. JitsiConferenceEvents.PARTICIPANT_KICKED,
  142. (kicker: any, kicked: any) => dispatch(participantKicked(kicker, kicked)));
  143. conference.on(
  144. JitsiConferenceEvents.PARTICIPANT_SOURCE_UPDATED,
  145. (jitsiParticipant: IJitsiParticipant) => dispatch(participantSourcesUpdated(jitsiParticipant)));
  146. conference.on(
  147. JitsiConferenceEvents.LOCK_STATE_CHANGED,
  148. (locked: boolean) => dispatch(lockStateChanged(conference, locked)));
  149. conference.on(
  150. JitsiConferenceEvents.PROPERTIES_CHANGED,
  151. (properties: Object) => dispatch(conferencePropertiesChanged(properties)));
  152. // Dispatches into features/base/media follow:
  153. conference.on(
  154. JitsiConferenceEvents.STARTED_MUTED,
  155. () => {
  156. const audioMuted = Boolean(conference.isStartAudioMuted());
  157. const videoMuted = Boolean(conference.isStartVideoMuted());
  158. const localTracks = getLocalTracks(state['features/base/tracks']);
  159. sendAnalytics(createStartMutedConfigurationEvent('remote', audioMuted, videoMuted));
  160. logger.log(`Start muted: ${audioMuted ? 'audio, ' : ''}${videoMuted ? 'video' : ''}`);
  161. // XXX Jicofo tells lib-jitsi-meet to start with audio and/or video
  162. // muted i.e. Jicofo expresses an intent. Lib-jitsi-meet has turned
  163. // Jicofo's intent into reality by actually muting the respective
  164. // tracks. The reality is expressed in base/tracks already so what
  165. // is left is to express Jicofo's intent in base/media.
  166. // TODO Maybe the app needs to learn about Jicofo's intent and
  167. // transfer that intent to lib-jitsi-meet instead of lib-jitsi-meet
  168. // acting on Jicofo's intent without the app's knowledge.
  169. dispatch(setAudioMuted(audioMuted));
  170. dispatch(setVideoMuted(videoMuted));
  171. // Remove the tracks from peerconnection as well.
  172. for (const track of localTracks) {
  173. const trackType = track.jitsiTrack.getType();
  174. // Do not remove the audio track on RN. Starting with iOS 15 it will fail to unmute otherwise.
  175. if ((audioMuted && trackType === MEDIA_TYPE.AUDIO && navigator.product !== 'ReactNative')
  176. || (videoMuted && trackType === MEDIA_TYPE.VIDEO)) {
  177. dispatch(replaceLocalTrack(track.jitsiTrack, null, conference));
  178. }
  179. }
  180. });
  181. conference.on(
  182. JitsiConferenceEvents.AUDIO_UNMUTE_PERMISSIONS_CHANGED,
  183. (disableAudioMuteChange: boolean) => {
  184. dispatch(setAudioUnmutePermissions(disableAudioMuteChange));
  185. });
  186. conference.on(
  187. JitsiConferenceEvents.VIDEO_UNMUTE_PERMISSIONS_CHANGED,
  188. (disableVideoMuteChange: boolean) => {
  189. dispatch(setVideoUnmutePermissions(disableVideoMuteChange));
  190. });
  191. // Dispatches into features/base/tracks follow:
  192. conference.on(
  193. JitsiConferenceEvents.TRACK_ADDED,
  194. (t: any) => t && !t.isLocal() && dispatch(trackAdded(t)));
  195. conference.on(
  196. JitsiConferenceEvents.TRACK_REMOVED,
  197. (t: any) => t && !t.isLocal() && dispatch(trackRemoved(t)));
  198. conference.on(
  199. JitsiConferenceEvents.TRACK_MUTE_CHANGED,
  200. (track: any, participantThatMutedUs: any) => {
  201. if (participantThatMutedUs) {
  202. dispatch(participantMutedUs(participantThatMutedUs, track));
  203. }
  204. });
  205. conference.on(JitsiConferenceEvents.TRACK_UNMUTE_REJECTED, (track: any) => dispatch(destroyLocalTracks(track)));
  206. // Dispatches into features/base/participants follow:
  207. conference.on(
  208. JitsiConferenceEvents.DISPLAY_NAME_CHANGED,
  209. (id: string, displayName: string) => dispatch(participantUpdated({
  210. conference,
  211. id,
  212. name: getNormalizedDisplayName(displayName)
  213. })));
  214. conference.on(
  215. JitsiConferenceEvents.SILENT_STATUS_CHANGED,
  216. (id: string, isSilent: boolean) => dispatch(participantUpdated({
  217. conference,
  218. id,
  219. isSilent
  220. })));
  221. conference.on(
  222. JitsiConferenceEvents.DOMINANT_SPEAKER_CHANGED,
  223. (dominant: string, previous: string[], silence: boolean | string) => {
  224. dispatch(dominantSpeakerChanged(dominant, previous, Boolean(silence), conference));
  225. });
  226. conference.on(
  227. JitsiConferenceEvents.ENDPOINT_MESSAGE_RECEIVED,
  228. (participant: Object, json: Object) => dispatch(endpointMessageReceived(participant, json)));
  229. conference.on(
  230. JitsiConferenceEvents.NON_PARTICIPANT_MESSAGE_RECEIVED,
  231. (id: string, json: Object) => dispatch(nonParticipantMessageReceived(id, json)));
  232. conference.on(
  233. JitsiConferenceEvents.USER_JOINED,
  234. (_id: string, user: any) => commonUserJoinedHandling({ dispatch }, conference, user));
  235. conference.on(
  236. JitsiConferenceEvents.USER_LEFT,
  237. (_id: string, user: any) => commonUserLeftHandling({ dispatch }, conference, user));
  238. conference.on(
  239. JitsiConferenceEvents.USER_ROLE_CHANGED,
  240. (id: string, role: string) => dispatch(participantRoleChanged(id, role)));
  241. conference.on(
  242. JitsiConferenceEvents.USER_STATUS_CHANGED,
  243. (id: string, presence: string) => dispatch(participantPresenceChanged(id, presence)));
  244. conference.on(
  245. JitsiE2ePingEvents.E2E_RTT_CHANGED,
  246. (participant: Object, rtt: number) => dispatch(e2eRttChanged(participant, rtt)));
  247. conference.on(
  248. JitsiConferenceEvents.BOT_TYPE_CHANGED,
  249. (id: string, botType: string) => dispatch(participantUpdated({
  250. conference,
  251. id,
  252. botType
  253. })));
  254. conference.on(
  255. JitsiConferenceEvents.TRANSCRIPTION_STATUS_CHANGED,
  256. (status: string, id: string, abruptly: boolean) => {
  257. if (status === JitsiMeetJS.constants.transcriptionStatus.ON) {
  258. dispatch(transcriberJoined(id));
  259. } else if (status === JitsiMeetJS.constants.transcriptionStatus.OFF) {
  260. dispatch(transcriberLeft(id, abruptly));
  261. }
  262. });
  263. conference.addCommandListener(
  264. AVATAR_URL_COMMAND,
  265. (data: { value: string; }, id: string) => {
  266. const participant = getParticipantByIdOrUndefined(state, id);
  267. // if already set from presence(jwt), skip the command processing
  268. if (!participant?.avatarURL) {
  269. return dispatch(participantUpdated({
  270. conference,
  271. id,
  272. avatarURL: data.value
  273. }));
  274. }
  275. });
  276. conference.addCommandListener(
  277. EMAIL_COMMAND,
  278. (data: { value: string; }, id: string) => dispatch(participantUpdated({
  279. conference,
  280. id,
  281. email: data.value
  282. })));
  283. }
  284. /**
  285. * Action for updating the conference metadata.
  286. *
  287. * @param {IConferenceMetadata} metadata - The metadata object.
  288. * @returns {{
  289. * type: UPDATE_CONFERENCE_METADATA,
  290. * metadata: IConferenceMetadata
  291. * }}
  292. */
  293. export function updateConferenceMetadata(metadata: IConferenceMetadata | null) {
  294. return {
  295. type: UPDATE_CONFERENCE_METADATA,
  296. metadata
  297. };
  298. }
  299. /**
  300. * Create an action for when the end-to-end RTT against a specific remote participant has changed.
  301. *
  302. * @param {Object} participant - The participant against which the rtt is measured.
  303. * @param {number} rtt - The rtt.
  304. * @returns {{
  305. * type: E2E_RTT_CHANGED,
  306. * e2eRtt: {
  307. * participant: Object,
  308. * rtt: number
  309. * }
  310. * }}
  311. */
  312. export function e2eRttChanged(participant: Object, rtt: number) {
  313. return {
  314. type: E2E_RTT_CHANGED,
  315. e2eRtt: {
  316. rtt,
  317. participant
  318. }
  319. };
  320. }
  321. /**
  322. * Updates the current known state of server-side authentication.
  323. *
  324. * @param {boolean} authEnabled - Whether or not server authentication is
  325. * enabled.
  326. * @param {string} authLogin - The current name of the logged in user, if any.
  327. * @returns {{
  328. * type: AUTH_STATUS_CHANGED,
  329. * authEnabled: boolean,
  330. * authLogin: string
  331. * }}
  332. */
  333. export function authStatusChanged(authEnabled: boolean, authLogin: string) {
  334. return {
  335. type: AUTH_STATUS_CHANGED,
  336. authEnabled,
  337. authLogin
  338. };
  339. }
  340. /**
  341. * Signals that a specific conference has failed.
  342. *
  343. * @param {JitsiConference} conference - The JitsiConference that has failed.
  344. * @param {string} error - The error describing/detailing the cause of the
  345. * failure.
  346. * @param {any} params - Rest of the params that we receive together with the event.
  347. * @returns {{
  348. * type: CONFERENCE_FAILED,
  349. * conference: JitsiConference,
  350. * error: Error
  351. * }}
  352. * @public
  353. */
  354. export function conferenceFailed(conference: IJitsiConference, error: string, ...params: any) {
  355. return {
  356. type: CONFERENCE_FAILED,
  357. conference,
  358. // Make the error resemble an Error instance (to the extent that
  359. // jitsi-meet needs it).
  360. error: {
  361. name: error,
  362. params,
  363. recoverable: undefined
  364. }
  365. };
  366. }
  367. /**
  368. * Signals that a specific conference has been joined.
  369. *
  370. * @param {JitsiConference} conference - The JitsiConference instance which was
  371. * joined by the local participant.
  372. * @returns {{
  373. * type: CONFERENCE_JOINED,
  374. * conference: JitsiConference
  375. * }}
  376. */
  377. export function conferenceJoined(conference: IJitsiConference) {
  378. return {
  379. type: CONFERENCE_JOINED,
  380. conference
  381. };
  382. }
  383. /**
  384. * Signals that a specific conference join is in progress.
  385. *
  386. * @param {JitsiConference} conference - The JitsiConference instance for which join by the local participant
  387. * is in progress.
  388. * @returns {{
  389. * type: CONFERENCE_JOIN_IN_PROGRESS,
  390. * conference: JitsiConference
  391. * }}
  392. */
  393. export function conferenceJoinInProgress(conference: IJitsiConference) {
  394. return {
  395. type: CONFERENCE_JOIN_IN_PROGRESS,
  396. conference
  397. };
  398. }
  399. /**
  400. * Signals that a specific conference has been left.
  401. *
  402. * @param {JitsiConference} conference - The JitsiConference instance which was
  403. * left by the local participant.
  404. * @returns {{
  405. * type: CONFERENCE_LEFT,
  406. * conference: JitsiConference
  407. * }}
  408. */
  409. export function conferenceLeft(conference?: IJitsiConference) {
  410. return {
  411. type: CONFERENCE_LEFT,
  412. conference
  413. };
  414. }
  415. /**
  416. * Signals that the conference properties have been changed.
  417. *
  418. * @param {Object} properties - The new properties set.
  419. * @returns {{
  420. * type: CONFERENCE_PROPERTIES_CHANGED,
  421. * properties: Object
  422. * }}
  423. */
  424. export function conferencePropertiesChanged(properties: object) {
  425. return {
  426. type: CONFERENCE_PROPERTIES_CHANGED,
  427. properties
  428. };
  429. }
  430. /**
  431. * Signals that the conference subject has been changed.
  432. *
  433. * @param {string} subject - The new subject.
  434. * @returns {{
  435. * type: CONFERENCE_SUBJECT_CHANGED,
  436. * subject: string
  437. * }}
  438. */
  439. export function conferenceSubjectChanged(subject: string) {
  440. return {
  441. type: CONFERENCE_SUBJECT_CHANGED,
  442. subject
  443. };
  444. }
  445. /**
  446. * Signals that the conference timestamp has been changed.
  447. *
  448. * @param {number} conferenceTimestamp - The UTC timestamp.
  449. * @returns {{
  450. * type: CONFERENCE_TIMESTAMP_CHANGED,
  451. * conferenceTimestamp
  452. * }}
  453. */
  454. export function conferenceTimestampChanged(conferenceTimestamp: number) {
  455. return {
  456. type: CONFERENCE_TIMESTAMP_CHANGED,
  457. conferenceTimestamp
  458. };
  459. }
  460. /**
  461. * Signals that the unique identifier for conference has been set.
  462. *
  463. * @param {JitsiConference} conference - The JitsiConference instance, where the uuid has been set.
  464. * @returns {{
  465. * type: CONFERENCE_UNIQUE_ID_SET,
  466. * conference: JitsiConference,
  467. * }}
  468. */
  469. export function conferenceUniqueIdSet(conference: IJitsiConference) {
  470. return {
  471. type: CONFERENCE_UNIQUE_ID_SET,
  472. conference
  473. };
  474. }
  475. /**
  476. * Adds any existing local tracks to a specific conference before the conference
  477. * is joined. Then signals the intention of the application to have the local
  478. * participant join the specified conference.
  479. *
  480. * @param {JitsiConference} conference - The {@code JitsiConference} instance
  481. * the local participant will (try to) join.
  482. * @returns {Function}
  483. */
  484. export function _conferenceWillJoin(conference: IJitsiConference) {
  485. return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
  486. const state = getState();
  487. const localTracks
  488. = getLocalTracks(state['features/base/tracks'])
  489. .map(t => t.jitsiTrack);
  490. if (localTracks.length && !iAmVisitor(state)) {
  491. _addLocalTracksToConference(conference, localTracks);
  492. }
  493. dispatch(conferenceWillJoin(conference));
  494. };
  495. }
  496. /**
  497. * Signals the intention of the application to have a conference initialized.
  498. *
  499. * @returns {{
  500. * type: CONFERENCE_WILL_INIT
  501. * }}
  502. */
  503. export function conferenceWillInit() {
  504. return {
  505. type: CONFERENCE_WILL_INIT
  506. };
  507. }
  508. /**
  509. * Signals the intention of the application to have the local participant
  510. * join the specified conference.
  511. *
  512. * @param {JitsiConference} conference - The {@code JitsiConference} instance
  513. * the local participant will (try to) join.
  514. * @returns {{
  515. * type: CONFERENCE_WILL_JOIN,
  516. * conference: JitsiConference
  517. * }}
  518. */
  519. export function conferenceWillJoin(conference?: IJitsiConference) {
  520. return {
  521. type: CONFERENCE_WILL_JOIN,
  522. conference
  523. };
  524. }
  525. /**
  526. * Signals the intention of the application to have the local participant leave
  527. * a specific conference. Similar in fashion to CONFERENCE_LEFT. Contrary to it
  528. * though, it's not guaranteed because CONFERENCE_LEFT may be triggered by
  529. * lib-jitsi-meet and not the application.
  530. *
  531. * @param {JitsiConference} conference - The JitsiConference instance which will
  532. * be left by the local participant.
  533. * @param {boolean} isRedirect - Indicates if the action has been dispatched as part of visitor promotion.
  534. * @returns {{
  535. * type: CONFERENCE_LEFT,
  536. * conference: JitsiConference,
  537. * isRedirect: boolean
  538. * }}
  539. */
  540. export function conferenceWillLeave(conference?: IJitsiConference, isRedirect?: boolean) {
  541. return {
  542. type: CONFERENCE_WILL_LEAVE,
  543. conference,
  544. isRedirect
  545. };
  546. }
  547. /**
  548. * Initializes a new conference.
  549. *
  550. * @param {string} overrideRoom - Override the room to join, instead of taking it
  551. * from Redux.
  552. * @returns {Function}
  553. */
  554. export function createConference(overrideRoom?: string | String) {
  555. return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
  556. const state = getState();
  557. const { connection, locationURL } = state['features/base/connection'];
  558. if (!connection) {
  559. throw new Error('Cannot create a conference without a connection!');
  560. }
  561. const { password, room } = state['features/base/conference'];
  562. if (!room) {
  563. throw new Error('Cannot join a conference without a room name!');
  564. }
  565. // XXX: revisit this.
  566. // Hide the custom domain in the room name.
  567. const tmp: any = overrideRoom || room;
  568. let _room: any = getBackendSafeRoomName(tmp);
  569. if (tmp.domain) {
  570. // eslint-disable-next-line no-new-wrappers
  571. _room = new String(tmp);
  572. _room.domain = tmp.domain;
  573. }
  574. const conference = connection.initJitsiConference(_room, getConferenceOptions(state));
  575. // @ts-ignore
  576. connection[JITSI_CONNECTION_CONFERENCE_KEY] = conference;
  577. conference[JITSI_CONFERENCE_URL_KEY] = locationURL;
  578. dispatch(_conferenceWillJoin(conference));
  579. _addConferenceListeners(conference, dispatch, state);
  580. sendLocalParticipant(state, conference);
  581. const replaceParticipant = getReplaceParticipant(state);
  582. conference.join(password, replaceParticipant);
  583. };
  584. }
  585. /**
  586. * Will try to join the conference again in case it failed earlier with
  587. * {@link JitsiConferenceErrors.AUTHENTICATION_REQUIRED}. It means that Jicofo
  588. * did not allow to create new room from anonymous domain, but it can be tried
  589. * again later in case authenticated user created it in the meantime.
  590. *
  591. * @returns {Function}
  592. */
  593. export function checkIfCanJoin() {
  594. return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
  595. const { authRequired, password }
  596. = getState()['features/base/conference'];
  597. const replaceParticipant = getReplaceParticipant(getState());
  598. authRequired && dispatch(_conferenceWillJoin(authRequired));
  599. authRequired?.join(password, replaceParticipant);
  600. };
  601. }
  602. /**
  603. * Signals the data channel with the bridge has successfully opened.
  604. *
  605. * @returns {{
  606. * type: DATA_CHANNEL_OPENED
  607. * }}
  608. */
  609. export function dataChannelOpened() {
  610. return {
  611. type: DATA_CHANNEL_OPENED
  612. };
  613. }
  614. /**
  615. * Signals the data channel with the bridge was abruptly closed.
  616. *
  617. * @param {number} code - Close code.
  618. * @param {string} reason - Close reason.
  619. *
  620. * @returns {{
  621. * type: DATA_CHANNEL_CLOSED,
  622. * code: number,
  623. * reason: string
  624. * }}
  625. */
  626. export function dataChannelClosed(code: number, reason: string) {
  627. return {
  628. type: DATA_CHANNEL_CLOSED,
  629. code,
  630. reason
  631. };
  632. }
  633. /**
  634. * Signals that a participant sent an endpoint message on the data channel.
  635. *
  636. * @param {Object} participant - The participant details sending the message.
  637. * @param {Object} data - The data carried by the endpoint message.
  638. * @returns {{
  639. * type: ENDPOINT_MESSAGE_RECEIVED,
  640. * participant: Object,
  641. * data: Object
  642. * }}
  643. */
  644. export function endpointMessageReceived(participant: Object, data: Object) {
  645. return {
  646. type: ENDPOINT_MESSAGE_RECEIVED,
  647. participant,
  648. data
  649. };
  650. }
  651. /**
  652. * Action to end a conference for all participants.
  653. *
  654. * @returns {Function}
  655. */
  656. export function endConference() {
  657. return async (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
  658. const { conference } = getConferenceState(toState(getState));
  659. conference?.end();
  660. };
  661. }
  662. /**
  663. * Signals that we've been kicked out of the conference.
  664. *
  665. * @param {JitsiConference} conference - The {@link JitsiConference} instance
  666. * for which the event is being signaled.
  667. * @param {JitsiParticipant} participant - The {@link JitsiParticipant}
  668. * instance which initiated the kick event.
  669. * @returns {{
  670. * type: KICKED_OUT,
  671. * conference: JitsiConference,
  672. * participant: JitsiParticipant
  673. * }}
  674. */
  675. export function kickedOut(conference: IJitsiConference, participant: Object) {
  676. return {
  677. type: KICKED_OUT,
  678. conference,
  679. participant
  680. };
  681. }
  682. /**
  683. * Action to leave a conference.
  684. *
  685. * @returns {Function}
  686. */
  687. export function leaveConference() {
  688. return async (dispatch: IStore['dispatch']) => dispatch(hangup(true));
  689. }
  690. /**
  691. * Signals that the lock state of a specific JitsiConference changed.
  692. *
  693. * @param {JitsiConference} conference - The JitsiConference which had its lock
  694. * state changed.
  695. * @param {boolean} locked - If the specified conference became locked, true;
  696. * otherwise, false.
  697. * @returns {{
  698. * type: LOCK_STATE_CHANGED,
  699. * conference: JitsiConference,
  700. * locked: boolean
  701. * }}
  702. */
  703. export function lockStateChanged(conference: IJitsiConference, locked: boolean) {
  704. return {
  705. type: LOCK_STATE_CHANGED,
  706. conference,
  707. locked
  708. };
  709. }
  710. /**
  711. * Signals that a non participant endpoint message has been received.
  712. *
  713. * @param {string} id - The resource id of the sender.
  714. * @param {Object} json - The json carried by the endpoint message.
  715. * @returns {{
  716. * type: NON_PARTICIPANT_MESSAGE_RECEIVED,
  717. * id: Object,
  718. * json: Object
  719. * }}
  720. */
  721. export function nonParticipantMessageReceived(id: string, json: Object) {
  722. return {
  723. type: NON_PARTICIPANT_MESSAGE_RECEIVED,
  724. id,
  725. json
  726. };
  727. }
  728. /**
  729. * Updates the known state of start muted policies.
  730. *
  731. * @param {boolean} audioMuted - Whether or not members will join the conference
  732. * as audio muted.
  733. * @param {boolean} videoMuted - Whether or not members will join the conference
  734. * as video muted.
  735. * @returns {{
  736. * type: SET_START_MUTED_POLICY,
  737. * startAudioMutedPolicy: boolean,
  738. * startVideoMutedPolicy: boolean
  739. * }}
  740. */
  741. export function onStartMutedPolicyChanged(
  742. audioMuted: boolean, videoMuted: boolean) {
  743. return {
  744. type: SET_START_MUTED_POLICY,
  745. startAudioMutedPolicy: audioMuted,
  746. startVideoMutedPolicy: videoMuted
  747. };
  748. }
  749. /**
  750. * Sets whether or not peer2peer is currently enabled.
  751. *
  752. * @param {boolean} p2p - Whether or not peer2peer is currently active.
  753. * @returns {{
  754. * type: P2P_STATUS_CHANGED,
  755. * p2p: boolean
  756. * }}
  757. */
  758. export function p2pStatusChanged(p2p: boolean) {
  759. return {
  760. type: P2P_STATUS_CHANGED,
  761. p2p
  762. };
  763. }
  764. /**
  765. * Signals to play touch tones.
  766. *
  767. * @param {string} tones - The tones to play.
  768. * @param {number} [duration] - How long to play each tone.
  769. * @param {number} [pause] - How long to pause between each tone.
  770. * @returns {{
  771. * type: SEND_TONES,
  772. * tones: string,
  773. * duration: number,
  774. * pause: number
  775. * }}
  776. */
  777. export function sendTones(tones: string, duration: number, pause: number) {
  778. return {
  779. type: SEND_TONES,
  780. tones,
  781. duration,
  782. pause
  783. };
  784. }
  785. /**
  786. * Enables or disables the Follow Me feature.
  787. *
  788. * @param {boolean} enabled - Whether or not Follow Me should be enabled.
  789. * @returns {{
  790. * type: SET_FOLLOW_ME,
  791. * enabled: boolean
  792. * }}
  793. */
  794. export function setFollowMe(enabled: boolean) {
  795. return {
  796. type: SET_FOLLOW_ME,
  797. enabled
  798. };
  799. }
  800. /**
  801. * Enables or disables the Follow Me feature used only for the recorder.
  802. *
  803. * @param {boolean} enabled - Whether Follow Me should be enabled and used only by the recorder.
  804. * @returns {{
  805. * type: SET_FOLLOW_ME_RECORDER,
  806. * enabled: boolean
  807. * }}
  808. */
  809. export function setFollowMeRecorder(enabled: boolean) {
  810. return {
  811. type: SET_FOLLOW_ME_RECORDER,
  812. enabled
  813. };
  814. }
  815. /**
  816. * Enables or disables the Mute reaction sounds feature.
  817. *
  818. * @param {boolean} muted - Whether or not reaction sounds should be muted for all participants.
  819. * @param {boolean} updateBackend - Whether or not the moderator should notify all participants for the new setting.
  820. * @returns {{
  821. * type: SET_START_REACTIONS_MUTED,
  822. * muted: boolean
  823. * }}
  824. */
  825. export function setStartReactionsMuted(muted: boolean, updateBackend = false) {
  826. return {
  827. type: SET_START_REACTIONS_MUTED,
  828. muted,
  829. updateBackend
  830. };
  831. }
  832. /**
  833. * Sets the password to join or lock a specific JitsiConference.
  834. *
  835. * @param {JitsiConference} conference - The JitsiConference which requires a
  836. * password to join or is to be locked with the specified password.
  837. * @param {Function} method - The JitsiConference method of password protection
  838. * such as join or lock.
  839. * @param {string} password - The password with which the specified conference
  840. * is to be joined or locked.
  841. * @returns {Function}
  842. */
  843. export function setPassword(
  844. conference: IJitsiConference | undefined,
  845. method: Function | undefined,
  846. password?: string) {
  847. return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
  848. if (!conference) {
  849. return Promise.reject();
  850. }
  851. switch (method) {
  852. case conference.join: {
  853. let state = getState()['features/base/conference'];
  854. dispatch({
  855. type: SET_PASSWORD,
  856. conference,
  857. method,
  858. password
  859. });
  860. // Join the conference with the newly-set password.
  861. // Make sure that the action did set the password.
  862. state = getState()['features/base/conference'];
  863. if (state.password === password
  864. // Make sure that the application still wants the
  865. // conference joined.
  866. && !state.conference) {
  867. method.call(conference, password);
  868. }
  869. break;
  870. }
  871. case conference.lock: {
  872. const state = getState()['features/base/conference'];
  873. if (state.conference === conference) {
  874. return (
  875. method.call(conference, password)
  876. .then(() => dispatch({
  877. type: SET_PASSWORD,
  878. conference,
  879. method,
  880. password
  881. }))
  882. .catch((error: Error) => dispatch({
  883. type: SET_PASSWORD_FAILED,
  884. error
  885. }))
  886. );
  887. }
  888. return Promise.reject();
  889. }
  890. }
  891. };
  892. }
  893. /**
  894. * Sets the obfuscated room name of the conference to be joined.
  895. *
  896. * @param {(string)} obfuscatedRoom - Obfuscated room name.
  897. * @param {(string)} obfuscatedRoomSource - The room name that was obfuscated.
  898. * @returns {{
  899. * type: SET_OBFUSCATED_ROOM,
  900. * room: string
  901. * }}
  902. */
  903. export function setObfuscatedRoom(obfuscatedRoom: string, obfuscatedRoomSource: string) {
  904. return {
  905. type: SET_OBFUSCATED_ROOM,
  906. obfuscatedRoom,
  907. obfuscatedRoomSource
  908. };
  909. }
  910. /**
  911. * Sets (the name of) the room of the conference to be joined.
  912. *
  913. * @param {(string|undefined)} room - The name of the room of the conference to
  914. * be joined.
  915. * @returns {{
  916. * type: SET_ROOM,
  917. * room: string
  918. * }}
  919. */
  920. export function setRoom(room?: string) {
  921. return {
  922. type: SET_ROOM,
  923. room
  924. };
  925. }
  926. /**
  927. * Sets whether or not members should join audio and/or video muted.
  928. *
  929. * @param {boolean} startAudioMuted - Whether or not members will join the
  930. * conference as audio muted.
  931. * @param {boolean} startVideoMuted - Whether or not members will join the
  932. * conference as video muted.
  933. * @returns {Function}
  934. */
  935. export function setStartMutedPolicy(
  936. startAudioMuted: boolean, startVideoMuted: boolean) {
  937. return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
  938. const conference = getCurrentConference(getState());
  939. conference?.setStartMutedPolicy({
  940. audio: startAudioMuted,
  941. video: startVideoMuted
  942. });
  943. dispatch(
  944. onStartMutedPolicyChanged(startAudioMuted, startVideoMuted));
  945. };
  946. }
  947. /**
  948. * Sets the conference subject.
  949. *
  950. * @param {string} subject - The new subject.
  951. * @returns {void}
  952. */
  953. export function setSubject(subject: string) {
  954. return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
  955. const { conference } = getState()['features/base/conference'];
  956. if (conference) {
  957. conference.setSubject(subject);
  958. } else {
  959. dispatch({
  960. type: SET_PENDING_SUBJECT_CHANGE,
  961. subject
  962. });
  963. }
  964. };
  965. }
  966. /**
  967. * Sets the conference local subject.
  968. *
  969. * @param {string} localSubject - The new local subject.
  970. * @returns {{
  971. * type: CONFERENCE_LOCAL_SUBJECT_CHANGED,
  972. * localSubject: string
  973. * }}
  974. */
  975. export function setLocalSubject(localSubject: string) {
  976. return {
  977. type: CONFERENCE_LOCAL_SUBJECT_CHANGED,
  978. localSubject
  979. };
  980. }
  981. /**
  982. * Sets the assumed bandwidth bps.
  983. *
  984. * @param {number} assumedBandwidthBps - The new assumed bandwidth.
  985. * @returns {{
  986. * type: SET_ASSUMED_BANDWIDTH_BPS,
  987. * assumedBandwidthBps: number
  988. * }}
  989. */
  990. export function setAssumedBandwidthBps(assumedBandwidthBps: number) {
  991. return {
  992. type: SET_ASSUMED_BANDWIDTH_BPS,
  993. assumedBandwidthBps
  994. };
  995. }
  996. /**
  997. * Redirects to a new visitor node.
  998. *
  999. * @param {string | undefined} vnode - The vnode to use or undefined if moving back to the main room.
  1000. * @param {string} focusJid - The focus jid to use.
  1001. * @param {string} username - The username to use.
  1002. * @returns {void}
  1003. */
  1004. export function redirect(vnode: string, focusJid: string, username: string) {
  1005. return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
  1006. const newConfig = getVisitorOptions(getState, vnode, focusJid, username);
  1007. if (!newConfig) {
  1008. logger.warn('Not redirected missing params');
  1009. return;
  1010. }
  1011. dispatch(overwriteConfig(newConfig));
  1012. dispatch(disconnect(true))
  1013. .then(() => {
  1014. dispatch(setIAmVisitor(Boolean(vnode)));
  1015. // we do not clear local tracks on error, so we need to manually clear them
  1016. return dispatch(destroyLocalTracks());
  1017. })
  1018. .then(() => {
  1019. dispatch(conferenceWillInit());
  1020. logger.info(`Dispatching connect from redirect (visitor = ${Boolean(vnode)}).`);
  1021. return dispatch(connect());
  1022. })
  1023. .then(() => {
  1024. const media: Array<MediaType> = [];
  1025. if (!vnode) {
  1026. const state = getState();
  1027. const { enableMediaOnPromote = {} } = state['features/base/config'].visitors ?? {};
  1028. const { audio = false, video = false } = enableMediaOnPromote;
  1029. if (audio) {
  1030. const { available, muted, unmuteBlocked } = state['features/base/media'].audio;
  1031. const { startSilent } = state['features/base/config'];
  1032. // do not unmute the user if he was muted before (on the prejoin, the config
  1033. // or URL param, etc.)
  1034. if (!unmuteBlocked && !muted && !startSilent && available) {
  1035. media.push(MEDIA_TYPE.AUDIO);
  1036. }
  1037. }
  1038. if (video) {
  1039. const { muted, unmuteBlocked } = state['features/base/media'].video;
  1040. // do not unmute the user if he was muted before (on the prejoin, the config, URL param or
  1041. // audo only, etc)
  1042. if (!unmuteBlocked && !muted && hasAvailableDevices(state, 'videoInput')) {
  1043. media.push(MEDIA_TYPE.VIDEO);
  1044. }
  1045. }
  1046. }
  1047. dispatch(setupVisitorStartupMedia(media));
  1048. });
  1049. };
  1050. }