/* * Copyright (C) 2014-2015 Ericsson AB. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer * in the documentation and/or other materials provided with the * distribution. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ "use strict"; if (typeof(SDP) == "undefined") var SDP = {}; (function () { var regexps = { "vline": "^v=([\\d]+).*$", "oline": "^o=([\\w\\-@\\.]+) ([\\d]+) ([\\d]+) IN (IP[46]) ([\\d\\.a-f\\:]+).*$", "sline": "^s=(.*)$", "tline": "^t=([\\d]+) ([\\d]+).*$", "cline": "^c=IN (IP[46]) ([\\d\\.a-f\\:]+).*$", "msidsemantic": "^a=msid-semantic: *WMS .*$", "mblock": "^m=(audio|video|application) ([\\d]+) ([A-Z/]+)([\\d ]*)$\\r?\\n", "mode": "^a=(sendrecv|sendonly|recvonly|inactive).*$", "mid": "^a=mid:([!#$%&'*+-.\\w]*).*$", "rtpmap": "^a=rtpmap:${type} ([\\w\\-]+)/([\\d]+)/?([\\d]+)?.*$", "fmtp": "^a=fmtp:${type} ([\\w\\-=; ]+).*$", "param": "([\\w\\-]+)=([\\w\\-]+);?", "nack": "^a=rtcp-fb:${type} nack$", "nackpli": "^a=rtcp-fb:${type} nack pli$", "ccmfir": "^a=rtcp-fb:${type} ccm fir$", "ericscream": "^a=rtcp-fb:${type} ericscream$", "rtcp": "^a=rtcp:([\\d]+)( IN (IP[46]) ([\\d\\.a-f\\:]+))?.*$", "rtcpmux": "^a=rtcp-mux.*$", "cname": "^a=ssrc:(\\d+) cname:([\\w+/\\-@\\.\\{\\}]+).*$", "msid": "^a=(ssrc:\\d+ )?msid:([\\w+/\\-=]+) +([\\w+/\\-=]+).*$", "ufrag": "^a=ice-ufrag:([\\w+/]*).*$", "pwd": "^a=ice-pwd:([\\w+/]*).*$", "candidate": "^a=candidate:(\\d+) (\\d) (UDP|TCP) ([\\d\\.]*) ([\\d\\.a-f\\:]*) (\\d*)" + " typ ([a-z]*)( raddr ([\\d\\.a-f\\:]*) rport (\\d*))?" + "( tcptype (active|passive|so))?.*$", "fingerprint": "^a=fingerprint:(sha-1|sha-256) ([A-Fa-f\\d\:]+).*$", "setup": "^a=setup:(actpass|active|passive).*$", "sctpmap": "^a=sctpmap:${port} ([\\w\\-]+)( [\\d]+)?.*$" }; var templates = { "sdp": "v=${version}\r\n" + "o=${username} ${sessionId} ${sessionVersion} ${netType} ${addressType} ${address}\r\n" + "s=${sessionName}\r\n" + "t=${startTime} ${stopTime}\r\n" + "${bundleLine}" + "${msidsemanticLine}", "msidsemantic": "a=msid-semantic:WMS ${mediaStreamIds}\r\n", "mblock": "m=${type} ${port} ${protocol} ${fmt}\r\n" + "c=${netType} ${addressType} ${address}\r\n" + "${rtcpLine}" + "${rtcpMuxLine}" + "${bundleOnlyLine}" + "a=${mode}\r\n" + "${midLine}" + "${rtpMapLines}" + "${fmtpLines}" + "${nackLines}" + "${nackpliLines}" + "${ccmfirLines}" + "${ericScreamLines}" + "${cnameLines}" + "${msidLines}" + "${iceCredentialLines}" + "${candidateLines}" + "${dtlsFingerprintLine}" + "${dtlsSetupLine}" + "${sctpmapLine}", "rtcp": "a=rtcp:${port}${[ ]netType}${[ ]addressType}${[ ]address}\r\n", "rtcpMux": "a=rtcp-mux\r\n", "mid": "a=mid:${mid}\r\n", "bundle": "a=group:BUNDLE ${midsBundle}\r\n", "rtpMap": "a=rtpmap:${type} ${encodingName}/${clockRate}${[/]channels}\r\n", "fmtp": "a=fmtp:${type} ${parameters}\r\n", "nack": "a=rtcp-fb:${type} nack\r\n", "nackpli": "a=rtcp-fb:${type} nack pli\r\n", "ccmfir": "a=rtcp-fb:${type} ccm fir\r\n", "ericscream": "a=rtcp-fb:${type} ericscream\r\n", "cname": "a=ssrc:${ssrc} cname:${cname}\r\n", "msid": "a=msid:${mediaStreamId} ${mediaStreamTrackId}\r\n", "iceCredentials": "a=ice-ufrag:${ufrag}\r\n" + "a=ice-pwd:${password}\r\n", "candidate": "a=candidate:${foundation} ${componentId} ${transport} ${priority} ${address} ${port}" + " typ ${type}${[ raddr ]relatedAddress}${[ rport ]relatedPort}${[ tcptype ]tcpType}\r\n", "dtlsFingerprint": "a=fingerprint:${fingerprintHashFunction} ${fingerprint}\r\n", "dtlsSetup": "a=setup:${setup}\r\n", "sctpmap": "a=sctpmap:${port} ${app}${[ ]streams}\r\n" }; function match(data, pattern, flags, alt) { var r = new RegExp(pattern, flags); return data.match(r) || alt && alt.match(r) || null; } function addDefaults(obj, defaults) { for (var p in defaults) { if (!defaults.hasOwnProperty(p)) continue; if (typeof(obj[p]) == "undefined") obj[p] = defaults[p]; } } function fillTemplate(template, info) { var text = template; for (var p in info) { if (!info.hasOwnProperty(p)) continue; var r = new RegExp("\\${(\\[[^\\]]+\\])?" + p + "(\\[[^\\]]+\\])?}"); text = text.replace(r, function (_, prefix, suffix) { if (!info[p] && info[p] != 0) return ""; prefix = prefix ? prefix.substr(1, prefix.length - 2) : ""; suffix = suffix ? suffix.substr(1, suffix.length - 2) : ""; return prefix + info[p] + suffix; }); } return text; } SDP.parse = function (sdpText) { sdpText = new String(sdpText); var sdpObj = {}; var parts = sdpText.split(new RegExp(regexps.mblock, "m")) || [sdpText]; var sblock = parts.shift(); var version = parseInt((match(sblock, regexps.vline, "m") || [])[1]); if (!isNaN(version)) sdpObj.version = version; var originator = match(sblock, regexps.oline, "m");; if (originator) { sdpObj.originator = { "username": originator[1], "sessionId": originator[2], "sessionVersion": parseInt(originator[3]), "netType": "IN", "addressType": originator[4], "address": originator[5] }; } var sessionName = match(sblock, regexps.sline, "m"); if (sessionName) sdpObj.sessionName = sessionName[1]; var sessionTime = match(sblock, regexps.tline, "m"); if (sessionTime) { sdpObj.startTime = parseInt(sessionTime[1]); sdpObj.stopTime = parseInt(sessionTime[2]); } var hasMediaStreamId = !!match(sblock, regexps.msidsemantic, "m"); sdpObj.mediaDescriptions = []; for (var i = 0; i < parts.length; i += 5) { var mediaDescription = { "type": parts[i], "port": parseInt(parts[i + 1]), "protocol": parts[i + 2], }; var fmt = parts[i + 3].replace(/^[\s\uFEFF\xA0]+/, '') .split(/ +/) .map(function (x) { return parseInt(x); }); var mblock = parts[i + 4]; var connection = match(mblock, regexps.cline, "m", sblock); if (connection) { mediaDescription.netType = "IN"; mediaDescription.addressType = connection[1]; mediaDescription.address = connection[2]; } var mode = match(mblock, regexps.mode, "m", sblock); if (mode) mediaDescription.mode = mode[1]; var mid = match(mblock, regexps.mid, "m", sblock); if (mid) mediaDescription.mid = mid[1]; var payloadTypes = []; if (match(mediaDescription.protocol, "(UDP/TLS)?RTP/S?AVPF?")) { mediaDescription.payloads = []; payloadTypes = fmt; } payloadTypes.forEach(function (payloadType) { var payload = { "type": payloadType }; var rtpmapLine = fillTemplate(regexps.rtpmap, payload); var rtpmap = match(mblock, rtpmapLine, "m"); if (rtpmap) { payload.encodingName = rtpmap[1]; payload.clockRate = parseInt(rtpmap[2]); if (mediaDescription.type == "audio") payload.channels = parseInt(rtpmap[3]) || 1; else if (mediaDescription.type == "video") { var nackLine = fillTemplate(regexps.nack, payload); payload.nack = !!match(mblock, nackLine, "m"); var nackpliLine = fillTemplate(regexps.nackpli, payload); payload.nackpli = !!match(mblock, nackpliLine, "m"); var ccmfirLine = fillTemplate(regexps.ccmfir, payload); payload.ccmfir = !!match(mblock, ccmfirLine, "m"); var ericScreamLine = fillTemplate(regexps.ericscream, payload); payload.ericscream = !!match(mblock, ericScreamLine, "m"); } } else if (payloadType == 0 || payloadType == 8) { payload.encodingName = payloadType == 8 ? "PCMA" : "PCMU"; payload.clockRate = 8000; payload.channels = 1; } var fmtpLine = fillTemplate(regexps.fmtp, payload); var fmtp = match(mblock, fmtpLine, "m"); if (fmtp) { payload.parameters = {}; fmtp[1].replace(new RegExp(regexps.param, "g"), function(_, key, value) { key = key.replace(/-([a-z])/g, function (_, c) { return c.toUpperCase(); }); payload.parameters[key] = isNaN(+value) ? value : +value; }); } mediaDescription.payloads.push(payload); }); var rtcp = match(mblock, regexps.rtcp, "m"); if (rtcp) { mediaDescription.rtcp = { "netType": "IN", "port": parseInt(rtcp[1]) }; if (rtcp[2]) { mediaDescription.rtcp.addressType = rtcp[3]; mediaDescription.rtcp.address = rtcp[4]; } } var rtcpmux = match(mblock, regexps.rtcpmux, "m", sblock); if (rtcpmux) { if (!mediaDescription.rtcp) mediaDescription.rtcp = {}; mediaDescription.rtcp.mux = true; } var cnameLines = match(mblock, regexps.cname, "mg"); if (cnameLines) { mediaDescription.ssrcs = []; cnameLines.forEach(function (line) { var cname = match(line, regexps.cname, "m"); mediaDescription.ssrcs.push(parseInt(cname[1])); if (!mediaDescription.cname) mediaDescription.cname = cname[2]; }); } if (hasMediaStreamId) { var msid = match(mblock, regexps.msid, "m"); if (msid) { mediaDescription.mediaStreamId = msid[2]; mediaDescription.mediaStreamTrackId = msid[3]; } } var ufrag = match(mblock, regexps.ufrag, "m", sblock); var pwd = match(mblock, regexps.pwd, "m", sblock); if (ufrag && pwd) { mediaDescription.ice = { "ufrag": ufrag[1], "password": pwd[1] }; } var candidateLines = match(mblock, regexps.candidate, "mig"); if (candidateLines) { if (!mediaDescription.ice) mediaDescription.ice = {}; mediaDescription.ice.candidates = []; candidateLines.forEach(function (line) { var candidateLine = match(line, regexps.candidate, "mi"); var candidate = { "foundation": candidateLine[1], "componentId": parseInt(candidateLine[2]), "transport": candidateLine[3].toUpperCase(), "priority": parseInt(candidateLine[4]), "address": candidateLine[5], "port": parseInt(candidateLine[6]), "type": candidateLine[7] }; if (candidateLine[9]) candidate.relatedAddress = candidateLine[9]; if (!isNaN(candidateLine[10])) candidate.relatedPort = parseInt(candidateLine[10]); if (candidateLine[12]) candidate.tcpType = candidateLine[12]; else if (candidate.transport == "TCP") { if (candidate.port == 0 || candidate.port == 9) { candidate.tcpType = "active"; candidate.port = 9; } else { return; } } mediaDescription.ice.candidates.push(candidate); }); } var fingerprint = match(mblock, regexps.fingerprint, "mi", sblock); if (fingerprint) { mediaDescription.dtls = { "fingerprintHashFunction": fingerprint[1].toLowerCase(), "fingerprint": fingerprint[2].toUpperCase() }; } var setup = match(mblock, regexps.setup, "m", sblock); if (setup) { if (!mediaDescription.dtls) mediaDescription.dtls = {}; mediaDescription.dtls.setup = setup[1]; } if (mediaDescription.protocol == "DTLS/SCTP") { mediaDescription.sctp = { "port": fmt[0] }; var sctpmapLine = fillTemplate(regexps.sctpmap, mediaDescription.sctp); var sctpmap = match(mblock, sctpmapLine, "m"); if (sctpmap) { mediaDescription.sctp.app = sctpmap[1]; if (sctpmap[2]) mediaDescription.sctp.streams = parseInt(sctpmap[2]); } } sdpObj.mediaDescriptions.push(mediaDescription); } return sdpObj; }; SDP.generate = function (sdpObj) { sdpObj = JSON.parse(JSON.stringify(sdpObj)); addDefaults(sdpObj, { "version": 0, "originator": {}, "sessionName": "-", "startTime": 0, "stopTime": 0, "bundlePolicy": "balanced", "mediaDescriptions": [] }); addDefaults(sdpObj.originator, { "username": "-", "sessionId": "" + Math.floor((Math.random() + +new Date()) * 1e6), "sessionVersion": 1, "netType": "IN", "addressType": "IP4", "address": "127.0.0.1" }); var sdpText = fillTemplate(templates.sdp, sdpObj); sdpText = fillTemplate(sdpText, sdpObj.originator); var midsBundle = []; var mediatypesBundle = []; var msidsemanticLine = ""; var mediaStreamIds = []; sdpObj.mediaDescriptions.forEach(function (mdesc) { if (mdesc.mediaStreamId && mdesc.mediaStreamTrackId && mediaStreamIds.indexOf(mdesc.mediaStreamId) == -1) mediaStreamIds.push(mdesc.mediaStreamId); }); if (mediaStreamIds.length) { var msidsemanticLine = fillTemplate(templates.msidsemantic, { "mediaStreamIds": mediaStreamIds.join(" ") }); } sdpText = fillTemplate(sdpText, { "msidsemanticLine": msidsemanticLine }); sdpObj.mediaDescriptions.forEach(function (mediaDescription) { addDefaults(mediaDescription, { "port": 9, "protocol": "UDP/TLS/RTP/SAVPF", "netType": "IN", "addressType": "IP4", "address": "0.0.0.0", "mode": "sendrecv", "payloads": [], "rtcp": {} }); var mblock = fillTemplate(templates.mblock, mediaDescription); var midBundleInfo = {"midLine": "", "bundleOnlyLine": ""}; if (mediaDescription.mid) { midBundleInfo.midLine = fillTemplate(templates.mid, mediaDescription); if ((sdpObj.bundlePolicy == "balanced" && mediatypesBundle.includes(mediaDescription.type)) || (sdpObj.bundlePolicy == "max-bundle" && mediatypesBundle.length > 0)) midBundleInfo.bundleOnlyLine = "a=bundle-only\r\n"; mediatypesBundle.push(mediaDescription.type) midsBundle.push(mediaDescription.mid); } mblock = fillTemplate(mblock, midBundleInfo); var payloadInfo = {"rtpMapLines": "", "fmtpLines": "", "nackLines": "", "nackpliLines": "", "ccmfirLines": "", "ericScreamLines": ""}; mediaDescription.payloads.forEach(function (payload) { if (payloadInfo.fmt) payloadInfo.fmt += " " + payload.type; else payloadInfo.fmt = payload.type; if (!payload.channels || payload.channels == 1) payload.channels = null; payloadInfo.rtpMapLines += fillTemplate(templates.rtpMap, payload); if (payload.parameters) { var fmtpInfo = { "type": payload.type, "parameters": "" }; for (var p in payload.parameters) { var param = p.replace(/([A-Z])([a-z])/g, function (_, a, b) { return "-" + a.toLowerCase() + b; }); if (fmtpInfo.parameters) fmtpInfo.parameters += ";"; fmtpInfo.parameters += param + "=" + payload.parameters[p]; } payloadInfo.fmtpLines += fillTemplate(templates.fmtp, fmtpInfo); } if (payload.nack) payloadInfo.nackLines += fillTemplate(templates.nack, payload); if (payload.nackpli) payloadInfo.nackpliLines += fillTemplate(templates.nackpli, payload); if (payload.ccmfir) payloadInfo.ccmfirLines += fillTemplate(templates.ccmfir, payload); if (payload.ericscream) payloadInfo.ericScreamLines += fillTemplate(templates.ericscream, payload); }); mblock = fillTemplate(mblock, payloadInfo); var rtcpInfo = {"rtcpLine": "", "rtcpMuxLine": ""}; if (mediaDescription.rtcp.port) { addDefaults(mediaDescription.rtcp, { "netType": "IN", "addressType": "IP4", "address": "" }); if (!mediaDescription.rtcp.address) mediaDescription.rtcp.netType = mediaDescription.rtcp.addressType = ""; rtcpInfo.rtcpLine = fillTemplate(templates.rtcp, mediaDescription.rtcp); } if (mediaDescription.rtcp.mux) rtcpInfo.rtcpMuxLine = templates.rtcpMux; mblock = fillTemplate(mblock, rtcpInfo); var srcAttributeLines = { "cnameLines": "", "msidLines": "" }; var srcAttributes = { "cname": mediaDescription.cname, "mediaStreamId": mediaDescription.mediaStreamId, "mediaStreamTrackId": mediaDescription.mediaStreamTrackId }; if (mediaDescription.cname && mediaDescription.ssrcs) { mediaDescription.ssrcs.forEach(function (ssrc) { srcAttributes.ssrc = ssrc; srcAttributeLines.cnameLines += fillTemplate(templates.cname, srcAttributes); if (mediaDescription.mediaStreamId && mediaDescription.mediaStreamTrackId) srcAttributeLines.msidLines += fillTemplate(templates.msid, srcAttributes); }); } else if (mediaDescription.mediaStreamId && mediaDescription.mediaStreamTrackId) { srcAttributes.ssrc = null; srcAttributeLines.msidLines += fillTemplate(templates.msid, srcAttributes); } mblock = fillTemplate(mblock, srcAttributeLines); var iceInfo = {"iceCredentialLines": "", "candidateLines": ""}; if (mediaDescription.ice) { iceInfo.iceCredentialLines = fillTemplate(templates.iceCredentials, mediaDescription.ice); if (mediaDescription.ice.candidates) { mediaDescription.ice.candidates.forEach(function (candidate) { addDefaults(candidate, { "relatedAddress": null, "relatedPort": null, "tcpType": null }); iceInfo.candidateLines += fillTemplate(templates.candidate, candidate); }); } } mblock = fillTemplate(mblock, iceInfo); var dtlsInfo = { "dtlsFingerprintLine": "", "dtlsSetupLine": "" }; if (mediaDescription.dtls) { if (mediaDescription.dtls.fingerprint) { dtlsInfo.dtlsFingerprintLine = fillTemplate(templates.dtlsFingerprint, mediaDescription.dtls); } addDefaults(mediaDescription.dtls, {"setup": "actpass"}); dtlsInfo.dtlsSetupLine = fillTemplate(templates.dtlsSetup, mediaDescription.dtls); } mblock = fillTemplate(mblock, dtlsInfo); var sctpInfo = {"sctpmapLine": "", "fmt": ""}; if (mediaDescription.sctp) { addDefaults(mediaDescription.sctp, {"streams": null}); sctpInfo.sctpmapLine = fillTemplate(templates.sctpmap, mediaDescription.sctp); sctpInfo.fmt = mediaDescription.sctp.port; } mblock = fillTemplate(mblock, sctpInfo); sdpText += mblock; }); var bundleLine = ""; if (midsBundle.length > 0) bundleLine = fillTemplate(templates.bundle, { "midsBundle": midsBundle.join(" ") }); sdpText = fillTemplate(sdpText, { "bundleLine": bundleLine }); return sdpText; }; SDP.generateCandidateLine = function (candidateObj) { addDefaults(candidateObj, { "relatedAddress": null, "relatedPort": null, "tcpType": null }); return fillTemplate(templates.candidate, candidateObj); }; var expectedProperties = { "session": [ "version", "originator", "sessionName", "startTime", "stopTime" ], "mline": [ "type", "port", "protocol", "mode", "payloads", "rtcp", "dtls", "ice" ], "mlineSubObjects": { "rtcp": [ "mux" ], "ice": [ "ufrag", "password" ], "dtls": [ "setup", "fingerprintHashFunction", "fingerprint" ] } }; function hasAllProperties(object, properties) { var missing = properties.filter(function (property) { return !object.hasOwnProperty(property); }); return !missing.length; } SDP.verifyObject = function (sdpObj) { if (!hasAllProperties(sdpObj, expectedProperties.session)) return false; for (var i = 0; i < sdpObj.mediaDescriptions.length; i++) { var mediaDescription = sdpObj.mediaDescriptions[i]; if (!hasAllProperties(mediaDescription, expectedProperties.mline)) return false; for (var p in expectedProperties.mlineSubObjects) { if (!hasAllProperties(mediaDescription[p], expectedProperties.mlineSubObjects[p])) return false; } } return true; }; })(); function generate(json) { var object = JSON.parse(json); return SDP.generate(object); } function parse(sdp) { var object = SDP.parse(sdp); if (!SDP.verifyObject(object)) return "ParseError"; return JSON.stringify(object); } function generateCandidateLine(json) { var candidate = JSON.parse(json); return SDP.generateCandidateLine(candidate).substr(2); } function parseCandidateLine(candidateLine) { var mdesc = SDP.parse("m=application 0 NONE\r\na=" + candidateLine + "\r\n").mediaDescriptions[0]; if (!mdesc.ice) return "ParseError"; return JSON.stringify(mdesc.ice.candidates[0]); } if (typeof(module) != "undefined" && typeof(exports) != "undefined") module.exports = SDP;