diff options
Diffstat (limited to 'Source/WebCore/Modules/mediastream/sdp.js')
-rw-r--r-- | Source/WebCore/Modules/mediastream/sdp.js | 606 |
1 files changed, 606 insertions, 0 deletions
diff --git a/Source/WebCore/Modules/mediastream/sdp.js b/Source/WebCore/Modules/mediastream/sdp.js new file mode 100644 index 000000000..ecd256070 --- /dev/null +++ b/Source/WebCore/Modules/mediastream/sdp.js @@ -0,0 +1,606 @@ +/* + * 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" + + "${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}" + + "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", + + "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, + "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 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 midInfo = {"midLine": ""}; + if (mediaDescription.mid) + midInfo.midLine = fillTemplate(templates.mid, mediaDescription); + mblock = fillTemplate(mblock, midInfo); + + 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; + }); + + 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; |