Du kan inte välja fler än 25 ämnen Ämnen måste starta med en bokstav eller siffra, kan innehålla bindestreck ('-') och vara max 35 tecken långa.

JitsiMediaDevices.js 9.2KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269
  1. import * as JitsiMediaDevicesEvents from './JitsiMediaDevicesEvents';
  2. import RTC from './modules/RTC/RTC';
  3. import browser from './modules/browser';
  4. import Listenable from './modules/util/Listenable';
  5. import { MediaType } from './service/RTC/MediaType';
  6. import RTCEvents from './service/RTC/RTCEvents';
  7. const AUDIO_PERMISSION_NAME = 'microphone';
  8. const PERMISSION_GRANTED_STATUS = 'granted';
  9. const VIDEO_PERMISSION_NAME = 'camera';
  10. /**
  11. * Media devices utilities for Jitsi.
  12. */
  13. class JitsiMediaDevices extends Listenable {
  14. /**
  15. * Initializes a {@code JitsiMediaDevices} object. There will be a single
  16. * instance of this class.
  17. */
  18. constructor() {
  19. super();
  20. this._permissions = {};
  21. RTC.addListener(
  22. RTCEvents.DEVICE_LIST_CHANGED,
  23. devices =>
  24. this.eventEmitter.emit(
  25. JitsiMediaDevicesEvents.DEVICE_LIST_CHANGED,
  26. devices));
  27. // We would still want to update the permissions cache in case the permissions API is not supported.
  28. RTC.addListener(
  29. RTCEvents.PERMISSIONS_CHANGED,
  30. permissions => this._handlePermissionsChange(permissions));
  31. // Test if the W3C Permissions API is implemented and the 'camera' and 'microphone' permissions are
  32. // implemented. If supported add onchange listeners.
  33. this._permissionsApiSupported = new Promise(resolve => {
  34. if (!navigator.permissions) {
  35. resolve(false);
  36. return;
  37. }
  38. const self = this;
  39. const promises = [];
  40. promises.push(navigator.permissions.query({ name: VIDEO_PERMISSION_NAME })
  41. .then(status => {
  42. this._handlePermissionsChange({
  43. [MediaType.VIDEO]: this._parsePermissionState(status)
  44. });
  45. status.onchange = function() {
  46. try {
  47. self._handlePermissionsChange({
  48. [MediaType.VIDEO]: self._parsePermissionState(this)
  49. });
  50. } catch (error) {
  51. // Nothing to do.
  52. }
  53. };
  54. return true;
  55. })
  56. .catch(() => false));
  57. promises.push(navigator.permissions.query({ name: AUDIO_PERMISSION_NAME })
  58. .then(status => {
  59. this._handlePermissionsChange({
  60. [MediaType.AUDIO]: this._parsePermissionState(status)
  61. });
  62. status.onchange = function() {
  63. try {
  64. self._handlePermissionsChange({
  65. [MediaType.AUDIO]: self._parsePermissionState(this)
  66. });
  67. } catch (error) {
  68. // Nothing to do.
  69. }
  70. };
  71. return true;
  72. })
  73. .catch(() => false));
  74. Promise.all(promises).then(results => resolve(results.every(supported => supported)));
  75. });
  76. }
  77. /**
  78. * Parses a PermissionState object and returns true for granted and false otherwise.
  79. *
  80. * @param {PermissionState} permissionStatus - The PermissionState object retrieved from the Permissions API.
  81. * @returns {boolean} - True for granted and false for denied.
  82. * @throws {TypeError}
  83. */
  84. _parsePermissionState(permissionStatus = {}) {
  85. // The status attribute is deprecated, and state
  86. // should be used instead, but check both for now
  87. // for backwards compatibility.
  88. const status = permissionStatus.state || permissionStatus.status;
  89. if (typeof status !== 'string') {
  90. throw new TypeError();
  91. }
  92. return status === PERMISSION_GRANTED_STATUS;
  93. }
  94. /**
  95. * Updates the local granted/denied permissions cache. A permissions might be
  96. * granted, denied, or undefined. This is represented by having its media
  97. * type key set to {@code true} or {@code false} respectively.
  98. *
  99. * @param {Object} permissions - Object with the permissions.
  100. */
  101. _handlePermissionsChange(permissions) {
  102. const hasPermissionsChanged
  103. = [ MediaType.AUDIO, MediaType.VIDEO ]
  104. .some(type => type in permissions && permissions[type] !== this._permissions[type]);
  105. if (hasPermissionsChanged) {
  106. this._permissions = {
  107. ...this._permissions,
  108. ...permissions
  109. };
  110. this.eventEmitter.emit(JitsiMediaDevicesEvents.PERMISSIONS_CHANGED, this._permissions);
  111. if (this._permissions[MediaType.AUDIO] || this._permissions[MediaType.VIDEO]) {
  112. // Triggering device list update when the permissiions are granted in order to update
  113. // the labels the devices.
  114. // eslint-disable-next-line no-empty-function
  115. this.enumerateDevices(() => {});
  116. }
  117. }
  118. }
  119. /**
  120. * Executes callback with list of media devices connected.
  121. * @param {function} callback
  122. */
  123. enumerateDevices(callback) {
  124. RTC.enumerateDevices(callback);
  125. }
  126. /**
  127. * Checks if its possible to enumerate available cameras/micropones.
  128. * @returns {Promise<boolean>} a Promise which will be resolved only once
  129. * the WebRTC stack is ready, either with true if the device listing is
  130. * available available or with false otherwise.
  131. */
  132. isDeviceListAvailable() {
  133. return RTC.isDeviceListAvailable();
  134. }
  135. /**
  136. * Returns true if changing the input (camera / microphone) or output
  137. * (audio) device is supported and false if not.
  138. * @param {string} [deviceType] - type of device to change. Default is
  139. * undefined or 'input', 'output' - for audio output device change.
  140. * @returns {boolean} true if available, false otherwise.
  141. */
  142. isDeviceChangeAvailable(deviceType) {
  143. return RTC.isDeviceChangeAvailable(deviceType);
  144. }
  145. /**
  146. * Checks if the permission for the given device was granted.
  147. *
  148. * @param {'audio'|'video'} [type] - type of devices to check,
  149. * undefined stands for both 'audio' and 'video' together
  150. * @returns {Promise<boolean>}
  151. */
  152. isDevicePermissionGranted(type) {
  153. return new Promise(resolve => {
  154. // Shortcut: first check if we already know the permission was
  155. // granted.
  156. if (type in this._permissions) {
  157. resolve(this._permissions[type]);
  158. return;
  159. }
  160. // Check using the Permissions API.
  161. this._permissionsApiSupported.then(supported => {
  162. if (!supported) {
  163. resolve(false);
  164. return;
  165. }
  166. const promises = [];
  167. switch (type) {
  168. case MediaType.VIDEO:
  169. promises.push(
  170. navigator.permissions.query({
  171. name: VIDEO_PERMISSION_NAME
  172. }));
  173. break;
  174. case MediaType.AUDIO:
  175. promises.push(
  176. navigator.permissions.query({
  177. name: AUDIO_PERMISSION_NAME
  178. }));
  179. break;
  180. default:
  181. promises.push(
  182. navigator.permissions.query({
  183. name: VIDEO_PERMISSION_NAME
  184. }));
  185. promises.push(
  186. navigator.permissions.query({
  187. name: AUDIO_PERMISSION_NAME
  188. }));
  189. }
  190. Promise.all(promises).then(
  191. results => resolve(results.every(permissionStatus => {
  192. try {
  193. return this._parsePermissionState(permissionStatus);
  194. } catch {
  195. return false;
  196. }
  197. })),
  198. () => resolve(false)
  199. );
  200. });
  201. });
  202. }
  203. /**
  204. * Returns true if it is possible to be simultaneously capturing audio from more than one device.
  205. *
  206. * @returns {boolean}
  207. */
  208. isMultipleAudioInputSupported() {
  209. return !(
  210. (browser.isFirefox() && browser.isVersionLessThan('101'))
  211. || browser.isIosBrowser()
  212. );
  213. }
  214. /**
  215. * Returns currently used audio output device id, 'default' stands
  216. * for default device
  217. * @returns {string}
  218. */
  219. getAudioOutputDevice() {
  220. return RTC.getAudioOutputDevice();
  221. }
  222. /**
  223. * Sets current audio output device.
  224. * @param {string} deviceId - id of 'audiooutput' device from
  225. * navigator.mediaDevices.enumerateDevices(), 'default' is for
  226. * default device
  227. * @returns {Promise} - resolves when audio output is changed, is rejected
  228. * otherwise
  229. */
  230. setAudioOutputDevice(deviceId) {
  231. return RTC.setAudioOutputDevice(deviceId);
  232. }
  233. }
  234. export default new JitsiMediaDevices();