| import { SDPInfo, MediaInfo, Direction } from 'semantic-sdp' |
| import Logger from '../Logger' |
| import UserAgent from './UserAgent' |
| |
| const logger = Logger.get('SdpParser') |
| |
| const firstPayloadTypeLowerRange = 35 |
| const lastPayloadTypeLowerRange = 65 |
| |
| const firstPayloadTypeUpperRange = 96 |
| const lastPayloadTypeUpperRange = 127 |
| |
| const payloadTypeLowerRange = Array.from({ length: (lastPayloadTypeLowerRange - firstPayloadTypeLowerRange) + 1 }, (_, i) => i + firstPayloadTypeLowerRange) |
| const payloadTypeUppperRange = Array.from({ length: (lastPayloadTypeUpperRange - firstPayloadTypeUpperRange) + 1 }, (_, i) => i + firstPayloadTypeUpperRange) |
| |
| const firstHeaderExtensionIdLowerRange = 1 |
| const lastHeaderExtensionIdLowerRange = 14 |
| |
| const firstHeaderExtensionIdUpperRange = 16 |
| const lastHeaderExtensionIdUpperRange = 255 |
| |
| const headerExtensionIdLowerRange = Array.from({ length: (lastHeaderExtensionIdLowerRange - firstHeaderExtensionIdLowerRange) + 1 }, (_, i) => i + firstHeaderExtensionIdLowerRange) |
| const headerExtensionIdUppperRange = Array.from({ length: (lastHeaderExtensionIdUpperRange - firstHeaderExtensionIdUpperRange) + 1 }, (_, i) => i + firstHeaderExtensionIdUpperRange) |
| |
| |
| |
| |
| |
| const SdpParser = { |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| setSimulcast (sdp, codec) { |
| logger.info('Setting simulcast. Codec: ', codec) |
| const browserData = new UserAgent() |
| if (!browserData.isChromium()) { |
| logger.warn('Your browser does not appear to support Simulcast. For a better experience, use a Chromium based browser.') |
| return sdp |
| } |
| if (codec !== 'h264' && codec !== 'vp8') { |
| logger.warn(`Your selected codec ${codec} does not appear to support Simulcast. To broadcast using simulcast, please use H.264 or VP8.`) |
| return sdp |
| } |
| |
| if (!/m=video/.test(sdp)) { |
| logger.warn('There is no available video for simulcast to be enabled.') |
| return sdp |
| } |
| |
| try { |
| const reg1 = /m=video.*?a=ssrc:(\d*) cname:(.+?)\r\n/s |
| const reg2 = /m=video.*?a=ssrc:(\d*) msid:(.+?)\r\n/s |
| |
| const res = reg1.exec(sdp) |
| const ssrc = res[1] |
| const cname = res[2] |
| const msid = reg2.exec(sdp)[2] |
| |
| const num = 2 |
| const ssrcs = [ssrc] |
| for (let i = 0; i < num; ++i) { |
| |
| const ssrc = 100 + i * 2 |
| const rtx = ssrc + 1 |
| |
| ssrcs.push(ssrc) |
| |
| sdp += 'a=ssrc-group:FID ' + ssrc + ' ' + rtx + '\r\n' + |
| 'a=ssrc:' + ssrc + ' cname:' + cname + '\r\n' + |
| 'a=ssrc:' + ssrc + ' msid:' + msid + '\r\n' + |
| 'a=ssrc:' + rtx + ' cname:' + cname + '\r\n' + |
| 'a=ssrc:' + rtx + ' msid:' + msid + '\r\n' |
| } |
| |
| sdp += 'a=ssrc-group:SIM ' + ssrcs.join(' ') + '\r\n' |
| |
| logger.info('Simulcast setted') |
| logger.debug('Simulcast SDP: ', sdp) |
| return sdp |
| } catch (e) { |
| logger.error('Error setting SDP for simulcast: ', e) |
| throw e |
| } |
| }, |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| setStereo (sdp) { |
| logger.info('Replacing SDP response for support stereo') |
| sdp = sdp.replace( |
| /useinbandfec=1/g, |
| 'useinbandfec=1; stereo=1' |
| ) |
| logger.info('Replaced SDP response for support stereo') |
| logger.debug('New SDP value: ', sdp) |
| return sdp |
| }, |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| setDTX (sdp) { |
| logger.info('Replacing SDP response for support dtx') |
| sdp = sdp.replace( |
| 'useinbandfec=1', |
| 'useinbandfec=1; usedtx=1' |
| ) |
| logger.info('Replaced SDP response for support dtx') |
| logger.debug('New SDP value: ', sdp) |
| return sdp |
| }, |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| setAbsoluteCaptureTime (sdp) { |
| const id = SdpParser.getAvailableHeaderExtensionIdRange(sdp)[0] |
| const header = 'a=extmap:' + id + ' http://www.webrtc.org/experiments/rtp-hdrext/abs-capture-time\r\n' |
| |
| const regex = /(m=.*\r\n(?:.*\r\n)*?)(a=extmap.*\r\n)/gm |
| |
| sdp = sdp.replace(regex, (match, p1, p2) => p1 + header + p2) |
| |
| logger.info('Replaced SDP response for setting absolute capture time') |
| logger.debug('New SDP value: ', sdp) |
| |
| return sdp |
| }, |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| setDependencyDescriptor (sdp) { |
| const id = SdpParser.getAvailableHeaderExtensionIdRange(sdp)[0] |
| const header = 'a=extmap:' + id + ' https://aomediacodec.github.io/av1-rtp-spec/#dependency-descriptor-rtp-header-extension\r\n' |
| |
| const regex = /(m=.*\r\n(?:.*\r\n)*?)(a=extmap.*\r\n)/gm |
| |
| sdp = sdp.replace(regex, (match, p1, p2) => p1 + header + p2) |
| |
| logger.info('Replaced SDP response for setting depency descriptor') |
| logger.debug('New SDP value: ', sdp) |
| |
| return sdp |
| }, |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| setVideoBitrate (sdp, bitrate) { |
| if (bitrate < 1) { |
| logger.info('Remove bitrate restrictions') |
| sdp = sdp.replace(/b=AS:.*\r\n/, '').replace(/b=TIAS:.*\r\n/, '') |
| } else { |
| const offer = SDPInfo.parse(sdp) |
| const videoOffer = offer.getMedia('video') |
| |
| logger.info('Setting video bitrate') |
| videoOffer.setBitrate(bitrate) |
| sdp = offer.toString() |
| } |
| return sdp |
| }, |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| removeSdpLine (sdp, sdpLine) { |
| logger.debug('SDP before trimming: ', sdp) |
| sdp = sdp |
| .split('\n') |
| .filter((line) => { |
| return line.trim() !== sdpLine |
| }) |
| .join('\n') |
| logger.debug('SDP trimmed result: ', sdp) |
| return sdp |
| }, |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| adaptCodecName (sdp, codec, newCodecName) { |
| if (!sdp) { |
| return sdp |
| } |
| const regex = new RegExp(`${codec}`, 'i') |
| |
| return sdp.replace(regex, newCodecName) |
| }, |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| setMultiopus (sdp, mediaStream) { |
| const browserData = new UserAgent() |
| if (!browserData.isFirefox() && (!mediaStream || hasAudioMultichannel(mediaStream))) { |
| if (!sdp.includes('multiopus/48000/6')) { |
| logger.info('Setting multiopus') |
| |
| const res = /m=audio 9 UDP\/TLS\/RTP\/SAVPF (.*)\r\n/.exec(sdp) |
| |
| const audio = res[0] |
| |
| const pt = SdpParser.getAvailablePayloadTypeRange(sdp)[0] |
| |
| const multiopus = audio.replace('\r\n', ' ') + pt + '\r\n' + |
| 'a=rtpmap:' + pt + ' multiopus/48000/6\r\n' + |
| 'a=fmtp:' + pt + ' channel_mapping=0,4,1,2,3,5;coupled_streams=2;minptime=10;num_streams=4;useinbandfec=1\r\n' |
| |
| sdp = sdp.replace(audio, multiopus) |
| logger.info('Multiopus offer created') |
| logger.debug('SDP parsed for multioups: ', sdp) |
| } else { |
| logger.info('Multiopus already setted') |
| } |
| } |
| return sdp |
| }, |
| |
| |
| |
| |
| |
| |
| |
| |
| getAvailablePayloadTypeRange (sdp) { |
| const regex = /m=(?:.*) (?:.*) UDP\/TLS\/RTP\/SAVPF (.*)\r\n/gm |
| |
| const matches = sdp.matchAll(regex) |
| let ptAvailable = payloadTypeUppperRange.concat(payloadTypeLowerRange) |
| |
| for (const match of matches) { |
| const usedNumbers = match[1].split(' ').map(n => parseInt(n)) |
| ptAvailable = ptAvailable.filter(n => !usedNumbers.includes(n)) |
| } |
| |
| return ptAvailable |
| }, |
| |
| |
| |
| |
| |
| |
| |
| |
| getAvailableHeaderExtensionIdRange (sdp) { |
| const regex = /a=extmap:(\d+)(?:.*)\r\n/gm |
| |
| const matches = sdp.matchAll(regex) |
| let idAvailable = headerExtensionIdLowerRange.concat(headerExtensionIdUppperRange) |
| |
| for (const match of matches) { |
| const usedNumbers = match[1].split(' ').map(n => parseInt(n)) |
| idAvailable = idAvailable.filter(n => !usedNumbers.includes(n)) |
| } |
| |
| return idAvailable |
| }, |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| renegotiate (localDescription, remoteDescription) { |
| const offer = SDPInfo.parse(localDescription) |
| const answer = SDPInfo.parse(remoteDescription) |
| |
| |
| for (const offeredMedia of offer.getMedias()) { |
| |
| let answeredMedia = answer.getMediaById(offeredMedia.getId()) |
| |
| if (!answeredMedia) { |
| |
| answeredMedia = new MediaInfo(offeredMedia.getId(), offeredMedia.getType()) |
| |
| answeredMedia.setDirection(Direction.reverse(offeredMedia.getDirection())) |
| |
| const first = answer.getMedia(offeredMedia.getType()) |
| |
| if (first) { |
| |
| answeredMedia.setCodecs(first.getCodecs()) |
| |
| for (const [id, extension] of first.getExtensions()) { |
| |
| answeredMedia.addExtension(id, extension) |
| } |
| } |
| |
| answer.addMedia(answeredMedia) |
| } |
| } |
| |
| return answer.toString() |
| }, |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| updateMissingVideoExtensions (localDescription, remoteDescription) { |
| const offer = SDPInfo.parse(localDescription) |
| const answer = SDPInfo.parse(remoteDescription) |
| |
| const remoteVideoExtensions = answer.getMediasByType('video')[0]?.getExtensions() |
| if (!remoteVideoExtensions && !remoteVideoExtensions.length) { |
| return |
| } |
| for (const offeredMedia of offer.getMediasByType('video')) { |
| const offerExtensions = offeredMedia.getExtensions() |
| remoteVideoExtensions.forEach((val, key) => { |
| |
| if (!offerExtensions.get(key)) { |
| const id = offeredMedia.getId() |
| const header = 'a=extmap:' + key + ' ' + val + '\r\n' |
| const regex = new RegExp('(a=mid:' + id + '\r\n(?:.*\r\n)*?)', 'g') |
| localDescription = localDescription.replace(regex, (_, p1, p2) => p1 + header) |
| } |
| }) |
| } |
| return localDescription |
| }, |
| getCodecPayloadType (sdp) { |
| const reg = /a=rtpmap:(\d+) (\w+)\/\d+/g |
| const matches = sdp.matchAll(reg) |
| const codecMap = {} |
| |
| for (const match of matches) { |
| codecMap[match[1]] = match[2] |
| } |
| return codecMap |
| } |
| } |
| |
| |
| const hasAudioMultichannel = (mediaStream) => { |
| return mediaStream.getAudioTracks().some(value => value.getSettings().channelCount > 2) |
| } |
| |
| export default SdpParser |