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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863
  1. import { createTrackMutedEvent } from '../../analytics/AnalyticsEvents';
  2. import { sendAnalytics } from '../../analytics/functions';
  3. import { IStore } from '../../app/types';
  4. import { showErrorNotification, showNotification } from '../../notifications/actions';
  5. import { NOTIFICATION_TIMEOUT, NOTIFICATION_TIMEOUT_TYPE } from '../../notifications/constants';
  6. import { getCurrentConference } from '../conference/functions';
  7. import { IJitsiConference } from '../conference/reducer';
  8. import { JitsiTrackErrors, JitsiTrackEvents } from '../lib-jitsi-meet';
  9. import { createLocalTrack } from '../lib-jitsi-meet/functions.any';
  10. import { setAudioMuted, setScreenshareMuted, setVideoMuted } from '../media/actions';
  11. import {
  12. CAMERA_FACING_MODE,
  13. MEDIA_TYPE,
  14. MediaType,
  15. VIDEO_MUTISM_AUTHORITY,
  16. VIDEO_TYPE,
  17. VideoType
  18. } from '../media/constants';
  19. import { getLocalParticipant } from '../participants/functions';
  20. import { updateSettings } from '../settings/actions';
  21. import {
  22. SET_NO_SRC_DATA_NOTIFICATION_UID,
  23. TRACK_ADDED,
  24. TRACK_CREATE_CANCELED,
  25. TRACK_CREATE_ERROR,
  26. TRACK_MUTE_UNMUTE_FAILED,
  27. TRACK_NO_DATA_FROM_SOURCE,
  28. TRACK_REMOVED,
  29. TRACK_STOPPED,
  30. TRACK_UPDATED,
  31. TRACK_WILL_CREATE
  32. } from './actionTypes';
  33. import {
  34. createLocalTracksF,
  35. getCameraFacingMode,
  36. getLocalTrack,
  37. getLocalTracks,
  38. getLocalVideoTrack,
  39. getTrackByJitsiTrack
  40. } from './functions';
  41. import logger from './logger';
  42. import { ITrack, ITrackOptions } from './types';
  43. /**
  44. * Add a given local track to the conference.
  45. *
  46. * @param {JitsiLocalTrack} newTrack - The local track to be added to the conference.
  47. * @returns {Function}
  48. */
  49. export function addLocalTrack(newTrack: any) {
  50. return async (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
  51. const conference = getCurrentConference(getState());
  52. if (conference) {
  53. await conference.addTrack(newTrack);
  54. }
  55. const setMuted = newTrack.isVideoTrack()
  56. ? newTrack.getVideoType() === VIDEO_TYPE.DESKTOP
  57. ? setScreenshareMuted
  58. : setVideoMuted
  59. : setAudioMuted;
  60. const isMuted = newTrack.isMuted();
  61. logger.log(`Adding ${newTrack.getType()} track - ${isMuted ? 'muted' : 'unmuted'}`);
  62. dispatch(setMuted(isMuted));
  63. return dispatch(_addTracks([ newTrack ]));
  64. };
  65. }
  66. /**
  67. * Requests the creating of the desired media type tracks. Desire is expressed
  68. * by base/media unless the function caller specifies desired media types
  69. * explicitly and thus override base/media. Dispatches a
  70. * {@code createLocalTracksA} action for the desired media types for which there
  71. * are no existing tracks yet.
  72. *
  73. * @returns {Function}
  74. */
  75. export function createDesiredLocalTracks(...desiredTypes: any) {
  76. return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
  77. const state = getState();
  78. dispatch(destroyLocalDesktopTrackIfExists());
  79. if (desiredTypes.length === 0) {
  80. const { startSilent } = state['features/base/config'];
  81. const { video } = state['features/base/media'];
  82. if (!startSilent) {
  83. // Always create the audio track early, even if it will be muted.
  84. // This fixes a timing issue when adding the track to the conference which
  85. // manifests primarily on iOS 15.
  86. // Unless we are silent, of course.
  87. desiredTypes.push(MEDIA_TYPE.AUDIO);
  88. }
  89. // XXX When the app is coming into the foreground from the
  90. // background in order to handle a URL, it may realize the new
  91. // background state soon after it has tried to create the local
  92. // tracks requested by the URL. Ignore
  93. // VIDEO_MUTISM_AUTHORITY.BACKGROUND and create the local video
  94. // track if no other VIDEO_MUTISM_AUTHORITY has muted it. The local
  95. // video track will be muted until the app realizes the new
  96. // background state.
  97. // eslint-disable-next-line no-bitwise
  98. (video.muted & ~VIDEO_MUTISM_AUTHORITY.BACKGROUND)
  99. || desiredTypes.push(MEDIA_TYPE.VIDEO);
  100. }
  101. const availableTypes
  102. = getLocalTracks(
  103. state['features/base/tracks'],
  104. /* includePending */ true)
  105. .map(t => t.mediaType);
  106. // We need to create the desired tracks which are not already available.
  107. const createTypes
  108. = desiredTypes.filter((type: MediaType) => availableTypes.indexOf(type) === -1);
  109. createTypes.length
  110. && dispatch(createLocalTracksA({ devices: createTypes }));
  111. };
  112. }
  113. /**
  114. * Request to start capturing local audio and/or video. By default, the user
  115. * facing camera will be selected.
  116. *
  117. * @param {Object} [options] - For info @see JitsiMeetJS.createLocalTracks.
  118. * @returns {Function}
  119. */
  120. export function createLocalTracksA(options: ITrackOptions = {}) {
  121. return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
  122. const devices
  123. = options.devices || [ MEDIA_TYPE.AUDIO, MEDIA_TYPE.VIDEO ];
  124. const store = {
  125. dispatch,
  126. getState
  127. };
  128. const promises = [];
  129. const state = getState();
  130. // The following executes on React Native only at the time of this
  131. // writing. The effort to port Web's createInitialLocalTracks
  132. // is significant and that's where the function createLocalTracksF got
  133. // born. I started with the idea a porting so that we could inherit the
  134. // ability to getUserMedia for audio only or video only if getUserMedia
  135. // for audio and video fails. Eventually though, I realized that on
  136. // mobile we do not have combined permission prompts implemented anyway
  137. // (either because there are no such prompts or it does not make sense
  138. // to implement them) and the right thing to do is to ask for each
  139. // device separately.
  140. for (const device of devices) {
  141. if (getLocalTrack(
  142. state['features/base/tracks'],
  143. device as MediaType,
  144. /* includePending */ true)) {
  145. throw new Error(`Local track for ${device} already exists`);
  146. }
  147. const gumProcess: any
  148. = createLocalTracksF(
  149. {
  150. cameraDeviceId: options.cameraDeviceId,
  151. devices: [ device ],
  152. facingMode:
  153. options.facingMode || getCameraFacingMode(state),
  154. micDeviceId: options.micDeviceId
  155. },
  156. store)
  157. .then( // @ts-ignore
  158. (localTracks: any[]) => {
  159. // Because GUM is called for 1 device (which is actually
  160. // a media type 'audio', 'video', 'screen', etc.) we
  161. // should not get more than one JitsiTrack.
  162. if (localTracks.length !== 1) {
  163. throw new Error(
  164. `Expected exactly 1 track, but was given ${
  165. localTracks.length} tracks for device: ${
  166. device}.`);
  167. }
  168. if (gumProcess.canceled) {
  169. return _disposeTracks(localTracks)
  170. .then(() =>
  171. dispatch(_trackCreateCanceled(device as MediaType)));
  172. }
  173. return dispatch(trackAdded(localTracks[0]));
  174. },
  175. (reason: Error) =>
  176. dispatch(
  177. gumProcess.canceled
  178. ? _trackCreateCanceled(device as MediaType)
  179. : _onCreateLocalTracksRejected(
  180. reason,
  181. device)));
  182. promises.push(gumProcess.catch(() => undefined));
  183. /**
  184. * Cancels the {@code getUserMedia} process represented by this
  185. * {@code Promise}.
  186. *
  187. * @returns {Promise} This {@code Promise} i.e. {@code gumProcess}.
  188. */
  189. gumProcess.cancel = () => {
  190. gumProcess.canceled = true;
  191. return gumProcess;
  192. };
  193. dispatch({
  194. type: TRACK_WILL_CREATE,
  195. track: {
  196. gumProcess,
  197. local: true,
  198. mediaType: device
  199. }
  200. });
  201. }
  202. return Promise.all(promises);
  203. };
  204. }
  205. /**
  206. * Calls JitsiLocalTrack#dispose() on the given track or on all local tracks (if none are passed) ignoring errors if
  207. * track is already disposed. After that signals tracks to be removed.
  208. *
  209. * @param {JitsiLocalTrack|null} [track] - The local track that needs to be destroyed.
  210. * @returns {Function}
  211. */
  212. export function destroyLocalTracks(track: any = null) {
  213. if (track) {
  214. return (dispatch: IStore['dispatch']) => dispatch(_disposeAndRemoveTracks([ track ]));
  215. }
  216. return (dispatch: IStore['dispatch'], getState: IStore['getState']) =>
  217. // First wait until any getUserMedia in progress is settled and then get
  218. // rid of all local tracks.
  219. _cancelGUMProcesses(getState)
  220. .then(() =>
  221. dispatch(
  222. _disposeAndRemoveTracks(
  223. getState()['features/base/tracks']
  224. .filter(t => t.local)
  225. .map(t => t.jitsiTrack))));
  226. }
  227. /**
  228. * Signals that the passed JitsiLocalTrack has triggered a no data from source event.
  229. *
  230. * @param {JitsiLocalTrack} track - The track.
  231. * @returns {{
  232. * type: TRACK_NO_DATA_FROM_SOURCE,
  233. * track: Track
  234. * }}
  235. */
  236. export function noDataFromSource(track: any) {
  237. return {
  238. type: TRACK_NO_DATA_FROM_SOURCE,
  239. track
  240. };
  241. }
  242. /**
  243. * Displays a no data from source video error if needed.
  244. *
  245. * @param {JitsiLocalTrack} jitsiTrack - The track.
  246. * @returns {Function}
  247. */
  248. export function showNoDataFromSourceVideoError(jitsiTrack: any) {
  249. return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
  250. let notificationInfo;
  251. const track = getTrackByJitsiTrack(getState()['features/base/tracks'], jitsiTrack);
  252. if (!track) {
  253. return;
  254. }
  255. if (track.isReceivingData) {
  256. notificationInfo = undefined;
  257. } else {
  258. const notificationAction = dispatch(showErrorNotification({
  259. descriptionKey: 'dialog.cameraNotSendingData',
  260. titleKey: 'dialog.cameraNotSendingDataTitle'
  261. }));
  262. notificationInfo = {
  263. uid: notificationAction?.uid
  264. };
  265. }
  266. dispatch(trackNoDataFromSourceNotificationInfoChanged(jitsiTrack, notificationInfo));
  267. };
  268. }
  269. /**
  270. * Replaces one track with another for one renegotiation instead of invoking
  271. * two renegotiations with a separate removeTrack and addTrack. Disposes the
  272. * removed track as well.
  273. *
  274. * @param {JitsiLocalTrack|null} oldTrack - The track to dispose.
  275. * @param {JitsiLocalTrack|null} newTrack - The track to use instead.
  276. * @param {JitsiConference} [conference] - The conference from which to remove
  277. * and add the tracks. If one is not provided, the conference in the redux store
  278. * will be used.
  279. * @returns {Function}
  280. */
  281. export function replaceLocalTrack(oldTrack: any, newTrack: any, conference?: IJitsiConference) {
  282. return async (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
  283. conference
  284. // eslint-disable-next-line no-param-reassign
  285. || (conference = getState()['features/base/conference'].conference);
  286. if (conference) {
  287. await conference.replaceTrack(oldTrack, newTrack);
  288. }
  289. return dispatch(replaceStoredTracks(oldTrack, newTrack));
  290. };
  291. }
  292. /**
  293. * Replaces a stored track with another.
  294. *
  295. * @param {JitsiLocalTrack|null} oldTrack - The track to dispose.
  296. * @param {JitsiLocalTrack|null} newTrack - The track to use instead.
  297. * @returns {Function}
  298. */
  299. function replaceStoredTracks(oldTrack: any, newTrack: any) {
  300. return async (dispatch: IStore['dispatch']) => {
  301. // We call dispose after doing the replace because dispose will
  302. // try and do a new o/a after the track removes itself. Doing it
  303. // after means the JitsiLocalTrack.conference is already
  304. // cleared, so it won't try and do the o/a.
  305. if (oldTrack) {
  306. await dispatch(_disposeAndRemoveTracks([ oldTrack ]));
  307. }
  308. if (newTrack) {
  309. // The mute state of the new track should be reflected in the app's mute state. For example, if the
  310. // app is currently muted and changing to a new track that is not muted, the app's mute state
  311. // should be falsey. As such, emit a mute event here to set up the app to reflect the track's mute
  312. // state. If this is not done, the current mute state of the app will be reflected on the track,
  313. // not vice-versa.
  314. const setMuted = newTrack.isVideoTrack()
  315. ? newTrack.getVideoType() === VIDEO_TYPE.DESKTOP
  316. ? setScreenshareMuted
  317. : setVideoMuted
  318. : setAudioMuted;
  319. const isMuted = newTrack.isMuted();
  320. sendAnalytics(createTrackMutedEvent(newTrack.getType(), 'track.replaced', isMuted));
  321. logger.log(`Replace ${newTrack.getType()} track - ${isMuted ? 'muted' : 'unmuted'}`);
  322. dispatch(setMuted(isMuted));
  323. await dispatch(_addTracks([ newTrack ]));
  324. }
  325. };
  326. }
  327. /**
  328. * Create an action for when a new track has been signaled to be added to the
  329. * conference.
  330. *
  331. * @param {(JitsiLocalTrack|JitsiRemoteTrack)} track - JitsiTrack instance.
  332. * @returns {Function}
  333. */
  334. export function trackAdded(track: any) {
  335. return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
  336. track.on(
  337. JitsiTrackEvents.TRACK_MUTE_CHANGED,
  338. () => dispatch(trackMutedChanged(track)));
  339. track.on(
  340. JitsiTrackEvents.TRACK_VIDEOTYPE_CHANGED,
  341. (type: VideoType) => dispatch(trackVideoTypeChanged(track, type)));
  342. const local = track.isLocal();
  343. const mediaType = track.getVideoType() === VIDEO_TYPE.DESKTOP
  344. ? MEDIA_TYPE.SCREENSHARE
  345. : track.getType();
  346. let isReceivingData, noDataFromSourceNotificationInfo, participantId;
  347. if (local) {
  348. // Reset the no data from src notification state when we change the track, as it's context is set
  349. // on a per device basis.
  350. dispatch(setNoSrcDataNotificationUid());
  351. const participant = getLocalParticipant(getState);
  352. if (participant) {
  353. participantId = participant.id;
  354. }
  355. isReceivingData = track.isReceivingData();
  356. track.on(JitsiTrackEvents.NO_DATA_FROM_SOURCE, () => dispatch(noDataFromSource({ jitsiTrack: track })));
  357. if (!isReceivingData) {
  358. if (mediaType === MEDIA_TYPE.AUDIO) {
  359. const notificationAction = dispatch(showNotification({
  360. descriptionKey: 'dialog.micNotSendingData',
  361. titleKey: 'dialog.micNotSendingDataTitle'
  362. }, NOTIFICATION_TIMEOUT_TYPE.LONG));
  363. // Set the notification ID so that other parts of the application know that this was
  364. // displayed in the context of the current device.
  365. // I.E. The no-audio-signal notification shouldn't be displayed if this was already shown.
  366. dispatch(setNoSrcDataNotificationUid(notificationAction?.uid));
  367. noDataFromSourceNotificationInfo = { uid: notificationAction?.uid };
  368. } else {
  369. const timeout = setTimeout(() => dispatch(
  370. showNoDataFromSourceVideoError(track)),
  371. NOTIFICATION_TIMEOUT.MEDIUM);
  372. noDataFromSourceNotificationInfo = { timeout };
  373. }
  374. }
  375. track.on(JitsiTrackEvents.LOCAL_TRACK_STOPPED,
  376. () => dispatch({
  377. type: TRACK_STOPPED,
  378. track: {
  379. jitsiTrack: track
  380. }
  381. }));
  382. } else {
  383. participantId = track.getParticipantId();
  384. isReceivingData = true;
  385. }
  386. return dispatch({
  387. type: TRACK_ADDED,
  388. track: {
  389. jitsiTrack: track,
  390. isReceivingData,
  391. local,
  392. mediaType,
  393. mirror: _shouldMirror(track),
  394. muted: track.isMuted(),
  395. noDataFromSourceNotificationInfo,
  396. participantId,
  397. videoStarted: false,
  398. videoType: track.videoType
  399. }
  400. });
  401. };
  402. }
  403. /**
  404. * Create an action for when a track's codec has been signaled to have been changed.
  405. *
  406. * @param {JitsiLocalTrack} track - JitsiLocalTrack instance.
  407. * @param {string} codec - The video codec.
  408. * @returns {{
  409. * type: TRACK_UPDATED,
  410. * track: Track
  411. * }}
  412. */
  413. export function trackCodecChanged(track: ITrack, codec: string): {
  414. track: {
  415. codec: string;
  416. jitsiTrack: any;
  417. };
  418. type: 'TRACK_UPDATED';
  419. } {
  420. return {
  421. type: TRACK_UPDATED,
  422. track: {
  423. codec,
  424. jitsiTrack: track
  425. }
  426. };
  427. }
  428. /**
  429. * Create an action for when a track's muted state has been signaled to be
  430. * changed.
  431. *
  432. * @param {(JitsiLocalTrack|JitsiRemoteTrack)} track - JitsiTrack instance.
  433. * @returns {{
  434. * type: TRACK_UPDATED,
  435. * track: Track
  436. * }}
  437. */
  438. export function trackMutedChanged(track: any): {
  439. track: {
  440. jitsiTrack: any;
  441. muted: boolean;
  442. };
  443. type: 'TRACK_UPDATED';
  444. } {
  445. return {
  446. type: TRACK_UPDATED,
  447. track: {
  448. jitsiTrack: track,
  449. muted: track.isMuted()
  450. }
  451. };
  452. }
  453. /**
  454. * Create an action for when a track's muted state change action has failed. This could happen because of
  455. * {@code getUserMedia} errors during unmute or replace track errors at the peerconnection level.
  456. *
  457. * @param {(JitsiLocalTrack|JitsiRemoteTrack)} track - JitsiTrack instance.
  458. * @param {boolean} wasMuting - If the operation that failed was a mute operation or an unmute operation.
  459. * @returns {{
  460. * type: TRACK_MUTE_UNMUTE_FAILED,
  461. * track: Track
  462. * }}
  463. */
  464. export function trackMuteUnmuteFailed(track: any, wasMuting: boolean): {
  465. track: any;
  466. type: 'TRACK_MUTE_UNMUTE_FAILED';
  467. wasMuting: boolean;
  468. } {
  469. return {
  470. type: TRACK_MUTE_UNMUTE_FAILED,
  471. track,
  472. wasMuting
  473. };
  474. }
  475. /**
  476. * Create an action for when a track's no data from source notification information changes.
  477. *
  478. * @param {JitsiLocalTrack} track - JitsiTrack instance.
  479. * @param {Object} noDataFromSourceNotificationInfo - Information about no data from source notification.
  480. * @returns {{
  481. * type: TRACK_UPDATED,
  482. * track: Track
  483. * }}
  484. */
  485. export function trackNoDataFromSourceNotificationInfoChanged(track: any, noDataFromSourceNotificationInfo?: Object) {
  486. return {
  487. type: TRACK_UPDATED,
  488. track: {
  489. jitsiTrack: track,
  490. noDataFromSourceNotificationInfo
  491. }
  492. };
  493. }
  494. /**
  495. * Create an action for when a track has been signaled for removal from the
  496. * conference.
  497. *
  498. * @param {(JitsiLocalTrack|JitsiRemoteTrack)} track - JitsiTrack instance.
  499. * @returns {{
  500. * type: TRACK_REMOVED,
  501. * track: Track
  502. * }}
  503. */
  504. export function trackRemoved(track: any): {
  505. track: {
  506. jitsiTrack: any;
  507. };
  508. type: 'TRACK_REMOVED';
  509. } {
  510. track.removeAllListeners(JitsiTrackEvents.TRACK_MUTE_CHANGED);
  511. track.removeAllListeners(JitsiTrackEvents.TRACK_VIDEOTYPE_CHANGED);
  512. track.removeAllListeners(JitsiTrackEvents.NO_DATA_FROM_SOURCE);
  513. return {
  514. type: TRACK_REMOVED,
  515. track: {
  516. jitsiTrack: track
  517. }
  518. };
  519. }
  520. /**
  521. * Signal that track's video started to play.
  522. *
  523. * @param {(JitsiLocalTrack|JitsiRemoteTrack)} track - JitsiTrack instance.
  524. * @returns {{
  525. * type: TRACK_UPDATED,
  526. * track: Track
  527. * }}
  528. */
  529. export function trackVideoStarted(track: any): {
  530. track: {
  531. jitsiTrack: any;
  532. videoStarted: true;
  533. };
  534. type: 'TRACK_UPDATED';
  535. } {
  536. return {
  537. type: TRACK_UPDATED,
  538. track: {
  539. jitsiTrack: track,
  540. videoStarted: true
  541. }
  542. };
  543. }
  544. /**
  545. * Create an action for when participant video type changes.
  546. *
  547. * @param {(JitsiLocalTrack|JitsiRemoteTrack)} track - JitsiTrack instance.
  548. * @param {VIDEO_TYPE|undefined} videoType - Video type.
  549. * @returns {{
  550. * type: TRACK_UPDATED,
  551. * track: Track
  552. * }}
  553. */
  554. export function trackVideoTypeChanged(track: any, videoType: VideoType) {
  555. const mediaType = videoType === VIDEO_TYPE.CAMERA ? MEDIA_TYPE.VIDEO : MEDIA_TYPE.SCREENSHARE;
  556. return {
  557. type: TRACK_UPDATED,
  558. track: {
  559. jitsiTrack: track,
  560. videoType,
  561. mediaType
  562. }
  563. };
  564. }
  565. /**
  566. * Create an action for when track streaming status changes.
  567. *
  568. * @param {(JitsiRemoteTrack)} track - JitsiTrack instance.
  569. * @param {string} streamingStatus - The new streaming status of the track.
  570. * @returns {{
  571. * type: TRACK_UPDATED,
  572. * track: Track
  573. * }}
  574. */
  575. export function trackStreamingStatusChanged(track: any, streamingStatus: string): {
  576. track: {
  577. jitsiTrack: any;
  578. streamingStatus: string;
  579. };
  580. type: 'TRACK_UPDATED';
  581. } {
  582. return {
  583. type: TRACK_UPDATED,
  584. track: {
  585. jitsiTrack: track,
  586. streamingStatus
  587. }
  588. };
  589. }
  590. /**
  591. * Signals passed tracks to be added.
  592. *
  593. * @param {(JitsiLocalTrack|JitsiRemoteTrack)[]} tracks - List of tracks.
  594. * @private
  595. * @returns {Function}
  596. */
  597. function _addTracks(tracks: any[]) {
  598. return (dispatch: IStore['dispatch']) => Promise.all(tracks.map(t => dispatch(trackAdded(t))));
  599. }
  600. /**
  601. * Cancels and waits for any {@code getUserMedia} process/currently in progress
  602. * to complete/settle.
  603. *
  604. * @param {Function} getState - The redux store {@code getState} function used
  605. * to obtain the state.
  606. * @private
  607. * @returns {Promise} - A {@code Promise} resolved once all
  608. * {@code gumProcess.cancel()} {@code Promise}s are settled because all we care
  609. * about here is to be sure that the {@code getUserMedia} callbacks have
  610. * completed (i.e. Returned from the native side).
  611. */
  612. function _cancelGUMProcesses(getState: IStore['getState']): Promise<any> {
  613. const logError
  614. = (error: Error) =>
  615. logger.error('gumProcess.cancel failed', JSON.stringify(error));
  616. return Promise.all(
  617. getState()['features/base/tracks']
  618. .filter(t => t.local)
  619. .map(({ gumProcess }: any) =>
  620. gumProcess?.cancel().catch(logError)));
  621. }
  622. /**
  623. * Disposes passed tracks and signals them to be removed.
  624. *
  625. * @param {(JitsiLocalTrack|JitsiRemoteTrack)[]} tracks - List of tracks.
  626. * @protected
  627. * @returns {Function}
  628. */
  629. export function _disposeAndRemoveTracks(tracks: any[]) {
  630. return (dispatch: IStore['dispatch']) =>
  631. _disposeTracks(tracks)
  632. .then(() =>
  633. Promise.all(tracks.map(t => dispatch(trackRemoved(t)))));
  634. }
  635. /**
  636. * Disposes passed tracks.
  637. *
  638. * @param {(JitsiLocalTrack|JitsiRemoteTrack)[]} tracks - List of tracks.
  639. * @private
  640. * @returns {Promise} - A Promise resolved once {@link JitsiTrack.dispose()} is
  641. * done for every track from the list.
  642. */
  643. function _disposeTracks(tracks: any[]): Promise<any> {
  644. return Promise.all(
  645. tracks.map(t =>
  646. t.dispose()
  647. .catch((err: Error) => {
  648. // Track might be already disposed so ignore such an error.
  649. // Of course, re-throw any other error(s).
  650. if (err.name !== JitsiTrackErrors.TRACK_IS_DISPOSED) {
  651. throw err;
  652. }
  653. })));
  654. }
  655. /**
  656. * Implements the {@code Promise} rejection handler of
  657. * {@code createLocalTracksA} and {@code createLocalTracksF}.
  658. *
  659. * @param {Object} error - The {@code Promise} rejection reason.
  660. * @param {string} device - The device/{@code MEDIA_TYPE} associated with the
  661. * rejection.
  662. * @private
  663. * @returns {Function}
  664. */
  665. function _onCreateLocalTracksRejected(error?: Error, device?: string) {
  666. return (dispatch: IStore['dispatch']) => {
  667. // If permissions are not allowed, alert the user.
  668. dispatch({
  669. type: TRACK_CREATE_ERROR,
  670. permissionDenied: error?.name === 'SecurityError',
  671. trackType: device
  672. });
  673. };
  674. }
  675. /**
  676. * Returns true if the provided {@code JitsiTrack} should be rendered as a
  677. * mirror.
  678. *
  679. * We only want to show a video in mirrored mode when:
  680. * 1) The video source is local, and not remote.
  681. * 2) The video source is a camera, not a desktop (capture).
  682. * 3) The camera is capturing the user, not the environment.
  683. *
  684. * TODO Similar functionality is part of lib-jitsi-meet. This function should be
  685. * removed after https://github.com/jitsi/lib-jitsi-meet/pull/187 is merged.
  686. *
  687. * @param {(JitsiLocalTrack|JitsiRemoteTrack)} track - JitsiTrack instance.
  688. * @private
  689. * @returns {boolean}
  690. */
  691. function _shouldMirror(track: any): boolean {
  692. return (
  693. track?.isLocal()
  694. && track?.isVideoTrack()
  695. // XXX The type of the return value of JitsiLocalTrack's
  696. // getCameraFacingMode happens to be named CAMERA_FACING_MODE as
  697. // well, it's defined by lib-jitsi-meet. Note though that the type
  698. // of the value on the right side of the equality check is defined
  699. // by jitsi-meet. The type definitions are surely compatible today
  700. // but that may not be the case tomorrow.
  701. && track?.getCameraFacingMode() === CAMERA_FACING_MODE.USER);
  702. }
  703. /**
  704. * Signals that track create operation for given media track has been canceled.
  705. * Will clean up local track stub from the redux state which holds the
  706. * {@code gumProcess} reference.
  707. *
  708. * @param {MEDIA_TYPE} mediaType - The type of the media for which the track was
  709. * being created.
  710. * @private
  711. * @returns {{
  712. * type,
  713. * trackType: MEDIA_TYPE
  714. * }}
  715. */
  716. function _trackCreateCanceled(mediaType: MediaType): {
  717. trackType: MediaType;
  718. type: 'TRACK_CREATE_CANCELED';
  719. } {
  720. return {
  721. type: TRACK_CREATE_CANCELED,
  722. trackType: mediaType
  723. };
  724. }
  725. /**
  726. * If the local track if of type Desktop, it calls _disposeAndRemoveTracks) on it.
  727. *
  728. * @returns {Function}
  729. */
  730. export function destroyLocalDesktopTrackIfExists() {
  731. return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
  732. const videoTrack = getLocalVideoTrack(getState()['features/base/tracks']);
  733. const isDesktopTrack = videoTrack && videoTrack.videoType === VIDEO_TYPE.DESKTOP;
  734. if (isDesktopTrack) {
  735. dispatch(_disposeAndRemoveTracks([ videoTrack.jitsiTrack ]));
  736. }
  737. };
  738. }
  739. /**
  740. * Sets UID of the displayed no data from source notification. Used to track
  741. * if the notification was previously displayed in this context.
  742. *
  743. * @param {number} uid - Notification UID.
  744. * @returns {{
  745. * type: SET_NO_AUDIO_SIGNAL_UID,
  746. * uid: string
  747. * }}
  748. */
  749. export function setNoSrcDataNotificationUid(uid?: string) {
  750. return {
  751. type: SET_NO_SRC_DATA_NOTIFICATION_UID,
  752. uid
  753. };
  754. }
  755. /**
  756. * Toggles the facingMode constraint on the video stream.
  757. *
  758. * @returns {Function}
  759. */
  760. export function toggleCamera() {
  761. return async (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
  762. const state = getState();
  763. const tracks = state['features/base/tracks'];
  764. const localVideoTrack = getLocalVideoTrack(tracks)?.jitsiTrack;
  765. const currentFacingMode = localVideoTrack.getCameraFacingMode();
  766. const { localFlipX } = state['features/base/settings'];
  767. /**
  768. * FIXME: Ideally, we should be dispatching {@code replaceLocalTrack} here,
  769. * but it seems to not trigger the re-rendering of the local video on Chrome;
  770. * could be due to a plan B vs unified plan issue. Therefore, we use the legacy
  771. * method defined in conference.js that manually takes care of updating the local
  772. * video as well.
  773. */
  774. await APP.conference.useVideoStream(null);
  775. const targetFacingMode = currentFacingMode === CAMERA_FACING_MODE.USER
  776. ? CAMERA_FACING_MODE.ENVIRONMENT
  777. : CAMERA_FACING_MODE.USER;
  778. // Update the flipX value so the environment facing camera is not flipped, before the new track is created.
  779. dispatch(updateSettings({ localFlipX: targetFacingMode === CAMERA_FACING_MODE.USER ? localFlipX : false }));
  780. const newVideoTrack = await createLocalTrack('video', null, null, { facingMode: targetFacingMode });
  781. // FIXME: See above.
  782. await APP.conference.useVideoStream(newVideoTrack);
  783. };
  784. }