modified lib-jitsi-meet dev repo
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.

JitsiMediaDevices.js 11KB

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