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

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