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.

functions.js 13KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363
  1. /* global APP */
  2. import { createScreenshotCaptureEffect } from '../../stream-effects/screenshot-capture';
  3. import { getBlurEffect } from '../../blur';
  4. import JitsiMeetJS, { JitsiTrackErrors, browser } from '../lib-jitsi-meet';
  5. import { MEDIA_TYPE } from '../media';
  6. import {
  7. getUserSelectedCameraDeviceId,
  8. getUserSelectedMicDeviceId
  9. } from '../settings';
  10. import logger from './logger';
  11. /**
  12. * Creates a local video track for presenter. The constraints are computed based
  13. * on the height of the desktop that is being shared.
  14. *
  15. * @param {Object} options - The options with which the local presenter track
  16. * is to be created.
  17. * @param {string|null} [options.cameraDeviceId] - Camera device id or
  18. * {@code undefined} to use app's settings.
  19. * @param {number} desktopHeight - The height of the desktop that is being
  20. * shared.
  21. * @returns {Promise<JitsiLocalTrack>}
  22. */
  23. export async function createLocalPresenterTrack(options, desktopHeight) {
  24. const { cameraDeviceId } = options;
  25. // compute the constraints of the camera track based on the resolution
  26. // of the desktop screen that is being shared.
  27. const cameraHeights = [ 180, 270, 360, 540, 720 ];
  28. const proportion = 4;
  29. const result = cameraHeights.find(
  30. height => (desktopHeight / proportion) < height);
  31. const constraints = {
  32. video: {
  33. aspectRatio: 4 / 3,
  34. height: {
  35. ideal: result
  36. }
  37. }
  38. };
  39. const [ videoTrack ] = await JitsiMeetJS.createLocalTracks(
  40. {
  41. cameraDeviceId,
  42. constraints,
  43. devices: [ 'video' ]
  44. });
  45. videoTrack.type = MEDIA_TYPE.PRESENTER;
  46. return videoTrack;
  47. }
  48. /**
  49. * Create local tracks of specific types.
  50. *
  51. * @param {Object} options - The options with which the local tracks are to be
  52. * created.
  53. * @param {string|null} [options.cameraDeviceId] - Camera device id or
  54. * {@code undefined} to use app's settings.
  55. * @param {string[]} options.devices - Required track types such as 'audio'
  56. * and/or 'video'.
  57. * @param {string|null} [options.micDeviceId] - Microphone device id or
  58. * {@code undefined} to use app's settings.
  59. * @param {boolean} [firePermissionPromptIsShownEvent] - Whether lib-jitsi-meet
  60. * should check for a {@code getUserMedia} permission prompt and fire a
  61. * corresponding event.
  62. * @param {Object} store - The redux store in the context of which the function
  63. * is to execute and from which state such as {@code config} is to be retrieved.
  64. * @returns {Promise<JitsiLocalTrack[]>}
  65. */
  66. export function createLocalTracksF(
  67. options,
  68. firePermissionPromptIsShownEvent,
  69. store) {
  70. options || (options = {}); // eslint-disable-line no-param-reassign
  71. let { cameraDeviceId, micDeviceId } = options;
  72. if (typeof APP !== 'undefined') {
  73. // TODO The app's settings should go in the redux store and then the
  74. // reliance on the global variable APP will go away.
  75. store || (store = APP.store); // eslint-disable-line no-param-reassign
  76. const state = store.getState();
  77. if (typeof cameraDeviceId === 'undefined' || cameraDeviceId === null) {
  78. cameraDeviceId = getUserSelectedCameraDeviceId(state);
  79. }
  80. if (typeof micDeviceId === 'undefined' || micDeviceId === null) {
  81. micDeviceId = getUserSelectedMicDeviceId(state);
  82. }
  83. }
  84. const state = store.getState();
  85. const {
  86. desktopSharingFrameRate,
  87. firefox_fake_device, // eslint-disable-line camelcase
  88. resolution
  89. } = state['features/base/config'];
  90. const constraints = options.constraints
  91. ?? state['features/base/config'].constraints;
  92. const blurPromise = state['features/blur'].blurEnabled
  93. ? getBlurEffect()
  94. .catch(error => {
  95. logger.error('Failed to obtain the blur effect instance with error: ', error);
  96. return Promise.resolve();
  97. })
  98. : Promise.resolve();
  99. const screenshotCapturePromise = state['features/screenshot-capture'].capturesEnabled
  100. ? createScreenshotCaptureEffect(state)
  101. .catch(error => {
  102. logger.error('Failed to obtain the screenshot capture effect effect instance with error: ', error);
  103. return Promise.resolve();
  104. })
  105. : Promise.resolve();
  106. const loadEffectsPromise = Promise.all([ blurPromise, screenshotCapturePromise ]);
  107. return (
  108. loadEffectsPromise.then(effectsArray => {
  109. // Filter any undefined values returned by Promise.resolve().
  110. const effects = effectsArray.filter(effect => Boolean(effect));
  111. return JitsiMeetJS.createLocalTracks(
  112. {
  113. cameraDeviceId,
  114. constraints,
  115. desktopSharingExtensionExternalInstallation:
  116. options.desktopSharingExtensionExternalInstallation,
  117. desktopSharingFrameRate,
  118. desktopSharingSourceDevice:
  119. options.desktopSharingSourceDevice,
  120. desktopSharingSources: options.desktopSharingSources,
  121. // Copy array to avoid mutations inside library.
  122. devices: options.devices.slice(0),
  123. effects,
  124. firefox_fake_device, // eslint-disable-line camelcase
  125. micDeviceId,
  126. resolution
  127. },
  128. firePermissionPromptIsShownEvent)
  129. .catch(err => {
  130. logger.error('Failed to create local tracks', options.devices, err);
  131. return Promise.reject(err);
  132. });
  133. }));
  134. }
  135. /**
  136. * Returns local audio track.
  137. *
  138. * @param {Track[]} tracks - List of all tracks.
  139. * @returns {(Track|undefined)}
  140. */
  141. export function getLocalAudioTrack(tracks) {
  142. return getLocalTrack(tracks, MEDIA_TYPE.AUDIO);
  143. }
  144. /**
  145. * Returns local track by media type.
  146. *
  147. * @param {Track[]} tracks - List of all tracks.
  148. * @param {MEDIA_TYPE} mediaType - Media type.
  149. * @param {boolean} [includePending] - Indicates whether a local track is to be
  150. * returned if it is still pending. A local track is pending if
  151. * {@code getUserMedia} is still executing to create it and, consequently, its
  152. * {@code jitsiTrack} property is {@code undefined}. By default a pending local
  153. * track is not returned.
  154. * @returns {(Track|undefined)}
  155. */
  156. export function getLocalTrack(tracks, mediaType, includePending = false) {
  157. return (
  158. getLocalTracks(tracks, includePending)
  159. .find(t => t.mediaType === mediaType));
  160. }
  161. /**
  162. * Returns an array containing the local tracks with or without a (valid)
  163. * {@code JitsiTrack}.
  164. *
  165. * @param {Track[]} tracks - An array containing all local tracks.
  166. * @param {boolean} [includePending] - Indicates whether a local track is to be
  167. * returned if it is still pending. A local track is pending if
  168. * {@code getUserMedia} is still executing to create it and, consequently, its
  169. * {@code jitsiTrack} property is {@code undefined}. By default a pending local
  170. * track is not returned.
  171. * @returns {Track[]}
  172. */
  173. export function getLocalTracks(tracks, includePending = false) {
  174. // XXX A local track is considered ready only once it has its `jitsiTrack`
  175. // property set by the `TRACK_ADDED` action. Until then there is a stub
  176. // added just before the `getUserMedia` call with a cancellable
  177. // `gumInProgress` property which then can be used to destroy the track that
  178. // has not yet been added to the redux store. Once GUM is cancelled, it will
  179. // never make it to the store nor there will be any
  180. // `TRACK_ADDED`/`TRACK_REMOVED` actions dispatched for it.
  181. return tracks.filter(t => t.local && (t.jitsiTrack || includePending));
  182. }
  183. /**
  184. * Returns local video track.
  185. *
  186. * @param {Track[]} tracks - List of all tracks.
  187. * @returns {(Track|undefined)}
  188. */
  189. export function getLocalVideoTrack(tracks) {
  190. return getLocalTrack(tracks, MEDIA_TYPE.VIDEO);
  191. }
  192. /**
  193. * Returns the media type of the local video, presenter or video.
  194. *
  195. * @param {Track[]} tracks - List of all tracks.
  196. * @returns {MEDIA_TYPE}
  197. */
  198. export function getLocalVideoType(tracks) {
  199. const presenterTrack = getLocalTrack(tracks, MEDIA_TYPE.PRESENTER);
  200. return presenterTrack ? MEDIA_TYPE.PRESENTER : MEDIA_TYPE.VIDEO;
  201. }
  202. /**
  203. * Returns track of specified media type for specified participant id.
  204. *
  205. * @param {Track[]} tracks - List of all tracks.
  206. * @param {MEDIA_TYPE} mediaType - Media type.
  207. * @param {string} participantId - Participant ID.
  208. * @returns {(Track|undefined)}
  209. */
  210. export function getTrackByMediaTypeAndParticipant(
  211. tracks,
  212. mediaType,
  213. participantId) {
  214. return tracks.find(
  215. t => t.participantId === participantId && t.mediaType === mediaType
  216. );
  217. }
  218. /**
  219. * Returns the track if any which corresponds to a specific instance
  220. * of JitsiLocalTrack or JitsiRemoteTrack.
  221. *
  222. * @param {Track[]} tracks - List of all tracks.
  223. * @param {(JitsiLocalTrack|JitsiRemoteTrack)} jitsiTrack - JitsiTrack instance.
  224. * @returns {(Track|undefined)}
  225. */
  226. export function getTrackByJitsiTrack(tracks, jitsiTrack) {
  227. return tracks.find(t => t.jitsiTrack === jitsiTrack);
  228. }
  229. /**
  230. * Returns tracks of specified media type.
  231. *
  232. * @param {Track[]} tracks - List of all tracks.
  233. * @param {MEDIA_TYPE} mediaType - Media type.
  234. * @returns {Track[]}
  235. */
  236. export function getTracksByMediaType(tracks, mediaType) {
  237. return tracks.filter(t => t.mediaType === mediaType);
  238. }
  239. /**
  240. * Checks if the local video track in the given set of tracks is muted.
  241. *
  242. * @param {Track[]} tracks - List of all tracks.
  243. * @returns {Track[]}
  244. */
  245. export function isLocalVideoTrackMuted(tracks) {
  246. const presenterTrack = getLocalTrack(tracks, MEDIA_TYPE.PRESENTER);
  247. const videoTrack = getLocalTrack(tracks, MEDIA_TYPE.VIDEO);
  248. // Make sure we check the mute status of only camera tracks, i.e.,
  249. // presenter track when it exists, camera track when the presenter
  250. // track doesn't exist.
  251. if (presenterTrack) {
  252. return isLocalTrackMuted(tracks, MEDIA_TYPE.PRESENTER);
  253. } else if (videoTrack) {
  254. return videoTrack.videoType === 'camera'
  255. ? isLocalTrackMuted(tracks, MEDIA_TYPE.VIDEO) : true;
  256. }
  257. return true;
  258. }
  259. /**
  260. * Checks if the first local track in the given tracks set is muted.
  261. *
  262. * @param {Track[]} tracks - List of all tracks.
  263. * @param {MEDIA_TYPE} mediaType - The media type of tracks to be checked.
  264. * @returns {boolean} True if local track is muted or false if the track is
  265. * unmuted or if there are no local tracks of the given media type in the given
  266. * set of tracks.
  267. */
  268. export function isLocalTrackMuted(tracks, mediaType) {
  269. const track = getLocalTrack(tracks, mediaType);
  270. return !track || track.muted;
  271. }
  272. /**
  273. * Returns true if the remote track of the given media type and the given
  274. * participant is muted, false otherwise.
  275. *
  276. * @param {Track[]} tracks - List of all tracks.
  277. * @param {MEDIA_TYPE} mediaType - The media type of tracks to be checked.
  278. * @param {*} participantId - Participant ID.
  279. * @returns {boolean}
  280. */
  281. export function isRemoteTrackMuted(tracks, mediaType, participantId) {
  282. const track = getTrackByMediaTypeAndParticipant(
  283. tracks, mediaType, participantId);
  284. return !track || track.muted;
  285. }
  286. /**
  287. * Returns whether or not the current environment needs a user interaction with
  288. * the page before any unmute can occur.
  289. *
  290. * @param {Object} state - The redux state.
  291. * @returns {boolean}
  292. */
  293. export function isUserInteractionRequiredForUnmute(state) {
  294. return browser.isUserInteractionRequiredForUnmute()
  295. && window
  296. && window.self !== window.top
  297. && !state['features/base/user-interaction'].interacted;
  298. }
  299. /**
  300. * Mutes or unmutes a specific {@code JitsiLocalTrack}. If the muted state of
  301. * the specified {@code track} is already in accord with the specified
  302. * {@code muted} value, then does nothing.
  303. *
  304. * @param {JitsiLocalTrack} track - The {@code JitsiLocalTrack} to mute or
  305. * unmute.
  306. * @param {boolean} muted - If the specified {@code track} is to be muted, then
  307. * {@code true}; otherwise, {@code false}.
  308. * @returns {Promise}
  309. */
  310. export function setTrackMuted(track, muted) {
  311. muted = Boolean(muted); // eslint-disable-line no-param-reassign
  312. if (track.isMuted() === muted) {
  313. return Promise.resolve();
  314. }
  315. const f = muted ? 'mute' : 'unmute';
  316. return track[f]().catch(error => {
  317. // Track might be already disposed so ignore such an error.
  318. if (error.name !== JitsiTrackErrors.TRACK_IS_DISPOSED) {
  319. // FIXME Emit mute failed, so that the app can show error dialog.
  320. logger.error(`set track ${f} failed`, error);
  321. }
  322. });
  323. }