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.

middleware.web.ts 7.0KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213
  1. import i18next from 'i18next';
  2. import {
  3. setPrejoinPageVisibility,
  4. setSkipPrejoinOnReload
  5. } from '../../prejoin/actions.web';
  6. import { isPrejoinPageVisible } from '../../prejoin/functions';
  7. import { iAmVisitor } from '../../visitors/functions';
  8. import { CONNECTION_DISCONNECTED, CONNECTION_ESTABLISHED } from '../connection/actionTypes';
  9. import { hangup } from '../connection/actions.web';
  10. import { JitsiConferenceErrors, browser } from '../lib-jitsi-meet';
  11. import { gumPending, setInitialGUMPromise } from '../media/actions';
  12. import { MEDIA_TYPE } from '../media/constants';
  13. import { IGUMPendingState } from '../media/types';
  14. import MiddlewareRegistry from '../redux/MiddlewareRegistry';
  15. import { replaceLocalTrack } from '../tracks/actions.any';
  16. import { getLocalTracks } from '../tracks/functions.any';
  17. import {
  18. CONFERENCE_FAILED,
  19. CONFERENCE_JOINED,
  20. CONFERENCE_JOIN_IN_PROGRESS,
  21. CONFERENCE_LEFT,
  22. KICKED_OUT
  23. } from './actionTypes';
  24. import { TRIGGER_READY_TO_CLOSE_REASONS } from './constants';
  25. import logger from './logger';
  26. import './middleware.any';
  27. let screenLock: WakeLockSentinel | undefined;
  28. /**
  29. * Releases the screen lock.
  30. *
  31. * @returns {Promise}
  32. */
  33. async function releaseScreenLock() {
  34. if (screenLock) {
  35. if (!screenLock.released) {
  36. logger.debug('Releasing wake lock.');
  37. try {
  38. await screenLock.release();
  39. } catch (e) {
  40. logger.error(`Error while releasing the screen wake lock: ${e}.`);
  41. }
  42. }
  43. screenLock.removeEventListener('release', onWakeLockReleased);
  44. screenLock = undefined;
  45. document.removeEventListener('visibilitychange', handleVisibilityChange);
  46. }
  47. }
  48. /**
  49. * Requests a new screen wake lock.
  50. *
  51. * @returns {void}
  52. */
  53. function requestWakeLock() {
  54. if (navigator.wakeLock?.request) {
  55. navigator.wakeLock.request('screen')
  56. .then(lock => {
  57. screenLock = lock;
  58. screenLock.addEventListener('release', onWakeLockReleased);
  59. document.addEventListener('visibilitychange', handleVisibilityChange);
  60. logger.debug('Wake lock created.');
  61. })
  62. .catch(e => {
  63. logger.error(`Error while requesting wake lock for screen: ${e}`);
  64. });
  65. }
  66. }
  67. /**
  68. * Page visibility change handler that re-requests the wake lock if it has been released by the OS.
  69. *
  70. * @returns {void}
  71. */
  72. async function handleVisibilityChange() {
  73. if (screenLock?.released && document.visibilityState === 'visible') {
  74. // The screen lock have been released by the OS because of document visibility change. Lets try to request the
  75. // wake lock again.
  76. await releaseScreenLock();
  77. requestWakeLock();
  78. }
  79. }
  80. /**
  81. * Wake lock released handler.
  82. *
  83. * @returns {void}
  84. */
  85. function onWakeLockReleased() {
  86. logger.debug('Wake lock released');
  87. }
  88. MiddlewareRegistry.register(store => next => action => {
  89. const { dispatch, getState } = store;
  90. const { enableForcedReload } = getState()['features/base/config'];
  91. switch (action.type) {
  92. case CONFERENCE_JOIN_IN_PROGRESS: {
  93. dispatch(setPrejoinPageVisibility(false));
  94. break;
  95. }
  96. case CONFERENCE_JOINED: {
  97. if (enableForcedReload) {
  98. dispatch(setSkipPrejoinOnReload(false));
  99. }
  100. requestWakeLock();
  101. break;
  102. }
  103. case CONFERENCE_FAILED: {
  104. const errorName = action.error?.name;
  105. if (enableForcedReload && errorName === JitsiConferenceErrors.CONFERENCE_RESTARTED) {
  106. dispatch(setSkipPrejoinOnReload(true));
  107. }
  108. if (errorName === JitsiConferenceErrors.CONFERENCE_DESTROYED) {
  109. const [ reason ] = action.error.params;
  110. const titlekey = Object.keys(TRIGGER_READY_TO_CLOSE_REASONS)[
  111. Object.values(TRIGGER_READY_TO_CLOSE_REASONS).indexOf(reason)
  112. ];
  113. dispatch(hangup(true, i18next.t(titlekey) || reason));
  114. }
  115. releaseScreenLock();
  116. break;
  117. }
  118. case CONFERENCE_LEFT:
  119. case KICKED_OUT:
  120. releaseScreenLock();
  121. break;
  122. case CONNECTION_DISCONNECTED: {
  123. const { initialGUMPromise } = getState()['features/base/media'];
  124. if (initialGUMPromise) {
  125. store.dispatch(setInitialGUMPromise());
  126. }
  127. break;
  128. }
  129. case CONNECTION_ESTABLISHED: {
  130. const { initialGUMPromise } = getState()['features/base/media'];
  131. const promise = initialGUMPromise ? initialGUMPromise.promise : Promise.resolve({ tracks: [] });
  132. const prejoinVisible = isPrejoinPageVisible(getState());
  133. logger.debug(`On connection established: prejoinVisible: ${prejoinVisible}, initialGUMPromiseExists=${
  134. Boolean(initialGUMPromise)}, promiseExists=${Boolean(promise)}`);
  135. if (prejoinVisible) {
  136. promise.then(() => {
  137. const state = getState();
  138. let localTracks = getLocalTracks(state['features/base/tracks']);
  139. const trackReplacePromises = [];
  140. // Do not signal audio/video tracks if the user joins muted.
  141. for (const track of localTracks) {
  142. // Always add the audio track on Safari because of a known issue where audio playout doesn't happen
  143. // if the user joins audio and video muted.
  144. if ((track.muted && !(browser.isWebKitBased() && track.jitsiTrack
  145. && track.jitsiTrack.getType() === MEDIA_TYPE.AUDIO)) || iAmVisitor(state)) {
  146. trackReplacePromises.push(dispatch(replaceLocalTrack(track.jitsiTrack, null))
  147. .catch((error: any) => {
  148. logger.error(`Failed to replace local track (${track.jitsiTrack}) with null: ${error}`);
  149. }));
  150. }
  151. }
  152. Promise.allSettled(trackReplacePromises).then(() => {
  153. // Re-fetch the local tracks after muted tracks have been removed above.
  154. // This is needed, because the tracks are effectively disposed by the replaceLocalTrack and should
  155. // not be used anymore.
  156. localTracks = getLocalTracks(getState()['features/base/tracks']);
  157. const jitsiTracks = localTracks.map((t: any) => t.jitsiTrack);
  158. return APP.conference.startConference(jitsiTracks);
  159. });
  160. });
  161. } else {
  162. promise.then(({ tracks }) => {
  163. let tracksToUse = tracks ?? [];
  164. if (iAmVisitor(getState())) {
  165. tracksToUse = [];
  166. tracks.forEach(track => track.dispose().catch(logger.error));
  167. dispatch(gumPending([ MEDIA_TYPE.AUDIO, MEDIA_TYPE.VIDEO ], IGUMPendingState.NONE));
  168. }
  169. dispatch(setInitialGUMPromise());
  170. return APP.conference.startConference(tracksToUse);
  171. })
  172. .catch(logger.error);
  173. }
  174. break;
  175. }
  176. }
  177. return next(action);
  178. });