Você não pode selecionar mais de 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.

SdpSimulcast.ts 11KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317
  1. import MediaDirection from '../../service/RTC/MediaDirection';
  2. import { MediaType } from '../../service/RTC/MediaType';
  3. import * as transform from 'sdp-transform';
  4. const DEFAULT_NUM_OF_LAYERS = 3;
  5. interface Description {
  6. type: RTCSdpType;
  7. sdp: string;
  8. }
  9. interface Options {
  10. numOfLayers?: number
  11. }
  12. /**
  13. * This class handles SDP munging for enabling simulcast for local video streams in Unified plan. A set of random SSRCs
  14. * are generated for the higher layer streams and they are cached for a given mid. The cached SSRCs are then reused on
  15. * the subsequent iterations while munging the local description. This class also handles imploding of the simulcast
  16. * SSRCs for remote endpoints into the primary FID group in remote description since Jicofo signals all SSRCs relevant
  17. * to a given endpoint.
  18. */
  19. export default class SdpSimulcast {
  20. private _options: Options;
  21. private _ssrcCache: Map<string, Array<number>>;
  22. /**
  23. * Creates a new instance.
  24. *
  25. * @param options
  26. */
  27. constructor(options: Options) {
  28. this._options = options;
  29. this._ssrcCache = new Map();
  30. if (!this._options.numOfLayers) {
  31. this._options.numOfLayers = DEFAULT_NUM_OF_LAYERS;
  32. }
  33. }
  34. /**
  35. * Updates the given media description using the SSRCs that were cached for the mid associated
  36. * with the media description and returns the modified media description.
  37. *
  38. * @param mLine
  39. * @returns
  40. */
  41. _fillSsrcsFromCache(mLine: transform.MediaDescription) : any {
  42. const mid = mLine.mid;
  43. const cachedSsrcs = this._ssrcCache.get(mid);
  44. const newSsrcs = this._parseSimLayers(mLine);
  45. const newMsid = this._getSsrcAttribute(mLine, newSsrcs[0], 'msid');
  46. const newCname = this._getSsrcAttribute(mLine, newSsrcs[0], 'cname');
  47. mLine.ssrcs = [];
  48. mLine.ssrcGroups = [];
  49. for (const ssrc of cachedSsrcs) {
  50. mLine.ssrcs.push({
  51. id: ssrc,
  52. attribute: 'msid',
  53. value: newMsid
  54. });
  55. mLine.ssrcs.push({
  56. id: ssrc,
  57. attribute: 'cname',
  58. value: newCname
  59. });
  60. }
  61. mLine.ssrcGroups.push({
  62. semantics: 'SIM',
  63. ssrcs: cachedSsrcs.join(' ')
  64. });
  65. return mLine;
  66. }
  67. /**
  68. * Generates a new set of SSRCs for the higher simulcast layers/streams and adds the attributes and SIM group to
  69. * the given media description and returns the modified media description.
  70. *
  71. * @param mLine
  72. * @param primarySsrc
  73. * @returns
  74. */
  75. _generateNewSsrcsForSimulcast(mLine: transform.MediaDescription, primarySsrc: number) : any {
  76. const cname = this._getSsrcAttribute(mLine, primarySsrc, 'cname');
  77. let msid = this._getSsrcAttribute(mLine, primarySsrc, 'msid');
  78. const addAssociatedAttributes = (mLine: transform.MediaDescription, ssrc: number) => {
  79. mLine.ssrcs.push({
  80. id: ssrc,
  81. attribute: 'cname',
  82. value: cname
  83. });
  84. mLine.ssrcs.push({
  85. id: ssrc,
  86. attribute: 'msid',
  87. value: msid
  88. });
  89. }
  90. // In Unified-plan mode, the a=ssrc lines with the msid attribute are not present (only cname attributes are
  91. // present) in the answers that Chrome and Safari generate for an offer received from Jicofo. Generate these
  92. // a=ssrc lines using the msid values from the a=msid line.
  93. if (!msid) {
  94. msid = mLine.msid;
  95. const primarySsrcs = mLine.ssrcs;
  96. primarySsrcs.forEach(ssrc => {
  97. mLine.ssrcs.push({
  98. id: ssrc.id,
  99. attribute: 'msid',
  100. value: msid
  101. });
  102. })
  103. }
  104. // Generate SIM layers.
  105. const simSsrcs = [];
  106. for (let i = 0; i < this._options.numOfLayers - 1; ++i) {
  107. const simSsrc = this._generateSsrc();
  108. addAssociatedAttributes(mLine, simSsrc);
  109. simSsrcs.push(simSsrc);
  110. }
  111. mLine.ssrcGroups = mLine.ssrcGroups || [];
  112. mLine.ssrcGroups.push({
  113. semantics: 'SIM',
  114. ssrcs: primarySsrc + ' ' + simSsrcs.join(' ')
  115. });
  116. return mLine;
  117. }
  118. /**
  119. * Returns a random number to be used for the SSRC.
  120. *
  121. * @returns
  122. */
  123. _generateSsrc() : number {
  124. const max = 0xffffffff;
  125. return Math.floor(Math.random() * max);
  126. }
  127. /**
  128. * Returns the requested attribute value for a SSRC from a given media description.
  129. *
  130. * @param mLine
  131. * @param ssrc
  132. * @param attributeName
  133. * @returns
  134. */
  135. _getSsrcAttribute(mLine: transform.MediaDescription, ssrc: number, attributeName: string) : string | undefined {
  136. return mLine.ssrcs?.find(
  137. ssrcInfo => Number(ssrcInfo.id) === ssrc
  138. && ssrcInfo.attribute === attributeName)?.value;
  139. }
  140. /**
  141. * Returns an array of all the primary SSRCs in the SIM group for a given media description.
  142. *
  143. * @param mLine
  144. * @returns
  145. */
  146. _parseSimLayers(mLine: transform.MediaDescription) : Array<number> | null {
  147. const simGroup = mLine.ssrcGroups?.find(group => group.semantics === 'SIM');
  148. if (simGroup) {
  149. return simGroup.ssrcs.split(' ').map(ssrc => Number(ssrc));
  150. }
  151. if (mLine.ssrcs?.length) {
  152. return [ Number(mLine.ssrcs[0].id) ];
  153. }
  154. return null;
  155. }
  156. /**
  157. * Munges the given media description to enable simulcast for the video media sections that are in either have
  158. * SENDRECV or SENDONLY as the media direction thereby ignoring all the RECVONLY transceivers created for remote
  159. * endpoints.
  160. * NOTE: This needs to be called only when simulcast is enabled.
  161. *
  162. * @param description
  163. * @returns
  164. */
  165. mungeLocalDescription(description: Description) : Description {
  166. if (!description || !description.sdp) {
  167. return description;
  168. }
  169. const session = transform.parse(description.sdp);
  170. for (let media of session.media) {
  171. // Ignore recvonly and inactive transceivers created for remote sources.
  172. if (media.direction === MediaDirection.RECVONLY || media.direction === MediaDirection.INACTIVE) {
  173. continue;
  174. }
  175. // Ignore audio m-lines.
  176. if (media.type !== MediaType.VIDEO) {
  177. continue;
  178. }
  179. const mid = media.mid;
  180. const numSsrcs = new Set(media.ssrcs?.map(ssrcInfo => ssrcInfo.id));
  181. const numGroups = media.ssrcGroups?.length ?? 0;
  182. let primarySsrc: number;
  183. // Do not munge if the description has no ssrcs or if simulcast is already enabled.
  184. if (numSsrcs.size === 0 || numSsrcs.size > 2 || (numSsrcs.size === 2 && numGroups === 0)) {
  185. continue;
  186. }
  187. if (numSsrcs.size === 1) {
  188. primarySsrc = Number(media.ssrcs[0]?.id);
  189. } else {
  190. const fidGroup = media.ssrcGroups.find(group => group.semantics === 'FID');
  191. if (fidGroup) {
  192. primarySsrc = Number(fidGroup.ssrcs.split(' ')[0]);
  193. }
  194. }
  195. if (this._ssrcCache.has(mid)) {
  196. media = this._fillSsrcsFromCache(media);
  197. } else {
  198. media = this._generateNewSsrcsForSimulcast(media, primarySsrc);
  199. const simulcastSsrcs = this._parseSimLayers(media);
  200. // Update the SSRCs in the cache so that they can re-used for the same mid again.
  201. this._ssrcCache.set(mid, simulcastSsrcs);
  202. }
  203. }
  204. return new RTCSessionDescription({
  205. type: description.type,
  206. sdp: transform.write(session)
  207. });
  208. }
  209. /**
  210. * Munges the given media description by removing the SSRCs and related FID groups for the higher layer streams.
  211. *
  212. * @param description
  213. * @returns
  214. */
  215. mungeRemoteDescription(description: Description) : Description {
  216. if (!description || !description.sdp) {
  217. return description;
  218. }
  219. const session = transform.parse(description.sdp);
  220. for (const media of session.media) {
  221. if (media.type !== MediaType.VIDEO) {
  222. continue;
  223. }
  224. if (media.direction !== MediaDirection.SENDONLY) {
  225. continue;
  226. }
  227. // Ignore m-lines that do not have any SSRCs or SSRC groups. These are the ones associated with remote
  228. // sources that have left the call. These will be recycled when a new remote source joins the call.
  229. if (!media.ssrcGroups?.length || !media?.ssrcs.length) {
  230. continue;
  231. }
  232. // Cache the SSRCs and the source groups.
  233. const mungedSsrcs = new Set(media.ssrcs.slice());
  234. const mungedSsrcGroups = new Set(media.ssrcGroups.slice());
  235. const fidGroups = media.ssrcGroups.filter(group => group.semantics === 'FID');
  236. const simGroup = media.ssrcGroups.find(group => group.semantics === 'SIM');
  237. const primarySsrc = simGroup?.ssrcs.split(' ')[0];;
  238. // When simulcast and RTX are both enabled.
  239. if (fidGroups.length && simGroup) {
  240. const fidGroup = fidGroups.find(group => group.ssrcs.includes(primarySsrc));
  241. const secondarySsrc = fidGroup.ssrcs.split(' ')[1];
  242. for (const ssrcGroup of media.ssrcGroups) {
  243. if (ssrcGroup !== fidGroup) {
  244. mungedSsrcGroups.delete(ssrcGroup);
  245. }
  246. }
  247. for (const ssrc of media.ssrcs) {
  248. if (ssrc.id.toString() !== primarySsrc
  249. && ssrc.id.toString() !== secondarySsrc) {
  250. mungedSsrcs.delete(ssrc);
  251. }
  252. }
  253. // When simulcast is enabled but RTX is disabled.
  254. } else if (simGroup) {
  255. mungedSsrcGroups.delete(simGroup);
  256. for (const ssrc of media.ssrcs) {
  257. if (ssrc.id.toString() !== primarySsrc) {
  258. mungedSsrcs.delete(ssrc);
  259. }
  260. }
  261. }
  262. media.ssrcs = Array.from(mungedSsrcs);
  263. media.ssrcGroups = Array.from(mungedSsrcGroups);
  264. }
  265. return new RTCSessionDescription ({
  266. type: description.type,
  267. sdp: transform.write(session)
  268. });
  269. }
  270. }