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.ts 7.8KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271
  1. import { findIndex } from 'lodash-es';
  2. import { IReduxState } from '../../app/types';
  3. import { CONNECTION_TYPE } from './constants';
  4. import logger from './logger';
  5. import { IPreCallResult, PreCallTestStatus } from './types';
  6. /**
  7. * The avatar size to container size ration.
  8. */
  9. const ratio = 1 / 3;
  10. /**
  11. * The max avatar size.
  12. */
  13. const maxSize = 190;
  14. /**
  15. * The window limit height over which the avatar should have the default dimension.
  16. */
  17. const upperHeightLimit = 760;
  18. /**
  19. * The window limit height under which the avatar should not be resized anymore.
  20. */
  21. const lowerHeightLimit = 460;
  22. /**
  23. * The default top margin of the avatar.
  24. */
  25. const defaultMarginTop = '10%';
  26. /**
  27. * The top margin of the avatar when its dimension is small.
  28. */
  29. const smallMarginTop = '5%';
  30. // loss in percentage overall the test duration
  31. const LOSS_AUDIO_THRESHOLDS = [ 0.33, 0.05 ];
  32. const LOSS_VIDEO_THRESHOLDS = [ 0.33, 0.1, 0.05 ];
  33. // throughput in kbps
  34. const THROUGHPUT_AUDIO_THRESHOLDS = [ 8, 20 ];
  35. const THROUGHPUT_VIDEO_THRESHOLDS = [ 60, 750 ];
  36. /**
  37. * Calculates avatar dimensions based on window height and position.
  38. *
  39. * @param {number} height - The window height.
  40. * @returns {{
  41. * marginTop: string,
  42. * size: number
  43. * }}
  44. */
  45. export function calculateAvatarDimensions(height: number) {
  46. if (height > upperHeightLimit) {
  47. return {
  48. size: maxSize,
  49. marginTop: defaultMarginTop
  50. };
  51. }
  52. if (height > lowerHeightLimit) {
  53. const diff = height - lowerHeightLimit;
  54. const percent = diff * ratio;
  55. const size = Math.floor(maxSize * percent / 100);
  56. let marginTop = defaultMarginTop;
  57. if (height < 600) {
  58. marginTop = smallMarginTop;
  59. }
  60. return {
  61. size,
  62. marginTop
  63. };
  64. }
  65. return {
  66. size: 0,
  67. marginTop: '0'
  68. };
  69. }
  70. /**
  71. * Returns the level based on a list of thresholds.
  72. *
  73. * @param {number[]} thresholds - The thresholds array.
  74. * @param {number} value - The value against which the level is calculated.
  75. * @param {boolean} descending - The order based on which the level is calculated.
  76. *
  77. * @returns {number}
  78. */
  79. function _getLevel(thresholds: number[], value: number, descending = true) {
  80. let predicate;
  81. if (descending) {
  82. predicate = function(threshold: number) {
  83. return value > threshold;
  84. };
  85. } else {
  86. predicate = function(threshold: number) {
  87. return value < threshold;
  88. };
  89. }
  90. const i = findIndex(thresholds, predicate);
  91. if (i === -1) {
  92. return thresholds.length;
  93. }
  94. return i;
  95. }
  96. /**
  97. * Returns the connection details from the test results.
  98. *
  99. * @param {number} testResults.fractionalLoss - Factional loss.
  100. * @param {number} testResults.throughput - Throughput.
  101. *
  102. * @returns {{
  103. * connectionType: string,
  104. * connectionDetails: string[]
  105. * }}
  106. */
  107. function _getConnectionDataFromTestResults({ fractionalLoss: l, throughput: t, mediaConnectivity }: IPreCallResult) {
  108. let connectionType = CONNECTION_TYPE.FAILED;
  109. const connectionDetails: Array<string> = [];
  110. if (!mediaConnectivity) {
  111. connectionType = CONNECTION_TYPE.POOR;
  112. connectionDetails.push('prejoin.connectionDetails.noMediaConnectivity');
  113. return {
  114. connectionType,
  115. connectionDetails
  116. };
  117. }
  118. const loss = {
  119. audioQuality: _getLevel(LOSS_AUDIO_THRESHOLDS, l),
  120. videoQuality: _getLevel(LOSS_VIDEO_THRESHOLDS, l)
  121. };
  122. const throughput = {
  123. audioQuality: _getLevel(THROUGHPUT_AUDIO_THRESHOLDS, t, false),
  124. videoQuality: _getLevel(THROUGHPUT_VIDEO_THRESHOLDS, t, false)
  125. };
  126. if (throughput.audioQuality === 0 || loss.audioQuality === 0) {
  127. // Calls are impossible.
  128. connectionType = CONNECTION_TYPE.POOR;
  129. connectionDetails.push('prejoin.connectionDetails.veryPoorConnection');
  130. } else if (
  131. throughput.audioQuality === 2
  132. && throughput.videoQuality === 2
  133. && loss.audioQuality === 2
  134. && loss.videoQuality === 3
  135. ) {
  136. // Ideal conditions for both audio and video. Show only one message.
  137. connectionType = CONNECTION_TYPE.GOOD;
  138. connectionDetails.push('prejoin.connectionDetails.goodQuality');
  139. } else {
  140. connectionType = CONNECTION_TYPE.NON_OPTIMAL;
  141. if (throughput.audioQuality === 1) {
  142. // Minimum requirements for a call are met.
  143. connectionDetails.push('prejoin.connectionDetails.audioLowNoVideo');
  144. } else {
  145. // There are two paragraphs: one saying something about audio and the other about video.
  146. if (loss.audioQuality === 1) {
  147. connectionDetails.push('prejoin.connectionDetails.audioClipping');
  148. } else {
  149. connectionDetails.push('prejoin.connectionDetails.audioHighQuality');
  150. }
  151. if (throughput.videoQuality === 0 || loss.videoQuality === 0) {
  152. connectionDetails.push('prejoin.connectionDetails.noVideo');
  153. } else if (throughput.videoQuality === 1) {
  154. connectionDetails.push('prejoin.connectionDetails.videoLowQuality');
  155. } else if (loss.videoQuality === 1) {
  156. connectionDetails.push('prejoin.connectionDetails.videoFreezing');
  157. } else if (loss.videoQuality === 2) {
  158. connectionDetails.push('prejoin.connectionDetails.videoTearing');
  159. } else {
  160. connectionDetails.push('prejoin.connectionDetails.videoHighQuality');
  161. }
  162. }
  163. connectionDetails.push('prejoin.connectionDetails.undetectable');
  164. }
  165. return {
  166. connectionType,
  167. connectionDetails
  168. };
  169. }
  170. /**
  171. * Selector for determining the connection type & details.
  172. *
  173. * @param {Object} state - The state of the app.
  174. * @returns {{
  175. * connectionType: string,
  176. * connectionDetails: string[]
  177. * }}
  178. */
  179. export function getConnectionData(state: IReduxState) {
  180. const { preCallTestState: { status, result } } = state['features/base/premeeting'];
  181. switch (status) {
  182. case PreCallTestStatus.INITIAL:
  183. return {
  184. connectionType: CONNECTION_TYPE.NONE,
  185. connectionDetails: []
  186. };
  187. case PreCallTestStatus.RUNNING:
  188. return {
  189. connectionType: CONNECTION_TYPE.RUNNING,
  190. connectionDetails: []
  191. };
  192. case PreCallTestStatus.FAILED:
  193. // A failed test means that something went wrong with our business logic and not necessarily
  194. // that the connection is bad. For instance, the endpoint providing the ICE credentials could be down.
  195. return {
  196. connectionType: CONNECTION_TYPE.FAILED,
  197. connectionDetails: [ 'prejoin.connectionDetails.testFailed' ]
  198. };
  199. case PreCallTestStatus.FINISHED:
  200. if (result) {
  201. return _getConnectionDataFromTestResults(result);
  202. }
  203. logger.error('Pre-call test finished but no test results were available');
  204. return {
  205. connectionType: CONNECTION_TYPE.FAILED,
  206. connectionDetails: [ 'prejoin.connectionDetails.testFailed' ]
  207. };
  208. default:
  209. return {
  210. connectionType: CONNECTION_TYPE.NONE,
  211. connectionDetails: []
  212. };
  213. }
  214. }
  215. /**
  216. * Selector for determining if the pre-call test is enabled.
  217. *
  218. * @param {Object} state - The state of the app.
  219. * @returns {boolean}
  220. */
  221. export function isPreCallTestEnabled(state: IReduxState): boolean {
  222. const { prejoinConfig } = state['features/base/config'];
  223. return prejoinConfig?.preCallTestEnabled ?? false;
  224. }
  225. /**
  226. * Selector for retrieving the pre-call test ICE URL.
  227. *
  228. * @param {Object} state - The state of the app.
  229. * @returns {string | undefined}
  230. */
  231. export function getPreCallICEUrl(state: IReduxState): string | undefined {
  232. const { prejoinConfig } = state['features/base/config'];
  233. return prejoinConfig?.preCallTestICEUrl;
  234. }