You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

actions.js 18KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526
  1. import { sendAnalyticsEvent } from '../../analytics';
  2. import { JitsiTrackErrors, JitsiTrackEvents } from '../lib-jitsi-meet';
  3. import {
  4. CAMERA_FACING_MODE,
  5. MEDIA_TYPE,
  6. setAudioMuted,
  7. setVideoMuted
  8. } from '../media';
  9. import { getLocalParticipant } from '../participants';
  10. import {
  11. TRACK_ADDED,
  12. TRACK_BEING_CREATED,
  13. TRACK_CREATE_CANCELED,
  14. TRACK_CREATE_ERROR,
  15. TRACK_REMOVED,
  16. TRACK_UPDATED
  17. } from './actionTypes';
  18. import { createLocalTracksF } from './functions';
  19. const logger = require('jitsi-meet-logger').getLogger(__filename);
  20. /**
  21. * Requests the creating of the desired media type tracks. Desire is expressed
  22. * by base/media unless the function caller specifies desired media types
  23. * explicitly and thus override base/media. Dispatches a
  24. * {@code createLocalTracksA} action for the desired media types for which there
  25. * are no existing tracks yet.
  26. *
  27. * @returns {Function}
  28. */
  29. export function createDesiredLocalTracks(...desiredTypes) {
  30. return (dispatch, getState) => {
  31. const state = getState();
  32. if (desiredTypes.length === 0) {
  33. const { audio, video } = state['features/base/media'];
  34. audio.muted || desiredTypes.push(MEDIA_TYPE.AUDIO);
  35. Boolean(video.muted) || desiredTypes.push(MEDIA_TYPE.VIDEO);
  36. }
  37. const availableTypes
  38. = state['features/base/tracks']
  39. .filter(t => t.local)
  40. .map(t => t.mediaType);
  41. // We need to create the desired tracks which are not already available.
  42. const createTypes
  43. = desiredTypes.filter(type => availableTypes.indexOf(type) === -1);
  44. createTypes.length
  45. && dispatch(createLocalTracksA({ devices: createTypes }));
  46. };
  47. }
  48. /**
  49. * Request to start capturing local audio and/or video. By default, the user
  50. * facing camera will be selected.
  51. *
  52. * @param {Object} [options] - For info @see JitsiMeetJS.createLocalTracks.
  53. * @returns {Function}
  54. */
  55. export function createLocalTracksA(options = {}) {
  56. return (dispatch, getState) => {
  57. const devices
  58. = options.devices || [ MEDIA_TYPE.AUDIO, MEDIA_TYPE.VIDEO ];
  59. const store = {
  60. dispatch,
  61. getState
  62. };
  63. // The following executes on React Native only at the time of this
  64. // writing. The effort to port Web's createInitialLocalTracksAndConnect
  65. // is significant and that's where the function createLocalTracksF got
  66. // born. I started with the idea a porting so that we could inherit the
  67. // ability to getUserMedia for audio only or video only if getUserMedia
  68. // for audio and video fails. Eventually though, I realized that on
  69. // mobile we do not have combined permission prompts implemented anyway
  70. // (either because there are no such prompts or it does not make sense
  71. // to implement them) and the right thing to do is to ask for each
  72. // device separately.
  73. for (const device of devices) {
  74. if (getState()['features/base/tracks']
  75. .find(t => t.local && t.mediaType === device)) {
  76. throw new Error(`Local track for ${device} already exists`);
  77. }
  78. const gumProcess = createLocalTracksF(
  79. {
  80. cameraDeviceId: options.cameraDeviceId,
  81. devices: [ device ],
  82. facingMode: options.facingMode || CAMERA_FACING_MODE.USER,
  83. micDeviceId: options.micDeviceId
  84. },
  85. /* firePermissionPromptIsShownEvent */ false,
  86. store)
  87. .then(
  88. localTracks => {
  89. // Because GUM is called for 1 device (which is actually
  90. // a media type 'audio','video', 'screen' etc.) we should
  91. // not get more than one JitsiTrack.
  92. if (localTracks.length !== 1) {
  93. throw new Error(
  94. 'Expected exactly 1 track, but was '
  95. + `given ${localTracks.length} tracks`
  96. + `for device: ${device}.`);
  97. }
  98. if (gumProcess.canceled) {
  99. return _disposeTracks(localTracks)
  100. .then(
  101. () =>
  102. dispatch(
  103. _trackCreateCanceled(device)));
  104. }
  105. return dispatch(trackAdded(localTracks[0]));
  106. },
  107. // eslint-disable-next-line no-confusing-arrow
  108. reason =>
  109. dispatch(
  110. gumProcess.canceled
  111. ? _trackCreateCanceled(device)
  112. : _onCreateLocalTracksRejected(reason, device)));
  113. gumProcess.cancel = () => {
  114. gumProcess.canceled = true;
  115. return gumProcess;
  116. };
  117. dispatch({
  118. type: TRACK_BEING_CREATED,
  119. track: {
  120. local: true,
  121. gumProcess,
  122. mediaType: device
  123. }
  124. });
  125. }
  126. };
  127. }
  128. /**
  129. * Calls JitsiLocalTrack#dispose() on all local tracks ignoring errors when
  130. * track is already disposed. After that signals tracks to be removed.
  131. *
  132. * @returns {Function}
  133. */
  134. export function destroyLocalTracks() {
  135. return (dispatch, getState) => {
  136. // First wait until any getUserMedia in progress is settled and then get
  137. // rid of all local tracks.
  138. _cancelAllGumInProgress(getState)
  139. .then(
  140. () => dispatch(
  141. _disposeAndRemoveTracks(
  142. getState()['features/base/tracks']
  143. .filter(t => t.local)
  144. .map(t => t.jitsiTrack))));
  145. };
  146. }
  147. /**
  148. * Replaces one track with another for one renegotiation instead of invoking
  149. * two renegotiations with a separate removeTrack and addTrack. Disposes the
  150. * removed track as well.
  151. *
  152. * @param {JitsiLocalTrack|null} oldTrack - The track to dispose.
  153. * @param {JitsiLocalTrack|null} newTrack - The track to use instead.
  154. * @param {JitsiConference} [conference] - The conference from which to remove
  155. * and add the tracks. If one is not provided, the conference in the redux store
  156. * will be used.
  157. * @returns {Function}
  158. */
  159. export function replaceLocalTrack(oldTrack, newTrack, conference) {
  160. return (dispatch, getState) => {
  161. conference
  162. // eslint-disable-next-line no-param-reassign
  163. || (conference = getState()['features/base/conference'].conference);
  164. return conference.replaceTrack(oldTrack, newTrack)
  165. .then(() => {
  166. // We call dispose after doing the replace because dispose will
  167. // try and do a new o/a after the track removes itself. Doing it
  168. // after means the JitsiLocalTrack.conference is already
  169. // cleared, so it won't try and do the o/a.
  170. const disposePromise
  171. = oldTrack
  172. ? dispatch(_disposeAndRemoveTracks([ oldTrack ]))
  173. : Promise.resolve();
  174. return disposePromise
  175. .then(() => {
  176. if (newTrack) {
  177. // The mute state of the new track should be
  178. // reflected in the app's mute state. For example,
  179. // if the app is currently muted and changing to a
  180. // new track that is not muted, the app's mute
  181. // state should be falsey. As such, emit a mute
  182. // event here to set up the app to reflect the
  183. // track's mute state. If this is not done, the
  184. // current mute state of the app will be reflected
  185. // on the track, not vice-versa.
  186. const setMuted
  187. = newTrack.isVideoTrack()
  188. ? setVideoMuted
  189. : setAudioMuted;
  190. const isMuted = newTrack.isMuted();
  191. sendAnalyticsEvent(`replacetrack.${
  192. newTrack.getType()}.${
  193. isMuted ? 'muted' : 'unmuted'}`);
  194. logger.log(`Replace ${newTrack.getType()} track - ${
  195. isMuted ? 'muted' : 'unmuted'}`);
  196. return dispatch(setMuted(isMuted));
  197. }
  198. })
  199. .then(() => {
  200. if (newTrack) {
  201. return dispatch(_addTracks([ newTrack ]));
  202. }
  203. });
  204. });
  205. };
  206. }
  207. /**
  208. * Create an action for when a new track has been signaled to be added to the
  209. * conference.
  210. *
  211. * @param {(JitsiLocalTrack|JitsiRemoteTrack)} track - JitsiTrack instance.
  212. * @returns {{ type: TRACK_ADDED, track: Track }}
  213. */
  214. export function trackAdded(track) {
  215. return (dispatch, getState) => {
  216. track.on(
  217. JitsiTrackEvents.TRACK_MUTE_CHANGED,
  218. () => dispatch(trackMutedChanged(track)));
  219. track.on(
  220. JitsiTrackEvents.TRACK_VIDEOTYPE_CHANGED,
  221. type => dispatch(trackVideoTypeChanged(track, type)));
  222. // participantId
  223. const local = track.isLocal();
  224. let participantId;
  225. if (local) {
  226. const participant = getLocalParticipant(getState);
  227. if (participant) {
  228. participantId = participant.id;
  229. }
  230. } else {
  231. participantId = track.getParticipantId();
  232. }
  233. return dispatch({
  234. type: TRACK_ADDED,
  235. track: {
  236. jitsiTrack: track,
  237. local,
  238. mediaType: track.getType(),
  239. mirror: _shouldMirror(track),
  240. muted: track.isMuted(),
  241. participantId,
  242. videoStarted: false,
  243. videoType: track.videoType
  244. }
  245. });
  246. };
  247. }
  248. /**
  249. * Create an action for when a track's muted state has been signaled to be
  250. * changed.
  251. *
  252. * @param {(JitsiLocalTrack|JitsiRemoteTrack)} track - JitsiTrack instance.
  253. * @returns {{
  254. * type: TRACK_UPDATED,
  255. * track: Track
  256. * }}
  257. */
  258. export function trackMutedChanged(track) {
  259. return {
  260. type: TRACK_UPDATED,
  261. track: {
  262. jitsiTrack: track,
  263. muted: track.isMuted()
  264. }
  265. };
  266. }
  267. /**
  268. * Create an action for when a track has been signaled for removal from the
  269. * conference.
  270. *
  271. * @param {(JitsiLocalTrack|JitsiRemoteTrack)} track - JitsiTrack instance.
  272. * @returns {{
  273. * type: TRACK_REMOVED,
  274. * track: Track
  275. * }}
  276. */
  277. export function trackRemoved(track) {
  278. track.removeAllListeners(JitsiTrackEvents.TRACK_MUTE_CHANGED);
  279. track.removeAllListeners(JitsiTrackEvents.TRACK_VIDEOTYPE_CHANGED);
  280. return {
  281. type: TRACK_REMOVED,
  282. track: {
  283. jitsiTrack: track
  284. }
  285. };
  286. }
  287. /**
  288. * Signal that track's video started to play.
  289. *
  290. * @param {(JitsiLocalTrack|JitsiRemoteTrack)} track - JitsiTrack instance.
  291. * @returns {{
  292. * type: TRACK_UPDATED,
  293. * track: Track
  294. * }}
  295. */
  296. export function trackVideoStarted(track) {
  297. return {
  298. type: TRACK_UPDATED,
  299. track: {
  300. jitsiTrack: track,
  301. videoStarted: true
  302. }
  303. };
  304. }
  305. /**
  306. * Create an action for when participant video type changes.
  307. *
  308. * @param {(JitsiLocalTrack|JitsiRemoteTrack)} track - JitsiTrack instance.
  309. * @param {VIDEO_TYPE|undefined} videoType - Video type.
  310. * @returns {{
  311. * type: TRACK_UPDATED,
  312. * track: Track
  313. * }}
  314. */
  315. export function trackVideoTypeChanged(track, videoType) {
  316. return {
  317. type: TRACK_UPDATED,
  318. track: {
  319. jitsiTrack: track,
  320. videoType
  321. }
  322. };
  323. }
  324. /**
  325. * Signals passed tracks to be added.
  326. *
  327. * @param {(JitsiLocalTrack|JitsiRemoteTrack)[]} tracks - List of tracks.
  328. * @private
  329. * @returns {Function}
  330. */
  331. function _addTracks(tracks) {
  332. return dispatch => Promise.all(tracks.map(t => dispatch(trackAdded(t))));
  333. }
  334. /**
  335. * Signals that track create operation for given media track has been canceled.
  336. * Will clean up local track stub from the Redux state which holds the
  337. * 'gumProcess' reference.
  338. *
  339. * @param {MEDIA_TYPE} mediaType - The type of the media for which the track was
  340. * being created.
  341. * @returns {{
  342. * type,
  343. * trackType: MEDIA_TYPE
  344. * }}
  345. * @private
  346. */
  347. function _trackCreateCanceled(mediaType) {
  348. return {
  349. type: TRACK_CREATE_CANCELED,
  350. trackType: mediaType
  351. };
  352. }
  353. /**
  354. * Cancels and waits for any get user media operations currently in progress to
  355. * complete.
  356. *
  357. * @param {Function} getState - The Redux store {@code getState} method used to
  358. * obtain the state.
  359. * @returns {Promise} - A Promise resolved once all {@code gumProcess.cancel}
  360. * Promises are settled. That is when they are either resolved or rejected,
  361. * because all we care about here is to be sure that get user media callbacks
  362. * have completed (returned from the native side).
  363. * @private
  364. */
  365. function _cancelAllGumInProgress(getState) {
  366. // FIXME use logger
  367. const logError
  368. = error =>
  369. console.error('gumProcess.cancel failed', JSON.stringify(error));
  370. return Promise.all(
  371. getState()['features/base/tracks']
  372. .filter(t => t.local)
  373. .map(
  374. t => t.gumProcess
  375. && t.gumProcess.cancel().catch(logError)));
  376. }
  377. /**
  378. * Disposes passed tracks and signals them to be removed.
  379. *
  380. * @param {(JitsiLocalTrack|JitsiRemoteTrack)[]} tracks - List of tracks.
  381. * @protected
  382. * @returns {Function}
  383. */
  384. export function _disposeAndRemoveTracks(tracks) {
  385. return dispatch =>
  386. _disposeTracks(tracks)
  387. .then(
  388. () => Promise.all(tracks.map(t => dispatch(trackRemoved(t)))));
  389. }
  390. /**
  391. * Disposes passed tracks.
  392. *
  393. * @param {(JitsiLocalTrack|JitsiRemoteTrack)[]} tracks - List of tracks.
  394. * @protected
  395. * @returns {Promise} - A Promise resolved once {@link JitsiTrack.dispose()} is
  396. * done for every track from the list.
  397. */
  398. function _disposeTracks(tracks) {
  399. return Promise.all(
  400. tracks.map(t =>
  401. t.dispose()
  402. .catch(err => {
  403. // Track might be already disposed so ignore such an
  404. // error. Of course, re-throw any other error(s).
  405. if (err.name !== JitsiTrackErrors.TRACK_IS_DISPOSED) {
  406. throw err;
  407. }
  408. })
  409. ));
  410. }
  411. /**
  412. * Implements the {@code Promise} rejection handler of
  413. * {@code createLocalTracksA} and {@code createLocalTracksF}.
  414. *
  415. * @param {Object} reason - The {@code Promise} rejection reason.
  416. * @param {string} device - The device/{@code MEDIA_TYPE} associated with the
  417. * rejection.
  418. * @private
  419. * @returns {Function}
  420. */
  421. function _onCreateLocalTracksRejected({ gum }, device) {
  422. return dispatch => {
  423. // If permissions are not allowed, alert the user.
  424. if (gum) {
  425. const { error } = gum;
  426. if (error) {
  427. // FIXME For whatever reason (which is probably an
  428. // implementation fault), react-native-webrtc will give the
  429. // error in one of the following formats depending on whether it
  430. // is attached to a remote debugger or not. (The remote debugger
  431. // scenario suggests that react-native-webrtc is at fault
  432. // because the remote debugger is Google Chrome and then its
  433. // JavaScript engine will define DOMException. I suspect I wrote
  434. // react-native-webrtc to return the error in the alternative
  435. // format if DOMException is not defined.)
  436. let trackPermissionError;
  437. switch (error.name) {
  438. case 'DOMException':
  439. trackPermissionError = error.message === 'NotAllowedError';
  440. break;
  441. case 'NotAllowedError':
  442. trackPermissionError = error instanceof DOMException;
  443. break;
  444. }
  445. dispatch({
  446. type: TRACK_CREATE_ERROR,
  447. permissionDenied: trackPermissionError,
  448. trackType: device
  449. });
  450. }
  451. }
  452. };
  453. }
  454. /**
  455. * Returns true if the provided JitsiTrack should be rendered as a mirror.
  456. *
  457. * We only want to show a video in mirrored mode when:
  458. * 1) The video source is local, and not remote.
  459. * 2) The video source is a camera, not a desktop (capture).
  460. * 3) The camera is capturing the user, not the environment.
  461. *
  462. * TODO Similar functionality is part of lib-jitsi-meet. This function should be
  463. * removed after https://github.com/jitsi/lib-jitsi-meet/pull/187 is merged.
  464. *
  465. * @param {(JitsiLocalTrack|JitsiRemoteTrack)} track - JitsiTrack instance.
  466. * @private
  467. * @returns {boolean}
  468. */
  469. function _shouldMirror(track) {
  470. return (
  471. track
  472. && track.isLocal()
  473. && track.isVideoTrack()
  474. // XXX The type of the return value of JitsiLocalTrack's
  475. // getCameraFacingMode happens to be named CAMERA_FACING_MODE as
  476. // well, it's defined by lib-jitsi-meet. Note though that the type
  477. // of the value on the right side of the equality check is defined
  478. // by jitsi-meet. The type definitions are surely compatible today
  479. // but that may not be the case tomorrow.
  480. && track.getCameraFacingMode() === CAMERA_FACING_MODE.USER);
  481. }