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.

SignalingLayerImpl.js 13KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404
  1. import { getLogger } from '@jitsi/logger';
  2. import { Strophe } from 'strophe.js';
  3. import { MediaType } from '../../service/RTC/MediaType';
  4. import * as SignalingEvents from '../../service/RTC/SignalingEvents';
  5. import SignalingLayer, { getMediaTypeFromSourceName } from '../../service/RTC/SignalingLayer';
  6. import { VideoType } from '../../service/RTC/VideoType';
  7. import { XMPPEvents } from '../../service/xmpp/XMPPEvents';
  8. import { filterNodeFromPresenceJSON } from './ChatRoom';
  9. const logger = getLogger(__filename);
  10. export const SOURCE_INFO_PRESENCE_ELEMENT = 'SourceInfo';
  11. /**
  12. * Default XMPP implementation of the {@link SignalingLayer} interface. Obtains
  13. * the data from the MUC presence.
  14. */
  15. export default class SignalingLayerImpl extends SignalingLayer {
  16. /**
  17. * Creates new instance.
  18. */
  19. constructor() {
  20. super();
  21. /**
  22. * A map that stores SSRCs of remote streams. And is used only locally
  23. * We store the mapping when jingle is received, and later is used
  24. * onaddstream webrtc event where we have only the ssrc
  25. * FIXME: This map got filled and never cleaned and can grow during long
  26. * conference
  27. * @type {Map<number, string>} maps SSRC number to jid
  28. */
  29. this.ssrcOwners = new Map();
  30. /**
  31. *
  32. * @type {ChatRoom|null}
  33. */
  34. this.chatRoom = null;
  35. /**
  36. * @type {Map<SourceName, SourceInfo>}
  37. * @private
  38. */
  39. this._localSourceState = { };
  40. /**
  41. * @type {Map<EndpointId, Map<SourceName, SourceInfo>>}
  42. * @private
  43. */
  44. this._remoteSourceState = { };
  45. /**
  46. * A map that stores the source name of a track identified by it's ssrc.
  47. * We store the mapping when jingle is received, and later is used
  48. * onaddstream webrtc event where we have only the ssrc
  49. * FIXME: This map got filled and never cleaned and can grow during long
  50. * conference
  51. * @type {Map<number, string>} maps SSRC number to source name
  52. */
  53. this._sourceNames = new Map();
  54. }
  55. /**
  56. * Adds <SourceInfo> element to the local presence.
  57. *
  58. * @returns {void}
  59. * @private
  60. */
  61. _addLocalSourceInfoToPresence() {
  62. if (this.chatRoom) {
  63. return this.chatRoom.addOrReplaceInPresence(
  64. SOURCE_INFO_PRESENCE_ELEMENT,
  65. { value: JSON.stringify(this._localSourceState) });
  66. }
  67. return false;
  68. }
  69. /**
  70. * Check is given endpoint has advertised <SourceInfo/> in it's presence which means that the source name signaling
  71. * is used by this endpoint.
  72. *
  73. * @param {EndpointId} endpointId
  74. * @returns {boolean}
  75. */
  76. _doesEndpointSendNewSourceInfo(endpointId) {
  77. const presence = this.chatRoom?.getLastPresence(endpointId);
  78. return Boolean(presence && presence.find(node => node.tagName === SOURCE_INFO_PRESENCE_ELEMENT));
  79. }
  80. /**
  81. * Sets the <tt>ChatRoom</tt> instance used and binds presence listeners.
  82. * @param {ChatRoom} room
  83. */
  84. setChatRoom(room) {
  85. const oldChatRoom = this.chatRoom;
  86. this.chatRoom = room;
  87. if (oldChatRoom) {
  88. oldChatRoom.removePresenceListener(
  89. 'audiomuted', this._audioMuteHandler);
  90. oldChatRoom.removePresenceListener(
  91. 'videomuted', this._videoMuteHandler);
  92. oldChatRoom.removePresenceListener(
  93. 'videoType', this._videoTypeHandler);
  94. this._sourceInfoHandler
  95. && oldChatRoom.removePresenceListener(SOURCE_INFO_PRESENCE_ELEMENT, this._sourceInfoHandler);
  96. this._memberLeftHandler
  97. && oldChatRoom.removeEventListener(XMPPEvents.MUC_MEMBER_LEFT, this._memberLeftHandler);
  98. }
  99. if (room) {
  100. this._bindChatRoomEventHandlers(room);
  101. this._addLocalSourceInfoToPresence();
  102. }
  103. }
  104. /**
  105. * Binds event listeners to the chat room instance.
  106. * @param {ChatRoom} room
  107. * @private
  108. * @returns {void}
  109. */
  110. _bindChatRoomEventHandlers(room) {
  111. // Add handlers for 'audiomuted', 'videomuted' and 'videoType' fields in presence in order to support interop
  112. // with very old versions of mobile clients and jigasi that do not support source-name signaling.
  113. const emitAudioMutedEvent = (endpointId, muted) => {
  114. this.eventEmitter.emit(
  115. SignalingEvents.PEER_MUTED_CHANGED,
  116. endpointId,
  117. MediaType.AUDIO,
  118. muted);
  119. };
  120. this._audioMuteHandler = (node, from) => {
  121. if (!this._doesEndpointSendNewSourceInfo(from)) {
  122. emitAudioMutedEvent(from, node.value === 'true');
  123. }
  124. };
  125. room.addPresenceListener('audiomuted', this._audioMuteHandler);
  126. const emitVideoMutedEvent = (endpointId, muted) => {
  127. this.eventEmitter.emit(
  128. SignalingEvents.PEER_MUTED_CHANGED,
  129. endpointId,
  130. MediaType.VIDEO,
  131. muted);
  132. };
  133. this._videoMuteHandler = (node, from) => {
  134. if (!this._doesEndpointSendNewSourceInfo(from)) {
  135. emitVideoMutedEvent(from, node.value === 'true');
  136. }
  137. };
  138. room.addPresenceListener('videomuted', this._videoMuteHandler);
  139. const emitVideoTypeEvent = (endpointId, videoType) => {
  140. this.eventEmitter.emit(
  141. SignalingEvents.PEER_VIDEO_TYPE_CHANGED,
  142. endpointId, videoType);
  143. };
  144. this._videoTypeHandler = (node, from) => {
  145. if (!this._doesEndpointSendNewSourceInfo(from)) {
  146. emitVideoTypeEvent(from, node.value);
  147. }
  148. };
  149. room.addPresenceListener('videoType', this._videoTypeHandler);
  150. // Add handlers for presence in the new format.
  151. this._sourceInfoHandler = (node, mucNick) => {
  152. const endpointId = mucNick;
  153. const { value } = node;
  154. const sourceInfoJSON = JSON.parse(value);
  155. const emitEventsFromHere = this._doesEndpointSendNewSourceInfo(endpointId);
  156. const endpointSourceState
  157. = this._remoteSourceState[endpointId] || (this._remoteSourceState[endpointId] = {});
  158. for (const sourceName of Object.keys(sourceInfoJSON)) {
  159. const mediaType = getMediaTypeFromSourceName(sourceName);
  160. const newMutedState = Boolean(sourceInfoJSON[sourceName].muted);
  161. const oldSourceState = endpointSourceState[sourceName]
  162. || (endpointSourceState[sourceName] = { sourceName });
  163. if (oldSourceState.muted !== newMutedState) {
  164. oldSourceState.muted = newMutedState;
  165. if (emitEventsFromHere && !this._localSourceState[sourceName]) {
  166. this.eventEmitter.emit(SignalingEvents.SOURCE_MUTED_CHANGED, sourceName, newMutedState);
  167. }
  168. }
  169. // Assume a default videoType of 'camera' for video sources.
  170. const newVideoType = mediaType === MediaType.VIDEO
  171. ? sourceInfoJSON[sourceName].videoType ?? VideoType.CAMERA
  172. : undefined;
  173. if (oldSourceState.videoType !== newVideoType) {
  174. oldSourceState.videoType = newVideoType;
  175. // Since having a mix of eps that do/don't support multi-stream in the same call is supported, emit
  176. // SOURCE_VIDEO_TYPE_CHANGED event when the remote source changes videoType.
  177. if (emitEventsFromHere && !this._localSourceState[sourceName]) {
  178. this.eventEmitter.emit(SignalingEvents.SOURCE_VIDEO_TYPE_CHANGED, sourceName, newVideoType);
  179. }
  180. }
  181. }
  182. // Cleanup removed source names
  183. const newSourceNames = Object.keys(sourceInfoJSON);
  184. for (const sourceName of Object.keys(endpointSourceState)) {
  185. if (newSourceNames.indexOf(sourceName) === -1) {
  186. delete endpointSourceState[sourceName];
  187. }
  188. }
  189. };
  190. room.addPresenceListener('SourceInfo', this._sourceInfoHandler);
  191. // Cleanup when participant leaves
  192. this._memberLeftHandler = jid => {
  193. const endpointId = Strophe.getResourceFromJid(jid);
  194. delete this._remoteSourceState[endpointId];
  195. for (const [ key, value ] of this.ssrcOwners.entries()) {
  196. if (value === endpointId) {
  197. delete this._sourceNames[key];
  198. }
  199. }
  200. };
  201. room.addEventListener(XMPPEvents.MUC_MEMBER_LEFT, this._memberLeftHandler);
  202. }
  203. /**
  204. * Finds the first source of given media type for the given endpoint.
  205. * @param endpointId
  206. * @param mediaType
  207. * @returns {SourceInfo|null}
  208. * @private
  209. */
  210. _findEndpointSourceInfoForMediaType(endpointId, mediaType) {
  211. const remoteSourceState = this._remoteSourceState[endpointId];
  212. if (!remoteSourceState) {
  213. return null;
  214. }
  215. for (const sourceInfo of Object.values(remoteSourceState)) {
  216. const _mediaType = getMediaTypeFromSourceName(sourceInfo.sourceName);
  217. if (_mediaType === mediaType) {
  218. return sourceInfo;
  219. }
  220. }
  221. return null;
  222. }
  223. /**
  224. * @inheritDoc
  225. */
  226. getPeerMediaInfo(owner, mediaType, sourceName) {
  227. const legacyGetPeerMediaInfo = () => {
  228. if (this.chatRoom) {
  229. return this.chatRoom.getMediaPresenceInfo(owner, mediaType);
  230. }
  231. logger.warn('Requested peer media info, before room was set');
  232. };
  233. const lastPresence = this.chatRoom?.getLastPresence(owner);
  234. if (!lastPresence) {
  235. logger.warn(`getPeerMediaInfo - no presence stored for: ${owner}`);
  236. return;
  237. }
  238. if (!this._doesEndpointSendNewSourceInfo(owner)) {
  239. return legacyGetPeerMediaInfo();
  240. }
  241. if (sourceName) {
  242. return this.getPeerSourceInfo(owner, sourceName);
  243. }
  244. const mediaInfo = {
  245. muted: true
  246. };
  247. if (mediaType === MediaType.VIDEO) {
  248. mediaInfo.videoType = undefined;
  249. const codecTypeNode = filterNodeFromPresenceJSON(lastPresence, 'jitsi_participant_codecType');
  250. if (codecTypeNode.length > 0) {
  251. mediaInfo.codecType = codecTypeNode[0].value;
  252. }
  253. }
  254. return mediaInfo;
  255. }
  256. /**
  257. * @inheritDoc
  258. */
  259. getPeerSourceInfo(owner, sourceName) {
  260. const mediaInfo = {
  261. muted: true, // muted by default
  262. videoType: VideoType.CAMERA // 'camera' by default
  263. };
  264. return this._remoteSourceState[owner]
  265. ? this._remoteSourceState[owner][sourceName] ?? mediaInfo
  266. : undefined;
  267. }
  268. /**
  269. * @inheritDoc
  270. */
  271. getSSRCOwner(ssrc) {
  272. return this.ssrcOwners.get(ssrc);
  273. }
  274. /**
  275. * @inheritDoc
  276. */
  277. setSSRCOwner(ssrc, endpointId) {
  278. if (typeof ssrc !== 'number') {
  279. throw new TypeError(`SSRC(${ssrc}) must be a number`);
  280. }
  281. // Now signaling layer instance is shared between different JingleSessionPC instances, so although very unlikely
  282. // an SSRC conflict could potentially occur. Log a message to make debugging easier.
  283. const existingOwner = this.ssrcOwners.get(ssrc);
  284. if (existingOwner && existingOwner !== endpointId) {
  285. logger.error(`SSRC owner re-assigned from ${existingOwner} to ${endpointId}`);
  286. }
  287. this.ssrcOwners.set(ssrc, endpointId);
  288. }
  289. /**
  290. * @inheritDoc
  291. */
  292. setTrackMuteStatus(sourceName, muted) {
  293. if (!this._localSourceState[sourceName]) {
  294. this._localSourceState[sourceName] = {};
  295. }
  296. this._localSourceState[sourceName].muted = muted;
  297. if (this.chatRoom) {
  298. return this._addLocalSourceInfoToPresence();
  299. }
  300. return false;
  301. }
  302. /**
  303. * @inheritDoc
  304. */
  305. setTrackVideoType(sourceName, videoType) {
  306. if (!this._localSourceState[sourceName]) {
  307. this._localSourceState[sourceName] = {};
  308. }
  309. if (this._localSourceState[sourceName].videoType !== videoType) {
  310. // Include only if not a camera (default)
  311. this._localSourceState[sourceName].videoType = videoType === VideoType.CAMERA ? undefined : videoType;
  312. return this._addLocalSourceInfoToPresence();
  313. }
  314. return false;
  315. }
  316. /**
  317. * @inheritDoc
  318. */
  319. getTrackSourceName(ssrc) {
  320. return this._sourceNames.get(ssrc);
  321. }
  322. /**
  323. * @inheritDoc
  324. */
  325. setTrackSourceName(ssrc, sourceName) {
  326. if (typeof ssrc !== 'number') {
  327. throw new TypeError(`SSRC(${ssrc}) must be a number`);
  328. }
  329. // Now signaling layer instance is shared between different JingleSessionPC instances, so although very unlikely
  330. // an SSRC conflict could potentially occur. Log a message to make debugging easier.
  331. const existingName = this._sourceNames.get(ssrc);
  332. if (existingName && existingName !== sourceName) {
  333. logger.error(`SSRC(${ssrc}) sourceName re-assigned from ${existingName} to ${sourceName}`);
  334. }
  335. this._sourceNames.set(ssrc, sourceName);
  336. }
  337. }