sdp.js   [plain text]


/*
 * 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;